diff options
Diffstat (limited to '')
387 files changed, 43075 insertions, 0 deletions
diff --git a/tools/lint/android-api-lint.yml b/tools/lint/android-api-lint.yml new file mode 100644 index 0000000000..0b81e79a83 --- /dev/null +++ b/tools/lint/android-api-lint.yml @@ -0,0 +1,15 @@ +--- +android-api-lint: + description: Android api-lint + include: ['mobile/android'] + exclude: [] + extensions: ['java', 'kt'] + support-files: + - 'mobile/android/**/Makefile.in' + - 'mobile/android/config/**' + - 'mobile/android/gradle.configure' + - 'mobile/android/**/moz.build' + - '**/*.gradle' + type: global + payload: android.lints:api_lint + setup: android.lints:setup diff --git a/tools/lint/android-checkstyle.yml b/tools/lint/android-checkstyle.yml new file mode 100644 index 0000000000..abd4974e6a --- /dev/null +++ b/tools/lint/android-checkstyle.yml @@ -0,0 +1,15 @@ +--- +android-checkstyle: + description: Android checkstyle + include: ['mobile/android'] + exclude: [] + extensions: ['java', 'kt'] + support-files: + - 'mobile/android/**/Makefile.in' + - 'mobile/android/config/**' + - 'mobile/android/gradle.configure' + - 'mobile/android/**/moz.build' + - '**/*.gradle' + type: global + payload: android.lints:checkstyle + setup: android.lints:setup diff --git a/tools/lint/android-format.yml b/tools/lint/android-format.yml new file mode 100644 index 0000000000..cacf3ff2d7 --- /dev/null +++ b/tools/lint/android-format.yml @@ -0,0 +1,15 @@ +--- +android-format: + description: Android formatting lint + include: ['mobile/android'] + exclude: [] + extensions: ['java', 'kt'] + support-files: + - 'mobile/android/**/Makefile.in' + - 'mobile/android/config/**' + - 'mobile/android/gradle.configure' + - 'mobile/android/**/moz.build' + - '**/*.gradle' + type: global + payload: android.lints:format + setup: android.lints:setup diff --git a/tools/lint/android-javadoc.yml b/tools/lint/android-javadoc.yml new file mode 100644 index 0000000000..a0811a08cd --- /dev/null +++ b/tools/lint/android-javadoc.yml @@ -0,0 +1,15 @@ +--- +android-javadoc: + description: Android javadoc + include: ['mobile/android/geckoview'] + exclude: [] + extensions: ['java', 'kt'] + support-files: + - 'mobile/android/**/Makefile.in' + - 'mobile/android/config/**' + - 'mobile/android/gradle.configure' + - 'mobile/android/**/moz.build' + - '**/*.gradle' + type: global + payload: android.lints:javadoc + setup: android.lints:setup diff --git a/tools/lint/android-lint.yml b/tools/lint/android-lint.yml new file mode 100644 index 0000000000..6a1dbe7b74 --- /dev/null +++ b/tools/lint/android-lint.yml @@ -0,0 +1,15 @@ +--- +android-lint: + description: Android lint + include: ['mobile/android'] + exclude: [] + extensions: ['java', 'kt'] + support-files: + - 'mobile/android/**/Makefile.in' + - 'mobile/android/config/**' + - 'mobile/android/gradle.configure' + - 'mobile/android/**/moz.build' + - '**/*.gradle' + type: global + payload: android.lints:lint + setup: android.lints:setup diff --git a/tools/lint/android-test.yml b/tools/lint/android-test.yml new file mode 100644 index 0000000000..65739295dd --- /dev/null +++ b/tools/lint/android-test.yml @@ -0,0 +1,15 @@ +--- +android-test: + description: Android test + include: ['mobile/android'] + exclude: [] + extensions: ['java', 'kt'] + support-files: + - 'mobile/android/**/Makefile.in' + - 'mobile/android/config/**' + - 'mobile/android/gradle.configure' + - 'mobile/android/**/moz.build' + - '**/*.gradle' + type: global + payload: android.lints:test + setup: android.lints:setup diff --git a/tools/lint/android/__init__.py b/tools/lint/android/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tools/lint/android/__init__.py diff --git a/tools/lint/android/lints.py b/tools/lint/android/lints.py new file mode 100644 index 0000000000..53c8626013 --- /dev/null +++ b/tools/lint/android/lints.py @@ -0,0 +1,420 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 glob +import itertools +import json +import os +import re +import shlex +import subprocess +import sys +import xml.etree.ElementTree as ET + +import mozpack.path as mozpath +from mozlint import result +from mozpack.files import FileFinder + +# The Gradle target invocations are serialized with a simple locking file scheme. It's fine for +# them to take a while, since the first will compile all the Java, etc, and then perform +# potentially expensive static analyses. +GRADLE_LOCK_MAX_WAIT_SECONDS = 20 * 60 + + +def setup(root, **setupargs): + if setupargs.get("substs", {}).get("MOZ_BUILD_APP") != "mobile/android": + return 1 + + if "topobjdir" not in setupargs: + print( + "Skipping {}: a configured Android build is required!".format( + setupargs["name"] + ) + ) + return 1 + + return 0 + + +def gradle(log, topsrcdir=None, topobjdir=None, tasks=[], extra_args=[], verbose=True): + sys.path.insert(0, os.path.join(topsrcdir, "mobile", "android")) + from gradle import gradle_lock + + with gradle_lock(topobjdir, max_wait_seconds=GRADLE_LOCK_MAX_WAIT_SECONDS): + # The android-lint parameter can be used by gradle tasks to run special + # logic when they are run for a lint using + # project.hasProperty('android-lint') + cmd_args = ( + [ + sys.executable, + os.path.join(topsrcdir, "mach"), + "gradle", + "--verbose", + "-Pandroid-lint", + "--", + ] + + tasks + + extra_args + ) + + cmd = " ".join(shlex.quote(arg) for arg in cmd_args) + log.debug(cmd) + + # Gradle and mozprocess do not get along well, so we use subprocess + # directly. + proc = subprocess.Popen(cmd_args, cwd=topsrcdir) + status = None + # Leave it to the subprocess to handle Ctrl+C. If it terminates as a result + # of Ctrl+C, proc.wait() will return a status code, and, we get out of the + # loop. If it doesn't, like e.g. gdb, we continue waiting. + while status is None: + try: + status = proc.wait() + except KeyboardInterrupt: + pass + + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + raise + + return proc.returncode + + +def format(config, fix=None, **lintargs): + topsrcdir = lintargs["root"] + topobjdir = lintargs["topobjdir"] + + if fix: + tasks = lintargs["substs"]["GRADLE_ANDROID_FORMAT_LINT_FIX_TASKS"] + else: + tasks = lintargs["substs"]["GRADLE_ANDROID_FORMAT_LINT_CHECK_TASKS"] + + ret = gradle( + lintargs["log"], + topsrcdir=topsrcdir, + topobjdir=topobjdir, + tasks=tasks, + extra_args=lintargs.get("extra_args") or [], + ) + + results = [] + for path in lintargs["substs"]["GRADLE_ANDROID_FORMAT_LINT_FOLDERS"]: + folder = os.path.join( + topobjdir, "gradle", "build", path, "spotless", "spotlessJava" + ) + for filename in glob.iglob(folder + "/**/*.java", recursive=True): + err = { + "rule": "spotless-java", + "path": os.path.join( + topsrcdir, path, mozpath.relpath(filename, folder) + ), + "lineno": 0, + "column": 0, + "message": "Formatting error, please run ./mach lint -l android-format --fix", + "level": "error", + } + results.append(result.from_config(config, **err)) + folder = os.path.join( + topobjdir, "gradle", "build", path, "spotless", "spotlessKotlin" + ) + for filename in glob.iglob(folder + "/**/*.kt", recursive=True): + err = { + "rule": "spotless-kt", + "path": os.path.join( + topsrcdir, path, mozpath.relpath(filename, folder) + ), + "lineno": 0, + "column": 0, + "message": "Formatting error, please run ./mach lint -l android-format --fix", + "level": "error", + } + results.append(result.from_config(config, **err)) + + if len(results) == 0 and ret != 0: + # spotless seems to hit unfixed error. + err = { + "rule": "spotless", + "path": "", + "lineno": 0, + "column": 0, + "message": "Unexpected error", + "level": "error", + } + results.append(result.from_config(config, **err)) + + # If --fix was passed, we just report the number of files that were changed + if fix: + return {"results": [], "fixed": len(results)} + return results + + +def api_lint(config, **lintargs): + topsrcdir = lintargs["root"] + topobjdir = lintargs["topobjdir"] + + gradle( + lintargs["log"], + topsrcdir=topsrcdir, + topobjdir=topobjdir, + tasks=lintargs["substs"]["GRADLE_ANDROID_API_LINT_TASKS"], + extra_args=lintargs.get("extra_args") or [], + ) + + folder = lintargs["substs"]["GRADLE_ANDROID_GECKOVIEW_APILINT_FOLDER"] + + results = [] + + with open(os.path.join(topobjdir, folder, "apilint-result.json")) as f: + issues = json.load(f) + + for rule in ("compat_failures", "failures"): + for r in issues[rule]: + err = { + "rule": r["rule"] if rule == "failures" else "compat_failures", + "path": r["file"], + "lineno": int(r["line"]), + "column": int(r.get("column") or 0), + "message": r["msg"], + "level": "error" if r["error"] else "warning", + } + results.append(result.from_config(config, **err)) + + for r in issues["api_changes"]: + err = { + "rule": "api_changes", + "path": r["file"], + "lineno": int(r["line"]), + "column": int(r.get("column") or 0), + "message": "Unexpected api change. Please run ./mach gradle {} for more " + "information".format( + " ".join(lintargs["substs"]["GRADLE_ANDROID_API_LINT_TASKS"]) + ), + } + results.append(result.from_config(config, **err)) + + return results + + +def javadoc(config, **lintargs): + topsrcdir = lintargs["root"] + topobjdir = lintargs["topobjdir"] + + gradle( + lintargs["log"], + topsrcdir=topsrcdir, + topobjdir=topobjdir, + tasks=lintargs["substs"]["GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS"], + extra_args=lintargs.get("extra_args") or [], + ) + + output_files = lintargs["substs"]["GRADLE_ANDROID_GECKOVIEW_DOCS_OUTPUT_FILES"] + + results = [] + + for output_file in output_files: + with open(os.path.join(topobjdir, output_file)) as f: + # Like: '[{"path":"/absolute/path/to/topsrcdir/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentBlocking.java","lineno":"462","level":"warning","message":"no @return"}]'. # NOQA: E501 + issues = json.load(f) + + for issue in issues: + # We want warnings to be errors for linting purposes. + # TODO: Bug 1316188 - resolve missing javadoc comments + issue["level"] = ( + "error" if issue["message"] != ": no comment" else "warning" + ) + results.append(result.from_config(config, **issue)) + + return results + + +def lint(config, **lintargs): + topsrcdir = lintargs["root"] + topobjdir = lintargs["topobjdir"] + + gradle( + lintargs["log"], + topsrcdir=topsrcdir, + topobjdir=topobjdir, + tasks=lintargs["substs"]["GRADLE_ANDROID_LINT_TASKS"], + extra_args=lintargs.get("extra_args") or [], + ) + + # It's surprising that this is the App variant name, but this is "withoutGeckoBinariesDebug" + # right now and the GeckoView variant name is "withGeckoBinariesDebug". This will be addressed + # as we unify variants. + path = os.path.join( + lintargs["topobjdir"], + "gradle/build/mobile/android/geckoview/reports", + "lint-results-{}.xml".format( + lintargs["substs"]["GRADLE_ANDROID_GECKOVIEW_VARIANT_NAME"] + ), + ) + tree = ET.parse(open(path, "rt")) + root = tree.getroot() + + results = [] + + for issue in root.findall("issue"): + location = issue[0] + if "third_party" in location.get("file") or "thirdparty" in location.get( + "file" + ): + continue + err = { + "level": issue.get("severity").lower(), + "rule": issue.get("id"), + "message": issue.get("message"), + "path": location.get("file"), + "lineno": int(location.get("line") or 0), + } + results.append(result.from_config(config, **err)) + + return results + + +def _parse_checkstyle_output(config, topsrcdir=None, report_path=None): + tree = ET.parse(open(report_path, "rt")) + root = tree.getroot() + + for file in root.findall("file"): + for error in file.findall("error"): + # Like <error column="42" line="22" message="Name 'mPorts' must match pattern 'xm[A-Z][A-Za-z]*$'." severity="error" source="com.puppycrawl.tools.checkstyle.checks.naming.MemberNameCheck" />. # NOQA: E501 + err = { + "level": "error", + "rule": error.get("source"), + "message": error.get("message"), + "path": file.get("name"), + "lineno": int(error.get("line") or 0), + "column": int(error.get("column") or 0), + } + yield result.from_config(config, **err) + + +def checkstyle(config, **lintargs): + topsrcdir = lintargs["root"] + topobjdir = lintargs["topobjdir"] + + gradle( + lintargs["log"], + topsrcdir=topsrcdir, + topobjdir=topobjdir, + tasks=lintargs["substs"]["GRADLE_ANDROID_CHECKSTYLE_TASKS"], + extra_args=lintargs.get("extra_args") or [], + ) + + results = [] + + for relative_path in lintargs["substs"]["GRADLE_ANDROID_CHECKSTYLE_OUTPUT_FILES"]: + report_path = os.path.join(lintargs["topobjdir"], relative_path) + results.extend( + _parse_checkstyle_output( + config, topsrcdir=lintargs["root"], report_path=report_path + ) + ) + + return results + + +def _parse_android_test_results(config, topsrcdir=None, report_dir=None): + # A brute force way to turn a Java FQN into a path on disk. Assumes Java + # and Kotlin sources are in mobile/android for performance and simplicity. + sourcepath_finder = FileFinder(os.path.join(topsrcdir, "mobile", "android")) + + finder = FileFinder(report_dir) + reports = list(finder.find("TEST-*.xml")) + if not reports: + raise RuntimeError("No reports found under {}".format(report_dir)) + + for report, _ in reports: + tree = ET.parse(open(os.path.join(finder.base, report), "rt")) + root = tree.getroot() + + class_name = root.get( + "name" + ) # Like 'org.mozilla.gecko.permissions.TestPermissions'. + path = ( + "**/" + class_name.replace(".", "/") + ".*" + ) # Like '**/org/mozilla/gecko/permissions/TestPermissions.*'. # NOQA: E501 + + for testcase in root.findall("testcase"): + function_name = testcase.get("name") + + # Schema cribbed from http://llg.cubic.org/docs/junit/. + for unexpected in itertools.chain( + testcase.findall("error"), testcase.findall("failure") + ): + sourcepaths = list(sourcepath_finder.find(path)) + if not sourcepaths: + raise RuntimeError( + "No sourcepath found for class {class_name}".format( + class_name=class_name + ) + ) + + for sourcepath, _ in sourcepaths: + lineno = 0 + message = unexpected.get("message") + # Turn '... at org.mozilla.gecko.permissions.TestPermissions.testMultipleRequestsAreQueuedAndDispatchedSequentially(TestPermissions.java:118)' into 118. # NOQA: E501 + pattern = r"at {class_name}\.{function_name}\(.*:(\d+)\)" + pattern = pattern.format( + class_name=class_name, function_name=function_name + ) + match = re.search(pattern, message) + if match: + lineno = int(match.group(1)) + else: + msg = "No source line found for {class_name}.{function_name}".format( + class_name=class_name, function_name=function_name + ) + raise RuntimeError(msg) + + err = { + "level": "error", + "rule": unexpected.get("type"), + "message": message, + "path": os.path.join( + topsrcdir, "mobile", "android", sourcepath + ), + "lineno": lineno, + } + yield result.from_config(config, **err) + + +def test(config, **lintargs): + topsrcdir = lintargs["root"] + topobjdir = lintargs["topobjdir"] + + gradle( + lintargs["log"], + topsrcdir=topsrcdir, + topobjdir=topobjdir, + tasks=lintargs["substs"]["GRADLE_ANDROID_TEST_TASKS"], + extra_args=lintargs.get("extra_args") or [], + ) + + results = [] + + def capitalize(s): + # Can't use str.capitalize because it lower cases trailing letters. + return (s[0].upper() + s[1:]) if s else "" + + pairs = [("geckoview", lintargs["substs"]["GRADLE_ANDROID_GECKOVIEW_VARIANT_NAME"])] + for project, variant in pairs: + report_dir = os.path.join( + lintargs["topobjdir"], + "gradle/build/mobile/android/{}/test-results/test{}UnitTest".format( + project, capitalize(variant) + ), + ) + results.extend( + _parse_android_test_results( + config, topsrcdir=lintargs["root"], report_dir=report_dir + ) + ) + + return results diff --git a/tools/lint/black.yml b/tools/lint/black.yml new file mode 100644 index 0000000000..c182f9939b --- /dev/null +++ b/tools/lint/black.yml @@ -0,0 +1,19 @@ +--- +black: + description: Reformat python + exclude: + - gfx/harfbuzz/src/meson.build + - '**/*.mako.py' + - python/mozbuild/mozbuild/test/frontend/data/reader-error-syntax/moz.build + - testing/mozharness/configs/test/test_malformed.py + - testing/web-platform/tests + extensions: + - build + - configure + - mozbuild + - py + support-files: + - 'tools/lint/python/**' + type: external + payload: python.black:lint + setup: python.black:setup diff --git a/tools/lint/clang-format.yml b/tools/lint/clang-format.yml new file mode 100644 index 0000000000..bcdf9d8591 --- /dev/null +++ b/tools/lint/clang-format.yml @@ -0,0 +1,12 @@ +--- +clang-format: + description: Reformat C/C++ + include: + - '.' + extensions: ['cpp', 'c', 'cc', 'h', 'm', 'mm'] + support-files: + - 'tools/lint/clang-format/**' + type: external + payload: clang-format:lint + code_review_warnings: false + setup: clang-format:setup diff --git a/tools/lint/clang-format/__init__.py b/tools/lint/clang-format/__init__.py new file mode 100644 index 0000000000..243ea51b39 --- /dev/null +++ b/tools/lint/clang-format/__init__.py @@ -0,0 +1,229 @@ +# 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 os +import re +import signal +import subprocess +import sys +import xml.etree.ElementTree as ET + +from mozboot.util import get_tools_dir +from mozlint import result +from mozlint.pathutils import expand_exclusions + +CLANG_FORMAT_NOT_FOUND = """ +Could not find clang-format! It should've been installed automatically - \ +please report a bug here: +https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox%20Build%20System&component=Lint%20and%20Formatting +""".strip() + + +def setup(root, mach_command_context, **lintargs): + if get_clang_format_binary(): + return 0 + + from mozbuild.code_analysis.mach_commands import get_clang_tools + + rc, _ = get_clang_tools(mach_command_context) + if rc: + return 1 + + +def run_process(config, cmd): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) + signal.signal(signal.SIGINT, orig) + try: + output, _ = proc.communicate() + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return output + + +def get_clang_format_binary(): + """ + Returns the path of the first clang-format binary available + if not found returns None + """ + binary = os.environ.get("CLANG_FORMAT") + if binary: + return binary + + clang_tools_path = os.path.join(get_tools_dir(), "clang-tools") + bin_path = os.path.join(clang_tools_path, "clang-tidy", "bin") + binary = os.path.join(bin_path, "clang-format") + + if sys.platform.startswith("win"): + binary += ".exe" + + if not os.path.isfile(binary): + return None + + return binary + + +def is_ignored_path(ignored_dir_re, topsrcdir, f): + # Remove up to topsrcdir in pathname and match + if f.startswith(topsrcdir + "/"): + match_f = f[len(topsrcdir + "/") :] + else: + match_f = f + return re.match(ignored_dir_re, match_f) + + +def remove_ignored_path(paths, topsrcdir, log): + path_to_third_party = os.path.join(topsrcdir, ".clang-format-ignore") + + ignored_dir = [] + with open(path_to_third_party, "r") as fh: + for l in fh: + # In case it starts with a space + line = l.strip() + # Remove comments and empty lines + if line.startswith("#") or len(line) == 0: + continue + # The regexp is to make sure we are managing relative paths + ignored_dir.append(r"^[\./]*" + line.rstrip()) + + # Generates the list of regexp + ignored_dir_re = "(%s)" % "|".join(ignored_dir) + + path_list = [] + for f in paths: + if is_ignored_path(ignored_dir_re, topsrcdir, f): + # Early exit if we have provided an ignored directory + log.debug("Ignored third party code '{0}'".format(f)) + continue + path_list.append(f) + + return path_list + + +def lint(paths, config, fix=None, **lintargs): + log = lintargs["log"] + paths = list(expand_exclusions(paths, config, lintargs["root"])) + + # We ignored some specific files for a bunch of reasons. + # Not using excluding to avoid duplication + if lintargs.get("use_filters", True): + paths = remove_ignored_path(paths, lintargs["root"], log) + + # An empty path array can occur when the user passes in `-n`. If we don't + # return early in this case, rustfmt will attempt to read stdin and hang. + if not paths: + return [] + + binary = get_clang_format_binary() + + if not binary: + print(CLANG_FORMAT_NOT_FOUND) + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + cmd_args = [binary] + + base_command = cmd_args + ["--version"] + version = run_process(config, base_command).rstrip("\r\n") + log.debug("Version: {}".format(version)) + + cmd_args.append("--output-replacements-xml") + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + output = run_process(config, base_command) + + def replacement(parser): + for end, e in parser.read_events(): + assert end == "end" + if e.tag == "replacement": + item = {k: int(v) for k, v in e.items()} + assert sorted(item.keys()) == ["length", "offset"] + item["with"] = (e.text or "").encode("utf-8") + yield item + + # When given multiple paths as input, --output-replacements-xml + # will output one xml per path, in the order they are given, but + # XML parsers don't know how to handle that, so do it manually. + parser = None + replacements = [] + for l in output.split("\n"): + line = l.rstrip("\r\n") + if line.startswith("<?xml "): + if parser: + replacements.append(list(replacement(parser))) + parser = ET.XMLPullParser(["end"]) + parser.feed(line) + replacements.append(list(replacement(parser))) + + results = [] + fixed = 0 + for path, replacement in zip(paths, replacements): + if not replacement: + continue + with open(path, "rb") as fh: + data = fh.read() + + linenos = [] + patched_data = b"" + last_offset = 0 + lineno_before = 1 + lineno_after = 1 + + for item in replacement: + offset = item["offset"] + length = item["length"] + replace_with = item["with"] + since_last_offset = data[last_offset:offset] + replaced = data[offset : offset + length] + + lines_since_last_offset = since_last_offset.count(b"\n") + lineno_before += lines_since_last_offset + lineno_after += lines_since_last_offset + start_lineno = (lineno_before, lineno_after) + + lineno_before += replaced.count(b"\n") + lineno_after += replace_with.count(b"\n") + end_lineno = (lineno_before, lineno_after) + + if linenos and start_lineno[0] <= linenos[-1][1][0]: + linenos[-1] = (linenos[-1][0], end_lineno) + else: + linenos.append((start_lineno, end_lineno)) + + patched_data += since_last_offset + replace_with + last_offset = offset + len(replaced) + patched_data += data[last_offset:] + + lines_before = data.decode("utf-8", "replace").splitlines() + lines_after = patched_data.decode("utf-8", "replace").splitlines() + for (start_before, start_after), (end_before, end_after) in linenos: + diff = "".join( + "-" + l + "\n" for l in lines_before[start_before - 1 : end_before] + ) + diff += "".join( + "+" + l + "\n" for l in lines_after[start_after - 1 : end_after] + ) + + results.append( + result.from_config( + config, + path=path, + diff=diff, + level="warning", + lineno=start_before, + column=0, + ) + ) + + if fix: + with open(path, "wb") as fh: + fh.write(patched_data) + fixed += len(linenos) + + return {"results": results, "fixed": fixed} diff --git a/tools/lint/clippy.yml b/tools/lint/clippy.yml new file mode 100644 index 0000000000..7b8d196e21 --- /dev/null +++ b/tools/lint/clippy.yml @@ -0,0 +1,110 @@ +--- +clippy: + description: Lint rust + include: + - build/workspace-hack/ + - dom/midi/midir_impl/ + - dom/media/gtest/ + - dom/webauthn/libudev-sys/ + - gfx/webrender_bindings/ + - gfx/wr/peek-poke/ + - gfx/wr/peek-poke/peek-poke-derive/ + - gfx/wr/webrender_build/ + - gfx/wr/wr_malloc_size_of/ + - js/src/frontend/smoosh/ + - js/src/rust/shared/ + - modules/libpref/init/static_prefs/ + - mozglue/static/rust/ + - netwerk/base/mozurl/ + - security/manager/ssl/data_storage/ + - servo/components/derive_common/ + - servo/components/selectors/ + - servo/components/servo_arc/ + - servo/components/style/ + - servo/components/style_derive/ + - servo/components/style_traits/ + - servo/components/to_shmem/ + - servo/components/to_shmem_derive/ + - servo/tests/unit/style/ + - testing/geckodriver/ + - testing/mozbase/rust/mozdevice/ + - testing/mozbase/rust/mozprofile/ + - testing/mozbase/rust/mozrunner/ + - testing/mozbase/rust/mozversion/ + - testing/webdriver/ + - third_party/rust/mp4parse/ + - third_party/rust/mp4parse_capi/ + - toolkit/crashreporter/mozannotation_client/ + - toolkit/crashreporter/mozannotation_server/ + - toolkit/components/kvstore/ + - toolkit/components/glean/ + - toolkit/library/rust/ + - tools/fuzzing/rust/ + - tools/profiler/rust-api/ + - xpcom/rust/gtest/bench-collections/ + - xpcom/rust/xpcom/xpcom_macros/ + exclude: + # Many are failing for the same reasons: + # https://bugzilla.mozilla.org/show_bug.cgi?id=1606073 + # https://bugzilla.mozilla.org/show_bug.cgi?id=1606077 + - Cargo.toml + # nsstring + # derive_hash_xor_eq + - gfx/wr/ + - gfx/wr/webrender/ + - gfx/wr/examples/ + # windows-only + - gfx/wr/example-compositor/compositor-windows/ + - gfx/wr/webrender_api/ + - gfx/wr/wrench/ + - gfx/wgpu_bindings/ + # not_unsafe_ptr_arg_deref + - modules/libpref/parser/ + - tools/profiler/rust-helper/ + - toolkit/library/rust/shared/ + - toolkit/library/gtest/rust/ + # not_unsafe_ptr_arg_deref + - remote/ + - dom/media/webrtc/sdp/rsdparsa_capi/ + - intl/encoding_glue/ + # not_unsafe_ptr_arg_deref + - storage/rust/ + - storage/variant/ + # nsstring + - servo/ports/geckolib/tests/ + - xpcom/rust/xpcom/ + - xpcom/rust/nsstring/ + - xpcom/rust/gtest/xpcom/ + - xpcom/rust/gtest/nsstring/ + - security/manager/ssl/cert_storage/ + - intl/locale/rust/fluent-langneg-ffi/ + - intl/locale/rust/unic-langid-ffi/ + - toolkit/components/places/bookmark_sync/ + - xpcom/rust/nserror/ + - xpcom/rust/moz_task/ + - xpcom/rust/gkrust_utils/ + - netwerk/socket/neqo_glue/ + - dom/media/webrtc/transport/mdns_service/ + - tools/lint/test/files/clippy/ + - servo/ports/geckolib/ + - servo/ports/geckolib/tests/ + - servo/tests/unit/malloc_size_of/ + - servo/components/malloc_size_of/ + - dom/media/webrtc/sdp/rsdparsa_capi/ + - testing/geckodriver/marionette/ + - toolkit/components/bitsdownload/bits_client/ + - gfx/wr/example-compositor/compositor/ + - toolkit/components/bitsdownload/bits_client/bits/ + # mac and windows only + - security/manager/ssl/osclientcerts/ + extensions: + - rs + support-files: + - 'tools/lint/clippy/**' + # the version of cargo-clippy is: + # clippy 0.1.65 (2019147 2022-09-19) + # we use the date instead to facilitate the check + # replacing - by . because Python packaging.version.Version expects this + min_clippy_version: 2022.09.19 + type: external + payload: clippy:lint diff --git a/tools/lint/clippy/__init__.py b/tools/lint/clippy/__init__.py new file mode 100644 index 0000000000..1facb246fc --- /dev/null +++ b/tools/lint/clippy/__init__.py @@ -0,0 +1,99 @@ +# 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 bisect +import json +import os +import subprocess +import sys + +from mozlint import result +from mozlint.pathutils import expand_exclusions + + +def in_sorted_list(l, x): + i = bisect.bisect_left(l, x) + return i < len(l) and l[i] == x + + +def handle_clippy_msg(config, line, log, base_path, files): + try: + detail = json.loads(line) + if "message" in detail: + p = detail["target"]["src_path"] + detail = detail["message"] + if "level" in detail: + if ( + detail["level"] == "error" or detail["level"] == "failure-note" + ) and not detail["code"]: + log.debug( + "Error outside of clippy." + "This means that the build failed. Therefore, skipping this" + ) + log.debug("File = {} / Detail = {}".format(p, detail)) + return + # We are in a clippy warning + if len(detail["spans"]) == 0: + # For some reason, at the end of the summary, we can + # get the following line + # {'rendered': 'warning: 5 warnings emitted\n\n', 'children': + # [], 'code': None, 'level': 'warning', 'message': + # '5 warnings emitted', 'spans': []} + # if this is the case, skip it + log.debug( + "Skipping the summary line {} for file {}".format(detail, p) + ) + return + + l = detail["spans"][0] + if not in_sorted_list(files, p): + return + p = os.path.join(base_path, l["file_name"]) + res = { + "path": p, + "level": detail["level"], + "lineno": l["line_start"], + "column": l["column_start"], + "message": detail["message"], + "hint": detail["rendered"], + "rule": detail["code"]["code"], + "lineoffset": l["line_end"] - l["line_start"], + } + return result.from_config(config, **res) + + except json.decoder.JSONDecodeError: + log.debug("Could not parse the output:") + log.debug("clippy output: {}".format(line)) + return + + +def lint(paths, config, fix=None, **lintargs): + files = list(expand_exclusions(paths, config, lintargs["root"])) + files.sort() + log = lintargs["log"] + results = [] + mach_path = lintargs["root"] + "/mach" + march_cargo_process = subprocess.Popen( + [ + sys.executable, + mach_path, + "--log-no-times", + "cargo", + "clippy", + "--", + "--message-format=json", + ], + stdout=subprocess.PIPE, + text=True, + ) + for l in march_cargo_process.stdout: + r = handle_clippy_msg(config, l, log, lintargs["root"], files) + if r is not None: + results.append(r) + march_cargo_process.wait() + + if fix: + log.error("Rust linting in mach does not support --fix") + + return results diff --git a/tools/lint/codespell.yml b/tools/lint/codespell.yml new file mode 100644 index 0000000000..9f821f6296 --- /dev/null +++ b/tools/lint/codespell.yml @@ -0,0 +1,98 @@ +--- +codespell: + description: Check code for common misspellings + include: + - browser/base/content/docs/ + - browser/branding/ + - browser/components/asrouter/docs/ + - browser/components/newtab/docs/ + - browser/components/places/docs/ + - browser/components/search/docs/ + - browser/components/search/schema/ + - browser/components/touchbar/docs/ + - browser/components/urlbar/docs/ + - browser/extensions/formautofill/locales/en-US/ + - browser/extensions/report-site-issue/locales/en-US/ + - browser/installer/windows/docs/ + - browser/locales/en-US/ + - build/docs/ + - devtools/client/locales/en-US/ + - devtools/docs/ + - devtools/shared/locales/en-US/ + - devtools/startup/locales/en-US/ + - docs/ + - dom/docs/ + - dom/locales/en-US/ + - gfx/docs/ + - intl/docs/ + - intl/locales/en-US/ + - ipc/docs/ + - js/src/doc/ + - layout/tools/layout-debug/ui/content/layoutdebug.ftl + - mobile/android/branding/ + - mobile/android/docs/ + - mobile/android/locales/en-US/ + - netwerk/locales/en-US/ + - netwerk/docs/ + - python/docs/ + - python/mach/docs/ + - python/mozlint/ + - python/mozperftest/perfdocs/ + - remote/doc/ + - security/manager/locales/en-US/ + - services/settings/docs/ + - taskcluster/docs/ + - testing/docs/xpcshell/ + - testing/geckodriver/doc/ + - testing/mozbase/docs/ + - testing/raptor/raptor/perfdocs/ + - toolkit/components/extensions/docs/ + - toolkit/components/normandy/docs/ + - toolkit/components/search/docs/ + - toolkit/components/search/schema/ + - toolkit/components/telemetry/docs/ + - toolkit/crashreporter/docs/ + - toolkit/docs/ + - toolkit/locales/en-US/ + - toolkit/modules/docs/ + - tools/code-coverage/docs/ + - tools/fuzzing/docs/ + - tools/lint/ + - tools/moztreedocs/ + - tools/profiler/docs/ + - tools/sanitizer/docs/ + - tools/tryselect/ + - uriloader/docs/ + - xpcom/docs/ + exclude: + - devtools/docs/contributor/tools/storage/ + - tools/lint/cpp/mingw-headers.txt + - tools/lint/test/test_codespell.py + - "**/package-lock.json" + # List of extensions coming from: + # tools/lint/{flake8,eslint}.yml + # tools/mach_commands.py (clang-format) + # + documentation + # + localization files + extensions: + - js + - jsm + - jxs + - mjs + - xml + - html + - xhtml + - cpp + - c + - h + - configure + - py + - properties + - rst + - md + - ftl + support-files: + - 'tools/lint/spell/**' + type: external + setup: spell:setup + payload: spell:lint diff --git a/tools/lint/condprof-addons.yml b/tools/lint/condprof-addons.yml new file mode 100644 index 0000000000..a62c1fb6b9 --- /dev/null +++ b/tools/lint/condprof-addons.yml @@ -0,0 +1,10 @@ +--- +condprof-addons: + description: Lint condprof customizations json files sideloading addons + include: + - 'testing/condprofile/condprof/customization' + exclude: [] + extensions: ['json'] + support-files: ['taskcluster/ci/fetch/browsertime.yml'] + type: structured_log + payload: condprof-addons:lint diff --git a/tools/lint/condprof-addons/__init__.py b/tools/lint/condprof-addons/__init__.py new file mode 100644 index 0000000000..f17ab26f3f --- /dev/null +++ b/tools/lint/condprof-addons/__init__.py @@ -0,0 +1,217 @@ +# 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 hashlib +import json +import os +import tarfile +import tempfile +from pathlib import Path + +import requests +import yaml +from mozlint.pathutils import expand_exclusions + +BROWSERTIME_FETCHES_PATH = Path("taskcluster/ci/fetch/browsertime.yml") +CUSTOMIZATIONS_PATH = Path("testing/condprofile/condprof/customization/") +DOWNLOAD_TIMEOUT = 30 +ERR_FETCH_TASK_MISSING = "firefox-addons taskcluster fetch config section not found" +ERR_FETCH_TASK_ADDPREFIX = "firefox-addons taskcluster config 'add-prefix' attribute should be set to 'firefox-addons/'" +ERR_FETCH_TASK_ARCHIVE = ( + "Error downloading or opening archive from firefox-addons taskcluster fetch url" +) +LINTER_NAME = "condprof-addons" +MOZ_FETCHES_DIR = os.environ.get("MOZ_FETCHES_DIR") +RULE_DESC = "condprof addons all listed in firefox-addons.tar fetched archive" +MOZ_AUTOMATION = "MOZ_AUTOMATION" in os.environ + +tempdir = tempfile.gettempdir() + + +def lint(paths, config, logger, fix=None, **lintargs): + filepaths = [Path(p) for p in expand_exclusions(paths, config, lintargs["root"])] + + if len(filepaths) == 0: + return + + linter = CondprofAddonsLinter(topsrcdir=lintargs["root"], logger=logger) + + for filepath in filepaths: + linter.lint(filepath) + + +class CondprofAddonsLinter: + def __init__(self, topsrcdir, logger): + self.topsrcdir = topsrcdir + self.logger = logger + self.BROWSERTIME_FETCHES_FULLPATH = Path( + self.topsrcdir, BROWSERTIME_FETCHES_PATH + ) + self.CUSTOMIZATIONS_FULLPATH = Path(self.topsrcdir, CUSTOMIZATIONS_PATH) + self.tar_xpi_filenames = self.get_firefox_addons_tar_names() + + def lint(self, filepath): + data = self.read_json(filepath) + + if "addons" not in data: + return + + for addon_key in data["addons"]: + xpi_url = data["addons"][addon_key] + xpi_filename = xpi_url.split("/")[-1] + self.logger.info(f"Found addon {xpi_filename}") + if xpi_filename not in self.tar_xpi_filenames: + self.logger.lint_error( + self.get_missing_xpi_msg(xpi_filename), + lineno=0, + column=None, + path=str(filepath), + linter=LINTER_NAME, + rule=RULE_DESC, + ) + + def get_missing_xpi_msg(self, xpi_filename): + return f"{xpi_filename} is missing from the firefox-addons.tar archive" + + def read_json(self, filepath): + with filepath.open("r") as f: + return json.load(f) + + def read_yaml(self, filepath): + with filepath.open("r") as f: + return yaml.safe_load(f) + + def download_firefox_addons_tar(self, firefox_addons_tar_url, tar_tmp_path): + self.logger.info(f"Downloading {firefox_addons_tar_url} to {tar_tmp_path}") + res = requests.get( + firefox_addons_tar_url, stream=True, timeout=DOWNLOAD_TIMEOUT + ) + res.raise_for_status() + with tar_tmp_path.open("wb") as f: + for chunk in res.iter_content(chunk_size=1024): + if chunk is not None: + f.write(chunk) + f.flush() + + def get_firefox_addons_tar_names(self): + # Get firefox-addons fetch task config. + browsertime_fetches = self.read_yaml(self.BROWSERTIME_FETCHES_FULLPATH) + + if not ( + "firefox-addons" in browsertime_fetches + and "fetch" in browsertime_fetches["firefox-addons"] + ): + self.logger.lint_error( + ERR_FETCH_TASK_MISSING, + lineno=0, + column=None, + path=BROWSERTIME_FETCHES_PATH, + linter=LINTER_NAME, + rule=RULE_DESC, + ) + return [] + + fetch_config = browsertime_fetches["firefox-addons"]["fetch"] + + if not ( + "add-prefix" in fetch_config + and fetch_config["add-prefix"] == "firefox-addons/" + ): + self.logger.lint_error( + ERR_FETCH_TASK_ADDPREFIX, + lineno=0, + column=None, + path=BROWSERTIME_FETCHES_PATH, + linter=LINTER_NAME, + rule=RULE_DESC, + ) + return [] + + firefox_addons_tar_url = fetch_config["url"] + firefox_addons_tar_sha256 = fetch_config["sha256"] + + tar_xpi_files = list() + + # When running on the CI, try to retrieve the list of xpi files from the target MOZ_FETCHES_DIR + # subdirectory instead of downloading the archive from the fetch url. + if MOZ_AUTOMATION: + fetches_path = ( + Path(MOZ_FETCHES_DIR) if MOZ_FETCHES_DIR is not None else None + ) + if fetches_path is not None and fetches_path.exists(): + self.logger.info( + "Detected MOZ_FETCHES_DIR, look for pre-downloaded firefox-addons fetch results" + ) + # add-prefix presence and value has been enforced at the start of this method. + fetches_addons = Path(fetches_path, "firefox-addons/") + if fetches_addons.exists(): + self.logger.info( + f"Retrieve list of xpi files from firefox-addons fetch result at {str(fetches_addons)}" + ) + for xpi_path in fetches_addons.iterdir(): + if xpi_path.suffix == ".xpi": + tar_xpi_files.append(xpi_path.name) + return tar_xpi_files + else: + self.logger.warning( + "No 'firefox-addons/' subdir found in MOZ_FETCHES_DIR" + ) + + # Fallback to download the tar archive and retrieve the list of xpi file from it + # (e.g. when linting the local changes on the developers environment). + tar_tmp_path = Path(tempdir, "firefox-addons.tar") + tar_tmp_ready = False + + # If the firefox-addons.tar file is found in the tempdir, check if the + # file hash matches, if it does then don't download it again. + if tar_tmp_path.exists(): + tar_tmp_hash = hashlib.sha256() + with tar_tmp_path.open("rb") as f: + while chunk := f.read(1024): + tar_tmp_hash.update(chunk) + if tar_tmp_hash.hexdigest() == firefox_addons_tar_sha256: + self.logger.info( + f"Pre-downloaded file for {tar_tmp_path} found and sha256 matching" + ) + tar_tmp_ready = True + else: + self.logger.info( + f"{tar_tmp_path} sha256 does not match the fetch config" + ) + + # If the file is not found or the hash doesn't match, download it from the fetch task url. + if not tar_tmp_ready: + try: + self.download_firefox_addons_tar(firefox_addons_tar_url, tar_tmp_path) + except requests.exceptions.HTTPError as http_err: + self.logger.lint_error( + f"{ERR_FETCH_TASK_ARCHIVE}, {str(http_err)}", + lineno=0, + column=None, + path=BROWSERTIME_FETCHES_PATH, + linter=LINTER_NAME, + rule=RULE_DESC, + ) + return [] + + # Retrieve and return the list of xpi file names. + try: + with tarfile.open(tar_tmp_path, "r") as tf: + names = tf.getnames() + for name in names: + file_path = Path(name) + if file_path.suffix == ".xpi": + tar_xpi_files.append(file_path.name) + except tarfile.ReadError as read_err: + self.logger.lint_error( + f"{ERR_FETCH_TASK_ARCHIVE}, {str(read_err)}", + lineno=0, + column=None, + path=BROWSERTIME_FETCHES_PATH, + linter=LINTER_NAME, + rule=RULE_DESC, + ) + return [] + + return tar_xpi_files diff --git a/tools/lint/cpp/__init__.py b/tools/lint/cpp/__init__.py new file mode 100644 index 0000000000..c580d191c1 --- /dev/null +++ b/tools/lint/cpp/__init__.py @@ -0,0 +1,3 @@ +# 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/. diff --git a/tools/lint/cpp/mingw-capitalization.py b/tools/lint/cpp/mingw-capitalization.py new file mode 100644 index 0000000000..b5a4b07c6a --- /dev/null +++ b/tools/lint/cpp/mingw-capitalization.py @@ -0,0 +1,37 @@ +# 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 os +import re + +from mozlint.types import LineType + +here = os.path.abspath(os.path.dirname(__file__)) +HEADERS_FILE = os.path.join(here, "mingw-headers.txt") +# generated by cd mingw-w64/mingw-w64-headers && +# find . -name "*.h" | xargs -I bob -- basename bob | sort | uniq) + + +class MinGWCapitalization(LineType): + def __init__(self, *args, **kwargs): + super(MinGWCapitalization, self).__init__(*args, **kwargs) + with open(HEADERS_FILE, "r") as fh: + self.headers = fh.read().strip().splitlines() + self.regex = re.compile("^#include\s*<(" + "|".join(self.headers) + ")>") + + def condition(self, payload, line, config): + if not line.startswith("#include"): + return False + + if self.regex.search(line, re.I): + return not self.regex.search(line) + + +def lint(paths, config, **lintargs): + results = [] + + m = MinGWCapitalization() + for path in paths: + results.extend(m._lint(path, config, **lintargs)) + return results diff --git a/tools/lint/cpp/mingw-headers.txt b/tools/lint/cpp/mingw-headers.txt new file mode 100644 index 0000000000..5ee737c393 --- /dev/null +++ b/tools/lint/cpp/mingw-headers.txt @@ -0,0 +1,1452 @@ +accctrl.h +aclapi.h +aclui.h +acpiioct.h +activation.h +activaut.h +activdbg100.h +activdbg.h +activecf.h +activeds.h +activprof.h +activscp.h +adc.h +adhoc.h +admex.h +adoctint.h +adodef.h +adogpool_backcompat.h +adogpool.h +adoguids.h +adoid.h +adoint_backcompat.h +adoint.h +adojet.h +adomd.h +adptif.h +adsdb.h +adserr.h +adshlp.h +adsiid.h +adsnms.h +adsprop.h +adssts.h +adtgen.h +advpub.h +afilter.h +af_irda.h +afxres.h +agtctl.h +agterr.h +agtsvr.h +alg.h +alink.h +amaudio.h +amstream.h +amtvuids.h +amvideo.h +apdevpkey.h +apisetcconv.h +apiset.h +appmgmt.h +aqadmtyp.h +asptlb.h +assert.h +atacct.h +atalkwsh.h +atm.h +atsmedia.h +audevcod.h +audioapotypes.h +audioclient.h +audioendpoints.h +audioengineendpoint.h +audiopolicy.h +audiosessiontypes.h +austream.h +authif.h +authz.h +aux_ulib.h +avifmt.h +aviriff.h +avrfsdk.h +avrt.h +axextendenums.h +azroles.h +basetsd.h +basetyps.h +batclass.h +bcrypt.h +bdaiface_enums.h +bdaiface.h +bdamedia.h +bdasup.h +bdatypes.h +bemapiset.h +bh.h +bidispl.h +bits1_5.h +bits2_0.h +bitscfg.h +bits.h +bitsmsg.h +blberr.h +bluetoothapis.h +_bsd_types.h +bthdef.h +bthsdpdef.h +bugcodes.h +callobj.h +cardmod.h +casetup.h +cchannel.h +cdefs.h +cderr.h +cdoexerr.h +cdoex.h +cdoexm.h +cdoexstr.h +cdonts.h +cdosyserr.h +cdosys.h +cdosysstr.h +celib.h +certadm.h +certbase.h +certbcli.h +certcli.h +certenc.h +certenroll.h +certexit.h +certif.h +certmod.h +certpol.h +certreqd.h +certsrv.h +certview.h +cfg.h +cfgmgr32.h +cguid.h +chanmgr.h +cierror.h +classpnp.h +clfs.h +clfsmgmt.h +clfsmgmtw32.h +clfsw32.h +client.h +cluadmex.h +clusapi.h +cluscfgguids.h +cluscfgserver.h +cluscfgwizard.h +cmdtree.h +cmnquery.h +codecapi.h +colordlg.h +comadmin.h +combaseapi.h +comcat.h +comdef.h +comdefsp.h +comip.h +comlite.h +commapi.h +commctrl.h +commdlg.h +commoncontrols.h +complex.h +compobj.h +compressapi.h +compstui.h +comsvcs.h +comutil.h +confpriv.h +conio.h +conio_s.h +control.h +corecrt_startup.h +corerror.h +corewrappers.h +cor.h +corhdr.h +correg.h +cplext.h +cpl.h +credssp.h +crtdbg.h +crtdbg_s.h +crtdefs.h +cryptuiapi.h +cryptxml.h +cscapi.h +cscobj.h +csq.h +ctfutb.h +ctxtcall.h +ctype.h +custcntl.h +_cygwin.h +d2d1_1.h +d2d1_1helper.h +d2d1effectauthor.h +d2d1effecthelpers.h +d2d1effects.h +d2d1.h +d2d1helper.h +d2dbasetypes.h +d2derr.h +d3d10_1.h +d3d10_1shader.h +d3d10effect.h +d3d10.h +d3d10misc.h +d3d10sdklayers.h +d3d10shader.h +d3d11_1.h +d3d11_2.h +d3d11_3.h +d3d11_4.h +d3d11.h +d3d11sdklayers.h +d3d11shader.h +d3d8caps.h +d3d8.h +d3d8types.h +d3d9caps.h +d3d9.h +d3d9types.h +d3dcaps.h +d3dcommon.h +d3dcompiler.h +d3d.h +d3dhalex.h +d3dhal.h +d3dnthal.h +d3drmdef.h +d3drm.h +d3drmobj.h +d3dtypes.h +d3dx9anim.h +d3dx9core.h +d3dx9effect.h +d3dx9.h +d3dx9math.h +d3dx9mesh.h +d3dx9shader.h +d3dx9shape.h +d3dx9tex.h +d3dx9xof.h +d4drvif.h +d4iface.h +daogetrw.h +datapath.h +datetimeapi.h +davclnt.h +dbdaoerr.h +_dbdao.h +dbdaoid.h +dbdaoint.h +dbgautoattach.h +_dbg_common.h +dbgeng.h +dbghelp.h +_dbg_LOAD_IMAGE.h +dbgprop.h +dbt.h +dciddi.h +dciman.h +dcommon.h +dcompanimation.h +dcomp.h +dcomptypes.h +dde.h +ddeml.h +dderror.h +ddkernel.h +ddkmapi.h +ddrawgdi.h +ddraw.h +ddrawi.h +ddrawint.h +ddstream.h +debugapi.h +delayimp.h +devguid.h +devicetopology.h +devioctl.h +devpkey.h +devpropdef.h +dhcpcsdk.h +dhcpsapi.h +dhcpssdk.h +dhcpv6csdk.h +dhtmldid.h +dhtmled.h +dhtmliid.h +digitalv.h +dimm.h +dinput.h +direct.h +dirent.h +dir.h +diskguid.h +dispatch.h +dispdib.h +dispex.h +dlcapi.h +dlgs.h +dls1.h +dls2.h +dmdls.h +dmemmgr.h +dmerror.h +dmksctrl.h +dmodshow.h +dmo.h +dmoreg.h +dmort.h +dmplugin.h +dmusbuff.h +dmusicc.h +dmusicf.h +dmusici.h +dmusicks.h +dmusics.h +docobjectservice.h +docobj.h +documenttarget.h +domdid.h +dos.h +downloadmgr.h +dpaddr.h +dpapi.h +dpfilter.h +dplay8.h +dplay.h +dplobby8.h +dplobby.h +dpnathlp.h +driverspecs.h +drivinit.h +drmexternals.h +drmk.h +dsadmin.h +dsclient.h +dsconf.h +dsdriver.h +dsgetdc.h +dshow.h +dskquota.h +dsound.h +dsquery.h +dsrole.h +dssec.h +dtchelp.h +dvbsiparser.h +dvdevcod.h +dvdmedia.h +dvec.h +dvobj.h +dvp.h +dwmapi.h +dwrite_1.h +dwrite_2.h +dwrite_3.h +dwrite.h +dxapi.h +dxdiag.h +dxerr8.h +dxerr9.h +dxfile.h +dxgi1_2.h +dxgi1_3.h +dxgi1_4.h +dxgi1_5.h +dxgicommon.h +dxgiformat.h +dxgi.h +dxgitype.h +dxtmpl.h +dxva2api.h +dxva.h +dxvahd.h +eapauthenticatoractiondefine.h +eapauthenticatortypes.h +eaphosterror.h +eaphostpeerconfigapis.h +eaphostpeertypes.h +eapmethodauthenticatorapis.h +eapmethodpeerapis.h +eapmethodtypes.h +eappapis.h +eaptypes.h +edevdefs.h +eh.h +ehstorapi.h +elscore.h +emostore.h +emptyvc.h +endpointvolume.h +errhandlingapi.h +errno.h +error.h +errorrep.h +errors.h +esent.h +evcode.h +evcoll.h +eventsys.h +evntcons.h +evntprov.h +evntrace.h +evr9.h +evr.h +exchform.h +excpt.h +exdisp.h +exdispid.h +fci.h +fcntl.h +fdi.h +_fd_types.h +fenv.h +fibersapi.h +fileapi.h +fileextd.h +file.h +filehc.h +filter.h +filterr.h +float.h +fltdefs.h +fltsafe.h +fltuser.h +fltuserstructures.h +fltwinerror.h +fpieee.h +fsrmenums.h +fsrmerr.h +fsrm.h +fsrmpipeline.h +fsrmquota.h +fsrmreports.h +fsrmscreen.h +ftsiface.h +ftw.h +functiondiscoveryapi.h +functiondiscoverycategories.h +functiondiscoveryconstraints.h +functiondiscoverykeys_devpkey.h +functiondiscoverykeys.h +functiondiscoverynotification.h +fusion.h +fvec.h +fwpmtypes.h +fwpmu.h +fwptypes.h +gb18030.h +gdiplusbase.h +gdiplusbrush.h +gdipluscolor.h +gdipluscolormatrix.h +gdipluseffects.h +gdiplusenums.h +gdiplusflat.h +gdiplusgpstubs.h +gdiplusgraphics.h +gdiplus.h +gdiplusheaders.h +gdiplusimageattributes.h +gdiplusimagecodec.h +gdiplusimaging.h +gdiplusimpl.h +gdiplusinit.h +gdipluslinecaps.h +gdiplusmatrix.h +gdiplusmem.h +gdiplusmetafile.h +gdiplusmetaheader.h +gdipluspath.h +gdipluspen.h +gdipluspixelformats.h +gdiplusstringformat.h +gdiplustypes.h +getopt.h +glaux.h +glcorearb.h +glext.h +gl.h +glu.h +glxext.h +gpedit.h +gpio.h +gpmgmt.h +guiddef.h +h323priv.h +handleapi.h +heapapi.h +hidclass.h +hidpi.h +hidsdi.h +hidusage.h +highlevelmonitorconfigurationapi.h +hlguids.h +hliface.h +hlink.h +hostinfo.h +hstring.h +htiface.h +htiframe.h +htmlguid.h +htmlhelp.h +httpext.h +httpfilt.h +http.h +httprequestid.h +hubbusif.h +ia64reg.h +iaccess.h +iadmext.h +iadmw.h +iads.h +icftypes.h +icm.h +icmpapi.h +icodecapi.h +icrsint.h +i_cryptasn1tls.h +ide.h +identitycommon.h +identitystore.h +idf.h +idispids.h +iedial.h +ieeefp.h +ieverp.h +ifdef.h +iiisext.h +iiis.h +iimgctx.h +iiscnfg.h +iisrsta.h +iketypes.h +imagehlp.h +ime.h +imessage.h +imm.h +in6addr.h +inaddr.h +indexsrv.h +inetreg.h +inetsdk.h +infstr.h +initguid.h +initoid.h +inputscope.h +inspectable.h +interlockedapi.h +internal.h +intrin.h +intrin-impl.h +intsafe.h +intshcut.h +inttypes.h +invkprxy.h +ioaccess.h +ioapiset.h +ioevent.h +io.h +ipexport.h +iphlpapi.h +ipifcons.h +ipinfoid.h +ipmib.h +_ip_mreq1.h +ipmsp.h +iprtrmib.h +ipsectypes.h +_ip_types.h +iptypes.h +ipxconst.h +ipxrip.h +ipxrtdef.h +ipxsap.h +ipxtfflt.h +iscsidsc.h +isguids.h +issper16.h +issperr.h +isysmon.h +ivec.h +iwamreg.h +jobapi.h +kbdmou.h +kcom.h +knownfolders.h +ksdebug.h +ksguid.h +ks.h +ksmedia.h +ksproxy.h +ksuuids.h +ktmtypes.h +ktmw32.h +kxia64.h +l2cmn.h +libgen.h +libloaderapi.h +limits.h +lmaccess.h +lmalert.h +lmapibuf.h +lmat.h +lmaudit.h +lmconfig.h +lmcons.h +lmdfs.h +lmerr.h +lmerrlog.h +lm.h +lmjoin.h +lmmsg.h +lmon.h +lmremutl.h +lmrepl.h +lmserver.h +lmshare.h +lmsname.h +lmstats.h +lmsvc.h +lmuseflg.h +lmuse.h +lmwksta.h +loadperf.h +locale.h +locationapi.h +locking.h +lpmapi.h +lzexpand.h +madcapcl.h +magnification.h +mailmsgprops.h +malloc.h +manipulations.h +mapicode.h +mapidbg.h +mapidefs.h +mapiform.h +mapiguid.h +mapi.h +mapihook.h +mapinls.h +mapioid.h +mapispi.h +mapitags.h +mapiutil.h +mapival.h +mapiwin.h +mapiwz.h +mapix.h +math.h +mbctype.h +mbstring.h +mbstring_s.h +mcd.h +mce.h +mciavi.h +mcx.h +mdcommsg.h +mddefw.h +mdhcp.h +mdmsg.h +mediaerr.h +mediaobj.h +medparam.h +mem.h +memoryapi.h +memory.h +mergemod.h +mfapi.h +mferror.h +mfidl.h +mfmp2dlna.h +mfobjects.h +mfplay.h +mfreadwrite.h +mftransform.h +mgm.h +mgmtapi.h +midles.h +mimedisp.h +mimeinfo.h +_mingw_ddk.h +_mingw_directx.h +_mingw_dxhelper.h +_mingw_mac.h +_mingw_off_t.h +_mingw_print_pop.h +_mingw_print_push.h +_mingw_secapi.h +_mingw_stat64.h +_mingw_stdarg.h +_mingw_unicode.h +miniport.h +minitape.h +minmax.h +minwinbase.h +minwindef.h +mlang.h +mmc.h +mmcobj.h +mmdeviceapi.h +mmreg.h +mmstream.h +mmsystem.h +mobsync.h +module.h +moniker.h +mountdev.h +mountmgr.h +mpeg2bits.h +mpeg2data.h +mpeg2psiparser.h +mpeg2structs.h +mprapi.h +mprerror.h +mq.h +mqmail.h +mqoai.h +msacmdlg.h +msacm.h +msado15.h +msasn1.h +msber.h +mscat.h +mschapp.h +msclus.h +mscoree.h +msctf.h +msctfmonitorapi.h +msdadc.h +msdaguid.h +msdaipper.h +msdaipp.h +msdaora.h +msdaosp.h +msdasc.h +msdasql.h +msdatsrc.h +msdrmdefs.h +msdrm.h +msdshape.h +msfs.h +mshtmcid.h +mshtmdid.h +mshtmhst.h +mshtmlc.h +mshtml.h +msidefs.h +msi.h +msimcntl.h +msimcsdk.h +msinkaut.h +msiquery.h +msoav.h +msopc.h +mspab.h +mspaddr.h +mspbase.h +mspcall.h +mspcoll.h +mspenum.h +msp.h +msplog.h +msports.h +mspst.h +mspstrm.h +mspterm.h +mspthrd.h +msptrmac.h +msptrmar.h +msptrmvc.h +msputils.h +msrdc.h +msremote.h +mssip.h +msstkppg.h +mstask.h +mstcpip.h +msterr.h +mswsock.h +msxml2did.h +msxml2.h +msxmldid.h +msxml.h +mtsadmin.h +mtsevents.h +mtsgrp.h +mtxadmin.h +mtxattr.h +mtxdm.h +mtx.h +muiload.h +multimon.h +multinfo.h +mxdc.h +namedpipeapi.h +namespaceapi.h +napcertrelyingparty.h +napcommon.h +napenforcementclient.h +napmanagement.h +napmicrosoftvendorids.h +napprotocol.h +napservermanagement.h +napsystemhealthagent.h +napsystemhealthvalidator.h +naptypes.h +naputil.h +nb30.h +ncrypt.h +ndattrib.h +ndfapi.h +ndhelper.h +ndisguid.h +ndis.h +ndistapi.h +ndiswan.h +ndkinfo.h +ndr64types.h +ndrtypes.h +netcon.h +neterr.h +netevent.h +netfw.h +netioapi.h +netlistmgr.h +netmon.h +netpnp.h +netprov.h +nettypes.h +newapis.h +newdev.h +new.h +nldef.h +nmsupp.h +npapi.h +nsemail.h +nspapi.h +ntagp.h +ntdd1394.h +ntdd8042.h +ntddbeep.h +ntddcdrm.h +ntddcdvd.h +ntddchgr.h +ntdddisk.h +ntddft.h +ntddkbd.h +ntddk.h +ntddmmc.h +ntddmodm.h +ntddmou.h +ntddndis.h +ntddpar.h +ntddpcm.h +ntddpsch.h +ntddscsi.h +ntddser.h +ntddsnd.h +ntddstor.h +ntddtape.h +ntddtdi.h +ntddvdeo.h +ntddvol.h +ntdef.h +ntdsapi.h +ntdsbcli.h +ntdsbmsg.h +ntgdi.h +ntifs.h +ntimage.h +ntiologc.h +ntldap.h +ntmsapi.h +ntmsmli.h +ntnls.h +ntpoapi.h +ntquery.h +ntsdexts.h +ntsecapi.h +ntsecpkg.h +ntstatus.h +ntstrsafe.h +ntverp.h +oaidl.h +objbase.h +objectarray.h +objerror.h +objidlbase.h +objidl.h +objsafe.h +objsel.h +ocidl.h +ocmm.h +odbcinst.h +odbcss.h +ole2.h +ole2ver.h +oleacc.h +oleauto.h +olectl.h +olectlid.h +oledbdep.h +oledberr.h +oledbguid.h +oledb.h +oledlg.h +ole.h +oleidl.h +oletx2xa.h +opmapi.h +oprghdlr.h +optary.h +p2p.h +packoff.h +packon.h +parallel.h +param.h +parser.h +patchapi.h +patchwiz.h +pathcch.h +pbt.h +pchannel.h +pciprop.h +pcrt32.h +pdh.h +pdhmsg.h +penwin.h +perflib.h +perhist.h +persist.h +pfhook.h +pgobootrun.h +physicalmonitorenumerationapi.h +pla.h +pnrpdef.h +pnrpns.h +poclass.h +polarity.h +_pop_BOOL.h +poppack.h +portabledeviceconnectapi.h +portabledevicetypes.h +portcls.h +powrprof.h +prnasnot.h +prntfont.h +processenv.h +process.h +processthreadsapi.h +processtopologyapi.h +profileapi.h +profile.h +profinfo.h +propidl.h +propkeydef.h +propkey.h +propsys.h +propvarutil.h +prsht.h +psapi.h +pshpack1.h +pshpack2.h +pshpack4.h +pshpack8.h +pshpck16.h +pstore.h +pthread_signal.h +pthread_time.h +pthread_unistd.h +punknown.h +_push_BOOL.h +qedit.h +qmgr.h +qnetwork.h +qos2.h +qos.h +qosname.h +qospol.h +qossp.h +rasdlg.h +raseapif.h +raserror.h +ras.h +rassapi.h +rasshost.h +ratings.h +rdpencomapi.h +realtimeapiset.h +reason.h +recguids.h +reconcil.h +regbag.h +regstr.h +rend.h +resapi.h +restartmanager.h +richedit.h +richole.h +rkeysvcc.h +rnderr.h +roapi.h +routprot.h +rpcasync.h +rpcdce.h +rpcdcep.h +rpc.h +rpcndr.h +rpcnsi.h +rpcnsip.h +rpcnterr.h +rpcproxy.h +rpcsal.h +rpcssl.h +rrascfg.h +rtcapi.h +rtccore.h +rtcerr.h +rtinfo.h +rtm.h +rtmv2.h +rtutils.h +sal.h +sapi51.h +sapi53.h +sapi54.h +sapi.h +sas.h +sbe.h +scarddat.h +scarderr.h +scardmgr.h +scardsrv.h +scardssp.h +scesvc.h +schannel.h +schedule.h +schemadef.h +schnlsp.h +scode.h +scrnsave.h +scrptids.h +scsi.h +scsiscan.h +scsiwmi.h +sddl.h +sdkddkver.h +sdoias.h +sdpblb.h +sdperr.h +search.h +search_s.h +secext.h +securityappcontainer.h +securitybaseapi.h +security.h +sehmap.h +sensapi.h +sensevts.h +sens.h +sensorsapi.h +sensors.h +servprov.h +setjmpex.h +setjmp.h +setupapi.h +sfc.h +shappmgr.h +share.h +shdeprecated.h +shdispid.h +shellapi.h +sherrors.h +shfolder.h +shldisp.h +shlguid.h +shlobj.h +shlwapi.h +shobjidl.h +shtypes.h +signal.h +simpdata.h +simpdc.h +sipbase.h +sisbkup.h +slerror.h +slpublic.h +smbus.h +smpab.h +smpms.h +smpxp.h +smtpguid.h +smx.h +snmp.h +_socket_types.h +softpub.h +specstrings.h +sperror.h +sphelper.h +sporder.h +sql_1.h +sqlext.h +sql.h +sqloledb.h +sqltypes.h +sqlucode.h +srb.h +srrestoreptapi.h +srv.h +sspguid.h +sspi.h +sspserr.h +sspsidl.h +stat.h +stdarg.h +stddef.h +stdexcpt.h +stdint.h +stdio.h +stdio_s.h +stdlib.h +stdlib_s.h +stdunk.h +stierr.h +sti.h +stireg.h +stllock.h +stm.h +storage.h +storduid.h +storport.h +storprop.h +stralign.h +stralign_s.h +stringapiset.h +string.h +string_s.h +strings.h +strmif.h +strmini.h +strsafe.h +structuredquerycondition.h +subauth.h +subsmgr.h +svcguid.h +svrapi.h +swenum.h +synchapi.h +sysinfoapi.h +syslimits.h +systemtopologyapi.h +t2embapi.h +tabflicks.h +tapi3cc.h +tapi3ds.h +tapi3err.h +tapi3.h +tapi3if.h +tapi.h +taskschd.h +tbs.h +tcerror.h +tcguid.h +tchar.h +tchar_s.h +tcpestats.h +tcpmib.h +tdh.h +tdi.h +tdiinfo.h +tdikrnl.h +tdistat.h +termmgr.h +textserv.h +textstor.h +threadpoolapiset.h +threadpoollegacyapiset.h +timeb.h +timeb_s.h +time.h +timeprov.h +_timeval.h +timezoneapi.h +tlbref.h +tlhelp32.h +tlogstg.h +tmschema.h +tnef.h +tom.h +tpcshrd.h +traffic.h +transact.h +triedcid.h +triediid.h +triedit.h +tsattrs.h +tspi.h +tssbx.h +tsuserex.h +tuner.h +tvout.h +txcoord.h +txctx.h +txdtc.h +txfw32.h +typeinfo.h +types.h +uastrfnc.h +uchar.h +udpmib.h +uianimation.h +uiautomationclient.h +uiautomationcoreapi.h +uiautomationcore.h +uiautomation.h +uiviewsettingsinterop.h +umx.h +unistd.h +unknown.h +unknwnbase.h +unknwn.h +upssvc.h +urlhist.h +urlmon.h +usb100.h +usb200.h +usbbusif.h +usbcamdi.h +usbdi.h +usbdlib.h +usbdrivr.h +usb.h +usbioctl.h +usbiodef.h +usbkern.h +usbprint.h +usbprotocoldefs.h +usbrpmif.h +usbscan.h +usbspec.h +usbstorioctl.h +usbuser.h +userenv.h +usp10.h +utilapiset.h +utime.h +uuids.h +uxtheme.h +vadefs.h +varargs.h +_varenum.h +vcr.h +vdmdbg.h +vds.h +vdslun.h +versionhelpers.h +vfw.h +vfwmsgs.h +videoagp.h +video.h +virtdisk.h +vmr9.h +vsadmin.h +vsbackup.h +vsmgmt.h +vsprov.h +vss.h +vsstyle.h +vssym32.h +vswriter.h +w32api.h +wabapi.h +wabcode.h +wabdefs.h +wab.h +wabiab.h +wabmem.h +wabnot.h +wabtags.h +wabutil.h +wbemads.h +wbemcli.h +wbemdisp.h +wbemidl.h +wbemprov.h +wbemtran.h +wchar.h +wchar_s.h +wcmconfig.h +wcsplugin.h +wct.h +wctype.h +wdmguid.h +wdm.h +wdsbp.h +wdsclientapi.h +wdspxe.h +wdstci.h +wdstpdi.h +wdstptmgmt.h +werapi.h +wfext.h +wglext.h +wiadef.h +wiadevd.h +wia.h +wiavideo.h +winable.h +winapifamily.h +winbase.h +winber.h +wincodec.h +wincon.h +wincred.h +wincrypt.h +winddi.h +winddiui.h +windef.h +windns.h +windot11.h +windows.foundation.h +windows.h +windows.security.cryptography.h +windows.storage.h +windows.storage.streams.h +windows.system.threading.h +windowsx.h +winefs.h +winerror.h +winevt.h +wingdi.h +winhttp.h +wininet.h +winineti.h +winioctl.h +winldap.h +winnetwk.h +winnls32.h +winnls.h +winnt.h +winperf.h +winreg.h +winresrc.h +winsafer.h +winsatcominterfacei.h +winscard.h +winsdkver.h +winsmcrd.h +winsnmp.h +winsock2.h +winsock.h +winsplp.h +winspool.h +winstring.h +winsvc.h +winsxs.h +winsync.h +winternl.h +wintrust.h +winusb.h +winusbio.h +winuser.h +winver.h +winwlx.h +wlanapi.h +wlanihvtypes.h +wlantypes.h +wmcodecdsp.h +wmcontainer.h +wmdrmsdk.h +wmiatlprov.h +wmidata.h +wmilib.h +wmistr.h +wmiutils.h +wmsbuffer.h +wmsdkidl.h +wnnc.h +wow64apiset.h +wownt16.h +wownt32.h +wpapi.h +wpapimsg.h +wpcapi.h +wpcevent.h +wpcrsmsg.h +wpftpmsg.h +wppstmsg.h +wpspihlp.h +wptypes.h +wpwizmsg.h +wrl.h +_ws1_undef.h +ws2atm.h +ws2bth.h +ws2def.h +ws2dnet.h +ws2ipdef.h +ws2san.h +ws2spi.h +ws2tcpip.h +_wsadata.h +_wsa_errnos.h +wsdapi.h +wsdattachment.h +wsdbase.h +wsdclient.h +wsddisco.h +wsdhost.h +wsdtypes.h +wsdutil.h +wsdxmldom.h +wsdxml.h +wshisotp.h +wsipv6ok.h +wsipx.h +wsmandisp.h +wsman.h +wsnetbs.h +wsnwlink.h +wspiapi.h +wsrm.h +wsvns.h +wtsapi32.h +wtypesbase.h +wtypes.h +xa.h +xcmcext.h +xcmc.h +xcmcmsx2.h +xcmcmsxt.h +xenroll.h +xfilter.h +xinput.h +xlocinfo.h +xmath.h +_xmitfile.h +xmldomdid.h +xmldsodid.h +xmllite.h +xmltrnsf.h +xolehlp.h +xpsdigitalsignature.h +xpsobjectmodel_1.h +xpsobjectmodel.h +xpsprint.h +xpsrassvc.h +ymath.h +yvals.h +zmouse.h diff --git a/tools/lint/eslint.yml b/tools/lint/eslint.yml new file mode 100644 index 0000000000..b54d328e05 --- /dev/null +++ b/tools/lint/eslint.yml @@ -0,0 +1,31 @@ +--- +eslint: + description: JavaScript linter + # ESLint infra handles its own path filtering, so just include cwd + include: ['.'] + exclude: [] + # When adding to this list, consider updating hooks_js_format.py as well. + extensions: ['mjs', 'js', 'jsm', 'json', 'jsx', 'html', 'sjs', 'xhtml'] + support-files: + - '**/.eslintrc.js' + - '.eslintrc-test-paths.js' + - '.eslintignore' + - 'tools/lint/eslint/**' + # Files that can influence global variables + - 'browser/base/content/nsContextMenu.js' + - 'browser/base/content/utilityOverlay.js' + - 'browser/components/customizableui/content/panelUI.js' + - 'browser/components/downloads/content/downloads.js' + - 'browser/components/downloads/content/indicator.js' + - 'testing/mochitest/tests/SimpleTest/EventUtils.js' + - 'testing/mochitest/tests/SimpleTest/MockObjects.js' + - 'testing/mochitest/tests/SimpleTest/SimpleTest.js' + - 'testing/mochitest/tests/SimpleTest/WindowSnapshot.js' + - 'toolkit/components/printing/content/printUtils.js' + - 'toolkit/components/viewsource/content/viewSourceUtils.js' + - 'toolkit/content/contentAreaUtils.js' + - 'toolkit/content/editMenuOverlay.js' + - 'toolkit/content/globalOverlay.js' + type: external + payload: eslint:lint + setup: eslint:setup diff --git a/tools/lint/eslint/.eslintrc.js b/tools/lint/eslint/.eslintrc.js new file mode 100644 index 0000000000..762ddba02c --- /dev/null +++ b/tools/lint/eslint/.eslintrc.js @@ -0,0 +1,31 @@ +/* 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/. */ + +"use strict"; + +module.exports = { + plugins: ["eslint-plugin"], + extends: ["plugin:eslint-plugin/recommended"], + // eslint-plugin-mozilla runs under node, so we need a more restrictive + // environment / parser setup here than the rest of mozilla-central. + env: { + browser: false, + node: true, + }, + parser: "espree", + parserOptions: { + // This should match with the minimum node version that the ESLint CI + // process uses (check the linux64-node toolchain). + ecmaVersion: 12, + }, + + rules: { + camelcase: ["error", { properties: "never" }], + "handle-callback-err": ["error", "er"], + "no-shadow": "error", + "no-undef-init": "error", + "one-var": ["error", "never"], + strict: ["error", "global"], + }, +}; diff --git a/tools/lint/eslint/__init__.py b/tools/lint/eslint/__init__.py new file mode 100644 index 0000000000..fdd7504c34 --- /dev/null +++ b/tools/lint/eslint/__init__.py @@ -0,0 +1,293 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 signal +import subprocess +import sys + +sys.path.append(os.path.join(os.path.dirname(__file__), "eslint")) +from mozbuild.nodeutil import find_node_executable +from mozlint import result + +from eslint import setup_helper + +ESLINT_ERROR_MESSAGE = """ +An error occurred running eslint. Please check the following error messages: + +{} +""".strip() + +ESLINT_NOT_FOUND_MESSAGE = """ +Could not find eslint! We looked at the --binary option, at the ESLINT +environment variable, and then at your local node_modules path. Please Install +eslint and needed plugins with: + +mach eslint --setup + +and try again. +""".strip() + +PRETTIER_ERROR_MESSAGE = """ +An error occurred running prettier. Please check the following error messages: + +{} +""".strip() + +PRETTIER_FORMATTING_MESSAGE = ( + "This file needs formatting with Prettier (use 'mach lint --fix <path>')." +) + + +def setup(root, **lintargs): + setup_helper.set_project_root(root) + + if not setup_helper.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 eslint.""" + log = lintargs["log"] + setup_helper.set_project_root(lintargs["root"]) + module_path = setup_helper.get_project_root() + + # Valid binaries are: + # - Any provided by the binary argument. + # - Any pointed at by the ESLINT environmental variable. + # - Those provided by |mach lint --setup|. + + if not binary: + binary, _ = find_node_executable() + + if not binary: + print(ESLINT_NOT_FOUND_MESSAGE) + return 1 + + extra_args = lintargs.get("extra_args") or [] + exclude_args = [] + for path in config.get("exclude", []): + exclude_args.extend( + ["--ignore-pattern", os.path.relpath(path, lintargs["root"])] + ) + + for rule in rules: + extra_args.extend(["--rule", rule]) + + # First run ESLint + cmd_args = ( + [ + binary, + os.path.join(module_path, "node_modules", "eslint", "bin", "eslint.js"), + # This keeps ext as a single argument. + "--ext", + "[{}]".format(",".join(config["extensions"])), + "--format", + "json", + "--no-error-on-unmatched-pattern", + ] + + rules + + extra_args + + exclude_args + + paths + ) + + if fix: + # eslint requires that --fix be set before the --ext argument. + cmd_args.insert(2, "--fix") + + log.debug("ESLint command: {}".format(" ".join(cmd_args))) + + result = run(cmd_args, config) + if result == 1: + return result + + # Then run Prettier + cmd_args = ( + [ + binary, + os.path.join(module_path, "node_modules", "prettier", "bin-prettier.js"), + "--list-different", + "--no-error-on-unmatched-pattern", + ] + + extra_args + # 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 = 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): + shell = False + if ( + os.environ.get("MSYSTEM") in ("MINGW32", "MINGW64") + or "MOZILLABUILD" in os.environ + ): + # The eslint 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 + ) + signal.signal(signal.SIGINT, orig) + + try: + output, errors = proc.communicate() + except KeyboardInterrupt: + proc.kill() + return {"results": [], "fixed": 0} + + if errors: + errors = errors.decode(encoding, "replace") + print(ESLINT_ERROR_MESSAGE.format(errors)) + + if proc.returncode >= 2: + return 1 + + if not output: + return {"results": [], "fixed": 0} # no output means success + output = output.decode(encoding, "replace") + try: + jsonresult = json.loads(output) + except ValueError: + print(ESLINT_ERROR_MESSAGE.format(output)) + return 1 + + results = [] + fixed = 0 + for obj in jsonresult: + errors = obj["messages"] + # This will return a count of files fixed, rather than issues fixed, as + # that is the only count we have. + if "output" in obj: + fixed = fixed + 1 + + for err in errors: + err.update( + { + "hint": err.get("fix"), + "level": "error" if err["severity"] == 2 else "warning", + "lineno": err.get("line") or 0, + "path": obj["filePath"], + "rule": err.get("ruleId"), + } + ) + results.append(result.from_config(config, **err)) + + return {"results": results, "fixed": fixed} + + +def run_prettier(cmd_args, config, fix): + shell = False + if is_windows(): + # The eslint 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 + ) + signal.signal(signal.SIGINT, orig) + + try: + output, errors = proc.communicate() + except KeyboardInterrupt: + proc.kill() + return {"results": [], "fixed": 0} + + results = [] + + if errors: + errors = errors.decode(encoding, "replace").strip().split("\n") + errors = [ + error + for error in errors + # Unknown options are not an issue for Prettier, this avoids + # errors during tests. + if not ("Ignored unknown option" in error) + ] + if len(errors): + results.append( + result.from_config( + config, + **{ + "name": "eslint", + "path": os.path.abspath("."), + "message": PRETTIER_ERROR_MESSAGE.format("\n".join(errors)), + "level": "error", + "rule": "prettier", + "lineno": 0, + "column": 0, + } + ) + ) + + if not output: + # If we have errors, but no output, we assume something really bad happened. + if errors and len(errors): + return {"results": results, "fixed": 0} + + return {"results": [], "fixed": 0} # no output means success + + output = output.decode(encoding, "replace").splitlines() + + fixed = 0 + + if fix: + # When Prettier is running in fix mode, it outputs the list of files + # that have been fixed, so sum them up here. + # If it can't fix files, it will throw an error, which will be handled + # above. + fixed = len(output) + else: + # When in "check" mode, Prettier will output the list of files that + # need changing, so we'll wrap them in our result structure here. + for file in output: + if not file: + continue + + file = os.path.abspath(file) + results.append( + result.from_config( + config, + **{ + "name": "eslint", + "path": file, + "message": PRETTIER_FORMATTING_MESSAGE, + "level": "error", + "rule": "prettier", + "lineno": 0, + "column": 0, + } + ) + ) + + return {"results": results, "fixed": fixed} + + +def is_windows(): + return ( + os.environ.get("MSYSTEM") in ("MINGW32", "MINGW64") + or "MOZILLABUILD" in os.environ + ) diff --git a/tools/lint/eslint/eslint-plugin-mozilla/.npmignore b/tools/lint/eslint/eslint-plugin-mozilla/.npmignore new file mode 100644 index 0000000000..3713448c7a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/.npmignore @@ -0,0 +1,8 @@ +.eslintrc.js +.npmignore +node_modules +reporters +scripts +tests +package-lock.json +update.sh diff --git a/tools/lint/eslint/eslint-plugin-mozilla/LICENSE b/tools/lint/eslint/eslint-plugin-mozilla/LICENSE new file mode 100644 index 0000000000..e87a115e46 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/LICENSE @@ -0,0 +1,363 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + 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/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/tools/lint/eslint/eslint-plugin-mozilla/README.md b/tools/lint/eslint/eslint-plugin-mozilla/README.md new file mode 100644 index 0000000000..650507754e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/README.md @@ -0,0 +1,56 @@ +# eslint-plugin-mozilla + +A collection of rules that help enforce JavaScript coding standard in the Mozilla project. + +These are primarily developed and used within the Firefox build system ([mozilla-central](https://hg.mozilla.org/mozilla-central/)), but are made available for other +related projects to use as well. + +## Installation + +### Within mozilla-central: + +``` +$ ./mach eslint --setup +``` + +### Outside mozilla-central: + +Install ESLint [ESLint](http://eslint.org): + +``` +$ npm i eslint --save-dev +``` + +Next, install `eslint-plugin-mozilla`: + +``` +$ npm install eslint-plugin-mozilla --save-dev +``` + +## Documentation + +For details about the rules, please see the [firefox documentation page](http://firefox-source-docs.mozilla.org/tools/lint/linters/eslint-plugin-mozilla.html). + +## Source Code + +The sources can be found at: + +* Code: https://searchfox.org/mozilla-central/source/tools/lint/eslint/eslint-plugin-mozilla +* Documentation: https://searchfox.org/mozilla-central/source/docs/code-quality/lint/linters + +## Bugs + +Please file bugs in Bugzilla in the Lint component of the Testing product. + +* [Existing bugs](https://bugzilla.mozilla.org/buglist.cgi?resolution=---&query_format=advanced&component=Lint&product=Testing) +* [New bugs](https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=Lint) + +## Tests + +The tests can only be run from within mozilla-central. To run the tests: + +``` +./mach eslint --setup +cd tools/lint/eslint/eslint-plugin-mozilla +npm run test +``` diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/.eslintrc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/.eslintrc.js new file mode 100644 index 0000000000..76df4134f5 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/.eslintrc.js @@ -0,0 +1,8 @@ +"use strict"; + +module.exports = { + rules: { + // Require object keys to be sorted. + "sort-keys": "error", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js new file mode 100644 index 0000000000..08747a3f88 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js @@ -0,0 +1,95 @@ +// Parent config file for all browser-chrome files. +"use strict"; + +module.exports = { + env: { + browser: true, + "mozilla/browser-window": true, + "mozilla/simpletest": true, + // "node": true + }, + + // All globals made available in the test environment. + globals: { + // `$` is defined in SimpleTest.js + $: false, + Assert: false, + BrowserTestUtils: false, + ContentTask: false, + ContentTaskUtils: false, + EventUtils: false, + IOUtils: false, + PathUtils: false, + PromiseDebugging: false, + SpecialPowers: false, + TestUtils: false, + addLoadEvent: false, + add_setup: false, + add_task: false, + content: false, + executeSoon: false, + expectUncaughtException: false, + export_assertions: false, + extractJarToTmp: false, + finish: false, + gTestPath: false, + getChromeDir: false, + getJar: false, + getResolvedURI: false, + getRootDirectory: false, + getTestFilePath: false, + ignoreAllUncaughtExceptions: false, + info: false, + is: false, + isnot: false, + ok: false, + record: false, + registerCleanupFunction: false, + requestLongerTimeout: false, + setExpectedFailuresForSelfTest: false, + stringContains: false, + stringMatches: false, + todo: false, + todo_is: false, + todo_isnot: false, + waitForClipboard: false, + waitForExplicitFinish: false, + waitForFocus: false, + }, + + plugins: ["mozilla", "@microsoft/sdl"], + + rules: { + // No using of insecure url, so no http urls + "@microsoft/sdl/no-insecure-url": [ + "error", + { + exceptions: [ + "^http:\\/\\/mochi\\.test?.*", + "^http:\\/\\/localhost?.*", + "^http:\\/\\/127\\.0\\.0\\.1?.*", + // Exempt xmlns urls + "^http:\\/\\/www\\.w3\\.org?.*", + "^http:\\/\\/www\\.mozilla\\.org\\/keymaster\\/gatekeeper?.*", + // Exempt urls that start with ftp or ws. + "^ws:?.*", + "^ftp:?.*", + ], + varExceptions: ["insecure?.*"], + }, + ], + "mozilla/import-content-task-globals": "error", + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + "mozilla/no-addtask-setup": "error", + "mozilla/no-arbitrary-setTimeout": "error", + "mozilla/no-redeclare-with-import-autofix": [ + "error", + { errorForNonImports: false }, + ], + // Turn off no-unsanitized for tests, as we do want to be able to use + // these for testing. + "no-unsanitized/method": "off", + "no-unsanitized/property": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/chrome-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/chrome-test.js new file mode 100644 index 0000000000..3b5bbc06e2 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/chrome-test.js @@ -0,0 +1,65 @@ +// Parent config file for all mochitest files. +"use strict"; + +module.exports = { + env: { + browser: true, + "mozilla/browser-window": true, + }, + + // All globals made available in the test environment. + globals: { + // SpecialPowers is injected into the window object via SimpleTest.js + SpecialPowers: false, + extractJarToTmp: false, + getChromeDir: false, + getJar: false, + getResolvedURI: false, + getRootDirectory: false, + }, + + overrides: [ + { + env: { + // Ideally we wouldn't be using the simpletest env here, but our uses of + // js files mean we pick up everything from the global scope, which could + // be any one of a number of html files. So we just allow the basics... + "mozilla/simpletest": true, + }, + files: ["*.js"], + }, + ], + + plugins: ["mozilla", "@microsoft/sdl"], + + rules: { + // No using of insecure url, so no http urls + "@microsoft/sdl/no-insecure-url": [ + "error", + { + exceptions: [ + "^http:\\/\\/mochi\\.test?.*", + "^http:\\/\\/localhost?.*", + "^http:\\/\\/127\\.0\\.0\\.1?.*", + // Exempt xmlns urls + "^http:\\/\\/www\\.w3\\.org?.*", + "^http:\\/\\/www\\.mozilla\\.org\\/keymaster\\/gatekeeper?.*", + // Exempt urls that start with ftp or ws. + "^ws:?.*", + "^ftp:?.*", + ], + varExceptions: ["insecure?.*"], + }, + ], + "mozilla/import-content-task-globals": "error", + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + // We mis-predict globals for HTML test files in directories shared + // with browser tests. + "mozilla/no-redeclare-with-import-autofix": "off", + // Turn off no-unsanitized for tests, as we do want to be able to use + // these for testing. + "no-unsanitized/method": "off", + "no-unsanitized/property": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/mochitest-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/mochitest-test.js new file mode 100644 index 0000000000..ceca2beec4 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/mochitest-test.js @@ -0,0 +1,66 @@ +// Parent config file for all mochitest files. +"use strict"; + +module.exports = { + env: { + browser: true, + }, + + // All globals made available in the test environment. + globals: { + // SpecialPowers is injected into the window object via SimpleTest.js + SpecialPowers: false, + }, + + overrides: [ + { + env: { + // Ideally we wouldn't be using the simpletest env here, but our uses of + // js files mean we pick up everything from the global scope, which could + // be any one of a number of html files. So we just allow the basics... + "mozilla/simpletest": true, + }, + files: ["*.js"], + }, + ], + plugins: ["mozilla", "@microsoft/sdl"], + + rules: { + // No using of insecure url, so no http urls + "@microsoft/sdl/no-insecure-url": [ + "error", + { + exceptions: [ + "^http:\\/\\/mochi\\.test?.*", + "^http:\\/\\/mochi\\.xorigin-test?.*", + "^http:\\/\\/localhost?.*", + "^http:\\/\\/127\\.0\\.0\\.1?.*", + // Exempt xmlns urls + "^http:\\/\\/www\\.w3\\.org?.*", + "^http:\\/\\/www\\.mozilla\\.org\\/keymaster\\/gatekeeper?.*", + // Exempt urls that start with ftp or ws. + "^ws:?.*", + "^ftp:?.*", + ], + varExceptions: ["insecure?.*"], + }, + ], + "mozilla/import-content-task-globals": "error", + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + // Turn off no-define-cc-etc for mochitests as these don't have Cc etc defined in the + // global scope. + "mozilla/no-define-cc-etc": "off", + // We mis-predict globals for HTML test files in directories shared + // with browser tests, so don't try to "fix" imports that are needed. + "mozilla/no-redeclare-with-import-autofix": "off", + // Turn off use-chromeutils-generateqi as these tests don't have ChromeUtils + // available. + "mozilla/use-chromeutils-generateqi": "off", + "no-shadow": "error", + // Turn off no-unsanitized for tests, as we do want to be able to use + // these for testing. + "no-unsanitized/method": "off", + "no-unsanitized/property": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js new file mode 100644 index 0000000000..db7a0dc731 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js @@ -0,0 +1,351 @@ +/* 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/. */ + +"use strict"; + +/** + * The configuration is based on eslint:recommended config. The details for all + * the ESLint rules, and which ones are in the recommended configuration can + * be found here: + * + * https://eslint.org/docs/rules/ + * + * Rules that we've explicitly decided not to enable: + * + * require-await - bug 1381030. + * no-prototype-builtins - bug 1551829. + * require-atomic-updates - bug 1551829. + * - This generates too many false positives that are not easy to work + * around, and false positives seem to be inherent in the rule. + */ +module.exports = { + env: { + browser: true, + es2022: true, + "mozilla/privileged": true, + "mozilla/specific": true, + }, + + // The prettier configuration here comes from eslint-config-prettier and + // turns off all of ESLint's rules related to formatting. + extends: [ + "eslint:recommended", + "prettier", + "plugin:json/recommended-with-comments", + ], + + overrides: [ + { + // System mjs files and jsm files are not loaded in the browser scope, + // so we turn that off for those. Though we do have our own special + // environment for them. + env: { + browser: false, + "mozilla/jsm": true, + }, + files: ["**/*.sys.mjs", "**/*.jsm"], + rules: { + "mozilla/lazy-getter-object-name": "error", + "mozilla/reject-eager-module-in-lazy-getter": "error", + "mozilla/reject-global-this": "error", + "mozilla/reject-globalThis-modification": "error", + // For all system modules, we expect no properties to need importing, + // hence reject everything. + "mozilla/reject-importGlobalProperties": ["error", "everything"], + "mozilla/reject-mixing-eager-and-lazy": "error", + "mozilla/reject-top-level-await": "error", + // TODO: Bug 1575506 turn `builtinGlobals` on here. + // We can enable builtinGlobals for jsms due to their scopes. + "no-redeclare": ["error", { builtinGlobals: false }], + }, + }, + { + files: ["**/*.mjs", "**/*.jsx", "**/*.jsm", "**/?(*.)worker.?(m)js"], + rules: { + // Modules and workers are far easier to check for no-unused-vars on a + // global scope, than our content files. Hence we turn that on here. + "no-unused-vars": [ + "error", + { + args: "none", + vars: "all", + }, + ], + }, + }, + { + excludedFiles: ["**/*.sys.mjs"], + files: ["**/*.mjs"], + rules: { + "mozilla/reject-import-system-module-from-non-system": "error", + "mozilla/reject-lazy-imports-into-globals": "error", + "no-shadow": ["error", { allow: ["event"], builtinGlobals: true }], + }, + }, + { + files: ["**/*.mjs", "**/*.jsx"], + parserOptions: { + sourceType: "module", + }, + rules: { + "mozilla/use-static-import": "error", + // This rule defaults to not allowing "use strict" in module files since + // they are always loaded in strict mode. + strict: "error", + }, + }, + { + files: ["**/*.jsm"], + rules: { + "mozilla/mark-exported-symbols-as-used": "error", + }, + }, + { + env: { + browser: false, + "mozilla/privileged": false, + "mozilla/sjs": true, + "mozilla/specific": false, + }, + files: ["**/*.sjs"], + rules: { + // For sjs files, reject everything as we should update the sandbox + // to include the globals we need, as these are test-only files. + "mozilla/reject-importGlobalProperties": ["error", "everything"], + }, + }, + { + env: { + browser: false, + worker: true, + }, + files: [ + // Most files should use the `.worker.` format to be consistent with + // other items like `.sys.mjs`, but we allow simply calling the file + // "worker" as well. + "**/?(*.)worker.?(m)js", + ], + }, + ], + + parserOptions: { + ecmaVersion: "latest", + }, + + // When adding items to this file please check for effects on sub-directories. + plugins: ["fetch-options", "html", "json", "no-unsanitized"], + + // When adding items to this file please check for effects on all of toolkit + // and browser + rules: { + // This may conflict with prettier, so we turn it off. + "arrow-body-style": "off", + + // Warn about cyclomatic complexity in functions. + // XXX Get this down to 20? + complexity: ["error", 34], + + // Functions must always return something or nothing + "consistent-return": "error", + + // XXX This rule line should be removed to enable it. See bug 1487642. + // Require super() calls in constructors + "constructor-super": "off", + + // Require braces around blocks that start a new line + curly: ["error", "all"], + + // Encourage the use of dot notation whenever possible. + "dot-notation": "error", + + // XXX This rule should be enabled, see Bug 1557040 + // No credentials submitted with fetch calls + "fetch-options/no-fetch-credentials": "off", + + // XXX This rule line should be removed to enable it. See bug 1487642. + // Enforce return statements in getters + "getter-return": "off", + + // Don't enforce the maximum depth that blocks can be nested. The complexity + // rule is a better rule to check this. + "max-depth": "off", + + // Maximum depth callbacks can be nested. + "max-nested-callbacks": ["error", 10], + + "mozilla/avoid-removeChild": "error", + "mozilla/consistent-if-bracing": "error", + "mozilla/import-browser-window-globals": "error", + "mozilla/import-globals": "error", + "mozilla/no-compare-against-boolean-literals": "error", + "mozilla/no-cu-reportError": "error", + "mozilla/no-define-cc-etc": "error", + "mozilla/no-throw-cr-literal": "error", + "mozilla/no-useless-parameters": "error", + "mozilla/no-useless-removeEventListener": "error", + "mozilla/prefer-boolean-length-check": "error", + "mozilla/prefer-formatValues": "error", + "mozilla/reject-addtask-only": "error", + "mozilla/reject-chromeutils-import-params": "error", + "mozilla/reject-importGlobalProperties": ["error", "allownonwebidl"], + "mozilla/reject-multiple-getters-calls": "error", + "mozilla/reject-scriptableunicodeconverter": "warn", + "mozilla/rejects-requires-await": "error", + "mozilla/use-cc-etc": "error", + "mozilla/use-chromeutils-definelazygetter": "error", + "mozilla/use-chromeutils-generateqi": "error", + "mozilla/use-chromeutils-import": "error", + "mozilla/use-console-createInstance": "error", + "mozilla/use-default-preference-values": "error", + "mozilla/use-includes-instead-of-indexOf": "error", + "mozilla/use-isInstance": "error", + "mozilla/use-ownerGlobal": "error", + "mozilla/use-returnValue": "error", + "mozilla/use-services": "error", + "mozilla/valid-lazy": "error", + "mozilla/valid-services": "error", + + // Use [] instead of Array() + "no-array-constructor": "error", + + // Disallow use of arguments.caller or arguments.callee. + "no-caller": "error", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow lexical declarations in case clauses + "no-case-declarations": "off", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow the use of console + "no-console": "off", + + // Disallows expressions where the operation doesn't affect the value. + // TODO: This is enabled by default in ESLint's v9 recommended configuration. + "no-constant-binary-expression": "error", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow constant expressions in conditions + "no-constant-condition": "off", + + // If an if block ends with a return no need for an else block + "no-else-return": "error", + + // No empty statements + "no-empty": ["error", { allowEmptyCatch: true }], + + // Disallow eval and setInteral/setTimeout with strings + "no-eval": "error", + + // Disallow unnecessary calls to .bind() + "no-extra-bind": "error", + + // Disallow fallthrough of case statements + "no-fallthrough": [ + "error", + { + // The eslint rule doesn't allow for case-insensitive regex option. + // The following pattern allows for a dash between "fall through" as + // well as alternate spelling of "fall thru". The pattern also allows + // for an optional "s" at the end of "fall" ("falls through"). + commentPattern: + "[Ff][Aa][Ll][Ll][Ss]?[\\s-]?([Tt][Hh][Rr][Oo][Uu][Gg][Hh]|[Tt][Hh][Rr][Uu])", + }, + ], + + // Disallow eval and setInteral/setTimeout with strings + "no-implied-eval": "error", + + // This has been superseded since we're using ES6. + // Disallow variable or function declarations in nested blocks + "no-inner-declarations": "off", + + // Disallow the use of the __iterator__ property + "no-iterator": "error", + + // No labels + "no-labels": "error", + + // Disallow unnecessary nested blocks + "no-lone-blocks": "error", + + // No single if block inside an else block + "no-lonely-if": "error", + + // Nested ternary statements are confusing + "no-nested-ternary": "error", + + // Disallow use of new wrappers + "no-new-wrappers": "error", + + // Use {} instead of new Object(), unless arguments are passed. + "no-object-constructor": "error", + + // We don't want this, see bug 1551829 + "no-prototype-builtins": "off", + + // Disable builtinGlobals for no-redeclare as this conflicts with our + // globals declarations especially for browser window. + "no-redeclare": ["error", { builtinGlobals: false }], + + // Disallow use of event global. + "no-restricted-globals": ["error", "event"], + + // No unnecessary comparisons + "no-self-compare": "error", + + // No comma sequenced statements + "no-sequences": "error", + + // No declaring variables from an outer scope + // "no-shadow": "error", + + // Disallow throwing literals (eg. throw "error" instead of + // throw new Error("error")). + "no-throw-literal": "error", + + // Disallow the use of Boolean literals in conditional expressions. + "no-unneeded-ternary": "error", + + // No unsanitized use of innerHTML=, document.write() etc. + // cf. https://github.com/mozilla/eslint-plugin-no-unsanitized#rule-details + "no-unsanitized/method": "error", + "no-unsanitized/property": "error", + + // No declaring variables that are never used + "no-unused-vars": [ + "error", + { + args: "none", + vars: "local", + }, + ], + + // No using variables before defined + // "no-use-before-define": ["error", "nofunc"], + + // Disallow unnecessary .call() and .apply() + "no-useless-call": "error", + + // Don't concatenate string literals together (unless they span multiple + // lines) + "no-useless-concat": "error", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow unnecessary escape characters + "no-useless-escape": "off", + + // Disallow redundant return statements + "no-useless-return": "error", + + // Require object-literal shorthand with ES6 method syntax + "object-shorthand": ["error", "always", { avoidQuotes: true }], + + // This may conflict with prettier, so turn it off. + "prefer-arrow-callback": "off", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Require generator functions to contain yield + "require-yield": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/require-jsdoc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/require-jsdoc.js new file mode 100644 index 0000000000..086fc8b1d3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/require-jsdoc.js @@ -0,0 +1,41 @@ +/* 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/. */ + +"use strict"; + +module.exports = { + plugins: ["jsdoc"], + + rules: { + "jsdoc/require-jsdoc": [ + "error", + { + require: { + ClassDeclaration: true, + FunctionDeclaration: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-name": "error", + "jsdoc/require-property": "error", + "jsdoc/require-property-description": "error", + "jsdoc/require-property-name": "error", + "jsdoc/require-property-type": "error", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-check": "error", + "jsdoc/require-yields": "error", + "jsdoc/require-yields-check": "error", + }, + settings: { + jsdoc: { + // This changes what's allowed in JSDocs, enabling more type-inference + // friendly types. This is the default in eslint-plugin-jsdoc versions + // since May 2023, but we're still on 39.9 and need opt-in for now. + // https://github.com/gajus/eslint-plugin-jsdoc/issues/834 + mode: "typescript", + }, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/valid-jsdoc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/valid-jsdoc.js new file mode 100644 index 0000000000..65fb760fe0 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/valid-jsdoc.js @@ -0,0 +1,34 @@ +/* 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/. */ + +"use strict"; + +module.exports = { + plugins: ["jsdoc"], + + rules: { + "jsdoc/check-access": "error", + // Handled by prettier + // "jsdoc/check-alignment": "error", + "jsdoc/check-param-names": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/empty-tags": "error", + "jsdoc/newline-after-description": "error", + "jsdoc/no-multi-asterisks": "error", + "jsdoc/require-param-type": "error", + "jsdoc/require-returns-type": "error", + "jsdoc/valid-types": "error", + }, + settings: { + jsdoc: { + // This changes what's allowed in JSDocs, enabling more type-inference + // friendly types. This is the default in eslint-plugin-jsdoc versions + // since May 2023, but we're still on 39.9 and need opt-in for now. + // https://github.com/gajus/eslint-plugin-jsdoc/issues/834 + mode: "typescript", + }, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/xpcshell-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/xpcshell-test.js new file mode 100644 index 0000000000..6a4d572911 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/xpcshell-test.js @@ -0,0 +1,54 @@ +// Parent config file for all xpcshell files. +"use strict"; + +module.exports = { + env: { + browser: false, + "mozilla/privileged": true, + "mozilla/xpcshell": true, + }, + + overrides: [ + { + // If it is a head file, we turn off global unused variable checks, as it + // would require searching the other test files to know if they are used or not. + // This would be expensive and slow, and it isn't worth it for head files. + // We could get developers to declare as exported, but that doesn't seem worth it. + files: "head*.js", + rules: { + "no-unused-vars": [ + "error", + { + args: "none", + vars: "local", + }, + ], + }, + }, + { + // No declaring variables that are never used + files: "test*.js", + rules: { + "no-unused-vars": [ + "error", + { + args: "none", + vars: "all", + }, + ], + }, + }, + ], + + rules: { + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + "mozilla/no-arbitrary-setTimeout": "error", + "mozilla/no-useless-run-test": "error", + "no-shadow": "error", + // Turn off no-unsanitized for tests, as we do want to be able to use + // these for testing. + "no-unsanitized/method": "off", + "no-unsanitized/property": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js new file mode 100644 index 0000000000..241299e2d3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js @@ -0,0 +1,121 @@ +/** + * @fileoverview Defines the environment when in the browser.xhtml window. + * Imports many globals from various files. + * + * 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/. + */ + +"use strict"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var fs = require("fs"); +var helpers = require("../helpers"); +var { getScriptGlobals } = require("./utils"); + +// When updating EXTRA_SCRIPTS or MAPPINGS, be sure to also update the +// 'support-files' config in `tools/lint/eslint.yml`. + +// These are scripts not loaded from browser.xhtml or global-scripts.inc +// but via other includes. +const EXTRA_SCRIPTS = [ + "browser/base/content/nsContextMenu.js", + "browser/components/downloads/content/downloads.js", + "browser/components/downloads/content/indicator.js", + "toolkit/content/customElements.js", + "toolkit/content/editMenuOverlay.js", +]; + +const extraDefinitions = [ + // Via Components.utils, defineModuleGetter, defineLazyModuleGetters or + // defineLazyScriptGetter (and map to + // single) variable. + { name: "XPCOMUtils", writable: false }, + { name: "Task", writable: false }, + { name: "windowGlobalChild", writable: false }, + // structuredClone is a new global that would be defined for the `browser` + // environment in ESLint, but only Firefox has implemented it currently and so + // it isn't in ESLint's globals yet. + // https://developer.mozilla.org/docs/Web/API/structuredClone + { name: "structuredClone", writable: false }, +]; + +// Some files in global-scripts.inc need mapping to specific locations. +const MAPPINGS = { + "printUtils.js": "toolkit/components/printing/content/printUtils.js", + "panelUI.js": "browser/components/customizableui/content/panelUI.js", + "viewSourceUtils.js": + "toolkit/components/viewsource/content/viewSourceUtils.js", + "browserPlacesViews.js": + "browser/components/places/content/browserPlacesViews.js", + "places-tree.js": "browser/components/places/content/places-tree.js", + "places-menupopup.js": + "browser/components/places/content/places-menupopup.js", + "shopping-sidebar.js": + "browser/components/shopping/content/shopping-sidebar.js", +}; + +const globalScriptsRegExp = + /^\s*Services.scriptloader.loadSubScript\(\"(.*?)\", this\);$/; + +function getGlobalScriptIncludes(scriptPath) { + let fileData; + try { + fileData = fs.readFileSync(scriptPath, { encoding: "utf8" }); + } catch (ex) { + // The file isn't present, so this isn't an m-c repository. + return null; + } + + fileData = fileData.split("\n"); + + let result = []; + + for (let line of fileData) { + let match = line.match(globalScriptsRegExp); + if (match) { + let sourceFile = match[1] + .replace( + "chrome://browser/content/search/", + "browser/components/search/content/" + ) + .replace( + "chrome://browser/content/screenshots/", + "browser/components/screenshots/content/" + ) + .replace("chrome://browser/content/", "browser/base/content/") + .replace("chrome://global/content/", "toolkit/content/"); + + for (let mapping of Object.getOwnPropertyNames(MAPPINGS)) { + if (sourceFile.includes(mapping)) { + sourceFile = MAPPINGS[mapping]; + } + } + + result.push(sourceFile); + } + } + + return result; +} + +function getGlobalScripts() { + let results = []; + for (let scriptPath of helpers.globalScriptPaths) { + results = results.concat(getGlobalScriptIncludes(scriptPath)); + } + return results; +} + +module.exports = getScriptGlobals( + "browser-window", + getGlobalScripts().concat(EXTRA_SCRIPTS), + extraDefinitions, + { + browserjsScripts: getGlobalScripts().concat(EXTRA_SCRIPTS), + } +); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-script.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-script.js new file mode 100644 index 0000000000..9b0ae54a2e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-script.js @@ -0,0 +1,28 @@ +/** + * @fileoverview Defines the environment for SpecialPowers chrome script. + * + * 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/. + */ + +"use strict"; + +var { globals } = require("./special-powers-sandbox"); +var util = require("util"); + +module.exports = { + globals: util._extend( + { + // testing/specialpowers/content/SpecialPowersParent.sys.mjs + + // SPLoadChromeScript block + createWindowlessBrowser: false, + sendAsyncMessage: false, + addMessageListener: false, + removeMessageListener: false, + actorParent: false, + }, + globals + ), +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-worker.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-worker.js new file mode 100644 index 0000000000..db5759b26c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-worker.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Defines the environment for chrome workers. This differs + * from normal workers by the fact that `ctypes` can be accessed + * as well. + * + * 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/. + */ + +"use strict"; + +var globals = require("globals"); +var util = require("util"); + +var workerGlobals = util._extend( + { + ctypes: false, + }, + globals.worker +); + +module.exports = { + globals: workerGlobals, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js new file mode 100644 index 0000000000..7ac5c941cf --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Defines the environment for frame scripts. + * + * 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/. + */ + +"use strict"; + +module.exports = { + globals: { + // dom/chrome-webidl/MessageManager.webidl + + // MessageManagerGlobal + dump: false, + atob: false, + btoa: false, + + // MessageListenerManagerMixin + addMessageListener: false, + removeMessageListener: false, + addWeakMessageListener: false, + removeWeakMessageListener: false, + + // MessageSenderMixin + sendAsyncMessage: false, + processMessageManager: false, + remoteType: false, + + // SyncMessageSenderMixin + sendSyncMessage: false, + + // ContentFrameMessageManager + content: false, + docShell: false, + tabEventTarget: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/jsm.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/jsm.js new file mode 100644 index 0000000000..30d8e0eb9c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/jsm.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Defines the environment for jsm files. + * + * 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/. + */ + +"use strict"; + +module.exports = { + globals: { + // These globals are hard-coded and available in .jsm scopes. + // https://searchfox.org/mozilla-central/rev/dcb0cfb66e4ed3b9c7fbef1e80572426ff5f3c3a/js/xpconnect/loader/mozJSModuleLoader.cpp#222-223 + // Although `debug` is allowed for jsm files, this is non-standard and something + // we don't want to allow in mjs files. Hence it is not included here. + atob: false, + btoa: false, + dump: false, + // The WebAssembly global is available in most (if not all) contexts where + // JS can run. It's definitely available in JSMs. So even if this is not + // the perfect place to add it, it's not wrong, and we can move it later. + WebAssembly: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js new file mode 100644 index 0000000000..c517de6209 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js @@ -0,0 +1,819 @@ +/** + * @fileoverview Defines the environment for privileges JS files. + * + * 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/. + */ + +"use strict"; + +module.exports = { + globals: { + // Intl and WebAssembly are available everywhere but are not webIDL definitions. + Intl: false, + WebAssembly: false, + // This list of items is currently obtained manually from the list of + // mozilla::dom::constructor::id::ID enumerations in an object directory + // generated dom/bindings/RegisterBindings.cpp + APZHitResultFlags: false, + AbortController: false, + AbortSignal: false, + AccessibleNode: false, + Addon: false, + AddonEvent: false, + AddonInstall: false, + AddonManager: true, + AddonManagerPermissions: false, + AnalyserNode: false, + Animation: false, + AnimationEffect: false, + AnimationEvent: false, + AnimationPlaybackEvent: false, + AnimationTimeline: false, + AnonymousContent: false, + Attr: false, + AudioBuffer: false, + AudioBufferSourceNode: false, + AudioContext: false, + AudioDecoder: false, + AudioDestinationNode: false, + AudioData: false, + AudioEncoder: false, + AudioListener: false, + AudioNode: false, + AudioParam: false, + AudioParamMap: false, + AudioProcessingEvent: false, + AudioScheduledSourceNode: false, + AudioTrack: false, + AudioTrackList: false, + AudioWorklet: false, + AudioWorkletNode: false, + AuthenticatorAssertionResponse: false, + AuthenticatorAttestationResponse: false, + AuthenticatorResponse: false, + BarProp: false, + BaseAudioContext: false, + BatteryManager: false, + BeforeUnloadEvent: false, + BiquadFilterNode: false, + Blob: false, + BlobEvent: false, + BoxObject: false, + BroadcastChannel: false, + BrowsingContext: false, + ByteLengthQueuingStrategy: false, + CanonicalBrowsingContext: false, + CDATASection: false, + CSS: false, + CSS2Properties: false, + CSSAnimation: false, + CSSConditionRule: false, + CSSCounterStyleRule: false, + CSSFontFaceRule: false, + CSSFontFeatureValuesRule: false, + CSSGroupingRule: false, + CSSImportRule: false, + CSSKeyframeRule: false, + CSSKeyframesRule: false, + CSSMediaRule: false, + CSSMozDocumentRule: false, + CSSNamespaceRule: false, + CSSPageRule: false, + CSSPseudoElement: false, + CSSRule: false, + CSSRuleList: false, + CSSStyleDeclaration: false, + CSSStyleRule: false, + CSSStyleSheet: false, + CSSSupportsRule: false, + CSSTransition: false, + Cache: false, + CacheStorage: false, + CanvasCaptureMediaStream: false, + CanvasGradient: false, + CanvasPattern: false, + CanvasRenderingContext2D: false, + CaretPosition: false, + CaretStateChangedEvent: false, + ChannelMergerNode: false, + ChannelSplitterNode: false, + ChannelWrapper: false, + CharacterData: false, + CheckerboardReportService: false, + ChildProcessMessageManager: false, + ChildSHistory: false, + ChromeMessageBroadcaster: false, + ChromeMessageSender: false, + ChromeNodeList: false, + ChromeUtils: false, + ChromeWorker: false, + Clipboard: false, + ClipboardEvent: false, + ClonedErrorHolder: false, + CloseEvent: false, + CommandEvent: false, + Comment: false, + CompositionEvent: false, + ConsoleInstance: false, + ConstantSourceNode: false, + ContentFrameMessageManager: false, + ContentProcessMessageManager: false, + ConvolverNode: false, + CountQueuingStrategy: false, + CreateOfferRequest: false, + Credential: false, + CredentialsContainer: false, + Crypto: false, + CryptoKey: false, + CustomElementRegistry: false, + CustomEvent: false, + DOMError: false, + DOMException: false, + DOMImplementation: false, + DOMLocalization: false, + DOMMatrix: false, + DOMMatrixReadOnly: false, + DOMParser: false, + DOMPoint: false, + DOMPointReadOnly: false, + DOMQuad: false, + DOMRect: false, + DOMRectList: false, + DOMRectReadOnly: false, + DOMRequest: false, + DOMStringList: false, + DOMStringMap: false, + DOMTokenList: false, + DataTransfer: false, + DataTransferItem: false, + DataTransferItemList: false, + DebuggerNotificationObserver: false, + DelayNode: false, + DeprecationReportBody: false, + DeviceLightEvent: false, + DeviceMotionEvent: false, + DeviceOrientationEvent: false, + DeviceProximityEvent: false, + Directory: false, + Document: false, + DocumentFragment: false, + DocumentTimeline: false, + DocumentType: false, + DominatorTree: false, + DragEvent: false, + DynamicsCompressorNode: false, + Element: false, + EncodedAudioChunk: false, + EncodedVideoChunk: false, + ErrorEvent: false, + Event: false, + EventSource: false, + EventTarget: false, + FeaturePolicyViolationReportBody: false, + FetchObserver: false, + File: false, + FileList: false, + FileReader: false, + FileSystem: false, + FileSystemDirectoryEntry: false, + FileSystemDirectoryReader: false, + FileSystemEntry: false, + FileSystemFileEntry: false, + Flex: false, + FlexItemValues: false, + FlexLineValues: false, + FluentBundle: false, + FluentResource: false, + FocusEvent: false, + FontFace: false, + FontFaceSet: false, + FontFaceSetLoadEvent: false, + FormData: false, + FrameCrashedEvent: false, + FrameLoader: false, + GainNode: false, + Gamepad: false, + GamepadAxisMoveEvent: false, + GamepadButton: false, + GamepadButtonEvent: false, + GamepadEvent: false, + GamepadHapticActuator: false, + GamepadPose: false, + GamepadServiceTest: false, + Glean: false, + GleanPings: false, + Grid: false, + GridArea: false, + GridDimension: false, + GridLine: false, + GridLines: false, + GridTrack: false, + GridTracks: false, + HTMLAllCollection: false, + HTMLAnchorElement: false, + HTMLAreaElement: false, + HTMLAudioElement: false, + Audio: false, + HTMLBRElement: false, + HTMLBaseElement: false, + HTMLBodyElement: false, + HTMLButtonElement: false, + HTMLCanvasElement: false, + HTMLCollection: false, + HTMLDListElement: false, + HTMLDataElement: false, + HTMLDataListElement: false, + HTMLDetailsElement: false, + HTMLDialogElement: false, + HTMLDirectoryElement: false, + HTMLDivElement: false, + HTMLDocument: false, + HTMLElement: false, + HTMLEmbedElement: false, + HTMLFieldSetElement: false, + HTMLFontElement: false, + HTMLFormControlsCollection: false, + HTMLFormElement: false, + HTMLFrameElement: false, + HTMLFrameSetElement: false, + HTMLHRElement: false, + HTMLHeadElement: false, + HTMLHeadingElement: false, + HTMLHtmlElement: false, + HTMLIFrameElement: false, + HTMLImageElement: false, + Image: false, + HTMLInputElement: false, + HTMLLIElement: false, + HTMLLabelElement: false, + HTMLLegendElement: false, + HTMLLinkElement: false, + HTMLMapElement: false, + HTMLMarqueeElement: false, + HTMLMediaElement: false, + HTMLMenuElement: false, + HTMLMenuItemElement: false, + HTMLMetaElement: false, + HTMLMeterElement: false, + HTMLModElement: false, + HTMLOListElement: false, + HTMLObjectElement: false, + HTMLOptGroupElement: false, + HTMLOptionElement: false, + Option: false, + HTMLOptionsCollection: false, + HTMLOutputElement: false, + HTMLParagraphElement: false, + HTMLParamElement: false, + HTMLPictureElement: false, + HTMLPreElement: false, + HTMLProgressElement: false, + HTMLQuoteElement: false, + HTMLScriptElement: false, + HTMLSelectElement: false, + HTMLSlotElement: false, + HTMLSourceElement: false, + HTMLSpanElement: false, + HTMLStyleElement: false, + HTMLTableCaptionElement: false, + HTMLTableCellElement: false, + HTMLTableColElement: false, + HTMLTableElement: false, + HTMLTableRowElement: false, + HTMLTableSectionElement: false, + HTMLTemplateElement: false, + HTMLTextAreaElement: false, + HTMLTimeElement: false, + HTMLTitleElement: false, + HTMLTrackElement: false, + HTMLUListElement: false, + HTMLUnknownElement: false, + HTMLVideoElement: false, + HashChangeEvent: false, + Headers: false, + HeapSnapshot: false, + History: false, + IDBCursor: false, + IDBCursorWithValue: false, + IDBDatabase: false, + IDBFactory: false, + IDBFileHandle: false, + IDBFileRequest: false, + IDBIndex: false, + IDBKeyRange: false, + IDBMutableFile: false, + IDBObjectStore: false, + IDBOpenDBRequest: false, + IDBRequest: false, + IDBTransaction: false, + IDBVersionChangeEvent: false, + IIRFilterNode: false, + IdleDeadline: false, + ImageBitmap: false, + ImageBitmapRenderingContext: false, + ImageCapture: false, + ImageCaptureErrorEvent: false, + ImageData: false, + ImageDocument: false, + InputEvent: false, + InspectorFontFace: false, + InspectorUtils: false, + InstallTriggerImpl: false, + IntersectionObserver: false, + IntersectionObserverEntry: false, + IOUtils: false, + JSProcessActorChild: false, + JSProcessActorParent: false, + JSWindowActorChild: false, + JSWindowActorParent: false, + KeyEvent: false, + KeyboardEvent: false, + KeyframeEffect: false, + L10nFileSource: false, + L10nRegistry: false, + Localization: false, + Location: false, + MIDIAccess: false, + MIDIConnectionEvent: false, + MIDIInput: false, + MIDIInputMap: false, + MIDIMessageEvent: false, + MIDIOutput: false, + MIDIOutputMap: false, + MIDIPort: false, + MatchGlob: false, + MatchPattern: false, + MatchPatternSet: false, + MediaCapabilities: false, + MediaCapabilitiesInfo: false, + MediaControlService: false, + MediaDeviceInfo: false, + MediaDevices: false, + MediaElementAudioSourceNode: false, + MediaEncryptedEvent: false, + MediaError: false, + MediaKeyError: false, + MediaKeyMessageEvent: false, + MediaKeySession: false, + MediaKeyStatusMap: false, + MediaKeySystemAccess: false, + MediaKeys: false, + MediaList: false, + MediaQueryList: false, + MediaQueryListEvent: false, + MediaRecorder: false, + MediaRecorderErrorEvent: false, + MediaSource: false, + MediaStream: false, + MediaStreamAudioDestinationNode: false, + MediaStreamAudioSourceNode: false, + MediaStreamEvent: false, + MediaStreamTrack: false, + MediaStreamTrackAudioSourceNode: false, + MediaStreamTrackEvent: false, + MerchantValidationEvent: false, + MessageBroadcaster: false, + MessageChannel: false, + MessageEvent: false, + MessageListenerManager: false, + MessagePort: false, + MessageSender: false, + MimeType: false, + MimeTypeArray: false, + MouseEvent: false, + MouseScrollEvent: false, + MozCanvasPrintState: false, + MozDocumentMatcher: false, + MozDocumentObserver: false, + MozQueryInterface: false, + MozSharedMap: false, + MozSharedMapChangeEvent: false, + MozStorageAsyncStatementParams: false, + MozStorageStatementParams: false, + MozStorageStatementRow: false, + MozWritableSharedMap: false, + MutationEvent: false, + MutationObserver: false, + MutationRecord: false, + NamedNodeMap: false, + Navigator: false, + NetworkInformation: false, + Node: false, + NodeFilter: false, + NodeIterator: false, + NodeList: false, + Notification: false, + NotifyPaintEvent: false, + OfflineAudioCompletionEvent: false, + OfflineAudioContext: false, + OfflineResourceList: false, + OffscreenCanvas: false, + OscillatorNode: false, + PageTransitionEvent: false, + PaintRequest: false, + PaintRequestList: false, + PannerNode: false, + ParentProcessMessageManager: false, + Path2D: false, + PathUtils: false, + PaymentAddress: false, + PaymentMethodChangeEvent: false, + PaymentRequest: false, + PaymentRequestUpdateEvent: false, + PaymentResponse: false, + PeerConnectionImpl: false, + PeerConnectionObserver: false, + Performance: false, + PerformanceEntry: false, + PerformanceEntryEvent: false, + PerformanceMark: false, + PerformanceMeasure: false, + PerformanceNavigation: false, + PerformanceNavigationTiming: false, + PerformanceObserver: false, + PerformanceObserverEntryList: false, + PerformanceResourceTiming: false, + PerformanceServerTiming: false, + PerformanceTiming: false, + PeriodicWave: false, + PermissionStatus: false, + Permissions: false, + PlacesBookmark: false, + PlacesBookmarkAddition: false, + PlacesBookmarkGuid: false, + PlacesBookmarkKeyword: false, + PlacesBookmarkMoved: false, + PlacesBookmarkRemoved: false, + PlacesBookmarkTags: false, + PlacesBookmarkTime: false, + PlacesBookmarkTitle: false, + PlacesBookmarkUrl: false, + PlacesEvent: false, + PlacesHistoryCleared: false, + PlacesObservers: false, + PlacesPurgeCaches: false, + PlacesRanking: false, + PlacesVisit: false, + PlacesVisitRemoved: false, + PlacesVisitTitle: false, + PlacesWeakCallbackWrapper: false, + Plugin: false, + PluginArray: false, + PluginCrashedEvent: false, + PointerEvent: false, + PopStateEvent: false, + PopupBlockedEvent: false, + PrecompiledScript: false, + Presentation: false, + PresentationAvailability: false, + PresentationConnection: false, + PresentationConnectionAvailableEvent: false, + PresentationConnectionCloseEvent: false, + PresentationConnectionList: false, + PresentationReceiver: false, + PresentationRequest: false, + PrioEncoder: false, + ProcessMessageManager: false, + ProcessingInstruction: false, + ProgressEvent: false, + PromiseDebugging: false, + PromiseRejectionEvent: false, + PublicKeyCredential: false, + PushManager: false, + PushManagerImpl: false, + PushSubscription: false, + PushSubscriptionOptions: false, + RTCCertificate: false, + RTCDTMFSender: false, + RTCDTMFToneChangeEvent: false, + RTCDataChannel: false, + RTCDataChannelEvent: false, + RTCIceCandidate: false, + RTCPeerConnection: false, + RTCPeerConnectionIceEvent: false, + RTCPeerConnectionStatic: false, + RTCRtpReceiver: false, + RTCRtpSender: false, + RTCRtpTransceiver: false, + RTCSessionDescription: false, + RTCStatsReport: false, + RTCTrackEvent: false, + RadioNodeList: false, + Range: false, + ReadableStreamBYOBReader: false, + ReadableStreamBYOBRequest: false, + ReadableByteStreamController: false, + ReadableStream: false, + ReadableStreamDefaultController: false, + ReadableStreamDefaultReader: false, + Report: false, + ReportBody: false, + ReportingObserver: false, + Request: false, + Response: false, + SessionStoreUtils: false, + SVGAElement: false, + SVGAngle: false, + SVGAnimateElement: false, + SVGAnimateMotionElement: false, + SVGAnimateTransformElement: false, + SVGAnimatedAngle: false, + SVGAnimatedBoolean: false, + SVGAnimatedEnumeration: false, + SVGAnimatedInteger: false, + SVGAnimatedLength: false, + SVGAnimatedLengthList: false, + SVGAnimatedNumber: false, + SVGAnimatedNumberList: false, + SVGAnimatedPreserveAspectRatio: false, + SVGAnimatedRect: false, + SVGAnimatedString: false, + SVGAnimatedTransformList: false, + SVGAnimationElement: false, + SVGCircleElement: false, + SVGClipPathElement: false, + SVGComponentTransferFunctionElement: false, + SVGDefsElement: false, + SVGDescElement: false, + SVGElement: false, + SVGEllipseElement: false, + SVGFEBlendElement: false, + SVGFEColorMatrixElement: false, + SVGFEComponentTransferElement: false, + SVGFECompositeElement: false, + SVGFEConvolveMatrixElement: false, + SVGFEDiffuseLightingElement: false, + SVGFEDisplacementMapElement: false, + SVGFEDistantLightElement: false, + SVGFEDropShadowElement: false, + SVGFEFloodElement: false, + SVGFEFuncAElement: false, + SVGFEFuncBElement: false, + SVGFEFuncGElement: false, + SVGFEFuncRElement: false, + SVGFEGaussianBlurElement: false, + SVGFEImageElement: false, + SVGFEMergeElement: false, + SVGFEMergeNodeElement: false, + SVGFEMorphologyElement: false, + SVGFEOffsetElement: false, + SVGFEPointLightElement: false, + SVGFESpecularLightingElement: false, + SVGFESpotLightElement: false, + SVGFETileElement: false, + SVGFETurbulenceElement: false, + SVGFilterElement: false, + SVGForeignObjectElement: false, + SVGGElement: false, + SVGGeometryElement: false, + SVGGradientElement: false, + SVGGraphicsElement: false, + SVGImageElement: false, + SVGLength: false, + SVGLengthList: false, + SVGLineElement: false, + SVGLinearGradientElement: false, + SVGMPathElement: false, + SVGMarkerElement: false, + SVGMaskElement: false, + SVGMatrix: false, + SVGMetadataElement: false, + SVGNumber: false, + SVGNumberList: false, + SVGPathElement: false, + SVGPathSegList: false, + SVGPatternElement: false, + SVGPoint: false, + SVGPointList: false, + SVGPolygonElement: false, + SVGPolylineElement: false, + SVGPreserveAspectRatio: false, + SVGRadialGradientElement: false, + SVGRect: false, + SVGRectElement: false, + SVGSVGElement: false, + SVGScriptElement: false, + SVGSetElement: false, + SVGStopElement: false, + SVGStringList: false, + SVGStyleElement: false, + SVGSwitchElement: false, + SVGSymbolElement: false, + SVGTSpanElement: false, + SVGTextContentElement: false, + SVGTextElement: false, + SVGTextPathElement: false, + SVGTextPositioningElement: false, + SVGTitleElement: false, + SVGTransform: false, + SVGTransformList: false, + SVGUnitTypes: false, + SVGUseElement: false, + SVGViewElement: false, + SVGZoomAndPan: false, + Screen: false, + ScreenLuminance: false, + ScreenOrientation: false, + ScriptProcessorNode: false, + ScrollAreaEvent: false, + ScrollViewChangeEvent: false, + SecurityPolicyViolationEvent: false, + Selection: false, + ServiceWorker: false, + ServiceWorkerContainer: false, + ServiceWorkerRegistration: false, + ShadowRoot: false, + SharedWorker: false, + SimpleGestureEvent: false, + SourceBuffer: false, + SourceBufferList: false, + SpeechGrammar: false, + SpeechGrammarList: false, + SpeechRecognition: false, + SpeechRecognitionAlternative: false, + SpeechRecognitionError: false, + SpeechRecognitionEvent: false, + SpeechRecognitionResult: false, + SpeechRecognitionResultList: false, + SpeechSynthesis: false, + SpeechSynthesisErrorEvent: false, + SpeechSynthesisEvent: false, + SpeechSynthesisUtterance: false, + SpeechSynthesisVoice: false, + StereoPannerNode: false, + Storage: false, + StorageEvent: false, + StorageManager: false, + StreamFilter: false, + StreamFilterDataEvent: false, + StructuredCloneHolder: false, + StructuredCloneTester: false, + StyleSheet: false, + StyleSheetApplicableStateChangeEvent: false, + StyleSheetList: false, + StyleSheetRemovedEvent: false, + SubtleCrypto: false, + SyncMessageSender: false, + TCPServerSocket: false, + TCPServerSocketEvent: false, + TCPSocket: false, + TCPSocketErrorEvent: false, + TCPSocketEvent: false, + TelemetryStopwatch: false, + TestingDeprecatedInterface: false, + Text: false, + TextClause: false, + TextDecoder: false, + TextEncoder: false, + TextMetrics: false, + TextTrack: false, + TextTrackCue: false, + TextTrackCueList: false, + TextTrackList: false, + TimeEvent: false, + TimeRanges: false, + Touch: false, + TouchEvent: false, + TouchList: false, + TrackEvent: false, + TransceiverImpl: false, + TransformStream: false, + TransformStreamDefaultController: false, + TransitionEvent: false, + TreeColumn: false, + TreeColumns: false, + TreeContentView: false, + TreeWalker: false, + U2F: false, + UDPMessageEvent: false, + UDPSocket: false, + UIEvent: false, + URL: false, + URLSearchParams: false, + UserInteraction: false, + UserProximityEvent: false, + VRDisplay: false, + VRDisplayCapabilities: false, + VRDisplayEvent: false, + VREyeParameters: false, + VRFieldOfView: false, + VRFrameData: false, + VRMockController: false, + VRMockDisplay: false, + VRPose: false, + VRServiceTest: false, + VRStageParameters: false, + VRSubmitFrameResult: false, + VTTCue: false, + VTTRegion: false, + ValidityState: false, + VideoColorSpace: false, + VideoDecoder: false, + VideoEncoder: false, + VideoFrame: false, + VideoPlaybackQuality: false, + VideoTrack: false, + VideoTrackList: false, + VisualViewport: false, + WaveShaperNode: false, + WebExtensionContentScript: false, + WebExtensionPolicy: false, + WebGL2RenderingContext: false, + WebGLActiveInfo: false, + WebGLBuffer: false, + WebGLContextEvent: false, + WebGLFramebuffer: false, + WebGLProgram: false, + WebGLQuery: false, + WebGLRenderbuffer: false, + WebGLRenderingContext: false, + WebGLSampler: false, + WebGLShader: false, + WebGLShaderPrecisionFormat: false, + WebGLSync: false, + WebGLTexture: false, + WebGLTransformFeedback: false, + WebGLUniformLocation: false, + WebGLVertexArrayObject: false, + WebGPU: false, + WebGPUAdapter: false, + WebGPUAttachmentState: false, + WebGPUBindGroup: false, + WebGPUBindGroupLayout: false, + WebGPUBindingType: false, + WebGPUBlendFactor: false, + WebGPUBlendOperation: false, + WebGPUBlendState: false, + WebGPUBuffer: false, + WebGPUBufferUsage: false, + WebGPUColorWriteBits: false, + WebGPUCommandBuffer: false, + WebGPUCommandEncoder: false, + WebGPUCompareFunction: false, + WebGPUComputePipeline: false, + WebGPUDepthStencilState: false, + WebGPUDevice: false, + WebGPUFence: false, + WebGPUFilterMode: false, + WebGPUIndexFormat: false, + WebGPUInputState: false, + WebGPUInputStepMode: false, + WebGPULoadOp: false, + WebGPULogEntry: false, + WebGPUPipelineLayout: false, + WebGPUPrimitiveTopology: false, + WebGPUQueue: false, + WebGPURenderPipeline: false, + WebGPUSampler: false, + WebGPUShaderModule: false, + WebGPUShaderStage: false, + WebGPUShaderStageBit: false, + WebGPUStencilOperation: false, + WebGPUStoreOp: false, + WebGPUSwapChain: false, + WebGPUTexture: false, + WebGPUTextureDimension: false, + WebGPUTextureFormat: false, + WebGPUTextureUsage: false, + WebGPUTextureView: false, + WebGPUVertexFormat: false, + WebKitCSSMatrix: false, + WebSocket: false, + WebrtcGlobalInformation: false, + WheelEvent: false, + Window: false, + WindowGlobalChild: false, + WindowGlobalParent: false, + WindowRoot: false, + Worker: false, + Worklet: false, + WritableStream: false, + WritableStreamDefaultController: false, + WritableStreamDefaultWriter: false, + XMLDocument: false, + XMLHttpRequest: false, + XMLHttpRequestEventTarget: false, + XMLHttpRequestUpload: false, + XMLSerializer: false, + XPathEvaluator: false, + XPathExpression: false, + XPathResult: false, + XSLTProcessor: false, + XULCommandEvent: false, + XULElement: false, + XULFrameElement: false, + XULMenuElement: false, + XULPopupElement: false, + XULScrollElement: false, + XULTextElement: false, + console: false, + // These are hard-coded and available in privileged scopes. + // See BackstagePass::Resolve. + fetch: false, + crypto: false, + indexedDB: false, + structuredClone: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/process-script.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/process-script.js new file mode 100644 index 0000000000..f329a6650b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/process-script.js @@ -0,0 +1,38 @@ +/** + * @fileoverview Defines the environment for process scripts. + * + * 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/. + */ + +"use strict"; + +module.exports = { + globals: { + // dom/chrome-webidl/MessageManager.webidl + + // MessageManagerGlobal + dump: false, + atob: false, + btoa: false, + + // MessageListenerManagerMixin + addMessageListener: false, + removeMessageListener: false, + addWeakMessageListener: false, + removeWeakMessageListener: false, + + // MessageSenderMixin + sendAsyncMessage: false, + processMessageManager: false, + remoteType: false, + + // SyncMessageSenderMixin + sendSyncMessage: false, + + // ContentProcessMessageManager + initialProcessData: false, + sharedData: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/remote-page.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/remote-page.js new file mode 100644 index 0000000000..74055457fe --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/remote-page.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Defines the environment for remote page. + * + * 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/. + */ + +"use strict"; + +module.exports = { + globals: { + atob: false, + btoa: false, + RPMAddTRRExcludedDomain: false, + RPMGetAppBuildID: false, + RPMGetInnerMostURI: false, + RPMGetIntPref: false, + RPMGetStringPref: false, + RPMGetBoolPref: false, + RPMSetPref: false, + RPMGetFormatURLPref: false, + RPMIsTRROnlyFailure: false, + RPMIsFirefox: false, + RPMIsNativeFallbackFailure: false, + RPMIsWindowPrivate: false, + RPMSendAsyncMessage: false, + RPMSendQuery: false, + RPMAddMessageListener: false, + RPMRecordTelemetryEvent: false, + RPMCheckAlternateHostAvailable: false, + RPMAddToHistogram: false, + RPMRemoveMessageListener: false, + RPMGetHttpResponseHeader: false, + RPMTryPingSecureWWWLink: false, + RPMOpenSecureWWWLink: false, + RPMOpenPreferences: false, + RPMGetTRRSkipReason: false, + RPMGetTRRDomain: false, + RPMIsSiteSpecificTRRError: false, + RPMSetTRRDisabledLoadFlags: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/simpletest.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/simpletest.js new file mode 100644 index 0000000000..2f5dd5c33e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/simpletest.js @@ -0,0 +1,35 @@ +/** + * @fileoverview Defines the environment for scripts that use the SimpleTest + * mochitest harness. Imports the globals from the relevant files. + * + * 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/. + */ + +"use strict"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var path = require("path"); +var { getScriptGlobals } = require("./utils"); + +// When updating this list, be sure to also update the 'support-files' config +// in `tools/lint/eslint.yml`. +const simpleTestFiles = [ + "AccessibilityUtils.js", + "ExtensionTestUtils.js", + "EventUtils.js", + "MockObjects.js", + "SimpleTest.js", + "WindowSnapshot.js", + "paint_listener.js", +]; +const simpleTestPath = "testing/mochitest/tests/SimpleTest"; + +module.exports = getScriptGlobals( + "simpletest", + simpleTestFiles.map(file => path.join(simpleTestPath, file)) +); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js new file mode 100644 index 0000000000..4f10641c09 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js @@ -0,0 +1,41 @@ +/** + * @fileoverview Defines the environment for sjs files. + * + * 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/. + */ + +"use strict"; + +module.exports = { + globals: { + // All these variables are hard-coded to be available for sjs scopes only. + // https://searchfox.org/mozilla-central/rev/26a1b0fce12e6dd495a954c542bb1e7bd6e0d548/netwerk/test/httpserver/httpd.js#2879 + atob: false, + btoa: false, + Cc: false, + ChromeUtils: false, + Ci: false, + Components: false, + Cr: false, + Cu: false, + dump: false, + IOUtils: false, + PathUtils: false, + TextDecoder: false, + TextEncoder: false, + URLSearchParams: false, + URL: false, + getState: false, + setState: false, + getSharedState: false, + setSharedState: false, + getObjectState: false, + setObjectState: false, + registerPathHandler: false, + Services: false, + // importScripts is also available. + importScripts: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/special-powers-sandbox.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/special-powers-sandbox.js new file mode 100644 index 0000000000..5a28c91883 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/special-powers-sandbox.js @@ -0,0 +1,46 @@ +/** + * @fileoverview Defines the environment for SpecialPowers sandbox. + * + * 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/. + */ + +"use strict"; + +module.exports = { + globals: { + // wantComponents defaults to true, + Components: false, + Ci: false, + Cr: false, + Cc: false, + Cu: false, + Services: false, + + // testing/specialpowers/content/SpecialPowersSandbox.sys.mjs + + // SANDBOX_GLOBALS + Blob: false, + ChromeUtils: false, + FileReader: false, + TextDecoder: false, + TextEncoder: false, + URL: false, + + // EXTRA_IMPORTS + EventUtils: false, + + // SpecialPowersSandbox constructor + assert: false, + Assert: false, + BrowsingContext: false, + InspectorUtils: false, + ok: false, + is: false, + isnot: false, + todo: false, + todo_is: false, + info: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/specific.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/specific.js new file mode 100644 index 0000000000..23ebcb5bb1 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/specific.js @@ -0,0 +1,31 @@ +/** + * @fileoverview Defines the environment for the Firefox browser. Allows global + * variables which are non-standard and specific to Firefox. + * + * 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/. + */ + +"use strict"; + +module.exports = { + globals: { + Cc: false, + ChromeUtils: false, + Ci: false, + Components: false, + Cr: false, + Cu: false, + Debugger: false, + InstallTrigger: false, + // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/InternalError + InternalError: true, + Services: false, + // https://developer.mozilla.org/docs/Web/API/Window/dump + dump: true, + openDialog: false, + // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/uneval + uneval: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/testharness.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/testharness.js new file mode 100644 index 0000000000..cea4088a4c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/testharness.js @@ -0,0 +1,61 @@ +/** + * @fileoverview Defines the environment for testharness.js files. This + * is automatically included in (x)html files including + * /resources/testharness.js. + * + * 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/. + */ + +"use strict"; + +// These globals are taken from dom/imptests/testharness.js, via the expose +// function. + +module.exports = { + globals: { + EventWatcher: false, + test: false, + async_test: false, + promise_test: false, + promise_rejects: false, + generate_tests: false, + setup: false, + done: false, + on_event: false, + step_timeout: false, + format_value: false, + assert_true: false, + assert_false: false, + assert_equals: false, + assert_not_equals: false, + assert_in_array: false, + assert_object_equals: false, + assert_array_equals: false, + assert_approx_equals: false, + assert_less_than: false, + assert_greater_than: false, + assert_between_exclusive: false, + assert_less_than_equal: false, + assert_greater_than_equal: false, + assert_between_inclusive: false, + assert_regexp_match: false, + assert_class_string: false, + assert_exists: false, + assert_own_property: false, + assert_not_exists: false, + assert_inherits: false, + assert_idl_attribute: false, + assert_readonly: false, + assert_throws: false, + assert_unreaded: false, + assert_any: false, + fetch_tests_from_worker: false, + timeout: false, + add_start_callback: false, + add_test_state_callback: false, + add_result_callback: false, + add_completion_callback: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/utils.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/utils.js new file mode 100644 index 0000000000..aeda690ba5 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/utils.js @@ -0,0 +1,62 @@ +/** + * @fileoverview Provides utilities for setting up environments. + * + * 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/. + */ + +"use strict"; + +var path = require("path"); +var helpers = require("../helpers"); +var globals = require("../globals"); + +/** + * Obtains the globals for a list of files. + * + * @param {Array.<String>} files + * The array of files to get globals for. The paths are relative to the topsrcdir. + * @returns {Object} + * Returns an object with keys of the global names and values of if they are + * writable or not. + */ +function getGlobalsForScripts(environmentName, files, extraDefinitions) { + let fileGlobals = extraDefinitions; + const root = helpers.rootDir; + for (const file of files) { + const fileName = path.join(root, file); + try { + fileGlobals = fileGlobals.concat(globals.getGlobalsForFile(fileName)); + } catch (e) { + console.error(`Could not load globals from file ${fileName}: ${e}`); + console.error( + `You may need to update the mappings for the ${environmentName} environment` + ); + throw new Error(`Could not load globals from file ${fileName}: ${e}`); + } + } + + var globalObjects = {}; + for (let global of fileGlobals) { + globalObjects[global.name] = global.writable; + } + return globalObjects; +} + +module.exports = { + getScriptGlobals( + environmentName, + files, + extraDefinitions = [], + extraEnv = {} + ) { + if (helpers.isMozillaCentralBased()) { + return { + globals: getGlobalsForScripts(environmentName, files, extraDefinitions), + ...extraEnv, + }; + } + return helpers.getSavedEnvironmentItems(environmentName); + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/xpcshell.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/xpcshell.js new file mode 100644 index 0000000000..408bc2e277 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/xpcshell.js @@ -0,0 +1,59 @@ +/** + * @fileoverview Defines the environment for xpcshell test files. + * + * 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/. + */ + +"use strict"; + +var { getScriptGlobals } = require("./utils"); + +const extraGlobals = [ + // Defined in XPCShellImpl.cpp + "print", + "readline", + "load", + "quit", + "dumpXPC", + "dump", + "gc", + "gczeal", + "options", + "sendCommand", + "atob", + "btoa", + "setInterruptCallback", + "simulateNoScriptActivity", + "registerXPCTestComponents", + + // Assert.sys.mjs globals. + "setReporter", + "report", + "ok", + "equal", + "notEqual", + "deepEqual", + "notDeepEqual", + "strictEqual", + "notStrictEqual", + "throws", + "rejects", + "greater", + "greaterOrEqual", + "less", + "lessOrEqual", + // TestingFunctions.cpp globals + "allocationMarker", + "byteSize", + "saveStack", +]; + +module.exports = getScriptGlobals( + "xpcshell", + ["testing/xpcshell/head.js"], + extraGlobals.map(g => { + return { name: g, writable: false }; + }) +); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js new file mode 100644 index 0000000000..25c149fa9f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js @@ -0,0 +1,668 @@ +/** + * @fileoverview functions for scanning an AST for globals including + * traversing referenced scripts. + * 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/. + */ + +"use strict"; + +const path = require("path"); +const fs = require("fs"); +const helpers = require("./helpers"); +const htmlparser = require("htmlparser2"); +const testharnessEnvironment = require("./environments/testharness.js"); + +const callExpressionDefinitions = [ + /^loader\.lazyGetter\((?:globalThis|this), "(\w+)"/, + /^loader\.lazyServiceGetter\((?:globalThis|this), "(\w+)"/, + /^loader\.lazyRequireGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/, + /^ChromeUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/, + /^ChromeUtils\.defineModuleGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineConstant\((?:globalThis|this), "(\w+)"/, + /^DevToolsUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/, + /^Object\.defineProperty\((?:globalThis|this), "(\w+)"/, + /^Reflect\.defineProperty\((?:globalThis|this), "(\w+)"/, + /^this\.__defineGetter__\("(\w+)"/, +]; + +const callExpressionMultiDefinitions = [ + "XPCOMUtils.defineLazyGlobalGetters(this,", + "XPCOMUtils.defineLazyGlobalGetters(globalThis,", + "XPCOMUtils.defineLazyModuleGetters(this,", + "XPCOMUtils.defineLazyModuleGetters(globalThis,", + "XPCOMUtils.defineLazyServiceGetters(this,", + "XPCOMUtils.defineLazyServiceGetters(globalThis,", + "ChromeUtils.defineESModuleGetters(this,", + "ChromeUtils.defineESModuleGetters(globalThis,", + "loader.lazyRequireGetter(this,", + "loader.lazyRequireGetter(globalThis,", +]; + +const subScriptMatches = [ + /Services\.scriptloader\.loadSubScript\("(.*?)", this\)/, +]; + +const workerImportFilenameMatch = /(.*\/)*((.*?)\.jsm?)/; + +/** + * Parses a list of "name:boolean_value" or/and "name" options divided by comma + * or whitespace. + * + * This function was copied from eslint.js + * + * @param {string} string The string to parse. + * @param {Comment} comment The comment node which has the string. + * @returns {Object} Result map object of names and boolean values + */ +function parseBooleanConfig(string, comment) { + let items = {}; + + // Collapse whitespace around : to make parsing easier + string = string.replace(/\s*:\s*/g, ":"); + // Collapse whitespace around , + string = string.replace(/\s*,\s*/g, ","); + + string.split(/\s|,+/).forEach(function (name) { + if (!name) { + return; + } + + let pos = name.indexOf(":"); + let value; + if (pos !== -1) { + value = name.substring(pos + 1, name.length); + name = name.substring(0, pos); + } + + items[name] = { + value: value === "true", + comment, + }; + }); + + return items; +} + +/** + * Global discovery can require parsing many files. This map of + * {String} => {Object} caches what globals were discovered for a file path. + */ +const globalCache = new Map(); + +/** + * Global discovery can occasionally meet circular dependencies due to the way + * js files are included via html/xhtml files etc. This set is used to avoid + * getting into loops whilst the discovery is in progress. + */ +var globalDiscoveryInProgressForFiles = new Set(); + +/** + * When looking for globals in HTML files, it can be common to have more than + * one script tag with inline javascript. These will normally be called together, + * so we store the globals for just the last HTML file processed. + */ +var lastHTMLGlobals = {}; + +/** + * Attempts to convert an CallExpressions that look like module imports + * into global variable definitions. + * + * @param {Object} node + * The AST node to convert. + * @param {boolean} isGlobal + * True if the current node is in the global scope. + * + * @return {Array} + * An array of objects that contain details about the globals: + * - {String} name + * The name of the global. + * - {Boolean} writable + * If the global is writeable or not. + */ +function convertCallExpressionToGlobals(node, isGlobal) { + let express = node.expression; + if ( + express.type === "CallExpression" && + express.callee.type === "MemberExpression" && + express.callee.object && + express.callee.object.type === "Identifier" && + express.arguments.length === 1 && + express.arguments[0].type === "ArrayExpression" && + express.callee.property.type === "Identifier" && + express.callee.property.name === "importGlobalProperties" + ) { + return express.arguments[0].elements.map(literal => { + return { + explicit: true, + name: literal.value, + writable: false, + }; + }); + } + + let source; + try { + source = helpers.getASTSource(node); + } catch (e) { + return []; + } + + // The definition matches below must be in the global scope for us to define + // a global, so bail out early if we're not a global. + if (!isGlobal) { + return []; + } + + for (let reg of subScriptMatches) { + let match = source.match(reg); + if (match) { + return getGlobalsForScript(match[1], "script").map(g => { + // We don't want any loadSubScript globals to be explicit, as this + // could trigger no-unused-vars when importing multiple variables + // from a script and not using all of them. + g.explicit = false; + return g; + }); + } + } + + for (let reg of callExpressionDefinitions) { + let match = source.match(reg); + if (match) { + return [{ name: match[1], writable: true, explicit: true }]; + } + } + + if ( + callExpressionMultiDefinitions.some(expr => source.startsWith(expr)) && + node.expression.arguments[1] + ) { + let arg = node.expression.arguments[1]; + if (arg.type === "ObjectExpression") { + return arg.properties + .map(p => ({ + name: p.type === "Property" && p.key.name, + writable: true, + explicit: true, + })) + .filter(g => g.name); + } + if (arg.type === "ArrayExpression") { + return arg.elements + .map(p => ({ + name: p.type === "Literal" && p.value, + writable: true, + explicit: true, + })) + .filter(g => typeof g.name == "string"); + } + } + + if ( + node.expression.callee.type == "MemberExpression" && + node.expression.callee.property.type == "Identifier" && + node.expression.callee.property.name == "defineLazyScriptGetter" + ) { + // The case where we have a single symbol as a string has already been + // handled by the regexp, so we have an array of symbols here. + return node.expression.arguments[1].elements.map(n => ({ + name: n.value, + writable: true, + explicit: true, + })); + } + + return []; +} + +/** + * Attempts to convert an AssignmentExpression into a global variable + * definition if it applies to `this` in the global scope. + * + * @param {Object} node + * The AST node to convert. + * @param {boolean} isGlobal + * True if the current node is in the global scope. + * + * @return {Array} + * An array of objects that contain details about the globals: + * - {String} name + * The name of the global. + * - {Boolean} writable + * If the global is writeable or not. + */ +function convertThisAssignmentExpressionToGlobals(node, isGlobal) { + if ( + isGlobal && + node.expression.left && + node.expression.left.object && + node.expression.left.object.type === "ThisExpression" && + node.expression.left.property && + node.expression.left.property.type === "Identifier" + ) { + return [{ name: node.expression.left.property.name, writable: true }]; + } + return []; +} + +/** + * Attempts to convert an ExpressionStatement to likely global variable + * definitions. + * + * @param {Object} node + * The AST node to convert. + * @param {boolean} isGlobal + * True if the current node is in the global scope. + * + * @return {Array} + * An array of objects that contain details about the globals: + * - {String} name + * The name of the global. + * - {Boolean} writable + * If the global is writeable or not. + */ +function convertWorkerExpressionToGlobals(node, isGlobal, dirname) { + let results = []; + let expr = node.expression; + + if ( + node.expression.type === "CallExpression" && + expr.callee && + expr.callee.type === "Identifier" && + expr.callee.name === "importScripts" + ) { + for (var arg of expr.arguments) { + var match = arg.value && arg.value.match(workerImportFilenameMatch); + if (match) { + if (!match[1]) { + let filePath = path.resolve(dirname, match[2]); + if (fs.existsSync(filePath)) { + let additionalGlobals = module.exports.getGlobalsForFile(filePath); + results = results.concat(additionalGlobals); + } + } + // Import with relative/absolute path should explicitly use + // `import-globals-from` comment. + } + } + } + + return results; +} + +/** + * Attempts to load the globals for a given script. + * + * @param {string} src + * The source path or url of the script to look for. + * @param {string} type + * The type of the current file (script/module). + * @param {string} [dir] + * The directory of the current file. + * @returns {object[]} + * An array of objects with details of the globals in them. + */ +function getGlobalsForScript(src, type, dir) { + let scriptName; + if (src.includes("http:")) { + // We don't handle this currently as the paths are complex to match. + } else if (src.startsWith("chrome://mochikit/content/")) { + // Various ways referencing test files. + src = src.replace("chrome://mochikit/content/", "/"); + scriptName = path.join(helpers.rootDir, "testing", "mochitest", src); + } else if (src.startsWith("chrome://mochitests/content/browser")) { + src = src.replace("chrome://mochitests/content/browser", ""); + scriptName = path.join(helpers.rootDir, src); + } else if (src.includes("SimpleTest")) { + // This is another way of referencing test files... + scriptName = path.join(helpers.rootDir, "testing", "mochitest", src); + } else if (src.startsWith("/tests/")) { + scriptName = path.join(helpers.rootDir, src.substring(7)); + } else if (src.startsWith("/resources/testharness.js")) { + return Object.keys(testharnessEnvironment.globals).map(name => ({ + name, + writable: true, + })); + } else if (dir) { + // Fallback to hoping this is a relative path. + scriptName = path.join(dir, src); + } + if (scriptName && fs.existsSync(scriptName)) { + return module.exports.getGlobalsForFile(scriptName, { + ecmaVersion: helpers.getECMAVersion(), + sourceType: type, + }); + } + return []; +} + +/** + * An object that returns found globals for given AST node types. Each prototype + * property should be named for a node type and accepts a node parameter and a + * parents parameter which is a list of the parent nodes of the current node. + * Each returns an array of globals found. + * + * @param {String} filePath + * The absolute path of the file being parsed. + */ +function GlobalsForNode(filePath, context) { + this.path = filePath; + this.context = context; + + if (this.path) { + this.dirname = path.dirname(this.path); + } else { + this.dirname = null; + } +} + +GlobalsForNode.prototype = { + Program(node) { + let globals = []; + for (let comment of node.comments) { + if (comment.type !== "Block") { + continue; + } + let value = comment.value.trim(); + value = value.replace(/\n/g, ""); + + // We have to discover any globals that ESLint would have defined through + // comment directives. + let match = /^globals?\s+(.+)/.exec(value); + if (match) { + let values = parseBooleanConfig(match[1].trim(), node); + for (let name of Object.keys(values)) { + globals.push({ + name, + writable: values[name].value, + }); + } + // We matched globals, so we won't match import-globals-from. + continue; + } + + match = /^import-globals-from\s+(.+)$/.exec(value); + if (!match) { + continue; + } + + if (!this.dirname) { + // If this is testing context without path, ignore import. + return globals; + } + + let filePath = match[1].trim(); + + if (filePath.endsWith(".mjs")) { + if (this.context) { + this.context.report( + comment, + "import-globals-from does not support module files - use a direct import instead" + ); + } else { + // Fall back to throwing an error, as we do not have a context in all situations, + // e.g. when loading the environment. + throw new Error( + "import-globals-from does not support module files - use a direct import instead" + ); + } + continue; + } + + if (!path.isAbsolute(filePath)) { + filePath = path.resolve(this.dirname, filePath); + } else { + filePath = path.join(helpers.rootDir, filePath); + } + globals = globals.concat(module.exports.getGlobalsForFile(filePath)); + } + + return globals; + }, + + ExpressionStatement(node, parents, globalScope) { + let isGlobal = helpers.getIsGlobalThis(parents); + let globals = []; + + // Note: We check the expression types here and only call the necessary + // functions to aid performance. + if (node.expression.type === "AssignmentExpression") { + globals = convertThisAssignmentExpressionToGlobals(node, isGlobal); + } else if (node.expression.type === "CallExpression") { + globals = convertCallExpressionToGlobals(node, isGlobal); + } + + // Here we assume that if importScripts is set in the global scope, then + // this is a worker. It would be nice if eslint gave us a way of getting + // the environment directly. + // + // If this is testing context without path, ignore import. + if (globalScope && globalScope.set.get("importScripts") && this.dirname) { + let workerDetails = convertWorkerExpressionToGlobals( + node, + isGlobal, + this.dirname + ); + globals = globals.concat(workerDetails); + } + + return globals; + }, +}; + +module.exports = { + /** + * Returns all globals for a given file. Recursively searches through + * import-globals-from directives and also includes globals defined by + * standard eslint directives. + * + * @param {String} filePath + * The absolute path of the file to be parsed. + * @param {Object} astOptions + * Extra options to pass to the parser. + * @return {Array} + * An array of objects that contain details about the globals: + * - {String} name + * The name of the global. + * - {Boolean} writable + * If the global is writeable or not. + */ + getGlobalsForFile(filePath, astOptions = {}) { + if (globalCache.has(filePath)) { + return globalCache.get(filePath); + } + + if (globalDiscoveryInProgressForFiles.has(filePath)) { + // We're already processing this file, so return an empty set for now - + // the initial processing will pick up on the globals for this file. + return []; + } + globalDiscoveryInProgressForFiles.add(filePath); + + let content = fs.readFileSync(filePath, "utf8"); + + // Parse the content into an AST + let { ast, scopeManager, visitorKeys } = helpers.parseCode( + content, + astOptions + ); + + // Discover global declarations + let globalScope = scopeManager.acquire(ast); + + let globals = Object.keys(globalScope.variables).map(v => ({ + name: globalScope.variables[v].name, + writable: true, + })); + + // Walk over the AST to find any of our custom globals + let handler = new GlobalsForNode(filePath); + + helpers.walkAST(ast, visitorKeys, (type, node, parents) => { + if (type in handler) { + let newGlobals = handler[type](node, parents, globalScope); + globals.push.apply(globals, newGlobals); + } + }); + + globalCache.set(filePath, globals); + + globalDiscoveryInProgressForFiles.delete(filePath); + return globals; + }, + + /** + * Returns all globals for a code. + * This is only for testing. + * + * @param {String} code + * The JS code + * @param {Object} astOptions + * Extra options to pass to the parser. + * @return {Array} + * An array of objects that contain details about the globals: + * - {String} name + * The name of the global. + * - {Boolean} writable + * If the global is writeable or not. + */ + getGlobalsForCode(code, astOptions = {}) { + // Parse the content into an AST + let { ast, scopeManager, visitorKeys } = helpers.parseCode( + code, + astOptions, + { useBabel: false } + ); + + // Discover global declarations + let globalScope = scopeManager.acquire(ast); + + let globals = Object.keys(globalScope.variables).map(v => ({ + name: globalScope.variables[v].name, + writable: true, + })); + + // Walk over the AST to find any of our custom globals + let handler = new GlobalsForNode(null); + + helpers.walkAST(ast, visitorKeys, (type, node, parents) => { + if (type in handler) { + let newGlobals = handler[type](node, parents, globalScope); + globals.push.apply(globals, newGlobals); + } + }); + + return globals; + }, + + /** + * Returns all the globals for an html file that are defined by imported + * scripts (i.e. <script src="foo.js">). + * + * This function will cache results for one html file only - we expect + * this to be called sequentially for each chunk of a HTML file, rather + * than chucks of different files in random order. + * + * @param {String} filePath + * The absolute path of the file to be parsed. + * @return {Array} + * An array of objects that contain details about the globals: + * - {String} name + * The name of the global. + * - {Boolean} writable + * If the global is writeable or not. + */ + getImportedGlobalsForHTMLFile(filePath) { + if (lastHTMLGlobals.filename === filePath) { + return lastHTMLGlobals.globals; + } + + let dir = path.dirname(filePath); + let globals = []; + + let content = fs.readFileSync(filePath, "utf8"); + let scriptSrcs = []; + + // We use htmlparser as this ensures we find the script tags correctly. + let parser = new htmlparser.Parser( + { + onopentag(name, attribs) { + if (name === "script" && "src" in attribs) { + scriptSrcs.push({ + src: attribs.src, + type: + "type" in attribs && attribs.type == "module" + ? "module" + : "script", + }); + } + }, + }, + { + xmlMode: filePath.endsWith("xhtml"), + } + ); + + parser.parseComplete(content); + + for (let script of scriptSrcs) { + // Ensure that the script src isn't just "". + if (!script.src) { + continue; + } + globals.push(...getGlobalsForScript(script.src, script.type, dir)); + } + + lastHTMLGlobals.filePath = filePath; + return (lastHTMLGlobals.globals = globals); + }, + + /** + * Intended to be used as-is for an ESLint rule that parses for globals in + * the current file and recurses through import-globals-from directives. + * + * @param {Object} context + * The ESLint parsing context. + */ + getESLintGlobalParser(context) { + let globalScope; + + let parser = { + Program(node) { + globalScope = context.getScope(); + }, + }; + let filename = context.getFilename(); + + let extraHTMLGlobals = []; + if (filename.endsWith(".html") || filename.endsWith(".xhtml")) { + extraHTMLGlobals = module.exports.getImportedGlobalsForHTMLFile(filename); + } + + // Install thin wrappers around GlobalsForNode + let handler = new GlobalsForNode(helpers.getAbsoluteFilePath(context)); + + for (let type of Object.keys(GlobalsForNode.prototype)) { + parser[type] = function (node) { + if (type === "Program") { + globalScope = context.getScope(); + helpers.addGlobals(extraHTMLGlobals, globalScope); + } + let globals = handler[type](node, context.getAncestors(), globalScope); + helpers.addGlobals( + globals, + globalScope, + node.type !== "Program" && node + ); + }; + } + + return parser; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js new file mode 100644 index 0000000000..dc4106631a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js @@ -0,0 +1,797 @@ +/** + * @fileoverview A collection of helper functions. + * 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/. + */ +"use strict"; + +const parser = require("espree"); +const { analyze } = require("eslint-scope"); +const { KEYS: defaultVisitorKeys } = require("eslint-visitor-keys"); +const estraverse = require("estraverse"); +const path = require("path"); +const fs = require("fs"); +const toml = require("toml-eslint-parser"); +const recommendedConfig = require("./configs/recommended"); + +var gRootDir = null; +var directoryManifests = new Map(); + +let xpidlData; + +module.exports = { + get servicesData() { + return require("./services.json"); + }, + + /** + * Obtains xpidl data from the object directory specified in the + * environment. + * + * @returns {Map<string, object>} + * A map of interface names to the interface details. + */ + get xpidlData() { + let xpidlDir; + + if (process.env.TASK_ID && !process.env.MOZ_XPT_ARTIFACTS_DIR) { + throw new Error( + "MOZ_XPT_ARTIFACTS_DIR must be set for this rule in automation" + ); + } + xpidlDir = process.env.MOZ_XPT_ARTIFACTS_DIR; + + if (!xpidlDir && process.env.MOZ_OBJDIR) { + xpidlDir = `${process.env.MOZ_OBJDIR}/dist/xpt_artifacts/`; + if (!fs.existsSync(xpidlDir)) { + xpidlDir = `${process.env.MOZ_OBJDIR}/config/makefiles/xpidl/`; + } + } + if (!xpidlDir) { + throw new Error( + "MOZ_OBJDIR must be defined in the environment for this rule, i.e. MOZ_OBJDIR=objdir-ff ./mach ..." + ); + } + if (xpidlData) { + return xpidlData; + } + let files = fs.readdirSync(`${xpidlDir}`); + // `Makefile` is an expected file in the directory. + if (files.length <= 1) { + throw new Error("Missing xpidl data files, maybe you need to build?"); + } + xpidlData = new Map(); + for (let file of files) { + if (!file.endsWith(".xpt")) { + continue; + } + let data = JSON.parse(fs.readFileSync(path.join(`${xpidlDir}`, file))); + for (let details of data) { + xpidlData.set(details.name, details); + } + } + return xpidlData; + }, + + /** + * Gets the abstract syntax tree (AST) of the JavaScript source code contained + * in sourceText. This matches the results for an eslint parser, see + * https://eslint.org/docs/developer-guide/working-with-custom-parsers. + * + * @param {String} sourceText + * Text containing valid JavaScript. + * @param {Object} astOptions + * Extra configuration to pass to the espree parser, these will override + * the configuration from getPermissiveConfig(). + * @param {Object} configOptions + * Extra options for getPermissiveConfig(). + * + * @return {Object} + * Returns an object containing `ast`, `scopeManager` and + * `visitorKeys` + */ + parseCode(sourceText, astOptions = {}, configOptions = {}) { + // Use a permissive config file to allow parsing of anything that Espree + // can parse. + let config = { ...this.getPermissiveConfig(configOptions), ...astOptions }; + + let parseResult = parser.parse(sourceText, config); + + let visitorKeys = parseResult.visitorKeys || defaultVisitorKeys; + + // eslint-scope doesn't support "latest" as a version, so we pass a really + // big number to ensure this always reads as the latest. + // xref https://github.com/eslint/eslint-scope/issues/74 + config.ecmaVersion = + config.ecmaVersion == "latest" ? 1e8 : config.ecmaVersion; + + return { + ast: parseResult, + scopeManager: parseResult.scopeManager || analyze(parseResult, config), + visitorKeys, + }; + }, + + /** + * A simplistic conversion of some AST nodes to a standard string form. + * + * @param {Object} node + * The AST node to convert. + * + * @return {String} + * The JS source for the node. + */ + getASTSource(node, context) { + switch (node.type) { + case "MemberExpression": + if (node.computed) { + let filename = context && context.getFilename(); + throw new Error( + `getASTSource unsupported computed MemberExpression in ${filename}` + ); + } + return ( + this.getASTSource(node.object) + + "." + + this.getASTSource(node.property) + ); + case "ThisExpression": + return "this"; + case "Identifier": + return node.name; + case "Literal": + return JSON.stringify(node.value); + case "CallExpression": + var args = node.arguments.map(a => this.getASTSource(a)).join(", "); + return this.getASTSource(node.callee) + "(" + args + ")"; + case "ObjectExpression": + return "{}"; + case "ExpressionStatement": + return this.getASTSource(node.expression) + ";"; + case "FunctionExpression": + return "function() {}"; + case "ArrayExpression": + return "[" + node.elements.map(this.getASTSource, this).join(",") + "]"; + case "ArrowFunctionExpression": + return "() => {}"; + case "AssignmentExpression": + return ( + this.getASTSource(node.left) + " = " + this.getASTSource(node.right) + ); + case "BinaryExpression": + return ( + this.getASTSource(node.left) + + " " + + node.operator + + " " + + this.getASTSource(node.right) + ); + case "UnaryExpression": + return node.operator + " " + this.getASTSource(node.argument); + default: + throw new Error("getASTSource unsupported node type: " + node.type); + } + }, + + /** + * This walks an AST in a manner similar to ESLint passing node events to the + * listener. The listener is expected to be a simple function + * which accepts node type, node and parents arguments. + * + * @param {Object} ast + * The AST to walk. + * @param {Array} visitorKeys + * The visitor keys to use for the AST. + * @param {Function} listener + * A callback function to call for the nodes. Passed three arguments, + * event type, node and an array of parent nodes for the current node. + */ + walkAST(ast, visitorKeys, listener) { + let parents = []; + + estraverse.traverse(ast, { + enter(node, parent) { + listener(node.type, node, parents); + + parents.push(node); + }, + + leave(node, parent) { + if (!parents.length) { + throw new Error("Left more nodes than entered."); + } + parents.pop(); + }, + + keys: visitorKeys, + }); + if (parents.length) { + throw new Error("Entered more nodes than left."); + } + }, + + /** + * Add a variable to the current scope. + * HACK: This relies on eslint internals so it could break at any time. + * + * @param {String} name + * The variable name to add to the scope. + * @param {ASTScope} scope + * The scope to add to. + * @param {boolean} writable + * Whether the global can be overwritten. + * @param {Object} [node] + * The AST node that defined the globals. + */ + addVarToScope(name, scope, writable, node) { + scope.__defineGeneric(name, scope.set, scope.variables, null, null); + + let variable = scope.set.get(name); + variable.eslintExplicitGlobal = false; + variable.writeable = writable; + if (node) { + variable.defs.push({ + type: "Variable", + node, + name: { name, parent: node.parent }, + }); + variable.identifiers.push(node); + } + + // Walk to the global scope which holds all undeclared variables. + while (scope.type != "global") { + scope = scope.upper; + } + + // "through" contains all references with no found definition. + scope.through = scope.through.filter(function (reference) { + if (reference.identifier.name != name) { + return true; + } + + // Links the variable and the reference. + // And this reference is removed from `Scope#through`. + reference.resolved = variable; + variable.references.push(reference); + return false; + }); + }, + + /** + * Adds a set of globals to a scope. + * + * @param {Array} globalVars + * An array of global variable names. + * @param {ASTScope} scope + * The scope. + * @param {Object} [node] + * The AST node that defined the globals. + */ + addGlobals(globalVars, scope, node) { + globalVars.forEach(v => + this.addVarToScope(v.name, scope, v.writable, v.explicit && node) + ); + }, + + /** + * To allow espree to parse almost any JavaScript we need as many features as + * possible turned on. This method returns that config. + * + * @param {Object} options + * { + * useBabel: {boolean} whether to set babelOptions. + * } + * @return {Object} + * Espree compatible permissive config. + */ + getPermissiveConfig({ useBabel = true } = {}) { + return { + range: true, + loc: true, + comment: true, + attachComment: true, + ecmaVersion: this.getECMAVersion(), + sourceType: "script", + }; + }, + + /** + * Returns the ECMA version of the recommended config. + * + * @return {Number} The ECMA version of the recommended config. + */ + getECMAVersion() { + return recommendedConfig.parserOptions.ecmaVersion; + }, + + /** + * Check whether it's inside top-level script. + * + * @param {Array} ancestors + * The parents of the current node. + * + * @return {Boolean} + * True or false + */ + getIsTopLevelScript(ancestors) { + for (let parent of ancestors) { + switch (parent.type) { + case "ArrowFunctionExpression": + case "FunctionDeclaration": + case "FunctionExpression": + case "PropertyDefinition": + case "StaticBlock": + return false; + } + } + return true; + }, + + isTopLevel(ancestors) { + for (let parent of ancestors) { + switch (parent.type) { + case "ArrowFunctionExpression": + case "FunctionDeclaration": + case "FunctionExpression": + case "PropertyDefinition": + case "StaticBlock": + case "BlockStatement": + return false; + } + } + return true; + }, + + /** + * Check whether `this` expression points the global this. + * + * @param {Array} ancestors + * The parents of the current node. + * + * @return {Boolean} + * True or false + */ + getIsGlobalThis(ancestors) { + for (let parent of ancestors) { + switch (parent.type) { + case "FunctionDeclaration": + case "FunctionExpression": + case "PropertyDefinition": + case "StaticBlock": + return false; + } + } + return true; + }, + + /** + * Check whether the node is evaluated at top-level script unconditionally. + * + * @param {Array} ancestors + * The parents of the current node. + * + * @return {Boolean} + * True or false + */ + getIsTopLevelAndUnconditionallyExecuted(ancestors) { + for (let parent of ancestors) { + switch (parent.type) { + // Control flow + case "IfStatement": + case "SwitchStatement": + case "TryStatement": + case "WhileStatement": + case "DoWhileStatement": + case "ForStatement": + case "ForInStatement": + case "ForOfStatement": + return false; + + // Function + case "FunctionDeclaration": + case "FunctionExpression": + case "ArrowFunctionExpression": + case "ClassBody": + return false; + + // Branch + case "LogicalExpression": + case "ConditionalExpression": + case "ChainExpression": + return false; + + case "AssignmentExpression": + switch (parent.operator) { + // Branch + case "||=": + case "&&=": + case "??=": + return false; + } + break; + + // Implicit branch (default value) + case "ObjectPattern": + case "ArrayPattern": + return false; + } + } + return true; + }, + + /** + * Check whether we might be in a test head file. + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsHeadFile(context) + * + * @return {Boolean} + * True or false + */ + getIsHeadFile(scope) { + var pathAndFilename = this.cleanUpPath(scope.getFilename()); + + return /.*[\\/]head(_.+)?\.js$/.test(pathAndFilename); + }, + + /** + * Gets the head files for a potential test file + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsHeadFile(context) + * + * @return {String[]} + * Paths to head files to load for the test + */ + getTestHeadFiles(scope) { + if (!this.getIsTest(scope)) { + return []; + } + + let filepath = this.cleanUpPath(scope.getFilename()); + let dir = path.dirname(filepath); + + let names = fs + .readdirSync(dir) + .filter( + name => + (name.startsWith("head") || name.startsWith("xpcshell-head")) && + name.endsWith(".js") + ) + .map(name => path.join(dir, name)); + return names; + }, + + /** + * Gets all the test manifest data for a directory + * + * @param {String} dir + * The directory + * + * @return {Array} + * An array of objects with file and manifest properties + */ + getManifestsForDirectory(dir) { + if (directoryManifests.has(dir)) { + return directoryManifests.get(dir); + } + + let manifests = []; + let names = []; + try { + names = fs.readdirSync(dir); + } catch (err) { + // Ignore directory not found, it might be faked by a test + if (err.code !== "ENOENT") { + throw err; + } + } + + for (let name of names) { + if (name.endsWith(".toml")) { + try { + const ast = toml.parseTOML( + fs.readFileSync(path.join(dir, name), "utf8") + ); + var manifest = {}; + ast.body.forEach(top => { + if (top.type == "TOMLTopLevelTable") { + top.body.forEach(obj => { + if (obj.type == "TOMLTable") { + manifest[obj.resolvedKey] = {}; + } + }); + } + }); + manifests.push({ + file: path.join(dir, name), + manifest, + }); + } catch (e) { + console.log( + "TOML ERROR: " + + e.message + + " @line: " + + e.lineNumber + + ", column: " + + e.column + ); + } + } + } + + directoryManifests.set(dir, manifests); + return manifests; + }, + + /** + * Gets the manifest file a test is listed in + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsHeadFile(context) + * + * @return {String} + * The path to the test manifest file + */ + getTestManifest(scope) { + let filepath = this.cleanUpPath(scope.getFilename()); + + let dir = path.dirname(filepath); + let filename = path.basename(filepath); + + for (let manifest of this.getManifestsForDirectory(dir)) { + if (filename in manifest.manifest) { + return manifest.file; + } + } + + return null; + }, + + /** + * Check whether we are in a test of some kind. + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsTest(context) + * + * @return {Boolean} + * True or false + */ + getIsTest(scope) { + // Regardless of the manifest name being in a manifest means we're a test. + let manifest = this.getTestManifest(scope); + if (manifest) { + return true; + } + + return !!this.getTestType(scope); + }, + + /* + * Check if this is an .sjs file. + */ + getIsSjs(scope) { + let filepath = this.cleanUpPath(scope.getFilename()); + + return path.extname(filepath) == ".sjs"; + }, + + /** + * Gets the type of test or null if this isn't a test. + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsHeadFile(context) + * + * @return {String or null} + * Test type: xpcshell, browser, chrome, mochitest + */ + getTestType(scope) { + let testTypes = ["browser", "xpcshell", "chrome", "mochitest", "a11y"]; + let manifest = this.getTestManifest(scope); + if (manifest) { + let name = path.basename(manifest); + for (let testType of testTypes) { + if (name.startsWith(testType)) { + return testType; + } + } + } + + let filepath = this.cleanUpPath(scope.getFilename()); + let filename = path.basename(filepath); + + if (filename.startsWith("browser_")) { + return "browser"; + } + + if (filename.startsWith("test_")) { + let parent = path.basename(path.dirname(filepath)); + for (let testType of testTypes) { + if (parent.startsWith(testType)) { + return testType; + } + } + + // It likely is a test, we're just not sure what kind. + return "unknown"; + } + + // Likely not a test + return null; + }, + + getIsWorker(filePath) { + let filename = path.basename(this.cleanUpPath(filePath)).toLowerCase(); + + return filename.includes("worker"); + }, + + /** + * Gets the root directory of the repository by walking up directories from + * this file until a .eslintignore file is found. If this fails, the same + * procedure will be attempted from the current working dir. + * @return {String} The absolute path of the repository directory + */ + get rootDir() { + if (!gRootDir) { + function searchUpForIgnore(dirName, filename) { + let parsed = path.parse(dirName); + while (parsed.root !== dirName) { + if (fs.existsSync(path.join(dirName, filename))) { + return dirName; + } + // Move up a level + dirName = parsed.dir; + parsed = path.parse(dirName); + } + return null; + } + + let possibleRoot = searchUpForIgnore( + path.dirname(module.filename), + ".eslintignore" + ); + if (!possibleRoot) { + possibleRoot = searchUpForIgnore(path.resolve(), ".eslintignore"); + } + if (!possibleRoot) { + possibleRoot = searchUpForIgnore(path.resolve(), "package.json"); + } + if (!possibleRoot) { + // We've couldn't find a root from the module or CWD, so lets just go + // for the CWD. We really don't want to throw if possible, as that + // tends to give confusing results when used with ESLint. + possibleRoot = process.cwd(); + } + + gRootDir = possibleRoot; + } + + return gRootDir; + }, + + /** + * ESLint may be executed from various places: from mach, at the root of the + * repository, or from a directory in the repository when, for instance, + * executed by a text editor's plugin. + * The value returned by context.getFileName() varies because of this. + * This helper function makes sure to return an absolute file path for the + * current context, by looking at process.cwd(). + * @param {Context} context + * @return {String} The absolute path + */ + getAbsoluteFilePath(context) { + var fileName = this.cleanUpPath(context.getFilename()); + var cwd = process.cwd(); + + if (path.isAbsolute(fileName)) { + // Case 2: executed from the repo's root with mach: + // fileName: /path/to/mozilla/repo/a/b/c/d.js + // cwd: /path/to/mozilla/repo + return fileName; + } else if (path.basename(fileName) == fileName) { + // Case 1b: executed from a nested directory, fileName is the base name + // without any path info (happens in Atom with linter-eslint) + return path.join(cwd, fileName); + } + // Case 1: executed form in a nested directory, e.g. from a text editor: + // fileName: a/b/c/d.js + // cwd: /path/to/mozilla/repo/a/b/c + var dirName = path.dirname(fileName); + return cwd.slice(0, cwd.length - dirName.length) + fileName; + }, + + /** + * When ESLint is run from SublimeText, paths retrieved from + * context.getFileName contain leading and trailing double-quote characters. + * These characters need to be removed. + */ + cleanUpPath(pathName) { + return pathName.replace(/^"/, "").replace(/"$/, ""); + }, + + get globalScriptPaths() { + return [ + path.join(this.rootDir, "browser", "base", "content", "browser.xhtml"), + path.join( + this.rootDir, + "browser", + "base", + "content", + "global-scripts.inc" + ), + ]; + }, + + isMozillaCentralBased() { + return fs.existsSync(this.globalScriptPaths[0]); + }, + + getSavedEnvironmentItems(environment) { + return require("./environments/saved-globals.json").environments[ + environment + ]; + }, + + getSavedRuleData(rule) { + return require("./rules/saved-rules-data.json").rulesData[rule]; + }, + + getBuildEnvironment() { + var { execFileSync } = require("child_process"); + var output = execFileSync( + path.join(this.rootDir, "mach"), + ["environment", "--format=json"], + { silent: true } + ); + return JSON.parse(output); + }, + + /** + * Extract the path of require (and require-like) helpers used in DevTools. + */ + getDevToolsRequirePath(node) { + if ( + node.callee.type == "Identifier" && + node.callee.name == "require" && + node.arguments.length == 1 && + node.arguments[0].type == "Literal" + ) { + return node.arguments[0].value; + } else if ( + node.callee.type == "MemberExpression" && + node.callee.property.type == "Identifier" && + node.callee.property.name == "lazyRequireGetter" && + node.arguments.length >= 3 && + node.arguments[2].type == "Literal" + ) { + return node.arguments[2].value; + } + return null; + }, + + /** + * Returns property name from MemberExpression. Also accepts Identifier for consistency. + * @param {import("estree").MemberExpression | import("estree").Identifier} node + * @returns {string | null} + * + * @example `foo` gives "foo" + * @example `foo.bar` gives "bar" + * @example `foo.bar.baz` gives "baz" + */ + maybeGetMemberPropertyName(node) { + if (node.type === "MemberExpression") { + return node.property.name; + } + if (node.type === "Identifier") { + return node.name; + } + return null; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js new file mode 100644 index 0000000000..0801958597 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js @@ -0,0 +1,102 @@ +/** + * @fileoverview A collection of rules that help enforce JavaScript coding + * standard and avoid common errors in the Mozilla project. + * 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/. + */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Plugin Definition +// ------------------------------------------------------------------------------ +module.exports = { + configs: { + "browser-test": require("../lib/configs/browser-test"), + "chrome-test": require("../lib/configs/chrome-test"), + "mochitest-test": require("../lib/configs/mochitest-test"), + recommended: require("../lib/configs/recommended"), + "require-jsdoc": require("../lib/configs/require-jsdoc"), + "valid-jsdoc": require("../lib/configs/valid-jsdoc"), + "xpcshell-test": require("../lib/configs/xpcshell-test"), + }, + environments: { + "browser-window": require("../lib/environments/browser-window.js"), + "chrome-script": require("../lib/environments/chrome-script.js"), + "chrome-worker": require("../lib/environments/chrome-worker.js"), + "frame-script": require("../lib/environments/frame-script.js"), + jsm: require("../lib/environments/jsm.js"), + privileged: require("../lib/environments/privileged.js"), + "process-script": require("../lib/environments/process-script.js"), + "remote-page": require("../lib/environments/remote-page.js"), + simpletest: require("../lib/environments/simpletest.js"), + sjs: require("../lib/environments/sjs.js"), + "special-powers-sandbox": require("../lib/environments/special-powers-sandbox.js"), + specific: require("../lib/environments/specific"), + testharness: require("../lib/environments/testharness.js"), + xpcshell: require("../lib/environments/xpcshell.js"), + }, + rules: { + "avoid-Date-timing": require("../lib/rules/avoid-Date-timing"), + "avoid-removeChild": require("../lib/rules/avoid-removeChild"), + "balanced-listeners": require("../lib/rules/balanced-listeners"), + "balanced-observers": require("../lib/rules/balanced-observers"), + "consistent-if-bracing": require("../lib/rules/consistent-if-bracing"), + "import-browser-window-globals": require("../lib/rules/import-browser-window-globals"), + "import-content-task-globals": require("../lib/rules/import-content-task-globals"), + "import-globals": require("../lib/rules/import-globals"), + "import-headjs-globals": require("../lib/rules/import-headjs-globals"), + "lazy-getter-object-name": require("../lib/rules/lazy-getter-object-name"), + "mark-exported-symbols-as-used": require("../lib/rules/mark-exported-symbols-as-used"), + "mark-test-function-used": require("../lib/rules/mark-test-function-used"), + "no-aArgs": require("../lib/rules/no-aArgs"), + "no-addtask-setup": require("../lib/rules/no-addtask-setup"), + "no-arbitrary-setTimeout": require("../lib/rules/no-arbitrary-setTimeout"), + "no-browser-refs-in-toolkit": require("../lib/rules/no-browser-refs-in-toolkit"), + "no-compare-against-boolean-literals": require("../lib/rules/no-compare-against-boolean-literals"), + "no-comparison-or-assignment-inside-ok": require("../lib/rules/no-comparison-or-assignment-inside-ok"), + "no-cu-reportError": require("../lib/rules/no-cu-reportError"), + "no-define-cc-etc": require("../lib/rules/no-define-cc-etc"), + "no-redeclare-with-import-autofix": require("../lib/rules/no-redeclare-with-import-autofix"), + "no-throw-cr-literal": require("../lib/rules/no-throw-cr-literal"), + "no-useless-parameters": require("../lib/rules/no-useless-parameters"), + "no-useless-removeEventListener": require("../lib/rules/no-useless-removeEventListener"), + "no-useless-run-test": require("../lib/rules/no-useless-run-test"), + "prefer-boolean-length-check": require("../lib/rules/prefer-boolean-length-check"), + "prefer-formatValues": require("../lib/rules/prefer-formatValues"), + "reject-addtask-only": require("../lib/rules/reject-addtask-only"), + "reject-chromeutils-import": require("../lib/rules/reject-chromeutils-import"), + "reject-chromeutils-import-params": require("../lib/rules/reject-chromeutils-import-params"), + "reject-eager-module-in-lazy-getter": require("../lib/rules/reject-eager-module-in-lazy-getter"), + "reject-global-this": require("../lib/rules/reject-global-this"), + "reject-globalThis-modification": require("../lib/rules/reject-globalThis-modification"), + "reject-import-system-module-from-non-system": require("../lib/rules/reject-import-system-module-from-non-system"), + "reject-importGlobalProperties": require("../lib/rules/reject-importGlobalProperties"), + "reject-lazy-imports-into-globals": require("../lib/rules/reject-lazy-imports-into-globals"), + "reject-mixing-eager-and-lazy": require("../lib/rules/reject-mixing-eager-and-lazy"), + "reject-multiple-getters-calls": require("../lib/rules/reject-multiple-getters-calls"), + "reject-scriptableunicodeconverter": require("../lib/rules/reject-scriptableunicodeconverter"), + "reject-relative-requires": require("../lib/rules/reject-relative-requires"), + "reject-some-requires": require("../lib/rules/reject-some-requires"), + "reject-top-level-await": require("../lib/rules/reject-top-level-await"), + "rejects-requires-await": require("../lib/rules/rejects-requires-await"), + "use-cc-etc": require("../lib/rules/use-cc-etc"), + "use-chromeutils-definelazygetter": require("../lib/rules/use-chromeutils-definelazygetter"), + "use-chromeutils-generateqi": require("../lib/rules/use-chromeutils-generateqi"), + "use-chromeutils-import": require("../lib/rules/use-chromeutils-import"), + "use-console-createInstance": require("../lib/rules/use-console-createInstance"), + "use-default-preference-values": require("../lib/rules/use-default-preference-values"), + "use-ownerGlobal": require("../lib/rules/use-ownerGlobal"), + "use-includes-instead-of-indexOf": require("../lib/rules/use-includes-instead-of-indexOf"), + "use-isInstance": require("./rules/use-isInstance"), + "use-returnValue": require("../lib/rules/use-returnValue"), + "use-services": require("../lib/rules/use-services"), + "use-static-import": require("../lib/rules/use-static-import"), + "valid-ci-uses": require("../lib/rules/valid-ci-uses"), + "valid-lazy": require("../lib/rules/valid-lazy"), + "valid-services": require("../lib/rules/valid-services"), + "valid-services-property": require("../lib/rules/valid-services-property"), + "var-only-at-top-level": require("../lib/rules/var-only-at-top-level"), + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js new file mode 100644 index 0000000000..437c53e244 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js @@ -0,0 +1,61 @@ +/** + * @fileoverview Disallow using Date for timing in performance sensitive code + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/avoid-Date-timing.html", + }, + messages: { + usePerfNow: + "use performance.now() instead of Date.now() for timing measurements", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type !== "MemberExpression" || + callee.object.type !== "Identifier" || + callee.object.name !== "Date" || + callee.property.type !== "Identifier" || + callee.property.name !== "now" + ) { + return; + } + + context.report({ + node, + messageId: "usePerfNow", + }); + }, + + NewExpression(node) { + let callee = node.callee; + if ( + callee.type !== "Identifier" || + callee.name !== "Date" || + node.arguments.length + ) { + return; + } + + context.report({ + node, + messageId: "usePerfNow", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js new file mode 100644 index 0000000000..6c74d8aa59 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js @@ -0,0 +1,70 @@ +/** + * @fileoverview Reject using element.parentNode.removeChild(element) when + * element.remove() can be used instead. + * + * 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/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/avoid-removeChild.html", + }, + messages: { + useRemove: + "use element.remove() instead of element.parentNode.removeChild(element)", + useFirstChildRemove: + "use element.firstChild.remove() instead of element.removeChild(element.firstChild)", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type !== "MemberExpression" || + callee.property.type !== "Identifier" || + callee.property.name != "removeChild" || + node.arguments.length != 1 + ) { + return; + } + + if ( + callee.object.type == "MemberExpression" && + callee.object.property.type == "Identifier" && + callee.object.property.name == "parentNode" && + helpers.getASTSource(callee.object.object, context) == + helpers.getASTSource(node.arguments[0]) + ) { + context.report({ + node, + messageId: "useRemove", + }); + } + + if ( + node.arguments[0].type == "MemberExpression" && + node.arguments[0].property.type == "Identifier" && + node.arguments[0].property.name == "firstChild" && + helpers.getASTSource(callee.object, context) == + helpers.getASTSource(node.arguments[0].object) + ) { + context.report({ + node, + messageId: "useFirstChildRemove", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js new file mode 100644 index 0000000000..f1c98a01bc --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js @@ -0,0 +1,149 @@ +/** + * @fileoverview Check that there's a removeEventListener for each + * addEventListener and an off for each on. + * Note that for now, this rule is rather simple in that it only checks that + * for each event name there is both an add and remove listener. It doesn't + * check that these are called on the right objects or with the same callback. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/balanced-listeners.html", + }, + messages: { + noCorresponding: + "No corresponding '{{functionName}}({{type}})' was found.", + }, + schema: [], + type: "problem", + }, + + create(context) { + var DICTIONARY = { + addEventListener: "removeEventListener", + on: "off", + }; + // Invert this dictionary to make it easy later. + var INVERTED_DICTIONARY = {}; + for (var i in DICTIONARY) { + INVERTED_DICTIONARY[DICTIONARY[i]] = i; + } + + // Collect the add/remove listeners in these 2 arrays. + var addedListeners = []; + var removedListeners = []; + + function addAddedListener(node) { + var capture = false; + let options = node.arguments[2]; + if (options) { + if (options.type == "ObjectExpression") { + if ( + options.properties.some( + p => p.key.name == "once" && p.value.value === true + ) + ) { + // No point in adding listeners using the 'once' option. + return; + } + capture = options.properties.some( + p => p.key.name == "capture" && p.value.value === true + ); + } else { + capture = options.value; + } + } + addedListeners.push({ + functionName: node.callee.property.name, + type: node.arguments[0].value, + node: node.callee.property, + useCapture: capture, + }); + } + + function addRemovedListener(node) { + var capture = false; + let options = node.arguments[2]; + if (options) { + if (options.type == "ObjectExpression") { + capture = options.properties.some( + p => p.key.name == "capture" && p.value.value === true + ); + } else { + capture = options.value; + } + } + removedListeners.push({ + functionName: node.callee.property.name, + type: node.arguments[0].value, + useCapture: capture, + }); + } + + function getUnbalancedListeners() { + var unbalanced = []; + + for (var j = 0; j < addedListeners.length; j++) { + if (!hasRemovedListener(addedListeners[j])) { + unbalanced.push(addedListeners[j]); + } + } + addedListeners = removedListeners = []; + + return unbalanced; + } + + function hasRemovedListener(addedListener) { + for (var k = 0; k < removedListeners.length; k++) { + var listener = removedListeners[k]; + if ( + DICTIONARY[addedListener.functionName] === listener.functionName && + addedListener.type === listener.type && + addedListener.useCapture === listener.useCapture + ) { + return true; + } + } + + return false; + } + + return { + CallExpression(node) { + if (node.arguments.length === 0) { + return; + } + + if (node.callee.type === "MemberExpression") { + var listenerMethodName = node.callee.property.name; + + if (DICTIONARY.hasOwnProperty(listenerMethodName)) { + addAddedListener(node); + } else if (INVERTED_DICTIONARY.hasOwnProperty(listenerMethodName)) { + addRemovedListener(node); + } + } + }, + + "Program:exit": function () { + getUnbalancedListeners().forEach(function (listener) { + context.report({ + node: listener.node, + messageId: "noCorresponding", + data: { + functionName: DICTIONARY[listener.functionName], + type: listener.type, + }, + }); + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js new file mode 100644 index 0000000000..854fbc9a63 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js @@ -0,0 +1,121 @@ +/** + * @fileoverview Check that there's a Services.(prefs|obs).removeObserver for + * each addObserver. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/balanced-observers.html", + }, + messages: { + noCorresponding: + "No corresponding 'removeObserver(\"{{observable}}\")' was found.", + }, + schema: [], + type: "problem", + }, + + create(context) { + var addedObservers = []; + var removedObservers = []; + + function getObserverAPI(node) { + const object = node.callee.object; + if ( + object.type == "MemberExpression" && + object.property.type == "Identifier" + ) { + return object.property.name; + } + return null; + } + + function isServicesObserver(api) { + return api == "obs" || api == "prefs"; + } + + function getObservableName(node, api) { + if (api === "obs") { + return node.arguments[1].value; + } + return node.arguments[0].value; + } + + function addAddedObserver(node) { + const api = getObserverAPI(node); + if (!isServicesObserver(api)) { + return; + } + + addedObservers.push({ + functionName: node.callee.property.name, + observable: getObservableName(node, api), + node: node.callee.property, + }); + } + + function addRemovedObserver(node) { + const api = getObserverAPI(node); + if (!isServicesObserver(api)) { + return; + } + + removedObservers.push({ + functionName: node.callee.property.name, + observable: getObservableName(node, api), + }); + } + + function getUnbalancedObservers() { + const unbalanced = addedObservers.filter( + observer => !hasRemovedObserver(observer) + ); + addedObservers = removedObservers = []; + + return unbalanced; + } + + function hasRemovedObserver(addedObserver) { + return removedObservers.some( + observer => addedObserver.observable === observer.observable + ); + } + + return { + CallExpression(node) { + if (node.arguments.length === 0) { + return; + } + + if (node.callee.type === "MemberExpression") { + var methodName = node.callee.property.name; + + if (methodName === "addObserver") { + addAddedObserver(node); + } else if (methodName === "removeObserver") { + addRemovedObserver(node); + } + } + }, + + "Program:exit": function () { + getUnbalancedObservers().forEach(function (observer) { + context.report({ + node: observer.node, + messageId: "noCorresponding", + data: { + observable: observer.observable, + }, + }); + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js new file mode 100644 index 0000000000..0c9c9a342f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js @@ -0,0 +1,54 @@ +/** + * @fileoverview checks if/else if/else bracing is consistent + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/consistent-if-bracing.html", + }, + messages: { + consistentIfBracing: "Bracing of if..else bodies should be consistent.", + }, + schema: [], + type: "layout", + }, + + create(context) { + return { + IfStatement(node) { + if (node.parent.type !== "IfStatement") { + let types = new Set(); + for ( + let currentNode = node; + currentNode; + currentNode = currentNode.alternate + ) { + let type = currentNode.consequent.type; + types.add(type == "BlockStatement" ? "Block" : "NotBlock"); + if ( + currentNode.alternate && + currentNode.alternate.type !== "IfStatement" + ) { + type = currentNode.alternate.type; + types.add(type == "BlockStatement" ? "Block" : "NotBlock"); + break; + } + } + if (types.size > 1) { + context.report({ + node, + messageId: "consistentIfBracing", + }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js new file mode 100644 index 0000000000..7a099ba340 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js @@ -0,0 +1,50 @@ +/** + * @fileoverview For scripts included in browser-window, this will automatically + * inject the browser-window global scopes into the file. + * + * 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/. + */ + +"use strict"; + +var path = require("path"); +var helpers = require("../helpers"); +var browserWindowEnv = require("../environments/browser-window"); + +module.exports = { + // This rule currently has no messages. + // eslint-disable-next-line eslint-plugin/prefer-message-ids + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-browser-window-globals.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + Program(node) { + let filePath = helpers.getAbsoluteFilePath(context); + let relativePath = path.relative(helpers.rootDir, filePath); + // We need to translate the path on Windows, due to the change + // from \ to /, and browserjsScripts assumes Posix. + if (path.win32) { + relativePath = relativePath.split(path.sep).join("/"); + } + + if (browserWindowEnv.browserjsScripts?.includes(relativePath)) { + for (let global in browserWindowEnv.globals) { + helpers.addVarToScope( + global, + context.getScope(), + browserWindowEnv.globals[global] + ); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js new file mode 100644 index 0000000000..e2b66ce8b0 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js @@ -0,0 +1,73 @@ +/** + * @fileoverview For ContentTask.spawn, this will automatically declare the + * frame script variables in the global scope. + * Note: due to the way ESLint works, it appears it is only + * easy to declare these variables on a file-global scope, rather + * than function global. + * + * 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/. + */ + +"use strict"; + +var helpers = require("../helpers"); +var frameScriptEnv = require("../environments/frame-script"); +var sandboxEnv = require("../environments/special-powers-sandbox"); + +module.exports = { + // eslint-disable-next-line eslint-plugin/prefer-message-ids + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-content-task-globals.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + "CallExpression[callee.object.name='ContentTask'][callee.property.name='spawn']": + function (node) { + // testing/mochitest/BrowserTestUtils/content/content-task.js + // This script is loaded as a sub script into a frame script. + for (let [name, value] of Object.entries(frameScriptEnv.globals)) { + helpers.addVarToScope(name, context.getScope(), value); + } + }, + "CallExpression[callee.object.name='SpecialPowers'][callee.property.name='spawn']": + function (node) { + for (let [name, value] of Object.entries(sandboxEnv.globals)) { + helpers.addVarToScope(name, context.getScope(), value); + } + let globals = [ + // testing/specialpowers/content/SpecialPowersChild.sys.mjs + // SpecialPowersChild._spawnTask + "SpecialPowers", + "ContentTaskUtils", + "content", + "docShell", + ]; + for (let global of globals) { + helpers.addVarToScope(global, context.getScope(), false); + } + }, + "CallExpression[callee.object.name='SpecialPowers'][callee.property.name='spawnChrome']": + function (node) { + for (let [name, value] of Object.entries(sandboxEnv.globals)) { + helpers.addVarToScope(name, context.getScope(), value); + } + let globals = [ + // testing/specialpowers/content/SpecialPowersParent.sys.mjs + // SpecialPowersParent._spawnChrome + "windowGlobalParent", + "browsingContext", + ]; + for (let global of globals) { + helpers.addVarToScope(global, context.getScope(), false); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js new file mode 100644 index 0000000000..abbab511ff --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js @@ -0,0 +1,21 @@ +/** + * @fileoverview Discovers all globals for the current file. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-globals.html", + }, + schema: [], + type: "problem", + }, + + create: require("../globals").getESLintGlobalParser, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js new file mode 100644 index 0000000000..d4fa484b99 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js @@ -0,0 +1,51 @@ +/** + * @fileoverview Import globals from head.js and from any files that were + * imported by head.js (as far as we can correctly resolve the path). + * + * 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/. + */ + +"use strict"; + +var fs = require("fs"); +var helpers = require("../helpers"); +var globals = require("../globals"); + +function importHead(context, path, node) { + try { + let stats = fs.statSync(path); + if (!stats.isFile()) { + return; + } + } catch (e) { + return; + } + + let newGlobals = globals.getGlobalsForFile(path); + helpers.addGlobals(newGlobals, context.getScope()); +} + +module.exports = { + // This rule currently has no messages. + // eslint-disable-next-line eslint-plugin/prefer-message-ids + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-headjs-globals.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + Program(node) { + let heads = helpers.getTestHeadFiles(context); + for (let head of heads) { + importHead(context, head, node); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js new file mode 100644 index 0000000000..b18cbc3725 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js @@ -0,0 +1,48 @@ +/** + * @fileoverview Enforce the standard object name for + * ChromeUtils.defineESModuleGetters + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node.type === "Identifier" && node.name === id; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/lazy-getter-object-name.html", + }, + messages: { + mustUseLazy: + "The variable name of the object passed to ChromeUtils.defineESModuleGetters must be `lazy`", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + let { callee } = node; + if ( + callee.type === "MemberExpression" && + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "defineESModuleGetters") && + node.arguments.length >= 1 && + !isIdentifier(node.arguments[0], "lazy") + ) { + context.report({ + node, + messageId: "mustUseLazy", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js new file mode 100644 index 0000000000..5d0e57e4c8 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js @@ -0,0 +1,90 @@ +/** + * @fileoverview Simply marks exported symbols as used. Designed for use in + * .jsm files only. + * + * 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/. + */ + +"use strict"; + +function markArrayElementsAsUsed(context, node, expression) { + if (expression.type != "ArrayExpression") { + context.report({ + node, + messageId: "nonArrayAssignedToImported", + }); + return; + } + + for (let element of expression.elements) { + context.markVariableAsUsed(element.value); + } + // Also mark EXPORTED_SYMBOLS as used. + context.markVariableAsUsed("EXPORTED_SYMBOLS"); +} + +// Ignore assignments not in the global scope, e.g. where special module +// definitions are required due to having different ways of importing files, +// e.g. osfile. +function isGlobalScope(context) { + return !context.getScope().upper; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/mark-exported-symbols-as-used.html", + }, + messages: { + useLetForExported: + "EXPORTED_SYMBOLS cannot be declared via `let`. Use `var` or `this.EXPORTED_SYMBOLS =`", + nonArrayAssignedToImported: + "Unexpected assignment of non-Array to EXPORTED_SYMBOLS", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + AssignmentExpression(node, parents) { + if ( + node.operator === "=" && + node.left.type === "MemberExpression" && + node.left.object.type === "ThisExpression" && + node.left.property.name === "EXPORTED_SYMBOLS" && + isGlobalScope(context) + ) { + markArrayElementsAsUsed(context, node, node.right); + } + }, + + VariableDeclaration(node, parents) { + if (!isGlobalScope(context)) { + return; + } + + for (let item of node.declarations) { + if ( + item.id && + item.id.type == "Identifier" && + item.id.name === "EXPORTED_SYMBOLS" + ) { + if (node.kind === "let") { + // The use of 'let' isn't allowed as the lexical scope may die after + // the script executes. + context.report({ + node, + messageId: "useLetForExported", + }); + } + + markArrayElementsAsUsed(context, node, item.init); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js new file mode 100644 index 0000000000..4afe8a70ac --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Simply marks `test` (the test method) or `run_test` as used + * when in mochitests or xpcshell tests respectively. This avoids ESLint telling + * us that the function is never called. + * + * 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/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + // This rule currently has no messages. + // eslint-disable-next-line eslint-plugin/prefer-message-ids + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/mark-test-function-used.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + Program() { + let testType = helpers.getTestType(context); + if (testType == "browser") { + context.markVariableAsUsed("test"); + } + + if (testType == "xpcshell") { + context.markVariableAsUsed("run_test"); + } + + if (helpers.getIsSjs(context)) { + context.markVariableAsUsed("handleRequest"); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js new file mode 100644 index 0000000000..7135890761 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js @@ -0,0 +1,57 @@ +/** + * @fileoverview warns against using hungarian notation in function arguments + * (i.e. aArg). + * + * 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/. + */ + +"use strict"; + +function isPrefixed(name) { + return name.length >= 2 && /^a[A-Z]/.test(name); +} + +function deHungarianize(name) { + return name.substring(1, 2).toLowerCase() + name.substring(2, name.length); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-aArgs.html", + }, + messages: { + dontUseHungarian: + "Parameter '{{name}}' uses Hungarian Notation, consider using '{{suggestion}}' instead.", + }, + schema: [], + type: "layout", + }, + + create(context) { + function checkFunction(node) { + for (var i = 0; i < node.params.length; i++) { + var param = node.params[i]; + if (param.name && isPrefixed(param.name)) { + var errorObj = { + name: param.name, + suggestion: deHungarianize(param.name), + }; + context.report({ + node: param, + messageId: "dontUseHungarian", + data: errorObj, + }); + } + } + } + + return { + FunctionDeclaration: checkFunction, + ArrowFunctionExpression: checkFunction, + FunctionExpression: checkFunction, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js new file mode 100644 index 0000000000..e711252e09 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Reject `add_task(async function setup` or similar patterns in + * favour of add_setup. + * + * 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/. + */ + +"use strict"; + +function isNamedLikeSetup(name) { + return /^(init|setup)$/i.test(name); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-addtask-setup.html", + }, + fixable: "code", + messages: { + useAddSetup: "Do not use add_task() for setup, use add_setup() instead.", + }, + schema: [], + type: "suggestion", + }, + create(context) { + return { + "Program > ExpressionStatement > CallExpression": function (node) { + let callee = node.callee; + if (callee.type === "Identifier" && callee.name === "add_task") { + let arg = node.arguments[0]; + if ( + arg.type !== "FunctionExpression" || + !arg.id || + !isNamedLikeSetup(arg.id.name) + ) { + return; + } + context.report({ + node, + messageId: "useAddSetup", + fix: fixer => { + let range = [node.callee.range[0], arg.id.range[1]]; + let asyncOrNot = arg.async ? "async " : ""; + return fixer.replaceTextRange( + range, + `add_setup(${asyncOrNot}function` + ); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js new file mode 100644 index 0000000000..d0e891292d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js @@ -0,0 +1,65 @@ +/** + * @fileoverview Reject use of non-zero values in setTimeout + * + * 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/. + */ + +"use strict"; + +var helpers = require("../helpers"); +var testTypes = new Set(["browser", "xpcshell"]); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-arbitrary-setTimeout.html", + }, + messages: { + listenForEvents: + "listen for events instead of setTimeout() with arbitrary delay", + }, + schema: [], + type: "problem", + }, + + create(context) { + // We don't want to run this on mochitest plain as it already + // prevents flaky setTimeout at runtime. This check is built-in + // to the rule itself as sometimes other tests can live alongside + // plain mochitests and so it can't be configured via eslintrc. + if (!testTypes.has(helpers.getTestType(context))) { + return {}; + } + + return { + CallExpression(node) { + let callee = node.callee; + if (callee.type === "MemberExpression") { + if ( + callee.property.name !== "setTimeout" || + callee.object.name !== "window" || + node.arguments.length < 2 + ) { + return; + } + } else if (callee.type === "Identifier") { + if (callee.name !== "setTimeout" || node.arguments.length < 2) { + return; + } + } else { + return; + } + + let timeout = node.arguments[1]; + if (timeout.type !== "Literal" || timeout.value > 0) { + context.report({ + node, + messageId: "listenForEvents", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js new file mode 100644 index 0000000000..fea94d364e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js @@ -0,0 +1,48 @@ +/** + * @fileoverview Reject use of browser/-based references from code in + * directories like toolkit/ that ought not to depend on + * running inside desktop Firefox. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-browser-refs-in-toolkit.html", + }, + messages: { + noBrowserChrome: + "> {{url}} is part of Desktop Firefox and cannot be unconditionally " + + "used by this code (which has to also work elsewhere).", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + Literal(node) { + if (typeof node.value != "string") { + return; + } + if ( + node.value.startsWith("chrome://browser") || + node.value.startsWith("resource:///") || + node.value.startsWith("resource://app/") || + (node.value.startsWith("browser/") && node.value.endsWith(".ftl")) + ) { + context.report({ + node, + messageId: "noBrowserChrome", + data: { url: node.value }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js new file mode 100644 index 0000000000..cf52b2ad21 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js @@ -0,0 +1,40 @@ +/** + * @fileoverview Restrict comparing against `true` or `false`. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-compare-against-boolean-literals.html", + }, + messages: { + noCompareBoolean: + "Don't compare for inexact equality against boolean literals", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + BinaryExpression(node) { + if ( + ["==", "!="].includes(node.operator) && + (["true", "false"].includes(node.left.raw) || + ["true", "false"].includes(node.right.raw)) + ) { + context.report({ + node, + messageId: "noCompareBoolean", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js new file mode 100644 index 0000000000..9bab06b000 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js @@ -0,0 +1,80 @@ +/** + * @fileoverview Don't allow accidental assignments inside `ok()`, + * and encourage people to use appropriate alternatives + * when using comparisons between 2 values. + * + * 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/. + */ + +"use strict"; + +const operatorToAssertionMap = { + "==": "Assert.equal", + "===": "Assert.strictEqual", + "!=": "Assert.notEqual", + "!==": "Assert.notStrictEqual", + ">": "Assert.greater", + "<": "Assert.less", + "<=": "Assert.lessOrEqual", + ">=": "Assert.greaterOrEqual", +}; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-comparison-or-assignment-inside-ok.html", + }, + fixable: "code", + messages: { + assignment: + "Assigning to a variable inside ok() is odd - did you mean to compare the two?", + comparison: + "Use dedicated assertion methods rather than ok(a {{operator}} b).", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + const exprs = new Set(["BinaryExpression", "AssignmentExpression"]); + return { + CallExpression(node) { + if (node.callee.type != "Identifier" || node.callee.name != "ok") { + return; + } + let firstArg = node.arguments[0]; + if (!exprs.has(firstArg.type)) { + return; + } + if (firstArg.type == "AssignmentExpression") { + context.report({ + node: firstArg, + messageId: "assignment", + }); + } else if ( + firstArg.type == "BinaryExpression" && + operatorToAssertionMap.hasOwnProperty(firstArg.operator) + ) { + context.report({ + node, + messageId: "comparison", + data: { operator: firstArg.operator }, + fix: fixer => { + let left = context.sourceCode.getText(firstArg.left); + let right = context.sourceCode.getText(firstArg.right); + return [ + fixer.replaceText(firstArg, left + ", " + right), + fixer.replaceText( + node.callee, + operatorToAssertionMap[firstArg.operator] + ), + ]; + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js new file mode 100644 index 0000000000..85daa8823e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js @@ -0,0 +1,130 @@ +/** + * @fileoverview Reject common XPCOM methods called with useless optional + * parameters, or non-existent parameters. + * + * 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/. + */ + +"use strict"; + +function isCuReportError(node) { + return ( + node.type == "MemberExpression" && + node.object.type == "Identifier" && + node.object.name == "Cu" && + node.property.type == "Identifier" && + node.property.name == "reportError" + ); +} + +function isConcatenation(node) { + return node.type == "BinaryExpression" && node.operator == "+"; +} + +function isIdentOrMember(node) { + return node.type == "MemberExpression" || node.type == "Identifier"; +} + +function isLiteralOrConcat(node) { + return node.type == "Literal" || isConcatenation(node); +} + +function replaceConcatWithComma(fixer, node) { + let fixes = []; + let didFixTrailingIdentifier = false; + let recursiveFixes; + let trailingIdentifier; + // Deal with recursion first: + if (isConcatenation(node.right)) { + // Uh oh. If the RHS is a concatenation, there are parens involved, + // e.g.: + // console.error("literal" + (b + "literal")); + // It's pretty much impossible to guess what to do here so bail out: + return { fixes: [], trailingIdentifier: false }; + } + if (isConcatenation(node.left)) { + ({ fixes: recursiveFixes, trailingIdentifier } = replaceConcatWithComma( + fixer, + node.left + )); + fixes.push(...recursiveFixes); + } + // If the left is an identifier or memberexpression, and the right is a + // literal or concatenation - or vice versa - replace a + with a comma: + if ( + (isIdentOrMember(node.left) && isLiteralOrConcat(node.right)) || + (isIdentOrMember(node.right) && isLiteralOrConcat(node.left)) || + // Or if the rhs is a literal/concatenation, while the right-most part of + // the lhs is also an identifier (need 2 commas either side!) + (trailingIdentifier && isLiteralOrConcat(node.right)) + ) { + fixes.push( + fixer.replaceTextRange([node.left.range[1], node.right.range[0]], ", ") + ); + didFixTrailingIdentifier = isIdentOrMember(node.right); + } + return { fixes, trailingIdentifier: didFixTrailingIdentifier }; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-cu-reportError.html", + }, + fixable: "code", + messages: { + useConsoleError: "Please use console.error instead of Cu.reportError", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let checkNodes = []; + if (isCuReportError(node.callee)) { + // Handles cases of `Cu.reportError()`. + if (node.arguments.length > 1) { + // TODO: Bug 1802347 For initial landing, we allow the two + // argument form of Cu.reportError as the second argument is a stack + // argument which is more complicated to deal with. + return; + } + checkNodes = [node.callee]; + } else if (node.arguments.length >= 1) { + // Handles cases of `.foo(Cu.reportError)`. + checkNodes = node.arguments.filter(n => isCuReportError(n)); + } + + for (let checkNode of checkNodes) { + context.report({ + node, + fix: fixer => { + let fixes = [ + fixer.replaceText(checkNode.object, "console"), + fixer.replaceText(checkNode.property, "error"), + ]; + // If we're adding stuff together as an argument, split + // into multiple arguments instead: + if ( + checkNode == node.callee && + isConcatenation(node.arguments[0]) + ) { + let { fixes: recursiveFixes } = replaceConcatWithComma( + fixer, + node.arguments[0] + ); + fixes.push(...recursiveFixes); + } + return fixes; + }, + messageId: "useConsoleError", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js new file mode 100644 index 0000000000..05e7648632 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Reject defining Cc/Ci/Cr/Cu. + * + * 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/. + */ + +"use strict"; + +const componentsBlacklist = ["Cc", "Ci", "Cr", "Cu"]; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-define-cc-etc.html", + }, + messages: { + noSeparateDefinition: + "{{name}} is now defined in global scope, a separate definition is no longer necessary.", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + VariableDeclarator(node) { + if ( + node.id.type == "Identifier" && + componentsBlacklist.includes(node.id.name) + ) { + context.report({ + node, + messageId: "noSeparateDefinition", + data: { name: node.id.name }, + }); + } + + if (node.id.type == "ObjectPattern") { + for (let property of node.id.properties) { + if ( + property.type == "Property" && + componentsBlacklist.includes(property.value.name) + ) { + context.report({ + node, + messageId: "noSeparateDefinition", + data: { name: property.value.name }, + }); + } + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js new file mode 100644 index 0000000000..d914e003d3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js @@ -0,0 +1,160 @@ +/** + * 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/. + */ + +"use strict"; + +const { dirname, join } = require("path"); + +const eslintBasePath = dirname(require.resolve("eslint")); + +const noredeclarePath = join(eslintBasePath, "rules/no-redeclare.js"); +const baseRule = require(noredeclarePath); +const astUtils = require(join(eslintBasePath, "rules/utils/ast-utils.js")); + +// Hack alert: our eslint env is pretty confused about `require` and +// `loader` for devtools modules - so ignore it for now. +// See bug 1812547 +const gIgnoredImports = new Set(["loader", "require"]); + +/** + * Create a trap for a call to `report` that the original rule is + * trying to make on `context`. + * + * Returns a function that forwards to `report` but provides a fixer + * for redeclared imports that just removes those imports. + * + * @return {function} + */ +function trapReport(context) { + return function (obj) { + let declarator = obj.node.parent; + while ( + declarator && + declarator.parent && + declarator.type != "VariableDeclarator" + ) { + declarator = declarator.parent; + } + if ( + declarator && + declarator.type == "VariableDeclarator" && + declarator.id.type == "ObjectPattern" && + declarator.init.type == "CallExpression" + ) { + let initialization = declarator.init; + if ( + astUtils.isSpecificMemberAccess( + initialization.callee, + "ChromeUtils", + /^import(ESModule|)$/ + ) + ) { + // Hack alert: our eslint env is pretty confused about `require` and + // `loader` for devtools modules - so ignore it for now. + // See bug 1812547 + if (gIgnoredImports.has(obj.node.name)) { + return; + } + // OK, we've got something we can fix. But we should be careful in case + // there are multiple imports being destructured. + // Do the easy (and common) case first - just one property: + if (declarator.id.properties.length == 1) { + context.report({ + node: declarator.parent, + messageId: "duplicateImport", + data: { + name: declarator.id.properties[0].key.name, + }, + fix(fixer) { + return fixer.remove(declarator.parent); + }, + }); + return; + } + + // OK, figure out which import is duplicated here: + let node = obj.node.parent; + // Then remove a comma after it, or a comma before + // if there's no comma after it. + let sourceCode = context.getSourceCode(); + let rangeToRemove = node.range; + let tokenAfter = sourceCode.getTokenAfter(node); + let tokenBefore = sourceCode.getTokenBefore(node); + if (astUtils.isCommaToken(tokenAfter)) { + rangeToRemove[1] = tokenAfter.range[1]; + } else if (astUtils.isCommaToken(tokenBefore)) { + rangeToRemove[0] = tokenBefore.range[0]; + } + context.report({ + node, + messageId: "duplicateImport", + data: { + name: node.key.name, + }, + fix(fixer) { + return fixer.removeRange(rangeToRemove); + }, + }); + return; + } + } + if (context.options[0]?.errorForNonImports) { + // Report the result from no-redeclare - we can't autofix it. + // This can happen for other redeclaration issues, e.g. naming + // variables in a way that conflicts with builtins like "URL" or + // "escape". + context.report(obj); + } + }; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-redeclare-with-import-autofix.html", + }, + messages: { + ...baseRule.meta.messages, + duplicateImport: + "The import of '{{ name }}' is redundant with one set up earlier (e.g. head.js or the browser window environment). It should be removed.", + }, + schema: [ + { + type: "object", + properties: { + errorForNonImports: { + type: "boolean", + default: true, + }, + }, + additionalProperties: false, + }, + ], + type: "suggestion", + fixable: "code", + }, + + create(context) { + // Test modules get the browser env applied wrongly in some cases, + // don't try and remove imports there. This works out of the box + // for sys.mjs modules because eslint won't check builtinGlobals + // for the no-redeclare rule. + if (context.getFilename().endsWith(".jsm")) { + return {}; + } + let newOptions = [{ builtinGlobals: true }]; + const contextForBaseRule = Object.create(context, { + report: { + value: trapReport(context), + writable: false, + }, + options: { + value: newOptions, + }, + }); + return baseRule.create(contextForBaseRule); + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js new file mode 100644 index 0000000000..5ff6bfd7c9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js @@ -0,0 +1,101 @@ +/** + * @fileoverview Rule to prevent throwing bare Cr.ERRORs. + * + * 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/. + */ + +"use strict"; + +function isCr(object) { + return object.type === "Identifier" && object.name === "Cr"; +} + +function isComponentsResults(object) { + return ( + object.type === "MemberExpression" && + object.object.type === "Identifier" && + object.object.name === "Components" && + object.property.type === "Identifier" && + object.property.name === "results" + ); +} + +function isNewError(argument) { + return ( + argument.type === "NewExpression" && + argument.callee.type === "Identifier" && + argument.callee.name === "Error" && + argument.arguments.length === 1 + ); +} + +function fixT(context, node, argument, fixer) { + const sourceText = context.getSourceCode().getText(argument); + return fixer.replaceText(node, `Components.Exception("", ${sourceText})`); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-throw-cr-literal.html", + }, + fixable: "code", + messages: { + bareCR: "Do not throw bare Cr.ERRORs, use Components.Exception instead", + bareComponentsResults: + "Do not throw bare Components.results.ERRORs, use Components.Exception instead", + newErrorCR: + "Do not pass Cr.ERRORs to new Error(), use Components.Exception instead", + newErrorComponentsResults: + "Do not pass Components.results.ERRORs to new Error(), use Components.Exception instead", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + ThrowStatement(node) { + if (node.argument.type === "MemberExpression") { + const fix = fixT.bind(null, context, node.argument, node.argument); + + if (isCr(node.argument.object)) { + context.report({ + node, + messageId: "bareCR", + fix, + }); + } else if (isComponentsResults(node.argument.object)) { + context.report({ + node, + messageId: "bareComponentsResults", + fix, + }); + } + } else if (isNewError(node.argument)) { + const argument = node.argument.arguments[0]; + + if (argument.type === "MemberExpression") { + const fix = fixT.bind(null, context, node.argument, argument); + + if (isCr(argument.object)) { + context.report({ + node, + messageId: "newErrorCR", + fix, + }); + } else if (isComponentsResults(argument.object)) { + context.report({ + node, + messageId: "newErrorComponentsResults", + fix, + }); + } + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js new file mode 100644 index 0000000000..ac1cc334e6 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js @@ -0,0 +1,156 @@ +/** + * @fileoverview Reject common XPCOM methods called with useless optional + * parameters, or non-existent parameters. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-parameters.html", + }, + fixable: "code", + messages: { + newURIParams: "newURI's last parameters are optional.", + obmittedWhenFalse: + "{{fnName}}'s {{index}} parameter can be omitted when it's false.", + onlyTakes: "{{fnName}} only takes {{params}}", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + function getRangeAfterArgToEnd(argNumber, args) { + let sourceCode = context.getSourceCode(); + return [ + sourceCode.getTokenAfter(args[argNumber]).range[0], + args[args.length - 1].range[1], + ]; + } + + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type !== "MemberExpression" || + callee.property.type !== "Identifier" + ) { + return; + } + + let isFalse = arg => arg.type === "Literal" && arg.value === false; + let isFalsy = arg => arg.type === "Literal" && !arg.value; + let isBool = arg => + arg.type === "Literal" && (arg.value === false || arg.value === true); + let name = callee.property.name; + let args = node.arguments; + + if ( + ["addEventListener", "removeEventListener", "addObserver"].includes( + name + ) && + args.length === 3 && + isFalse(args[2]) + ) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(1, args)); + }, + messageId: "obmittedWhenFalse", + data: { fnName: name, index: "third" }, + }); + } + + if (name === "clearUserPref" && args.length > 1) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + messageId: "onlyTakes", + data: { fnName: name, params: "1 parameter" }, + }); + } + + if (name === "removeObserver" && args.length === 3 && isBool(args[2])) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(1, args)); + }, + messageId: "onlyTakes", + data: { fnName: name, params: "2 parameters" }, + }); + } + + if (name === "appendElement" && args.length === 2 && isFalse(args[1])) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + messageId: "obmittedWhenFalse", + data: { fnName: name, index: "second" }, + }); + } + + if ( + name === "notifyObservers" && + args.length === 3 && + isFalsy(args[2]) + ) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(1, args)); + }, + messageId: "obmittedWhenFalse", + data: { fnName: name, index: "third" }, + }); + } + + if ( + name === "getComputedStyle" && + args.length === 2 && + isFalsy(args[1]) + ) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + messageId: "obmittedWhenFalse", + data: { fnName: "getComputedStyle", index: "second" }, + }); + } + + if ( + name === "newURI" && + args.length > 1 && + isFalsy(args[args.length - 1]) + ) { + context.report({ + node, + fix: fixer => { + if (args.length > 2 && isFalsy(args[args.length - 2])) { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + } + + return fixer.removeRange( + getRangeAfterArgToEnd(args.length - 2, args) + ); + }, + messageId: "newURIParams", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js new file mode 100644 index 0000000000..d5f19ab717 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js @@ -0,0 +1,69 @@ +/** + * @fileoverview Reject calls to removeEventListenter where {once: true} could + * be used instead. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-removeEventListener.html", + }, + messages: { + useOnce: + "use {once: true} instead of removeEventListener as the first instruction of the listener", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type !== "MemberExpression" || + callee.property.type !== "Identifier" || + callee.property.name !== "addEventListener" || + node.arguments.length == 4 + ) { + return; + } + + let listener = node.arguments[1]; + if ( + !listener || + listener.type != "FunctionExpression" || + !listener.body || + listener.body.type != "BlockStatement" || + !listener.body.body.length || + listener.body.body[0].type != "ExpressionStatement" || + listener.body.body[0].expression.type != "CallExpression" + ) { + return; + } + + let call = listener.body.body[0].expression; + if ( + call.callee.type == "MemberExpression" && + call.callee.property.type == "Identifier" && + call.callee.property.name == "removeEventListener" && + ((call.arguments[0].type == "Literal" && + call.arguments[0].value == node.arguments[0].value) || + (call.arguments[0].type == "Identifier" && + call.arguments[0].name == node.arguments[0].name)) + ) { + context.report({ + node: call, + messageId: "useOnce", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js new file mode 100644 index 0000000000..ddfbea05e3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js @@ -0,0 +1,76 @@ +/** + * @fileoverview Reject run_test() definitions where they aren't necessary. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-run-test.html", + }, + fixable: "code", + messages: { + noUselessRunTest: + "Useless run_test function - only contains run_next_test; whole function can be removed", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + "Program > FunctionDeclaration": function (node) { + if ( + node.id.name === "run_test" && + node.body.type === "BlockStatement" && + node.body.body.length === 1 && + node.body.body[0].type === "ExpressionStatement" && + node.body.body[0].expression.type === "CallExpression" && + node.body.body[0].expression.callee.name === "run_next_test" + ) { + context.report({ + node, + fix: fixer => { + let sourceCode = context.getSourceCode(); + let startNode; + if (sourceCode.getCommentsBefore) { + // ESLint 4 has getCommentsBefore. + startNode = sourceCode.getCommentsBefore(node); + } else if (node && node.body && node.leadingComments) { + // This is for ESLint 3. + startNode = node.leadingComments; + } + + // If we have comments, we want the start node to be the comments, + // rather than the token before the comments, so that we don't + // remove the comments - for run_test, these are likely to be useful + // information about the test. + if (startNode?.length) { + startNode = startNode[startNode.length - 1]; + } else { + startNode = sourceCode.getTokenBefore(node); + } + + return fixer.removeRange([ + // If there's no startNode, we fall back to zero, i.e. start of + // file. + startNode ? startNode.range[1] + 1 : 0, + // We know the function is a block and it'll end with }. Normally + // there's a new line after that, so just advance past it. This + // may be slightly not dodgy in some cases, but covers the existing + // cases. + node.range[1] + 1, + ]); + }, + messageId: "noUselessRunTest", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js new file mode 100644 index 0000000000..41c0aa1d30 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js @@ -0,0 +1,129 @@ +/** + * @fileoverview Prefer boolean length check + * + * 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/. + */ + +"use strict"; + +function funcForBooleanLength(context, node, conditionCheck) { + let newText = ""; + const sourceCode = context.getSourceCode(); + switch (node.operator) { + case ">": + if (node.right.value == 0) { + if (conditionCheck) { + newText = sourceCode.getText(node.left); + } else { + newText = "!!" + sourceCode.getText(node.left); + } + } else { + newText = "!" + sourceCode.getText(node.right); + } + break; + case "<": + if (node.right.value == 0) { + newText = "!" + sourceCode.getText(node.left); + } else if (conditionCheck) { + newText = sourceCode.getText(node.right); + } else { + newText = "!!" + sourceCode.getText(node.right); + } + break; + case "==": + if (node.right.value == 0) { + newText = "!" + sourceCode.getText(node.left); + } else { + newText = "!" + sourceCode.getText(node.right); + } + break; + case "!=": + if (node.right.value == 0) { + if (conditionCheck) { + newText = sourceCode.getText(node.left); + } else { + newText = "!!" + sourceCode.getText(node.left); + } + } else if (conditionCheck) { + newText = sourceCode.getText(node.right); + } else { + newText = "!!" + sourceCode.getText(node.right); + } + break; + } + return newText; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/prefer-boolean-length-check.html", + }, + fixable: "code", + messages: { + preferBooleanCheck: "Prefer boolean length check", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + const conditionStatement = [ + "IfStatement", + "WhileStatement", + "DoWhileStatement", + "ForStatement", + "ForInStatement", + "ConditionalExpression", + ]; + + return { + BinaryExpression(node) { + if ( + ["==", "!=", ">", "<"].includes(node.operator) && + ((node.right.type == "Literal" && + node.right.value == 0 && + node.left.property?.name == "length") || + (node.left.type == "Literal" && + node.left.value == 0 && + node.right.property?.name == "length")) + ) { + if ( + conditionStatement.includes(node.parent.type) || + (node.parent.type == "LogicalExpression" && + conditionStatement.includes(node.parent.parent.type)) + ) { + context.report({ + node, + fix: fixer => { + let generateExpression = funcForBooleanLength( + context, + node, + true + ); + + return fixer.replaceText(node, generateExpression); + }, + messageId: "preferBooleanCheck", + }); + } else { + context.report({ + node, + fix: fixer => { + let generateExpression = funcForBooleanLength( + context, + node, + false + ); + return fixer.replaceText(node, generateExpression); + }, + messageId: "preferBooleanCheck", + }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js new file mode 100644 index 0000000000..4807cf1f1f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js @@ -0,0 +1,97 @@ +/** + * @fileoverview Reject multiple calls to document.l10n.formatValue in the same + * code block. + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +/** + * As we enter blocks new sets are pushed onto this stack and then popped when + * we exit the block. + */ +const BlockStack = []; + +module.exports = { + meta: { + docs: { + description: "disallow multiple document.l10n.formatValue calls", + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/prefer-formatValues.html", + }, + messages: { + outsideCallBlock: "call expression found outside of known block", + useSingleCall: + "prefer to use a single document.l10n.formatValues call instead " + + "of multiple calls to document.l10n.formatValue or document.l10n.formatValues", + }, + schema: [], + type: "problem", + }, + + create(context) { + function enterBlock() { + BlockStack.push(new Set()); + } + + function exitBlock() { + let calls = BlockStack.pop(); + if (calls.size > 1) { + for (let callNode of calls) { + context.report({ + node: callNode, + messageId: "useSingleCall", + }); + } + } + } + + return { + Program: enterBlock, + "Program:exit": exitBlock, + BlockStatement: enterBlock, + "BlockStatement:exit": exitBlock, + + CallExpression(node) { + if (!BlockStack.length) { + context.report({ + node, + messageId: "outsideCallBlock", + }); + } + + let callee = node.callee; + if (callee.type !== "MemberExpression") { + return; + } + + if ( + !isIdentifier(callee.property, "formatValue") && + !isIdentifier(callee.property, "formatValues") + ) { + return; + } + + if (callee.object.type !== "MemberExpression") { + return; + } + + if ( + !isIdentifier(callee.object.object, "document") || + !isIdentifier(callee.object.property, "l10n") + ) { + return; + } + + let calls = BlockStack[BlockStack.length - 1]; + calls.add(node); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js new file mode 100644 index 0000000000..b1a67cad7d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Don't allow only() in tests + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-addtask-only.html", + }, + hasSuggestions: true, + messages: { + addTaskNotAllowed: + "add_task(...).only() not allowed - add an exception if this is intentional", + addTaskNotAllowedSuggestion: "Remove only() call from task", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + if ( + ["add_task", "decorate_task"].includes( + node.callee.object?.callee?.name + ) && + node.callee.property?.name == "only" + ) { + context.report({ + node, + messageId: "addTaskNotAllowed", + suggest: [ + { + messageId: "addTaskNotAllowedSuggestion", + fix: fixer => + fixer.replaceTextRange( + [node.callee.object.range[1], node.range[1]], + "" + ), + }, + ], + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js new file mode 100644 index 0000000000..ccfb0a1cb0 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js @@ -0,0 +1,66 @@ +/** + * @fileoverview Reject calls to ChromeUtils.import(..., null). This allows to + * retrieve the global object for the JSM, instead we should rely on explicitly + * exported symbols. + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function getRangeAfterArgToEnd(context, argNumber, args) { + let sourceCode = context.getSourceCode(); + return [ + sourceCode.getTokenAfter(args[argNumber]).range[0], + args[args.length - 1].range[1], + ]; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-chromeutils-import-params.html", + }, + hasSuggestions: true, + messages: { + importOnlyOneArg: "ChromeUtils.import only takes one argument.", + importOnlyOneArgSuggestion: "Remove the unnecessary parameters.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + let { callee } = node; + if ( + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "import") && + node.arguments.length >= 2 + ) { + context.report({ + node, + messageId: "importOnlyOneArg", + suggest: [ + { + messageId: "importOnlyOneArgSuggestion", + fix: fixer => { + return fixer.removeRange( + getRangeAfterArgToEnd(context, 0, node.arguments) + ); + }, + }, + ], + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js new file mode 100644 index 0000000000..1f746db730 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js @@ -0,0 +1,80 @@ +/** + * @fileoverview Reject use of Cu.import and ChromeUtils.import + * in favor of ChromeUtils.importESModule. + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isMemberExpression(node, object, member) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, member) + ); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-chromeutils-import.html", + }, + messages: { + useImportESModule: + "Please use ChromeUtils.importESModule instead of " + + "ChromeUtils.import unless the module is not yet ESMified", + useImportESModuleLazy: + "Please use ChromeUtils.defineESModuleGetters instead of " + + "ChromeUtils.defineModuleGetter " + + "unless the module is not yet ESMified", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let { callee } = node; + + if ( + (isIdentifier(callee.object, "ChromeUtils") || + isMemberExpression( + callee.object, + "SpecialPowers", + "ChromeUtils" + )) && + isIdentifier(callee.property, "import") + ) { + context.report({ + node, + messageId: "useImportESModule", + }); + } + + if ( + (isMemberExpression(callee.object, "SpecialPowers", "ChromeUtils") && + isIdentifier(callee.property, "defineModuleGetter")) || + isMemberExpression(callee, "ChromeUtils", "defineModuleGetter") || + isMemberExpression(callee, "XPCOMUtils", "defineLazyModuleGetters") + ) { + context.report({ + node, + messageId: "useImportESModuleLazy", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js new file mode 100644 index 0000000000..133dd6d71f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js @@ -0,0 +1,99 @@ +/** + * @fileoverview Reject use of lazy getters for modules that's loaded early in + * the startup process and not necessarily be lazy. + * + * 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/. + */ + +"use strict"; +const helpers = require("../helpers"); + +function isString(node) { + return node.type === "Literal" && typeof node.value === "string"; +} + +function isEagerModule(resourceURI) { + return [ + "resource://gre/modules/XPCOMUtils", + "resource://gre/modules/AppConstants", + ].includes(resourceURI.replace(/(\.jsm|\.jsm\.js|\.js|\.sys\.mjs)$/, "")); +} + +function checkEagerModule(context, node, resourceURI) { + if (!isEagerModule(resourceURI)) { + return; + } + context.report({ + node, + messageId: "eagerModule", + data: { uri: resourceURI }, + }); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.html", + }, + messages: { + eagerModule: + 'Module "{{uri}}" is known to be loaded early in the startup process, and should be loaded eagerly, instead of defining a lazy getter.', + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let callerSource; + try { + callerSource = helpers.getASTSource(node.callee); + } catch (e) { + return; + } + + if (callerSource === "ChromeUtils.defineModuleGetter") { + if (node.arguments.length < 3) { + return; + } + const resourceURINode = node.arguments[2]; + if (!isString(resourceURINode)) { + return; + } + checkEagerModule(context, node, resourceURINode.value); + } else if ( + callerSource === "XPCOMUtils.defineLazyModuleGetters" || + callerSource === "ChromeUtils.defineESModuleGetters" + ) { + if (node.arguments.length < 2) { + return; + } + const obj = node.arguments[1]; + if (obj.type !== "ObjectExpression") { + return; + } + for (let prop of obj.properties) { + if (prop.type !== "Property") { + continue; + } + if (prop.kind !== "init") { + continue; + } + const resourceURINode = prop.value; + if (!isString(resourceURINode)) { + continue; + } + checkEagerModule(context, node, resourceURINode.value); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js new file mode 100644 index 0000000000..ec4b5fd43d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Reject attempts to use the global object in jsms. + * + * 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/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-global-this.html", + }, + messages: { + avoidGlobalThis: "JSM should not use the global this", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + ThisExpression(node) { + if (!helpers.getIsGlobalThis(context.getAncestors())) { + return; + } + + context.report({ + node, + messageId: "avoidGlobalThis", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js new file mode 100644 index 0000000000..13052db80c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js @@ -0,0 +1,74 @@ +/** + * @fileoverview Enforce the standard object name for + * ChromeUtils.defineESMGetters + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node.type === "Identifier" && node.name === id; +} + +function calleeToString(node) { + if (node.type === "Identifier") { + return node.name; + } + + if (node.type === "MemberExpression" && !node.computed) { + return calleeToString(node.object) + "." + node.property.name; + } + + return "???"; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-globalThis-modification.html", + }, + messages: { + rejectModifyGlobalThis: + "`globalThis` shouldn't be modified. `globalThis` is the shared global inside the system module, and properties defined on it is visible from all modules.", + rejectPassingGlobalThis: + "`globalThis` shouldn't be passed to function that can modify it. `globalThis` is the shared global inside the system module, and properties defined on it is visible from all modules.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + AssignmentExpression(node, parents) { + let target = node.left; + while (target.type === "MemberExpression") { + target = target.object; + } + if (isIdentifier(target, "globalThis")) { + context.report({ + node, + messageId: "rejectModifyGlobalThis", + }); + } + }, + CallExpression(node) { + const calleeStr = calleeToString(node.callee); + if (calleeStr.endsWith(".deserialize")) { + return; + } + + for (const arg of node.arguments) { + if (isIdentifier(arg, "globalThis")) { + context.report({ + node, + messageId: "rejectPassingGlobalThis", + }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js new file mode 100644 index 0000000000..2cbc4e7652 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js @@ -0,0 +1,36 @@ +/** + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-import-system-module-from-non-system.html", + }, + messages: { + rejectStaticImportSystemModuleFromNonSystem: + "System modules (*.sys.mjs) can be imported with static import declaration only from system modules.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + ImportDeclaration(node) { + if (!node.source.value.endsWith(".sys.mjs")) { + return; + } + + context.report({ + node, + messageId: "rejectStaticImportSystemModuleFromNonSystem", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js new file mode 100644 index 0000000000..b2f0aad1ae --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js @@ -0,0 +1,97 @@ +/** + * @fileoverview Reject use of Cu.importGlobalProperties + * + * 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/. + */ + +"use strict"; + +const path = require("path"); + +const privilegedGlobals = Object.keys( + require("../environments/privileged.js").globals +); + +function getMessageId(context) { + return path.extname(context.getFilename()) == ".sjs" + ? "unexpectedCallSjs" + : "unexpectedCall"; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-importGlobalProperties.html", + }, + messages: { + unexpectedCall: "Unexpected call to Cu.importGlobalProperties", + unexpectedCallCuWebIdl: + "Unnecessary call to Cu.importGlobalProperties for {{name}} (webidl names are automatically imported)", + unexpectedCallSjs: + "Do not call Cu.importGlobalProperties in sjs files, expand the global instead (see rule docs).", + unexpectedCallXPCOMWebIdl: + "Unnecessary call to XPCOMUtils.defineLazyGlobalGetters for {{name}} (webidl names are automatically imported)", + }, + schema: [ + { + enum: ["everything", "allownonwebidl"], + }, + ], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + let memexp = node.callee; + if ( + memexp.object.type === "Identifier" && + // Only Cu, not Components.utils as `use-cc-etc` handles this for us. + memexp.object.name === "Cu" && + memexp.property.type === "Identifier" && + memexp.property.name === "importGlobalProperties" + ) { + if (context.options.includes("allownonwebidl")) { + for (let element of node.arguments[0].elements) { + if (privilegedGlobals.includes(element.value)) { + context.report({ + node, + messageId: "unexpectedCallCuWebIdl", + data: { name: element.value }, + }); + } + } + } else { + context.report({ node, messageId: getMessageId(context) }); + } + } + if ( + memexp.object.type === "Identifier" && + memexp.object.name === "XPCOMUtils" && + memexp.property.type === "Identifier" && + memexp.property.name === "defineLazyGlobalGetters" && + node.arguments.length >= 2 + ) { + if (context.options.includes("allownonwebidl")) { + for (let element of node.arguments[1].elements) { + if (privilegedGlobals.includes(element.value)) { + context.report({ + node, + messageId: "unexpectedCallXPCOMWebIdl", + data: { name: element.value }, + }); + } + } + } else { + context.report({ node, messageId: getMessageId(context) }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js new file mode 100644 index 0000000000..492a1e3bd7 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js @@ -0,0 +1,72 @@ +/** + * 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/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +const callExpressionDefinitions = [ + /^loader\.lazyGetter\((?:globalThis|window), "(\w+)"/, + /^loader\.lazyServiceGetter\((?:globalThis|window), "(\w+)"/, + /^loader\.lazyRequireGetter\((?:globalThis|window), "(\w+)"/, + /^ChromeUtils\.defineLazyGetter\((?:globalThis|window), "(\w+)"/, + /^ChromeUtils\.defineModuleGetter\((?:globalThis|window), "(\w+)"/, + /^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|window), "(\w+)"/, + /^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|window), "(\w+)"/, + /^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|window), "(\w+)"/, + /^XPCOMUtils\.defineConstant\((?:globalThis|window), "(\w+)"/, + /^DevToolsUtils\.defineLazyGetter\((?:globalThis|window), "(\w+)"/, + /^Object\.defineProperty\((?:globalThis|window), "(\w+)"/, + /^Reflect\.defineProperty\((?:globalThis|window), "(\w+)"/, + /^this\.__defineGetter__\("(\w+)"/, +]; + +const callExpressionMultiDefinitions = [ + "XPCOMUtils.defineLazyGlobalGetters(window,", + "XPCOMUtils.defineLazyGlobalGetters(globalThis,", + "XPCOMUtils.defineLazyModuleGetters(window,", + "XPCOMUtils.defineLazyModuleGetters(globalThis,", + "XPCOMUtils.defineLazyServiceGetters(window,", + "XPCOMUtils.defineLazyServiceGetters(globalThis,", + "ChromeUtils.defineESModuleGetters(window,", + "ChromeUtils.defineESModuleGetters(globalThis,", + "loader.lazyRequireGetter(window,", + "loader.lazyRequireGetter(globalThis,", +]; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-lazy-imports-into-globals.html", + }, + messages: { + rejectLazyImportsIntoGlobals: + "Non-system modules should not import into globalThis nor window. Prefer a lazy object holder", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let source; + try { + source = helpers.getASTSource(node); + } catch (e) { + return; + } + + if ( + callExpressionDefinitions.some(expr => source.match(expr)) || + callExpressionMultiDefinitions.some(expr => source.startsWith(expr)) + ) { + context.report({ node, messageId: "rejectLazyImportsIntoGlobals" }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js new file mode 100644 index 0000000000..5779a90afd --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js @@ -0,0 +1,150 @@ +/** + * @fileoverview Reject use of lazy getters for modules that's loaded early in + * the startup process and not necessarily be lazy. + * + * 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/. + */ + +"use strict"; +const helpers = require("../helpers"); + +function isIdentifier(node, id) { + return node.type === "Identifier" && node.name === id; +} + +function isString(node) { + return node.type === "Literal" && typeof node.value === "string"; +} + +function checkMixed(loadedModules, context, node, type, resourceURI) { + if (!loadedModules.has(resourceURI)) { + loadedModules.set(resourceURI, type); + } + + if (loadedModules.get(resourceURI) === type) { + return; + } + + context.report({ + node, + messageId: "mixedEagerAndLazy", + data: { uri: resourceURI }, + }); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixed-eager-and-lazy.html", + }, + messages: { + mixedEagerAndLazy: + 'Module "{{uri}}" is loaded eagerly, and should not be used for lazy getter.', + }, + schema: [], + type: "problem", + }, + + create(context) { + const loadedModules = new Map(); + + return { + ImportDeclaration(node) { + const resourceURI = node.source.value; + checkMixed(loadedModules, context, node, "eager", resourceURI); + }, + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let callerSource; + try { + callerSource = helpers.getASTSource(node.callee); + } catch (e) { + return; + } + + if ( + (callerSource === "ChromeUtils.import" || + callerSource === "ChromeUtils.importESModule") && + helpers.getIsTopLevelAndUnconditionallyExecuted( + context.getAncestors() + ) + ) { + if (node.arguments.length < 1) { + return; + } + const resourceURINode = node.arguments[0]; + if (!isString(resourceURINode)) { + return; + } + checkMixed( + loadedModules, + context, + node, + "eager", + resourceURINode.value + ); + } + + if (callerSource === "ChromeUtils.defineModuleGetter") { + if (node.arguments.length < 3) { + return; + } + if (!isIdentifier(node.arguments[0], "lazy")) { + return; + } + + const resourceURINode = node.arguments[2]; + if (!isString(resourceURINode)) { + return; + } + checkMixed( + loadedModules, + context, + node, + "lazy", + resourceURINode.value + ); + } else if ( + callerSource === "XPCOMUtils.defineLazyModuleGetters" || + callerSource === "ChromeUtils.defineESModuleGetters" + ) { + if (node.arguments.length < 2) { + return; + } + if (!isIdentifier(node.arguments[0], "lazy")) { + return; + } + + const obj = node.arguments[1]; + if (obj.type !== "ObjectExpression") { + return; + } + for (let prop of obj.properties) { + if (prop.type !== "Property") { + continue; + } + if (prop.kind !== "init") { + continue; + } + const resourceURINode = prop.value; + if (!isString(resourceURINode)) { + continue; + } + checkMixed( + loadedModules, + context, + node, + "lazy", + resourceURINode.value + ); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js new file mode 100644 index 0000000000..e6e37ad035 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js @@ -0,0 +1,81 @@ +/** + * 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/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +function findStatement(node) { + while (node && node.type !== "ExpressionStatement") { + node = node.parent; + } + + return node; +} + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-multiple-getters-calls.html", + }, + messages: { + rejectMultipleCalls: + "ChromeUtils.defineESModuleGetters is already called for {{target}} in the same context. Please merge those calls", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + const parentToTargets = new Map(); + + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type === "MemberExpression" && + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "defineESModuleGetters") + ) { + const stmt = findStatement(node); + if (!stmt) { + return; + } + + let target; + try { + target = helpers.getASTSource(node.arguments[0]); + } catch (e) { + return; + } + + const parent = stmt.parent; + let targets; + if (parentToTargets.has(parent)) { + targets = parentToTargets.get(parent); + } else { + targets = new Set(); + parentToTargets.set(parent, targets); + } + + if (targets.has(target)) { + context.report({ + node, + messageId: "rejectMultipleCalls", + data: { target }, + }); + } + + targets.add(target); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js new file mode 100644 index 0000000000..34e4b6bd5e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js @@ -0,0 +1,42 @@ +/** + * @fileoverview Reject some uses of require. + * + * 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/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +const isRelativePath = function (path) { + return path.startsWith("./") || path.startsWith("../"); +}; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-relative-requires.html", + }, + messages: { + rejectRelativeRequires: "relative paths are not allowed with require()", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + const path = helpers.getDevToolsRequirePath(node); + if (path && isRelativePath(path)) { + context.report({ + node, + messageId: "rejectRelativeRequires", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js new file mode 100644 index 0000000000..e29a60089c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Reject calls into Ci.nsIScriptableUnicodeConverter. We're phasing this out in + * favour of TextEncoder or TextDecoder. + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-scriptableunicodeconverter.html", + }, + messages: { + rejectScriptableUnicodeConverter: + "Ci.nsIScriptableUnicodeConverter is deprecated. You should use TextEncoder or TextDecoder instead.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + MemberExpression(node) { + if ( + isIdentifier(node.object, "Ci") && + isIdentifier(node.property, "nsIScriptableUnicodeConverter") + ) { + context.report({ + node, + messageId: "rejectScriptableUnicodeConverter", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js new file mode 100644 index 0000000000..5a4c6b4df7 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Reject some uses of require. + * + * 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/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-some-requires.html", + }, + messages: { + rejectRequire: `require({{path}}) is not allowed`, + }, + schema: [ + { + type: "string", + }, + ], + type: "problem", + }, + + create(context) { + if (typeof context.options[0] !== "string") { + throw new Error("reject-some-requires expects a regexp"); + } + const RX = new RegExp(context.options[0]); + + return { + CallExpression(node) { + const path = helpers.getDevToolsRequirePath(node); + if (path && RX.test(path)) { + context.report({ node, messageId: "rejectRequire", data: { path } }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js new file mode 100644 index 0000000000..dff7db0f9a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js @@ -0,0 +1,45 @@ +/** + * @fileoverview Don't allow only() in tests + * + * 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/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-top-level-await.html", + }, + messages: { + rejectTopLevelAwait: + "Top-level await is not currently supported in component files.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + AwaitExpression(node) { + if (!helpers.getIsTopLevelScript(context.getAncestors())) { + return; + } + context.report({ node, messageId: "rejectTopLevelAwait" }); + }, + ForOfStatement(node) { + if ( + !node.await || + !helpers.getIsTopLevelScript(context.getAncestors()) + ) { + return; + } + context.report({ node, messageId: "rejectTopLevelAwait" }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js new file mode 100644 index 0000000000..a7e7d9d7e2 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Ensure Assert.rejects is preceded by await. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-requires-await.html", + }, + messages: { + rejectRequiresAwait: "Assert.rejects needs to be preceded by await.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type === "MemberExpression") { + let memexp = node.callee; + if ( + memexp.object.type === "Identifier" && + memexp.object.name === "Assert" && + memexp.property.type === "Identifier" && + memexp.property.name === "rejects" + ) { + // We have ourselves an Assert.rejects. + + if (node.parent.type !== "AwaitExpression") { + context.report({ + node, + messageId: "rejectRequiresAwait", + }); + } + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js new file mode 100644 index 0000000000..f47f03f0d2 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Reject use of Components.classes etc, prefer the shorthand instead. + * + * 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/. + */ + +"use strict"; + +const componentsMap = { + classes: "Cc", + interfaces: "Ci", + results: "Cr", + utils: "Cu", +}; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-cc-etc.html", + }, + fixable: "code", + messages: { + useCcEtc: "Use {{ shortName }} rather than Components.{{ oldName }}", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + MemberExpression(node) { + if ( + node.object.type === "Identifier" && + node.object.name === "Components" && + node.property.type === "Identifier" && + Object.getOwnPropertyNames(componentsMap).includes(node.property.name) + ) { + context.report({ + node, + messageId: "useCcEtc", + data: { + shortName: componentsMap[node.property.name], + oldName: node.property.name, + }, + fix: fixer => + fixer.replaceTextRange( + [node.range[0], node.range[1]], + componentsMap[node.property.name] + ), + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js new file mode 100644 index 0000000000..a9f43a945a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js @@ -0,0 +1,58 @@ +/** + * @fileoverview Reject use of XPCOMUtils.defineLazyGetter in favor of ChromeUtils.defineLazyGetter. + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isMemberExpression(node, object, member) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, member) + ); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-definelazygetter.html", + }, + fixable: "code", + messages: { + useChromeUtilsDefineLazyGetter: + "Please use ChromeUtils.defineLazyGetter instead of XPCOMUtils.defineLazyGetter", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let { callee } = node; + + if (isMemberExpression(callee, "XPCOMUtils", "defineLazyGetter")) { + context.report({ + node, + messageId: "useChromeUtilsDefineLazyGetter", + fix(fixer) { + return fixer.replaceText(callee, "ChromeUtils.defineLazyGetter"); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js new file mode 100644 index 0000000000..d654b0410c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js @@ -0,0 +1,105 @@ +/** + * @fileoverview Reject use of XPCOMUtils.generateQI and JS-implemented + * QueryInterface methods in favor of ChromeUtils. + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isMemberExpression(node, object, member) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, member) + ); +} + +function funcToGenerateQI(context, node) { + const sourceCode = context.getSourceCode(); + const text = sourceCode.getText(node); + + let interfaces = []; + let match; + let re = /\bCi\.([a-zA-Z0-9]+)\b|\b(nsI[A-Z][a-zA-Z0-9]+)\b/g; + while ((match = re.exec(text))) { + interfaces.push(match[1] || match[2]); + } + + let ifaces = interfaces + .filter(iface => iface != "nsISupports") + .map(iface => JSON.stringify(iface)) + .join(", "); + + return `ChromeUtils.generateQI([${ifaces}])`; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-generateqi.html", + }, + fixable: "code", + messages: { + noJSQueryInterface: + "Please use ChromeUtils.generateQI rather than " + + "manually creating JavaScript QueryInterface functions", + noXpcomUtilsGenerateQI: + "Please use ChromeUtils.generateQI instead of XPCOMUtils.generateQI", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let { callee } = node; + if (isMemberExpression(callee, "XPCOMUtils", "generateQI")) { + context.report({ + node, + messageId: "noXpcomUtilsGenerateQI", + fix(fixer) { + return fixer.replaceText(callee, "ChromeUtils.generateQI"); + }, + }); + } + }, + + "AssignmentExpression > MemberExpression[property.name='QueryInterface']": + function (node) { + const { right } = node.parent; + if (right.type === "FunctionExpression") { + context.report({ + node: node.parent, + messageId: "noJSQueryInterface", + fix(fixer) { + return fixer.replaceText( + right, + funcToGenerateQI(context, right) + ); + }, + }); + } + }, + + "Property[key.name='QueryInterface'][value.type='FunctionExpression']": + function (node) { + context.report({ + node, + messageId: "noJSQueryInterface", + fix(fixer) { + let generateQI = funcToGenerateQI(context, node.value); + return fixer.replaceText(node, `QueryInterface: ${generateQI}`); + }, + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js new file mode 100644 index 0000000000..925b4800bc --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js @@ -0,0 +1,63 @@ +/** + * @fileoverview Reject use of Cu.import in favor of ChromeUtils. + * + * 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/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isMemberExpression(node, object, member) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, member) + ); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-import.html", + }, + fixable: "code", + messages: { + useChromeUtilsImport: + "Please use ChromeUtils.import instead of Cu.import", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let { callee } = node; + + // Is the expression starting with `Cu` or `Components.utils`? + if ( + (isIdentifier(callee.object, "Cu") || + isMemberExpression(callee.object, "Components", "utils")) && + isIdentifier(callee.property, "import") + ) { + context.report({ + node, + messageId: "useChromeUtilsImport", + fix(fixer) { + return fixer.replaceText(callee, "ChromeUtils.import"); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js new file mode 100644 index 0000000000..72add0ab24 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Reject use of Console.sys.mjs and Log.sys.mjs. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-console-createInstance.html", + }, + messages: { + useConsoleRatherThanModule: + "Use console.createInstance rather than {{module}}", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + Literal(node) { + if (typeof node.value != "string") { + return; + } + /* eslint-disable mozilla/use-console-createInstance */ + if ( + node.value == "resource://gre/modules/Console.sys.mjs" || + node.value == "resource://gre/modules/Log.sys.mjs" + ) { + context.report({ + node, + messageId: "useConsoleRatherThanModule", + data: { module: node.value.split("/").at(-1) }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js new file mode 100644 index 0000000000..edc1e28405 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js @@ -0,0 +1,56 @@ +/** + * @fileoverview Require providing a second parameter to get*Pref + * methods instead of using a try/catch block. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-default-preference-values.html", + }, + messages: { + provideDefaultValue: + "provide a default value instead of using a try/catch block", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + TryStatement(node) { + let types = ["Bool", "Char", "Float", "Int"]; + let methods = types.map(type => "get" + type + "Pref"); + if ( + node.block.type != "BlockStatement" || + node.block.body.length != 1 + ) { + return; + } + + let firstStm = node.block.body[0]; + if ( + firstStm.type != "ExpressionStatement" || + firstStm.expression.type != "AssignmentExpression" || + firstStm.expression.right.type != "CallExpression" || + firstStm.expression.right.callee.type != "MemberExpression" || + firstStm.expression.right.callee.property.type != "Identifier" || + !methods.includes(firstStm.expression.right.callee.property.name) + ) { + return; + } + + context.report({ + node, + messageId: "provideDefaultValue", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js new file mode 100644 index 0000000000..245c89a095 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Use .includes instead of .indexOf + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-includes-instead-of-indexOf.html", + }, + messages: { + useIncludes: "use .includes instead of .indexOf", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + BinaryExpression(node) { + if ( + node.left.type != "CallExpression" || + node.left.callee.type != "MemberExpression" || + node.left.callee.property.type != "Identifier" || + node.left.callee.property.name != "indexOf" + ) { + return; + } + + if ( + (["!=", "!==", "==", "==="].includes(node.operator) && + node.right.type == "UnaryExpression" && + node.right.operator == "-" && + node.right.argument.type == "Literal" && + node.right.argument.value == 1) || + ([">=", "<"].includes(node.operator) && + node.right.type == "Literal" && + node.right.value == 0) + ) { + context.report({ + node, + messageId: "useIncludes", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js new file mode 100644 index 0000000000..ffd9bc9566 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js @@ -0,0 +1,155 @@ +/** + * @fileoverview Reject use of instanceof against DOM interfaces + * + * 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/. + */ + +"use strict"; + +const fs = require("fs"); + +const { maybeGetMemberPropertyName } = require("../helpers"); + +const privilegedGlobals = Object.keys( + require("../environments/privileged.js").globals +); + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +/** + * Whether an identifier is defined by eslint configuration. + * `env: { browser: true }` or `globals: []` for example. + * @param {import("eslint-scope").Scope} currentScope + * @param {import("estree").Identifier} id + */ +function refersToEnvironmentGlobals(currentScope, id) { + const reference = currentScope.references.find(ref => ref.identifier === id); + const { resolved } = reference || {}; + if (!resolved) { + return false; + } + + // No definition in script files; defined via .eslintrc + return resolved.scope.type === "global" && resolved.defs.length === 0; +} + +/** + * Whether a node points to a DOM interface. + * Includes direct references to interfaces objects and also indirect references + * via property access. + * OS.File and lazy.(Foo) are explicitly excluded. + * + * @example HTMLElement + * @example win.HTMLElement + * @example iframe.contentWindow.HTMLElement + * @example foo.HTMLElement + * + * @param {import("eslint-scope").Scope} currentScope + * @param {import("estree").Node} node + */ +function pointsToDOMInterface(currentScope, node) { + if (node.type === "MemberExpression") { + const objectName = maybeGetMemberPropertyName(node.object); + if (objectName === "lazy") { + // lazy.Foo is probably a non-IDL import. + return false; + } + if (objectName === "OS" && node.property.name === "File") { + // OS.File is an exception that is not a Web IDL interface + return false; + } + // For `win.Foo`, `iframe.contentWindow.Foo`, or such. + return privilegedGlobals.includes(node.property.name); + } + + if ( + node.type === "Identifier" && + refersToEnvironmentGlobals(currentScope, node) + ) { + return privilegedGlobals.includes(node.name); + } + + return false; +} + +/** + * @param {import("eslint").Rule.RuleContext} context + */ +function isChromeContext(context) { + const filename = context.getFilename(); + const isChromeFileName = + filename.endsWith(".sys.mjs") || filename.endsWith(".jsm"); + if (isChromeFileName) { + return true; + } + + if (filename.endsWith(".xhtml")) { + // Treat scripts in XUL files as chrome scripts + // Note: readFile is needed as getSourceCode() only gives JS blocks + return fs.readFileSync(filename).includes("there.is.only.xul"); + } + + // Treat scripts as chrome privileged when using: + // 1. ChromeUtils, but not SpecialPowers.ChromeUtils + // 2. BrowserTestUtils, PlacesUtils + // 3. document.createXULElement + // 4. loader.lazyRequireGetter + // 5. Services.foo, but not SpecialPowers.Services.foo + // 6. evalInSandbox + const source = context.getSourceCode().text; + return !!source.match( + /(^|\s)ChromeUtils|BrowserTestUtils|PlacesUtils|createXULElement|lazyRequireGetter|(^|\s)Services\.|evalInSandbox/ + ); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-isInstance.html", + }, + fixable: "code", + messages: { + preferIsInstance: + "Please prefer .isInstance() in chrome scripts over the standard instanceof operator for DOM interfaces, " + + "since the latter will return false when the object is created from a different context.", + }, + schema: [], + type: "problem", + }, + /** + * @param {import("eslint").Rule.RuleContext} context + */ + create(context) { + if (!isChromeContext(context)) { + return {}; + } + + return { + BinaryExpression(node) { + const { operator, right } = node; + if ( + operator === "instanceof" && + pointsToDOMInterface(context.getScope(), right) + ) { + context.report({ + node, + messageId: "preferIsInstance", + fix(fixer) { + const sourceCode = context.getSourceCode(); + return fixer.replaceText( + node, + `${sourceCode.getText(right)}.isInstance(${sourceCode.getText( + node.left + )})` + ); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js new file mode 100644 index 0000000000..1d71e82b8f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Require .ownerGlobal instead of .ownerDocument.defaultView. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-ownerGlobal.html", + }, + messages: { + useOwnerGlobal: "use .ownerGlobal instead of .ownerDocument.defaultView", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + MemberExpression(node) { + if ( + node.property.type != "Identifier" || + node.property.name != "defaultView" || + node.object.type != "MemberExpression" || + node.object.property.type != "Identifier" || + node.object.property.name != "ownerDocument" + ) { + return; + } + + context.report({ + node, + messageId: "useOwnerGlobal", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js new file mode 100644 index 0000000000..23bbc040b9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js @@ -0,0 +1,48 @@ +/** + * @fileoverview Warn when idempotent methods are called and their return value is unused. + * + * 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/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-returnValue.html", + }, + messages: { + useReturnValue: + "{Array/String}.{{ property }} doesn't modify the instance in-place", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + ExpressionStatement(node) { + if ( + node.expression?.type != "CallExpression" || + node.expression.callee?.type != "MemberExpression" || + node.expression.callee.property?.type != "Identifier" || + !["concat", "join", "slice"].includes( + node.expression.callee.property?.name + ) + ) { + return; + } + + context.report({ + node, + messageId: "useReturnValue", + data: { + property: node.expression.callee.property.name, + }, + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js new file mode 100644 index 0000000000..3a7cc34633 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js @@ -0,0 +1,120 @@ +/** + * @fileoverview Require use of Services.* rather than getService. + * + * 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/. + */ + +"use strict"; +const helpers = require("../helpers"); + +let servicesInterfaceMap = helpers.servicesData; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-services.html", + }, + // fixable: "code", + messages: { + useServices: + "Use Services.{{ serviceName }} rather than {{ getterName }}.", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + if (!node.callee || !node.callee.property) { + return; + } + + if ( + node.callee.property.type == "Identifier" && + node.callee.property.name == "defineLazyServiceGetter" && + node.arguments.length == 4 && + node.arguments[3].type == "Literal" && + node.arguments[3].value in servicesInterfaceMap + ) { + let serviceName = servicesInterfaceMap[node.arguments[3].value]; + + context.report({ + node, + messageId: "useServices", + data: { + serviceName, + getterName: "defineLazyServiceGetter", + }, + }); + return; + } + + if ( + node.callee.property.type == "Identifier" && + node.callee.property.name == "defineLazyServiceGetters" && + node.arguments.length == 2 && + node.arguments[1].type == "ObjectExpression" + ) { + for (let property of node.arguments[1].properties) { + if ( + property.value.type == "ArrayExpression" && + property.value.elements.length == 2 && + property.value.elements[1].value in servicesInterfaceMap + ) { + let serviceName = + servicesInterfaceMap[property.value.elements[1].value]; + + context.report({ + node: property.value, + messageId: "useServices", + data: { + serviceName, + getterName: "defineLazyServiceGetters", + }, + }); + } + } + return; + } + + if ( + node.callee.property.type != "Identifier" || + node.callee.property.name != "getService" || + node.arguments.length != 1 || + !node.arguments[0].property || + node.arguments[0].property.type != "Identifier" || + !node.arguments[0].property.name || + !(node.arguments[0].property.name in servicesInterfaceMap) + ) { + return; + } + + let serviceName = servicesInterfaceMap[node.arguments[0].property.name]; + context.report({ + node, + messageId: "useServices", + data: { + serviceName, + getterName: "getService()", + }, + // This is not enabled by default as for mochitest plain tests we + // would need to replace with `SpecialPowers.Services.${serviceName}`. + // At the moment we do not have an easy way to detect that. + // fix(fixer) { + // let sourceCode = context.getSourceCode(); + // return fixer.replaceTextRange( + // [ + // sourceCode.getFirstToken(node.callee).range[0], + // sourceCode.getLastToken(node).range[1], + // ], + // `Services.${serviceName}` + // ); + // }, + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js new file mode 100644 index 0000000000..100b5682de --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js @@ -0,0 +1,87 @@ +/** + * @fileoverview Require use of static imports where possible. + * + * 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/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-static-import.html", + }, + fixable: "code", + messages: { + useStaticImport: + "Please use static import instead of ChromeUtils.importESModule", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + VariableDeclarator(node) { + if ( + node.init?.type != "CallExpression" || + node.init?.callee?.type != "MemberExpression" || + !context.getFilename().endsWith(".sys.mjs") || + !helpers.isTopLevel(context.getAncestors()) + ) { + return; + } + + let callee = node.init.callee; + + if ( + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "importESModule") && + callee.parent.arguments.length == 1 + ) { + let sourceCode = context.getSourceCode(); + let importItemSource; + if (node.id.type != "ObjectPattern") { + importItemSource = sourceCode.getText(node.id); + } else { + importItemSource = "{ "; + let initial = true; + for (let property of node.id.properties) { + if (!initial) { + importItemSource += ", "; + } + initial = false; + if (property.key.name == property.value.name) { + importItemSource += property.key.name; + } else { + importItemSource += `${property.key.name} as ${property.value.name}`; + } + } + importItemSource += " }"; + } + + context.report({ + node: node.parent, + messageId: "useStaticImport", + fix(fixer) { + return fixer.replaceText( + node.parent, + `import ${importItemSource} from ${sourceCode.getText( + callee.parent.arguments[0] + )}` + ); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js new file mode 100644 index 0000000000..4036a72928 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js @@ -0,0 +1,172 @@ +/** + * @fileoverview Reject uses of unknown interfaces on Ci and properties of those + * interfaces. + * + * 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/. + */ + +"use strict"; + +const os = require("os"); +const helpers = require("../helpers"); + +// These interfaces are all platform specific, so may be not present +// on all platforms. +const platformSpecificInterfaces = new Map([ + ["nsIAboutThirdParty", "windows"], + ["nsIAboutWindowsMessages", "windows"], + ["nsIDefaultAgent", "windows"], + ["nsIJumpListBuilder", "windows"], + ["nsILegacyJumpListBuilder", "windows"], + ["nsILegacyJumpListItem", "windows"], + ["nsILegacyJumpListLink", "windows"], + ["nsILegacyJumpListSeparator", "windows"], + ["nsILegacyJumpListShortcut", "windows"], + ["nsITaskbarWindowPreview", "windows"], + ["nsIWindowsAlertsService", "windows"], + ["nsIWindowsAlertNotification", "windows"], + ["nsIWindowsMutexFactory", "windows"], + ["nsIWinAppHelper", "windows"], + ["nsIWinTaskbar", "windows"], + ["nsIWinTaskSchedulerService", "windows"], + ["nsIWindowsRegKey", "windows"], + ["nsIWindowsPackageManager", "windows"], + ["nsIWindowsShellService", "windows"], + ["nsIAccessibleMacEvent", "darwin"], + ["nsIAccessibleMacInterface", "darwin"], + ["nsILocalFileMac", "darwin"], + ["nsIAccessibleMacEvent", "darwin"], + ["nsIMacAttributionService", "darwin"], + ["nsIMacShellService", "darwin"], + ["nsIMacDockSupport", "darwin"], + ["nsIMacFinderProgress", "darwin"], + ["nsIMacPreferencesReader", "darwin"], + ["nsIMacSharingService", "darwin"], + ["nsIMacUserActivityUpdater", "darwin"], + ["nsIMacWebAppUtils", "darwin"], + ["nsIStandaloneNativeMenu", "darwin"], + ["nsITouchBarHelper", "darwin"], + ["nsITouchBarInput", "darwin"], + ["nsITouchBarUpdater", "darwin"], + ["mozISandboxReporter", "linux"], + ["nsIApplicationChooser", "linux"], + ["nsIGNOMEShellService", "linux"], + ["nsIGtkTaskbarProgress", "linux"], + + // These are used in the ESLint test code. + ["amIFoo", "any"], + ["nsIMeh", "any"], + // Can't easily detect android builds from ESLint at the moment. + ["nsIAndroidBridge", "any"], + ["nsIAndroidView", "any"], + // Code coverage is enabled only for certain builds (MOZ_CODE_COVERAGE). + ["nsICodeCoverage", "any"], + // Layout debugging is enabled only for certain builds (MOZ_LAYOUT_DEBUGGER). + ["nsILayoutDebuggingTools", "any"], + // Sandbox test is only enabled for certain configurations (MOZ_SANDBOX, + // MOZ_DEBUG, ENABLE_TESTS). + ["mozISandboxTest", "any"], +]); + +function interfaceHasProperty(interfaceName, propertyName) { + // `Ci.nsIFoo.number` is valid, it returns the iid. + if (propertyName == "number") { + return true; + } + + let interfaceInfo = helpers.xpidlData.get(interfaceName); + + if (!interfaceInfo) { + return true; + } + + // If the property is not in the lists of consts for this interface, check + // any parents as well. + if (!interfaceInfo.consts.find(e => e.name === propertyName)) { + if (interfaceInfo.parent && interfaceInfo.parent != "nsISupports") { + return interfaceHasProperty(interfaceName.parent, propertyName); + } + return false; + } + return true; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-ci-uses.html", + }, + messages: { + missingInterface: + "{{ interface }} is defined in this rule's platform specific list, but is not available", + unknownInterface: "Use of unknown interface Ci.{{ interface}}", + unknownProperty: + "Use of unknown property Ci.{{ interface }}.{{ property }}", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + MemberExpression(node) { + if ( + node.computed === false && + node.type === "MemberExpression" && + node.object.type === "Identifier" && + node.object.name === "Ci" && + node.property.type === "Identifier" && + node.property.name.includes("I") + ) { + if (!helpers.xpidlData.get(node.property.name)) { + let platformSpecific = platformSpecificInterfaces.get( + node.property.name + ); + if (!platformSpecific) { + context.report({ + node, + messageId: "unknownInterface", + data: { + interface: node.property.name, + }, + }); + } else if (platformSpecific == os.platform) { + context.report({ + node, + messageId: "missingInterface", + data: { + interface: node.property.name, + }, + }); + } + } + } + + if ( + node.computed === false && + node.object.type === "MemberExpression" && + node.object.object.type === "Identifier" && + node.object.object.name === "Ci" && + node.object.property.type === "Identifier" && + node.object.property.name.includes("I") && + node.property.type === "Identifier" + ) { + if ( + !interfaceHasProperty(node.object.property.name, node.property.name) + ) { + context.report({ + node, + messageId: "unknownProperty", + data: { + interface: node.object.property.name, + property: node.property.name, + }, + }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js new file mode 100644 index 0000000000..048ed17e3e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js @@ -0,0 +1,276 @@ +/** + * @fileoverview Ensures that definitions and uses of properties on the + * ``lazy`` object are valid. + * + * 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/. + */ + +"use strict"; +const helpers = require("../helpers"); + +const items = [ + "loader", + "XPCOMUtils", + "Integration", + "ChromeUtils", + "DevToolsUtils", + "Object", + "Reflect", +]; + +const callExpressionDefinitions = [ + /^loader\.lazyGetter\(lazy, "(\w+)"/, + /^loader\.lazyServiceGetter\(lazy, "(\w+)"/, + /^loader\.lazyRequireGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineLazyGetter\(lazy, "(\w+)"/, + /^Integration\.downloads\.defineESModuleGetter\(lazy, "(\w+)"/, + /^ChromeUtils\.defineLazyGetter\(lazy, "(\w+)"/, + /^ChromeUtils\.defineModuleGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineLazyPreferenceGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineLazyScriptGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineLazyServiceGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineConstant\(lazy, "(\w+)"/, + /^DevToolsUtils\.defineLazyGetter\(lazy, "(\w+)"/, + /^Object\.defineProperty\(lazy, "(\w+)"/, + /^Reflect\.defineProperty\(lazy, "(\w+)"/, +]; + +const callExpressionMultiDefinitions = [ + "ChromeUtils.defineESModuleGetters(lazy,", + "XPCOMUtils.defineLazyModuleGetters(lazy,", + "XPCOMUtils.defineLazyServiceGetters(lazy,", + "Object.defineProperties(lazy,", + "loader.lazyRequireGetter(lazy,", +]; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-lazy.html", + }, + messages: { + duplicateSymbol: "Duplicate symbol {{name}} being added to lazy.", + incorrectType: "Unexpected literal for property name {{name}}", + unknownProperty: "Unknown lazy member property {{name}}", + unusedProperty: "Unused lazy property {{name}}", + topLevelAndUnconditional: + "Lazy property {{name}} is used at top-level unconditionally. It should be non-lazy.", + }, + schema: [], + type: "problem", + }, + + create(context) { + let lazyProperties = new Map(); + let unknownProperties = []; + let isLazyExported = false; + + function getAncestorNodes(node) { + const ancestors = []; + node = node.parent; + while (node) { + ancestors.unshift(node); + node = node.parent; + } + return ancestors; + } + + // Returns true if lazy getter definitions in prevNode and currNode are + // duplicate. + // This returns false if prevNode and currNode have the same IfStatement as + // ancestor and they're in different branches. + function isDuplicate(prevNode, currNode) { + const prevAncestors = getAncestorNodes(prevNode); + const currAncestors = getAncestorNodes(currNode); + + for ( + let i = 0; + i < prevAncestors.length && i < currAncestors.length; + i++ + ) { + const prev = prevAncestors[i]; + const curr = currAncestors[i]; + if (prev === curr && prev.type === "IfStatement") { + if (prevAncestors[i + 1] !== currAncestors[i + 1]) { + return false; + } + } + } + + return true; + } + + function addProp(callNode, propNode, name) { + if ( + lazyProperties.has(name) && + isDuplicate(lazyProperties.get(name).callNode, callNode) + ) { + context.report({ + node: propNode, + messageId: "duplicateSymbol", + data: { name }, + }); + return; + } + lazyProperties.set(name, { used: false, callNode, propNode }); + } + + function setPropertiesFromArgument(callNode, arg) { + if (arg.type === "ObjectExpression") { + for (let propNode of arg.properties) { + if (propNode.key.type == "Literal") { + context.report({ + node: propNode, + messageId: "incorrectType", + data: { name: propNode.key.value }, + }); + continue; + } + addProp(callNode, propNode, propNode.key.name); + } + } else if (arg.type === "ArrayExpression") { + for (let propNode of arg.elements) { + if (propNode.type != "Literal") { + continue; + } + addProp(callNode, propNode, propNode.value); + } + } + } + + return { + VariableDeclarator(node) { + if ( + node.id.type === "Identifier" && + node.id.name == "lazy" && + node.init.type == "CallExpression" && + node.init.callee.name == "createLazyLoaders" + ) { + setPropertiesFromArgument(node.init, node.init.arguments[0]); + } + }, + + CallExpression(node) { + if ( + node.callee.type != "MemberExpression" || + (node.callee.object.type == "MemberExpression" && + !items.includes(node.callee.object.object.name)) || + (node.callee.object.type != "MemberExpression" && + !items.includes(node.callee.object.name)) + ) { + return; + } + + let source; + try { + source = helpers.getASTSource(node); + } catch (e) { + return; + } + + for (let reg of callExpressionDefinitions) { + let match = source.match(reg); + if (match) { + if ( + lazyProperties.has(match[1]) && + isDuplicate(lazyProperties.get(match[1]).callNode, node) + ) { + context.report({ + node, + messageId: "duplicateSymbol", + data: { name: match[1] }, + }); + return; + } + lazyProperties.set(match[1], { + used: false, + callNode: node, + propNode: node, + }); + break; + } + } + + if ( + callExpressionMultiDefinitions.some(expr => + source.startsWith(expr) + ) && + node.arguments[1] + ) { + setPropertiesFromArgument(node, node.arguments[1]); + } + }, + + MemberExpression(node) { + if (node.computed || node.object.type !== "Identifier") { + return; + } + + let name; + if (node.object.name == "lazy") { + name = node.property.name; + } else { + return; + } + let property = lazyProperties.get(name); + if (!property) { + // These will be reported on Program:exit - some definitions may + // be after first use, so we need to wait until we've processed + // the whole file before reporting. + unknownProperties.push({ name, node }); + } else { + property.used = true; + } + if ( + helpers.getIsTopLevelAndUnconditionallyExecuted( + context.getAncestors() + ) + ) { + context.report({ + node, + messageId: "topLevelAndUnconditional", + data: { name }, + }); + } + }, + + ExportNamedDeclaration(node) { + for (const spec of node.specifiers) { + if (spec.local.name === "lazy") { + // If the lazy object is exported, do not check unused property. + isLazyExported = true; + break; + } + } + }, + + "Program:exit": function () { + for (let { name, node } of unknownProperties) { + let property = lazyProperties.get(name); + if (!property) { + context.report({ + node, + messageId: "unknownProperty", + data: { name }, + }); + } else { + property.used = true; + } + } + if (!isLazyExported) { + for (let [name, property] of lazyProperties.entries()) { + if (!property.used) { + context.report({ + node: property.propNode, + messageId: "unusedProperty", + data: { name }, + }); + } + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js new file mode 100644 index 0000000000..8f665d6d8a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js @@ -0,0 +1,126 @@ +/** + * @fileoverview Ensures that property accesses on Services.<alias> are valid. + * Although this largely duplicates the valid-services rule, the checks here + * require an objdir and a manual run. + * + * 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/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +function findInterfaceNames(name) { + let interfaces = []; + for (let [key, value] of Object.entries(helpers.servicesData)) { + if (value == name) { + interfaces.push(key); + } + } + return interfaces; +} + +function isInInterface(interfaceName, name) { + let interfaceDetails = helpers.xpidlData.get(interfaceName); + + // TODO: Bug 1790261 - check only methods if the expression is callable. + if (interfaceDetails.methods.some(m => m.name == name)) { + return true; + } + + if (interfaceDetails.consts.some(c => c.name == name)) { + return true; + } + + if (interfaceDetails.parent) { + return isInInterface(interfaceDetails.parent, name); + } + return false; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-services-property.html", + }, + messages: { + unknownProperty: + "Unknown property access Services.{{ alias }}.{{ propertyName }}, Interfaces: {{ checkedInterfaces }}", + }, + schema: [], + type: "problem", + }, + + create(context) { + let servicesInterfaceMap = helpers.servicesData; + let serviceAliases = new Set([ + ...Object.values(servicesInterfaceMap), + // This is defined only for Android, so most builds won't pick it up. + "androidBridge", + // These are defined without interfaces and hence are not in the services map. + "cpmm", + "crashmanager", + "mm", + "ppmm", + // The new xulStore also does not have an interface. + "xulStore", + ]); + return { + MemberExpression(node) { + if (node.computed || node.object.type !== "Identifier") { + return; + } + + let mainNode; + if (node.object.name == "Services") { + mainNode = node; + } else if ( + node.property.name == "Services" && + node.parent.type == "MemberExpression" + ) { + mainNode = node.parent; + } else { + return; + } + + let alias = mainNode.property.name; + if (!serviceAliases.has(alias)) { + return; + } + + if ( + mainNode.parent.type == "MemberExpression" && + !mainNode.parent.computed + ) { + let propertyName = mainNode.parent.property.name; + if (propertyName == "wrappedJSObject") { + return; + } + let interfaces = findInterfaceNames(alias); + if (!interfaces.length) { + return; + } + + let checkedInterfaces = []; + for (let item of interfaces) { + if (isInInterface(item, propertyName)) { + return; + } + checkedInterfaces.push(item); + } + context.report({ + node, + messageId: "unknownProperty", + data: { + alias, + propertyName, + checkedInterfaces, + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js new file mode 100644 index 0000000000..7380fda491 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js @@ -0,0 +1,68 @@ +/** + * @fileoverview Ensures that Services uses have valid property names. + * + * 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/. + */ + +"use strict"; +const helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-services.html", + }, + messages: { + unknownProperty: "Unknown Services member property {{ alias }}", + }, + schema: [], + type: "problem", + }, + + create(context) { + let servicesInterfaceMap = helpers.servicesData; + let serviceAliases = new Set([ + ...Object.values(servicesInterfaceMap), + // This is defined only for Android, so most builds won't pick it up. + "androidBridge", + // These are defined without interfaces and hence are not in the services map. + "cpmm", + "crashmanager", + "mm", + "ppmm", + // The new xulStore also does not have an interface. + "xulStore", + ]); + return { + MemberExpression(node) { + if (node.computed || node.object.type !== "Identifier") { + return; + } + + let alias; + if (node.object.name == "Services") { + alias = node.property.name; + } else if ( + node.property.name == "Services" && + node.parent.type == "MemberExpression" + ) { + alias = node.parent.property.name; + } else { + return; + } + + if (!serviceAliases.has(alias)) { + context.report({ + node, + messageId: "unknownProperty", + data: { + alias, + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js new file mode 100644 index 0000000000..5da799c643 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js @@ -0,0 +1,42 @@ +/** + * @fileoverview Marks all var declarations that are not at the top level + * invalid. + * + * 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/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/var-only-at-top-level.html", + }, + messages: { + unexpectedVar: "Unexpected var, use let or const instead.", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + VariableDeclaration(node) { + if (node.kind === "var") { + if (helpers.getIsTopLevelScript(context.getAncestors())) { + return; + } + + context.report({ + node, + messageId: "unexpectedVar", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/services.json b/tools/lint/eslint/eslint-plugin-mozilla/lib/services.json new file mode 100644 index 0000000000..476c6bf784 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/services.json @@ -0,0 +1,63 @@ +{ + "mozIJSSubScriptLoader": "scriptloader", + "mozILocaleService": "locale", + "mozIMozIntl": "intl", + "mozIStorageService": "storage", + "nsIAppShellService": "appShell", + "nsIAppStartup": "startup", + "nsIBlocklistService": "blocklist", + "nsICacheStorageService": "cache2", + "nsICategoryManager": "catMan", + "nsIClearDataService": "clearData", + "nsIClipboard": "clipboard", + "nsIConsoleService": "console", + "nsICookieBannerService": "cookieBanners", + "nsICookieManager": "cookies", + "nsICookieService": "cookies", + "nsICrashReporter": "appinfo", + "nsIDAPTelemetry": "DAPTelemetry", + "nsIDOMRequestService": "DOMRequest", + "nsIDOMStorageManager": "domStorageManager", + "nsIDNSService": "dns", + "nsIDirectoryService": "dirsvc", + "nsIDroppedLinkHandler": "droppedLinkHandler", + "nsIEffectiveTLDService": "eTLD", + "nsIEnterprisePolicies": "policies", + "nsIEnvironment": "env", + "nsIEventListenerService": "els", + "nsIFOG": "fog", + "nsIFocusManager": "focus", + "nsIIOService": "io", + "nsILoadContextInfoFactory": "loadContextInfo", + "nsILocalStorageManager": "domStorageManager", + "nsILoginManager": "logins", + "nsINetUtil": "io", + "nsIObserverService": "obs", + "nsIPermissionManager": "perms", + "nsIPrefBranch": "prefs", + "nsIPrefService": "prefs", + "nsIProfiler": "profiler", + "nsIPromptService": "prompt", + "nsIProperties": "dirsvc", + "nsIPropertyBag2": "sysinfo", + "nsIQuotaManagerService": "qms", + "nsIRFPService": "rfp", + "nsIScriptSecurityManager": "scriptSecurityManager", + "nsISearchService": "search", + "nsISessionStorageService": "sessionStorage", + "nsISpeculativeConnect": "io", + "nsIStringBundleService": "strings", + "nsISystemInfo": "sysinfo", + "nsITelemetry": "telemetry", + "nsITextToSubURI": "textToSubURI", + "nsIThreadManager": "tm", + "nsIURIFixup": "uriFixup", + "nsIURLFormatter": "urlFormatter", + "nsIUUIDGenerator": "uuid", + "nsIVersionComparator": "vc", + "nsIWindowMediator": "wm", + "nsIWindowWatcher": "ww", + "nsIXULAppInfo": "appinfo", + "nsIXULRuntime": "appinfo", + "nsIXULStore": "xulStore" +} diff --git a/tools/lint/eslint/eslint-plugin-mozilla/manifest.tt b/tools/lint/eslint/eslint-plugin-mozilla/manifest.tt new file mode 100644 index 0000000000..47ca98109b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/manifest.tt @@ -0,0 +1,10 @@ +[ + { + "filename": "eslint-plugin-mozilla.tar.gz", + "size": 5546844, + "algorithm": "sha512", + "digest": "7df05ad96a7e892f04079570e7847cef71048e3d96e7a338f7e5aa431d5bf34225196200fffc42a630678f2bb074b806dd24d76da25827fbfc16d0ae5e8e6cd5", + "unpack": true, + "visibility": "public" + } +]
\ No newline at end of file diff --git a/tools/lint/eslint/eslint-plugin-mozilla/package-lock.json b/tools/lint/eslint/eslint-plugin-mozilla/package-lock.json new file mode 100644 index 0000000000..8265c41a94 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/package-lock.json @@ -0,0 +1,5249 @@ +{ + "name": "eslint-plugin-mozilla", + "version": "3.7.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "eslint-plugin-mozilla", + "version": "3.7.0", + "license": "MPL-2.0", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "estraverse": "^5.3.0", + "htmlparser2": "^8.0.1", + "toml-eslint-parser": "0.9.3" + }, + "devDependencies": { + "eslint": "8.56.0", + "mocha": "10.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@microsoft/eslint-plugin-sdl": "^0.2.2", + "eslint": "^7.23.0 || ^8.0.0", + "eslint-config-prettier": "^8.0.0 || ^9.0.0", + "eslint-plugin-fetch-options": "^0.0.5", + "eslint-plugin-html": "^7.0.0", + "eslint-plugin-json": "^3.1.0", + "eslint-plugin-no-unsanitized": "^4.0.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + }, + "node_modules/@microsoft/eslint-plugin-sdl": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@microsoft/eslint-plugin-sdl/-/eslint-plugin-sdl-0.2.2.tgz", + "integrity": "sha512-TiBepeQMSxHpvIbKA03TbO9nZqRrKR1th47wGdjY1sH2SSer+JgKlSF3S8GURGA8/zp2T/HwSiAJelclJ3hEvg==", + "peer": true, + "dependencies": { + "eslint-plugin-node": "11.1.0", + "eslint-plugin-react": "7.33.0", + "eslint-plugin-security": "1.4.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": "^4.19.1 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "peer": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", + "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "peer": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "peer": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "peer": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "peer": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "peer": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-fetch-options": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-fetch-options/-/eslint-plugin-fetch-options-0.0.5.tgz", + "integrity": "sha512-ZMxrccsOAZ7uMQ4nMvPJLqLg6oyLF96YOEwTKWAIbDHpwWUp1raXALZom8ikKucaEnhqWSRuBWI8jBXveFwkJg==", + "peer": true, + "engines": { + "node": ">=0.9.0" + } + }, + "node_modules/eslint-plugin-html": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-7.1.0.tgz", + "integrity": "sha512-fNLRraV/e6j8e3XYOC9xgND4j+U7b1Rq+OygMlLcMg+wI/IpVbF+ubQa3R78EjKB9njT6TQOlcK5rFKBVVtdfg==", + "peer": true, + "dependencies": { + "htmlparser2": "^8.0.1" + } + }, + "node_modules/eslint-plugin-json": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-3.1.0.tgz", + "integrity": "sha512-MrlG2ynFEHe7wDGwbUuFPsaT2b1uhuEFhJ+W1f1u+1C2EkXmTYJp4B1aAdQQ8M+CC3t//N/oRKiIVw14L2HR1g==", + "peer": true, + "dependencies": { + "lodash": "^4.17.21", + "vscode-json-languageservice": "^4.1.6" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/eslint-plugin-no-unsanitized": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.0.2.tgz", + "integrity": "sha512-Pry0S9YmHoz8NCEMRQh7N0Yexh2MYCNPIlrV52hTmS7qXnTghWsjXouF08bgsrrZqaW9tt1ZiK3j5NEmPE+EjQ==", + "peer": true, + "peerDependencies": { + "eslint": "^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "peer": true, + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.0.tgz", + "integrity": "sha512-qewL/8P34WkY8jAqdQxsiL82pDUeT7nhs8IsuXgfgnsEloKCT4miAV9N9kGtx7/KM9NH/NCGUE7Edt9iGxLXFw==", + "peer": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "peer": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-security": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.4.0.tgz", + "integrity": "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA==", + "peer": true, + "dependencies": { + "safe-regex": "^1.1.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fastq": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", + "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "peer": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "peer": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "peer": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "peer": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "peer": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "peer": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "peer": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "peer": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "peer": true + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "peer": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "peer": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "peer": true, + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "peer": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "peer": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "peer": true, + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "peer": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toml-eslint-parser": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/toml-eslint-parser/-/toml-eslint-parser-0.9.3.tgz", + "integrity": "sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw==", + "dependencies": { + "eslint-visitor-keys": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "peer": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.2.1.tgz", + "integrity": "sha512-xGmv9QIWs2H8obGbWg+sIPI/3/pFgj/5OWBhNzs00BkYQ9UaB2F6JJaGB/2/YOZJ3BvLXQTC4Q7muqU25QgAhA==", + "peer": true, + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.3", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.3" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==", + "peer": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "peer": true + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "peer": true + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "peer": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "peer": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "peer": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==" + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==" + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==" + }, + "@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "requires": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + }, + "@microsoft/eslint-plugin-sdl": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@microsoft/eslint-plugin-sdl/-/eslint-plugin-sdl-0.2.2.tgz", + "integrity": "sha512-TiBepeQMSxHpvIbKA03TbO9nZqRrKR1th47wGdjY1sH2SSer+JgKlSF3S8GURGA8/zp2T/HwSiAJelclJ3hEvg==", + "peer": true, + "requires": { + "eslint-plugin-node": "11.1.0", + "eslint-plugin-react": "7.33.0", + "eslint-plugin-security": "1.4.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==" + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.tosorted": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "peer": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, + "available-typed-arrays": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", + "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", + "peer": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "peer": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "peer": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "peer": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "peer": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + } + }, + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "peer": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "peer": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "peer": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + } + }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "peer": true, + "requires": {} + }, + "eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "peer": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + } + }, + "eslint-plugin-fetch-options": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-fetch-options/-/eslint-plugin-fetch-options-0.0.5.tgz", + "integrity": "sha512-ZMxrccsOAZ7uMQ4nMvPJLqLg6oyLF96YOEwTKWAIbDHpwWUp1raXALZom8ikKucaEnhqWSRuBWI8jBXveFwkJg==", + "peer": true + }, + "eslint-plugin-html": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-7.1.0.tgz", + "integrity": "sha512-fNLRraV/e6j8e3XYOC9xgND4j+U7b1Rq+OygMlLcMg+wI/IpVbF+ubQa3R78EjKB9njT6TQOlcK5rFKBVVtdfg==", + "peer": true, + "requires": { + "htmlparser2": "^8.0.1" + } + }, + "eslint-plugin-json": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-3.1.0.tgz", + "integrity": "sha512-MrlG2ynFEHe7wDGwbUuFPsaT2b1uhuEFhJ+W1f1u+1C2EkXmTYJp4B1aAdQQ8M+CC3t//N/oRKiIVw14L2HR1g==", + "peer": true, + "requires": { + "lodash": "^4.17.21", + "vscode-json-languageservice": "^4.1.6" + } + }, + "eslint-plugin-no-unsanitized": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.0.2.tgz", + "integrity": "sha512-Pry0S9YmHoz8NCEMRQh7N0Yexh2MYCNPIlrV52hTmS7qXnTghWsjXouF08bgsrrZqaW9tt1ZiK3j5NEmPE+EjQ==", + "peer": true, + "requires": {} + }, + "eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "peer": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + } + }, + "eslint-plugin-react": { + "version": "7.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.0.tgz", + "integrity": "sha512-qewL/8P34WkY8jAqdQxsiL82pDUeT7nhs8IsuXgfgnsEloKCT4miAV9N9kGtx7/KM9NH/NCGUE7Edt9iGxLXFw==", + "peer": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "peer": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "peer": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "eslint-plugin-security": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.4.0.tgz", + "integrity": "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA==", + "peer": true, + "requires": { + "safe-regex": "^1.1.0" + } + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "peer": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "peer": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fastq": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", + "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "peer": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "peer": true + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "peer": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "peer": true, + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "requires": { + "type-fest": "^0.20.2" + } + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "peer": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "peer": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "peer": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "peer": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "peer": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "peer": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "peer": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "peer": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==" + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "peer": true, + "requires": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "peer": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "peer": true + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "peer": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "peer": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "peer": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "peer": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "peer": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "peer": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "peer": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "peer": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "peer": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "peer": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "peer": true + }, + "jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "peer": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "peer": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "peer": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "peer": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "peer": true + }, + "object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "peer": true, + "requires": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "peer": true, + "requires": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "peer": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "peer": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "peer": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "peer": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "peer": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-array-concat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "peer": true, + "requires": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "peer": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safe-regex-test": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "peer": true, + "requires": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "is-regex": "^1.1.4" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "peer": true + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "peer": true, + "requires": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + } + }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "peer": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "peer": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.matchall": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "peer": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toml-eslint-parser": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/toml-eslint-parser/-/toml-eslint-parser-0.9.3.tgz", + "integrity": "sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw==", + "requires": { + "eslint-visitor-keys": "^3.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "peer": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "peer": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "vscode-json-languageservice": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.2.1.tgz", + "integrity": "sha512-xGmv9QIWs2H8obGbWg+sIPI/3/pFgj/5OWBhNzs00BkYQ9UaB2F6JJaGB/2/YOZJ3BvLXQTC4Q7muqU25QgAhA==", + "peer": true, + "requires": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.3", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.3" + } + }, + "vscode-languageserver-textdocument": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==", + "peer": true + }, + "vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "peer": true + }, + "vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "peer": true + }, + "vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "peer": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "peer": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "peer": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } +} diff --git a/tools/lint/eslint/eslint-plugin-mozilla/package.json b/tools/lint/eslint/eslint-plugin-mozilla/package.json new file mode 100644 index 0000000000..9d4a111dd2 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/package.json @@ -0,0 +1,53 @@ +{ + "name": "eslint-plugin-mozilla", + "version": "3.7.0", + "description": "A collection of rules that help enforce JavaScript coding standard in the Mozilla project.", + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin", + "mozilla", + "firefox" + ], + "bugs": { + "url": "https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=Lint" + }, + "homepage": "http://firefox-source-docs.mozilla.org/tools/lint/linters/eslint-plugin-mozilla.html", + "repository": { + "type": "hg", + "url": "https://hg.mozilla.org/mozilla-central/" + }, + "author": "Mike Ratcliffe", + "main": "lib/index.js", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "estraverse": "^5.3.0", + "htmlparser2": "^8.0.1", + "toml-eslint-parser": "0.9.3" + }, + "devDependencies": { + "eslint": "8.56.0", + "mocha": "10.2.0" + }, + "peerDependencies": { + "@microsoft/eslint-plugin-sdl": "^0.2.2", + "eslint": "^7.23.0 || ^8.0.0", + "eslint-config-prettier": "^8.0.0 || ^9.0.0", + "eslint-plugin-fetch-options": "^0.0.5", + "eslint-plugin-html": "^7.0.0", + "eslint-plugin-json": "^3.1.0", + "eslint-plugin-no-unsanitized": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "scripts": { + "prepack": "node scripts/createExports.js", + "test": "mocha --reporter 'reporters/mozilla-format.js' tests", + "postpublish": "rm -f lib/environments/saved-globals.json lib/rules/saved-rules-data.json", + "update-tooltool": "./update.sh" + }, + "license": "MPL-2.0" +} diff --git a/tools/lint/eslint/eslint-plugin-mozilla/reporters/mozilla-format.js b/tools/lint/eslint/eslint-plugin-mozilla/reporters/mozilla-format.js new file mode 100644 index 0000000000..a3f96e7bef --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/reporters/mozilla-format.js @@ -0,0 +1,57 @@ +/* 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/. */ + +/** + * This file outputs the format that treeherder requires. If we integrate + * these tests with ./mach, then we may replace this with a json handler within + * mach itself. + */ + +"use strict"; + +var mocha = require("mocha"); +var path = require("path"); +module.exports = MozillaFormatter; + +function MozillaFormatter(runner) { + mocha.reporters.Base.call(this, runner); + var passes = 0; + var failures = []; + + runner.on("start", () => { + console.log("SUITE-START | eslint-plugin-mozilla"); + }); + + runner.on("pass", function (test) { + passes++; + let title = test.title.replace(/\n/g, "|"); + console.log(`TEST-PASS | ${path.basename(test.file)} | ${title}`); + }); + + runner.on("fail", function (test, err) { + failures.push(test); + // Replace any newlines in the title. + let title = test.title.replace(/\n/g, "|"); + console.log( + `TEST-UNEXPECTED-FAIL | ${path.basename(test.file)} | ${title} | ${ + err.message + }` + ); + mocha.reporters.Base.list([test]); + }); + + runner.on("end", function () { + // Space the results out visually with an additional blank line. + console.log(""); + console.log("INFO | Result summary:"); + console.log(`INFO | Passed: ${passes}`); + console.log(`INFO | Failed: ${failures.length}`); + console.log("SUITE-END"); + // Space the failures out visually with an additional blank line. + console.log(""); + console.log("Failure summary:"); + mocha.reporters.Base.list(failures); + process.exit(failures.length); + }); +} diff --git a/tools/lint/eslint/eslint-plugin-mozilla/scripts/createExports.js b/tools/lint/eslint/eslint-plugin-mozilla/scripts/createExports.js new file mode 100644 index 0000000000..7f839edfba --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/scripts/createExports.js @@ -0,0 +1,77 @@ +/** + * @fileoverview A script to export the known globals to a file. + * 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/. + */ +"use strict"; + +var fs = require("fs"); +var path = require("path"); +var helpers = require("../lib/helpers"); + +const eslintDir = path.join(helpers.rootDir, "tools", "lint", "eslint"); + +const globalsFile = path.join( + eslintDir, + "eslint-plugin-mozilla", + "lib", + "environments", + "saved-globals.json" +); +const rulesFile = path.join( + eslintDir, + "eslint-plugin-mozilla", + "lib", + "rules", + "saved-rules-data.json" +); + +console.log("Copying services.json"); + +const env = helpers.getBuildEnvironment(); + +const servicesFile = path.join( + env.topobjdir, + "xpcom", + "components", + "services.json" +); +const shipServicesFile = path.join( + eslintDir, + "eslint-plugin-mozilla", + "lib", + "services.json" +); + +fs.writeFileSync(shipServicesFile, fs.readFileSync(servicesFile)); + +console.log("Generating globals file"); + +// Export the environments. +let environmentGlobals = require("../lib/index.js").environments; + +return fs.writeFile( + globalsFile, + JSON.stringify({ environments: environmentGlobals }), + err => { + if (err) { + console.error(err); + process.exit(1); + } + + console.log("Globals file generation complete"); + + console.log("Creating rules data file"); + let rulesData = {}; + + return fs.writeFile(rulesFile, JSON.stringify({ rulesData }), err1 => { + if (err1) { + console.error(err1); + process.exit(1); + } + + console.log("Globals file generation complete"); + }); + } +); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/avoid-Date-timing.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/avoid-Date-timing.js new file mode 100644 index 0000000000..37fc424617 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/avoid-Date-timing.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/avoid-Date-timing"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, type, message) { + return { code, errors: [{ messageId: "usePerfNow", type }] }; +} + +ruleTester.run("avoid-Date-timing", rule, { + valid: [ + "new Date('2017-07-11');", + "new Date(1499790192440);", + "new Date(2017, 7, 11);", + "Date.UTC(2017, 7);", + ], + invalid: [ + invalidCode("Date.now();", "CallExpression"), + invalidCode("new Date();", "NewExpression"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/avoid-removeChild.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/avoid-removeChild.js new file mode 100644 index 0000000000..3736f25853 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/avoid-removeChild.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/avoid-removeChild"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, messageId = "useRemove") { + return { code, errors: [{ messageId, type: "CallExpression" }] }; +} + +ruleTester.run("avoid-removeChild", rule, { + valid: [ + "elt.remove();", + "elt.parentNode.parentNode.removeChild(elt2.parentNode);", + "elt.parentNode.removeChild(elt2);", + "elt.removeChild(elt2);", + ], + invalid: [ + invalidCode("elt.parentNode.removeChild(elt);"), + invalidCode("elt.parentNode.parentNode.removeChild(elt.parentNode);"), + invalidCode("$(e).parentNode.removeChild($(e));"), + invalidCode("$('e').parentNode.removeChild($('e'));"), + invalidCode("elt.removeChild(elt.firstChild);", "useFirstChildRemove"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/balanced-listeners.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/balanced-listeners.js new file mode 100644 index 0000000000..b7cd4e2c2f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/balanced-listeners.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/balanced-listeners"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function error(code, functionName, type) { + return { + code, + errors: [ + { + messageId: "noCorresponding", + type: "Identifier", + data: { functionName, type }, + }, + ], + }; +} + +ruleTester.run("balanced-listeners", rule, { + valid: [ + "elt.addEventListener('event', handler);" + + "elt.removeEventListener('event', handler);", + + "elt.addEventListener('event', handler, true);" + + "elt.removeEventListener('event', handler, true);", + + "elt.addEventListener('event', handler, false);" + + "elt.removeEventListener('event', handler, false);", + + "elt.addEventListener('event', handler);" + + "elt.removeEventListener('event', handler, false);", + + "elt.addEventListener('event', handler, false);" + + "elt.removeEventListener('event', handler);", + + "elt.addEventListener('event', handler, {capture: false});" + + "elt.removeEventListener('event', handler);", + + "elt.addEventListener('event', handler);" + + "elt.removeEventListener('event', handler, {capture: false});", + + "elt.addEventListener('event', handler, {capture: true});" + + "elt.removeEventListener('event', handler, true);", + + "elt.addEventListener('event', handler, true);" + + "elt.removeEventListener('event', handler, {capture: true});", + + "elt.addEventListener('event', handler, {once: true});", + + "elt.addEventListener('event', handler, {once: true, capture: true});", + ], + invalid: [ + error( + "elt.addEventListener('click', handler, false);", + "removeEventListener", + "click" + ), + + error( + "elt.addEventListener('click', handler, false);" + + "elt.removeEventListener('click', handler, true);", + "removeEventListener", + "click" + ), + + error( + "elt.addEventListener('click', handler, {capture: false});" + + "elt.removeEventListener('click', handler, true);", + "removeEventListener", + "click" + ), + + error( + "elt.addEventListener('click', handler, {capture: true});" + + "elt.removeEventListener('click', handler);", + "removeEventListener", + "click" + ), + + error( + "elt.addEventListener('click', handler, true);" + + "elt.removeEventListener('click', handler);", + "removeEventListener", + "click" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/balanced-observers.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/balanced-observers.js new file mode 100644 index 0000000000..97e578a1f4 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/balanced-observers.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/balanced-observers"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function error(code, observable) { + return { + code, + errors: [ + { + messageId: "noCorresponding", + type: "Identifier", + data: { observable }, + }, + ], + }; +} + +ruleTester.run("balanced-observers", rule, { + valid: [ + "Services.obs.addObserver(observer, 'observable');" + + "Services.obs.removeObserver(observer, 'observable');", + "Services.prefs.addObserver('preference.name', otherObserver);" + + "Services.prefs.removeObserver('preference.name', otherObserver);", + ], + invalid: [ + error( + // missing Services.obs.removeObserver + "Services.obs.addObserver(observer, 'observable');", + "observable" + ), + + error( + // wrong observable name for Services.obs.removeObserver + "Services.obs.addObserver(observer, 'observable');" + + "Services.obs.removeObserver(observer, 'different-observable');", + "observable" + ), + + error( + // missing Services.prefs.removeObserver + "Services.prefs.addObserver('preference.name', otherObserver);", + "preference.name" + ), + + error( + // wrong observable name for Services.prefs.removeObserver + "Services.prefs.addObserver('preference.name', otherObserver);" + + "Services.prefs.removeObserver('other.preference', otherObserver);", + "preference.name" + ), + + error( + // mismatch Services.prefs vs Services.obs + "Services.obs.addObserver(observer, 'observable');" + + "Services.prefs.removeObserver(observer, 'observable');", + "observable" + ), + + error( + "Services.prefs.addObserver('preference.name', otherObserver);" + + // mismatch Services.prefs vs Services.obs + "Services.obs.removeObserver('preference.name', otherObserver);", + "preference.name" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/consistent-if-bracing.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/consistent-if-bracing.js new file mode 100644 index 0000000000..9712fd9a78 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/consistent-if-bracing.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/consistent-if-bracing"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { code, errors: [{ messageId: "consistentIfBracing" }] }; +} + +ruleTester.run("consistent-if-bracing", rule, { + valid: [ + "if (true) {1} else {0}", + "if (false) 1; else 0", + "if (true) {1} else if (true) {2} else {0}", + "if (true) {1} else if (true) {2} else if (true) {3} else {0}", + ], + invalid: [ + invalidCode(`if (true) {1} else 0`), + invalidCode("if (true) 1; else {0}"), + invalidCode("if (true) {1} else if (true) 2; else {0}"), + invalidCode("if (true) 1; else if (true) {2} else {0}"), + invalidCode("if (true) {1} else if (true) {2} else 0"), + invalidCode("if (true) {1} else if (true) {2} else if (true) 3; else {0}"), + invalidCode("if (true) {if (true) 1; else {0}} else {0}"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/globals.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/globals.js new file mode 100644 index 0000000000..1098cb8a34 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/globals.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { getGlobalsForCode } = require("../lib/globals"); +var assert = require("assert"); + +/* global describe, it */ + +describe("globals", function () { + it("should reflect top-level this property assignment", function () { + const globals = getGlobalsForCode(` +this.foo = 10; +`); + assert.deepEqual(globals, [{ name: "foo", writable: true }]); + }); + + it("should reflect this property assignment inside block", function () { + const globals = getGlobalsForCode(` +{ + this.foo = 10; +} +`); + assert.deepEqual(globals, [{ name: "foo", writable: true }]); + }); + + it("should ignore this property assignment inside function declaration", function () { + const globals = getGlobalsForCode(` +function f() { + this.foo = 10; +} +`); + assert.deepEqual(globals, [{ name: "f", writable: true }]); + }); + + it("should ignore this property assignment inside function expression", function () { + const globals = getGlobalsForCode(` +(function f() { + this.foo = 10; +}); +`); + assert.deepEqual(globals, []); + }); + + it("should ignore this property assignment inside method", function () { + const globals = getGlobalsForCode(` +({ + method() { + this.foo = 10; + } +}); +`); + assert.deepEqual(globals, []); + }); + + it("should ignore this property assignment inside accessor", function () { + const globals = getGlobalsForCode(` +({ + get x() { + this.foo = 10; + }, + set x(v) { + this.bar = 10; + } +}); +`); + assert.deepEqual(globals, []); + }); + + it("should reflect this property assignment inside arrow function", function () { + const globals = getGlobalsForCode(` +() => { + this.foo = 10; +}; +`); + assert.deepEqual(globals, [{ name: "foo", writable: true }]); + }); + + it("should ignore this property assignment inside arrow function inside function expression", function () { + const globals = getGlobalsForCode(` +(function f() { + () => { + this.foo = 10; + }; +}); +`); + assert.deepEqual(globals, []); + }); + + it("should ignore this property assignment inside class static", function () { + const globals = getGlobalsForCode(` +class A { + static { + this.foo = 10; + (() => { + this.bar = 10; + })(); + } +} +`); + assert.deepEqual(globals, [{ name: "A", writable: true }]); + }); + + it("should ignore this property assignment inside class property", function () { + const globals = getGlobalsForCode(` +class A { + a = this.foo = 10; + b = (() => { + this.bar = 10; + })(); +} +`); + assert.deepEqual(globals, [{ name: "A", writable: true }]); + }); + + it("should ignore this property assignment inside class static property", function () { + const globals = getGlobalsForCode(` +class A { + static a = this.foo = 10; + static b = (() => { + this.bar = 10; + })(); +} +`); + assert.deepEqual(globals, [{ name: "A", writable: true }]); + }); + + it("should ignore this property assignment inside class private property", function () { + const globals = getGlobalsForCode(` +class A { + #a = this.foo = 10; + #b = (() => { + this.bar = 10; + })(); +} +`); + assert.deepEqual(globals, [{ name: "A", writable: true }]); + }); + + it("should ignore this property assignment inside class static private property", function () { + const globals = getGlobalsForCode(` +class A { + static #a = this.foo = 10; + static #b = (() => { + this.bar = 10; + })(); +} +`); + assert.deepEqual(globals, [{ name: "A", writable: true }]); + }); + + it("should reflect lazy getters", function () { + const globals = getGlobalsForCode(` +ChromeUtils.defineESModuleGetters(this, { + A: "B", +}); +`); + assert.deepEqual(globals, [{ name: "A", writable: true, explicit: true }]); + }); +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/lazy-getter-object-name.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/lazy-getter-object-name.js new file mode 100644 index 0000000000..33a2a6c130 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/lazy-getter-object-name.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/lazy-getter-object-name"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { + code, + errors: [{ messageId: "mustUseLazy", type: "CallExpression" }], + }; +} + +ruleTester.run("lazy-getter-object-name", rule, { + valid: [ + ` + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`, + ], + invalid: [ + invalidCode(` + ChromeUtils.defineESModuleGetters(obj, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`), + invalidCode(` + ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`), + invalidCode(` + ChromeUtils.defineESModuleGetters(window, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/mark-exported-symbols-as-used.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/mark-exported-symbols-as-used.js new file mode 100644 index 0000000000..e738825fab --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/mark-exported-symbols-as-used.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/mark-exported-symbols-as-used"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, type, messageId) { + return { code, errors: [{ messageId, type }] }; +} + +ruleTester.run("mark-exported-symbols-as-used", rule, { + valid: [ + "var EXPORTED_SYMBOLS = ['foo'];", + "this.EXPORTED_SYMBOLS = ['foo'];", + ], + invalid: [ + invalidCode( + "let EXPORTED_SYMBOLS = ['foo'];", + "VariableDeclaration", + "useLetForExported" + ), + invalidCode( + "var EXPORTED_SYMBOLS = 'foo';", + "VariableDeclaration", + "nonArrayAssignedToImported" + ), + invalidCode( + "this.EXPORTED_SYMBOLS = 'foo';", + "AssignmentExpression", + "nonArrayAssignedToImported" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-addtask-setup.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-addtask-setup.js new file mode 100644 index 0000000000..cd036d7d91 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-addtask-setup.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-addtask-setup"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError() { + return [{ messageId: "useAddSetup", type: "CallExpression" }]; +} + +ruleTester.run("no-addtask-setup", rule, { + valid: [ + "add_task(function() {});", + "add_task(function () {});", + "add_task(function foo() {});", + "add_task(async function() {});", + "add_task(async function () {});", + "add_task(async function foo() {});", + "something(function setup() {});", + "something(async function setup() {});", + "add_task(setup);", + "add_task(setup => {});", + "add_task(async setup => {});", + ], + invalid: [ + { + code: "add_task(function setup() {});", + output: "add_setup(function() {});", + errors: callError(), + }, + { + code: "add_task(function setup () {});", + output: "add_setup(function () {});", + errors: callError(), + }, + { + code: "add_task(async function setup() {});", + output: "add_setup(async function() {});", + errors: callError(), + }, + { + code: "add_task(async function setup () {});", + output: "add_setup(async function () {});", + errors: callError(), + }, + { + code: "add_task(async function setUp() {});", + output: "add_setup(async function() {});", + errors: callError(), + }, + { + code: "add_task(async function init() {});", + output: "add_setup(async function() {});", + errors: callError(), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-arbitrary-setTimeout.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-arbitrary-setTimeout.js new file mode 100644 index 0000000000..ea194af6e3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-arbitrary-setTimeout.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-arbitrary-setTimeout"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function wrapCode(code, filename = "xpcshell/test_foo.js") { + return { code, filename }; +} + +function invalidCode(code) { + let obj = wrapCode(code); + obj.errors = [{ messageId: "listenForEvents", type: "CallExpression" }]; + return obj; +} + +ruleTester.run("no-arbitrary-setTimeout", rule, { + valid: [ + wrapCode("setTimeout(function() {}, 0);"), + wrapCode("setTimeout(function() {});"), + wrapCode("setTimeout(function() {}, 10);", "test_foo.js"), + ], + invalid: [ + invalidCode("setTimeout(function() {}, 10);"), + invalidCode("setTimeout(function() {}, timeout);"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-compare-against-boolean-literals.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-compare-against-boolean-literals.js new file mode 100644 index 0000000000..983ff0583e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-compare-against-boolean-literals.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-compare-against-boolean-literals"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError() { + return [{ messageId: "noCompareBoolean", type: "BinaryExpression" }]; +} + +ruleTester.run("no-compare-against-boolean-literals", rule, { + valid: [`if (!foo) {}`, `if (!!foo) {}`], + invalid: [ + { + code: `if (foo == true) {}`, + errors: callError(), + }, + { + code: `if (foo != true) {}`, + errors: callError(), + }, + { + code: `if (foo == false) {}`, + errors: callError(), + }, + { + code: `if (foo != false) {}`, + errors: callError(), + }, + { + code: `if (true == foo) {}`, + errors: callError(), + }, + { + code: `if (true != foo) {}`, + errors: callError(), + }, + { + code: `if (false == foo) {}`, + errors: callError(), + }, + { + code: `if (false != foo) {}`, + errors: callError(), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-comparison-or-assignment-inside-ok.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-comparison-or-assignment-inside-ok.js new file mode 100644 index 0000000000..3c43e5879f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-comparison-or-assignment-inside-ok.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-comparison-or-assignment-inside-ok"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, output, messageId, options = []) { + let rv = { + code, + errors: [{ messageId }], + }; + if (output) { + rv.output = output; + } + return rv; +} + +ruleTester.run("no-comparison-or-assignment-inside-ok", rule, { + valid: [ + "ok(foo)", + "ok(!bar)", + "ok(!foo, 'Message')", + "ok(bar, 'Message')", + "ok(!!foo, 'Message')", + ], + invalid: [ + // Assignment + invalidCode("ok(foo = bar)", null, "assignment"), + invalidCode("ok(foo = bar, 'msg')", null, "assignment"), + + // Comparisons: + invalidCode("ok(foo == bar)", "Assert.equal(foo, bar)", "comparison", { + operator: "==", + }), + invalidCode("ok(foo != bar)", "Assert.notEqual(foo, bar)", "comparison", { + operator: "!=", + }), + invalidCode("ok(foo < bar)", "Assert.less(foo, bar)", "comparison", { + operator: "<", + }), + invalidCode("ok(foo > bar)", "Assert.greater(foo, bar)", "comparison", { + operator: ">", + }), + invalidCode( + "ok(foo <= bar)", + "Assert.lessOrEqual(foo, bar)", + "comparison", + { operator: "<=" } + ), + invalidCode( + "ok(foo >= bar)", + "Assert.greaterOrEqual(foo, bar)", + "comparison", + { operator: ">=" } + ), + invalidCode( + "ok(foo === bar)", + "Assert.strictEqual(foo, bar)", + "comparison", + { operator: "===" } + ), + invalidCode( + "ok(foo !== bar)", + "Assert.notStrictEqual(foo, bar)", + "comparison", + { operator: "!==" } + ), + + // Comparisons with messages: + invalidCode( + "ok(foo == bar, 'hi')", + "Assert.equal(foo, bar, 'hi')", + "comparison", + { operator: "==" } + ), + invalidCode( + "ok(foo != bar, 'hi')", + "Assert.notEqual(foo, bar, 'hi')", + "comparison", + { operator: "!=" } + ), + invalidCode( + "ok(foo < bar, 'hi')", + "Assert.less(foo, bar, 'hi')", + "comparison", + { operator: "<" } + ), + invalidCode( + "ok(foo > bar, 'hi')", + "Assert.greater(foo, bar, 'hi')", + "comparison", + { operator: ">" } + ), + invalidCode( + "ok(foo <= bar, 'hi')", + "Assert.lessOrEqual(foo, bar, 'hi')", + "comparison", + { operator: "<=" } + ), + invalidCode( + "ok(foo >= bar, 'hi')", + "Assert.greaterOrEqual(foo, bar, 'hi')", + "comparison", + { operator: ">=" } + ), + invalidCode( + "ok(foo === bar, 'hi')", + "Assert.strictEqual(foo, bar, 'hi')", + "comparison", + { operator: "===" } + ), + invalidCode( + "ok(foo !== bar, 'hi')", + "Assert.notStrictEqual(foo, bar, 'hi')", + "comparison", + { operator: "!==" } + ), + + // Confusing bits that break fixup: + invalidCode( + "async () => ok((await foo) === bar, 'Oh no')", + "async () => Assert.strictEqual(await foo, bar, 'Oh no')", + "comparison", + { operator: "===" } + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-cu-reportError.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-cu-reportError.js new file mode 100644 index 0000000000..6650c8bf4e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-cu-reportError.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-cu-reportError"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError() { + return [{ messageId: "useConsoleError", type: "CallExpression" }]; +} + +ruleTester.run("no-cu-reportError", rule, { + valid: [ + "console.error('foo')", + "Cu.cloneInto({}, {})", + "foo().catch(console.error)", + // TODO: Bug 1802347 - this should be treated as an error as well. + "Cu.reportError('foo', stack)", + ], + invalid: [ + { + code: "Cu.reportError('foo')", + output: "console.error('foo')", + errors: callError(), + }, + { + code: "Cu.reportError(bar)", + output: "console.error(bar)", + errors: callError(), + }, + { + code: "Cu.reportError(bar.stack)", + output: "console.error(bar.stack)", + errors: callError(), + }, + { + code: "foo().catch(Cu.reportError)", + output: "foo().catch(console.error)", + errors: callError(), + }, + { + code: "foo().then(bar, Cu.reportError)", + output: "foo().then(bar, console.error)", + errors: callError(), + }, + // When referencing identifiers/members, try to reference them rather + // than stringifying: + { + code: "Cu.reportError('foo' + e)", + output: "console.error('foo', e)", + errors: callError(), + }, + { + code: "Cu.reportError('foo' + msg.data)", + output: "console.error('foo', msg.data)", + errors: callError(), + }, + // Don't touch existing concatenation of literals (usually done for + // wrapping reasons) + { + code: "Cu.reportError('foo' + 'bar' + 'baz')", + output: "console.error('foo' + 'bar' + 'baz')", + errors: callError(), + }, + // Ensure things work when nested: + { + code: "Cu.reportError('foo' + e + 'baz')", + output: "console.error('foo', e, 'baz')", + errors: callError(), + }, + // Ensure things work when nested in different places: + { + code: "Cu.reportError('foo' + e + 'quux' + 'baz')", + output: "console.error('foo', e, 'quux' + 'baz')", + errors: callError(), + }, + { + code: "Cu.reportError('foo' + 'quux' + e + 'baz')", + output: "console.error('foo' + 'quux', e, 'baz')", + errors: callError(), + }, + // Ensure things work with parens changing order of operations: + { + code: "Cu.reportError('foo' + 'quux' + (e + 'baz'))", + output: "console.error('foo' + 'quux' + (e + 'baz'))", + errors: callError(), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-define-cc-etc.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-define-cc-etc.js new file mode 100644 index 0000000000..ed71f6087e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-define-cc-etc.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-define-cc-etc"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, varNames) { + if (!Array.isArray(varNames)) { + varNames = [varNames]; + } + return { + code, + errors: varNames.map(name => { + return { + messageId: "noSeparateDefinition", + data: { name }, + type: "VariableDeclarator", + }; + }), + }; +} + +ruleTester.run("no-define-cc-etc", rule, { + valid: [ + "var Cm = Components.manager;", + "const CC = Components.Constructor;", + "var {Constructor: CC, manager: Cm} = Components;", + "const {Constructor: CC, manager: Cm} = Components;", + "foo.Cc.test();", + "const {bar, ...foo} = obj;", + ], + invalid: [ + invalidCode("var Cc;", "Cc"), + invalidCode("let Cc;", "Cc"), + invalidCode("let Ci;", "Ci"), + invalidCode("let Cr;", "Cr"), + invalidCode("let Cu;", "Cu"), + invalidCode("var Cc = Components.classes;", "Cc"), + invalidCode("const {classes: Cc} = Components;", "Cc"), + invalidCode("let {classes: Cc, manager: Cm} = Components", "Cc"), + invalidCode("const Cu = Components.utils;", "Cu"), + invalidCode("var Ci = Components.interfaces, Cc = Components.classes;", [ + "Ci", + "Cc", + ]), + invalidCode("var {'interfaces': Ci, 'classes': Cc} = Components;", [ + "Ci", + "Cc", + ]), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-redeclare-with-import-autofix.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-redeclare-with-import-autofix.js new file mode 100644 index 0000000000..36842000d5 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-redeclare-with-import-autofix.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-redeclare-with-import-autofix"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, output, messageId, options = []) { + let rv = { + code, + errors: [{ messageId }], + options, + }; + if (output) { + rv.output = output; + } + return rv; +} + +ruleTester.run("no-redeclare-with-import-autofix", rule, { + valid: [ + 'const foo = "whatever";', + 'let foo = "whatever";', + 'const {foo} = {foo: "whatever"};', + 'const {foo} = ChromeUtils.import("foo.jsm")', + 'let {foo} = ChromeUtils.import("foo.jsm")', + 'const {foo} = ChromeUtils.importESModule("foo.sys.mjs")', + 'let {foo} = ChromeUtils.importESModule("foo.sys.mjs")', + ], + invalid: [ + invalidCode( + `var {foo} = ChromeUtils.importESModule("foo.sys.mjs"); +var {foo} = ChromeUtils.importESModule("foo.sys.mjs");`, + 'var {foo} = ChromeUtils.importESModule("foo.sys.mjs");\n', + "duplicateImport" + ), + invalidCode( + `var {foo} = ChromeUtils.import("foo.jsm"); +var {foo} = ChromeUtils.import("foo.jsm");`, + 'var {foo} = ChromeUtils.import("foo.jsm");\n', + "duplicateImport" + ), + + invalidCode( + `var {foo} = ChromeUtils.import("foo.jsm"); +var {foo, bar} = ChromeUtils.import("foo.jsm");`, + `var {foo} = ChromeUtils.import("foo.jsm"); +var { bar} = ChromeUtils.import("foo.jsm");`, + "duplicateImport" + ), + + invalidCode( + `var {foo} = ChromeUtils.import("foo.jsm"); +var {bar, foo} = ChromeUtils.import("foo.jsm");`, + `var {foo} = ChromeUtils.import("foo.jsm"); +var {bar} = ChromeUtils.import("foo.jsm");`, + "duplicateImport" + ), + + invalidCode(`var foo = 5; var foo = 10;`, "", "redeclared", [ + { + errorForNonImports: true, + }, + ]), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-throw-cr-literal.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-throw-cr-literal.js new file mode 100644 index 0000000000..aecc5cd971 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-throw-cr-literal.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-throw-cr-literal"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, output, messageId) { + return { + code, + output, + errors: [{ messageId, type: "ThrowStatement" }], + }; +} + +ruleTester.run("no-throw-cr-literal", rule, { + valid: [ + 'throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);', + 'throw Components.Exception("", Components.results.NS_ERROR_UNEXPECTED);', + 'function t() { throw Components.Exception("", Cr.NS_ERROR_NO_CONTENT); }', + // We don't handle combined values, regular no-throw-literal catches them + 'throw Components.results.NS_ERROR_UNEXPECTED + "whoops";', + ], + invalid: [ + invalidCode( + "throw Cr.NS_ERROR_NO_INTERFACE;", + 'throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);', + "bareCR" + ), + invalidCode( + "throw Components.results.NS_ERROR_ABORT;", + 'throw Components.Exception("", Components.results.NS_ERROR_ABORT);', + "bareComponentsResults" + ), + invalidCode( + "function t() { throw Cr.NS_ERROR_NULL_POINTER; }", + 'function t() { throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); }', + "bareCR" + ), + invalidCode( + "throw new Error(Cr.NS_ERROR_ABORT);", + 'throw Components.Exception("", Cr.NS_ERROR_ABORT);', + "newErrorCR" + ), + invalidCode( + "throw new Error(Components.results.NS_ERROR_ABORT);", + 'throw Components.Exception("", Components.results.NS_ERROR_ABORT);', + "newErrorComponentsResults" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-parameters.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-parameters.js new file mode 100644 index 0000000000..28d4d0d151 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-parameters.js @@ -0,0 +1,175 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-useless-parameters"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError(messageId, data = {}) { + return [{ messageId, data, type: "CallExpression" }]; +} + +ruleTester.run("no-useless-parameters", rule, { + valid: [ + "Services.prefs.clearUserPref('browser.search.custom');", + "Services.removeObserver('notification name', {});", + "Services.io.newURI('http://example.com');", + "Services.io.newURI('http://example.com', 'utf8');", + "elt.addEventListener('click', handler);", + "elt.addEventListener('click', handler, true);", + "elt.addEventListener('click', handler, {once: true});", + "elt.removeEventListener('click', handler);", + "elt.removeEventListener('click', handler, true);", + "Services.obs.addObserver(this, 'topic', true);", + "Services.obs.addObserver(this, 'topic');", + "Services.prefs.addObserver('branch', this, true);", + "Services.prefs.addObserver('branch', this);", + "array.appendElement(elt);", + "Services.obs.notifyObservers(obj, 'topic', 'data');", + "Services.obs.notifyObservers(obj, 'topic');", + "window.getComputedStyle(elt);", + "window.getComputedStyle(elt, ':before');", + ], + invalid: [ + { + code: "Services.prefs.clearUserPref('browser.search.custom', false);", + output: "Services.prefs.clearUserPref('browser.search.custom');", + errors: callError("onlyTakes", { + fnName: "clearUserPref", + params: "1 parameter", + }), + }, + { + code: "Services.prefs.clearUserPref('browser.search.custom',\n false);", + output: "Services.prefs.clearUserPref('browser.search.custom');", + errors: callError("onlyTakes", { + fnName: "clearUserPref", + params: "1 parameter", + }), + }, + { + code: "Services.removeObserver('notification name', {}, false);", + output: "Services.removeObserver('notification name', {});", + errors: callError("onlyTakes", { + fnName: "removeObserver", + params: "2 parameters", + }), + }, + { + code: "Services.removeObserver('notification name', {}, true);", + output: "Services.removeObserver('notification name', {});", + errors: callError("onlyTakes", { + fnName: "removeObserver", + params: "2 parameters", + }), + }, + { + code: "Services.io.newURI('http://example.com', null, null);", + output: "Services.io.newURI('http://example.com');", + errors: callError("newURIParams"), + }, + { + code: "Services.io.newURI('http://example.com', 'utf8', null);", + output: "Services.io.newURI('http://example.com', 'utf8');", + errors: callError("newURIParams"), + }, + { + code: "Services.io.newURI('http://example.com', null);", + output: "Services.io.newURI('http://example.com');", + errors: callError("newURIParams"), + }, + { + code: "Services.io.newURI('http://example.com', '', '');", + output: "Services.io.newURI('http://example.com');", + errors: callError("newURIParams"), + }, + { + code: "Services.io.newURI('http://example.com', '');", + output: "Services.io.newURI('http://example.com');", + errors: callError("newURIParams"), + }, + { + code: "elt.addEventListener('click', handler, false);", + output: "elt.addEventListener('click', handler);", + errors: callError("obmittedWhenFalse", { + fnName: "addEventListener", + index: "third", + }), + }, + { + code: "elt.removeEventListener('click', handler, false);", + output: "elt.removeEventListener('click', handler);", + errors: callError("obmittedWhenFalse", { + fnName: "removeEventListener", + index: "third", + }), + }, + { + code: "Services.obs.addObserver(this, 'topic', false);", + output: "Services.obs.addObserver(this, 'topic');", + errors: callError("obmittedWhenFalse", { + fnName: "addObserver", + index: "third", + }), + }, + { + code: "Services.prefs.addObserver('branch', this, false);", + output: "Services.prefs.addObserver('branch', this);", + errors: callError("obmittedWhenFalse", { + fnName: "addObserver", + index: "third", + }), + }, + { + code: "array.appendElement(elt, false);", + output: "array.appendElement(elt);", + errors: callError("obmittedWhenFalse", { + fnName: "appendElement", + index: "second", + }), + }, + { + code: "Services.obs.notifyObservers(obj, 'topic', null);", + output: "Services.obs.notifyObservers(obj, 'topic');", + errors: callError("obmittedWhenFalse", { + fnName: "notifyObservers", + index: "third", + }), + }, + { + code: "Services.obs.notifyObservers(obj, 'topic', '');", + output: "Services.obs.notifyObservers(obj, 'topic');", + errors: callError("obmittedWhenFalse", { + fnName: "notifyObservers", + index: "third", + }), + }, + { + code: "window.getComputedStyle(elt, null);", + output: "window.getComputedStyle(elt);", + errors: callError("obmittedWhenFalse", { + fnName: "getComputedStyle", + index: "second", + }), + }, + { + code: "window.getComputedStyle(elt, '');", + output: "window.getComputedStyle(elt);", + errors: callError("obmittedWhenFalse", { + fnName: "getComputedStyle", + index: "second", + }), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-removeEventListener.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-removeEventListener.js new file mode 100644 index 0000000000..f5fc60158d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-removeEventListener.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-useless-removeEventListener"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { code, errors: [{ messageId: "useOnce", type: "CallExpression" }] }; +} + +ruleTester.run("no-useless-removeEventListener", rule, { + valid: [ + // Listeners that aren't a function are always valid. + "elt.addEventListener('click', handler);", + "elt.addEventListener('click', handler, true);", + "elt.addEventListener('click', handler, {once: true});", + + // Should not fail on empty functions. + "elt.addEventListener('click', function() {});", + + // Should not reject when removing a listener for another event. + "elt.addEventListener('click', function listener() {" + + " elt.removeEventListener('keypress', listener);" + + "});", + + // Should not reject when there's another instruction before + // removeEventListener. + "elt.addEventListener('click', function listener() {" + + " elt.focus();" + + " elt.removeEventListener('click', listener);" + + "});", + + // Should not reject when wantsUntrusted is true. + "elt.addEventListener('click', function listener() {" + + " elt.removeEventListener('click', listener);" + + "}, false, true);", + + // Should not reject when there's a literal and a variable + "elt.addEventListener('click', function listener() {" + + " elt.removeEventListener(eventName, listener);" + + "});", + + // Should not reject when there's 2 different variables + "elt.addEventListener(event1, function listener() {" + + " elt.removeEventListener(event2, listener);" + + "});", + + // Should not fail if this is a different type of event listener function. + "myfunc.addEventListener(listener);", + ], + invalid: [ + invalidCode( + "elt.addEventListener('click', function listener() {" + + " elt.removeEventListener('click', listener);" + + "});" + ), + invalidCode( + "elt.addEventListener('click', function listener() {" + + " elt.removeEventListener('click', listener, true);" + + "}, true);" + ), + invalidCode( + "elt.addEventListener('click', function listener() {" + + " elt.removeEventListener('click', listener);" + + "}, {once: true});" + ), + invalidCode( + "elt.addEventListener('click', function listener() {" + + " /* Comment */" + + " elt.removeEventListener('click', listener);" + + "});" + ), + invalidCode( + "elt.addEventListener('click', function() {" + + " elt.removeEventListener('click', arguments.callee);" + + "});" + ), + invalidCode( + "elt.addEventListener(eventName, function listener() {" + + " elt.removeEventListener(eventName, listener);" + + "});" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-run-test.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-run-test.js new file mode 100644 index 0000000000..5ee6e0e575 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-run-test.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/no-useless-run-test"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, output) { + return { + code, + output, + errors: [{ messageId: "noUselessRunTest", type: "FunctionDeclaration" }], + }; +} + +ruleTester.run("no-useless-run-test", rule, { + valid: [ + "function run_test() {}", + "function run_test() {let args = 1; run_next_test();}", + "function run_test() {fakeCall(); run_next_test();}", + ], + invalid: [ + // Single-line case. + invalidCode("function run_test() { run_next_test(); }", ""), + // Multiple-line cases + invalidCode( + ` +function run_test() { + run_next_test(); +}`, + `` + ), + invalidCode( + ` +foo(); +function run_test() { + run_next_test(); +} +`, + ` +foo(); +` + ), + invalidCode( + ` +foo(); +function run_test() { + run_next_test(); +} +bar(); +`, + ` +foo(); +bar(); +` + ), + invalidCode( + ` +foo(); + +function run_test() { + run_next_test(); +} + +bar();`, + ` +foo(); + +bar();` + ), + invalidCode( + ` +foo(); + +function run_test() { + run_next_test(); +} + +// A comment +bar(); +`, + ` +foo(); + +// A comment +bar(); +` + ), + invalidCode( + ` +foo(); + +/** + * A useful comment. + */ +function run_test() { + run_next_test(); +} + +// A comment +bar(); +`, + ` +foo(); + +/** + * A useful comment. + */ + +// A comment +bar(); +` + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/prefer-boolean-length-check.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/prefer-boolean-length-check.js new file mode 100644 index 0000000000..102112a3f5 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/prefer-boolean-length-check.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/prefer-boolean-length-check"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError() { + return [{ messageId: "preferBooleanCheck", type: "BinaryExpression" }]; +} + +ruleTester.run("check-length", rule, { + valid: [ + "if (foo.length && foo.length) {}", + "if (!foo.length) {}", + "if (foo.value == 0) {}", + "if (foo.value > 0) {}", + "if (0 == foo.value) {}", + "if (0 < foo.value) {}", + "var a = !!foo.length", + "function bar() { return !!foo.length }", + ], + invalid: [ + { + code: "if (foo.length == 0) {}", + output: "if (!foo.length) {}", + errors: invalidError(), + }, + { + code: "if (foo.length > 0) {}", + output: "if (foo.length) {}", + errors: invalidError(), + }, + { + code: "if (0 < foo.length) {}", + output: "if (foo.length) {}", + errors: invalidError(), + }, + { + code: "if (0 == foo.length) {}", + output: "if (!foo.length) {}", + errors: invalidError(), + }, + { + code: "if (foo && foo.length == 0) {}", + output: "if (foo && !foo.length) {}", + errors: invalidError(), + }, + { + code: "if (foo.bar.length == 0) {}", + output: "if (!foo.bar.length) {}", + errors: invalidError(), + }, + { + code: "var a = foo.length>0", + output: "var a = !!foo.length", + errors: invalidError(), + }, + { + code: "function bar() { return foo.length>0 }", + output: "function bar() { return !!foo.length }", + errors: invalidError(), + }, + { + code: "if (foo && bar.length>0) {}", + output: "if (foo && bar.length) {}", + errors: invalidError(), + }, + { + code: "while (foo && bar.length>0) {}", + output: "while (foo && bar.length) {}", + errors: invalidError(), + }, + { + code: "x = y && bar.length > 0", + output: "x = y && !!bar.length", + errors: invalidError(), + }, + { + code: "function bar() { return x && foo.length > 0}", + output: "function bar() { return x && !!foo.length}", + errors: invalidError(), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/prefer-formatValues.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/prefer-formatValues.js new file mode 100644 index 0000000000..4aa5cd1d3c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/prefer-formatValues.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/prefer-formatValues"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function error(line, column = undefined) { + return { + messageId: "useSingleCall", + type: "CallExpression", + line, + column, + }; +} + +ruleTester.run("check-length", rule, { + valid: [ + "document.l10n.formatValue('foobar');", + "document.l10n.formatValues(['foobar', 'foobaz']);", + `if (foo) { + document.l10n.formatValue('foobar'); + } else { + document.l10n.formatValue('foobaz'); + }`, + `document.l10n.formatValue('foobaz'); + if (foo) { + document.l10n.formatValue('foobar'); + }`, + `if (foo) { + document.l10n.formatValue('foobar'); + } + document.l10n.formatValue('foobaz');`, + `if (foo) { + document.l10n.formatValue('foobar'); + } + document.l10n.formatValues(['foobaz']);`, + ], + invalid: [ + { + code: `document.l10n.formatValue('foobar'); + document.l10n.formatValue('foobaz');`, + errors: [error(1, 1), error(2, 14)], + }, + { + code: `document.l10n.formatValue('foobar'); + if (foo) { + document.l10n.formatValue('foobiz'); + } + document.l10n.formatValue('foobaz');`, + errors: [error(1, 1), error(5, 14)], + }, + { + code: `document.l10n.formatValues('foobar'); + document.l10n.formatValue('foobaz');`, + errors: [error(1, 1), error(2, 14)], + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-addtask-only.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-addtask-only.js new file mode 100644 index 0000000000..f29726d9fc --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-addtask-only.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-addtask-only"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError(output) { + return [ + { + messageId: "addTaskNotAllowed", + type: "CallExpression", + suggestions: [{ messageId: "addTaskNotAllowedSuggestion", output }], + }, + ]; +} + +ruleTester.run("reject-addtask-only", rule, { + valid: [ + "add_task(foo())", + "add_task(foo()).skip()", + "add_task(function() {})", + "add_task(function() {}).skip()", + ], + invalid: [ + { + code: "add_task(foo()).only()", + errors: invalidError("add_task(foo())"), + }, + { + code: "add_task(foo()).only(bar())", + errors: invalidError("add_task(foo())"), + }, + { + code: "add_task(function() {}).only()", + errors: invalidError("add_task(function() {})"), + }, + { + code: "add_task(function() {}).only(bar())", + errors: invalidError("add_task(function() {})"), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-chromeutils-import-params.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-chromeutils-import-params.js new file mode 100644 index 0000000000..c2b5fd9349 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-chromeutils-import-params.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-chromeutils-import-params"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError(suggested) { + return [ + { + messageId: "importOnlyOneArg", + type: "CallExpression", + suggestions: [ + { + messageId: "importOnlyOneArgSuggestion", + output: suggested, + }, + ], + }, + ]; +} + +ruleTester.run("reject-chromeutils-import-params", rule, { + valid: ['ChromeUtils.import("resource://some/path/to/My.jsm")'], + invalid: [ + { + code: 'ChromeUtils.import("resource://some/path/to/My.jsm", null)', + errors: invalidError( + `ChromeUtils.import("resource://some/path/to/My.jsm")` + ), + }, + { + code: ` +ChromeUtils.import( + "resource://some/path/to/My.jsm", + null +);`, + errors: invalidError(` +ChromeUtils.import( + "resource://some/path/to/My.jsm" +);`), + }, + { + code: 'ChromeUtils.import("resource://some/path/to/My.jsm", this)', + errors: invalidError( + `ChromeUtils.import("resource://some/path/to/My.jsm")` + ), + }, + { + code: 'ChromeUtils.import("resource://some/path/to/My.jsm", foo, bar)', + errors: invalidError( + `ChromeUtils.import("resource://some/path/to/My.jsm")` + ), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-chromeutils-import.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-chromeutils-import.js new file mode 100644 index 0000000000..ced46f4a0f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-chromeutils-import.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-chromeutils-import"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const invalidError = [ + { + messageId: "useImportESModule", + type: "CallExpression", + }, +]; + +const invalidErrorLazy = [ + { + messageId: "useImportESModuleLazy", + type: "CallExpression", + }, +]; + +ruleTester.run("reject-chromeutils-import", rule, { + valid: [ + 'ChromeUtils.importESModule("resource://some/path/to/My.sys.mjs")', + 'ChromeUtils.defineESModuleGetters(obj, { My: "resource://some/path/to/My.sys.mjs" })', + ], + invalid: [ + { + code: 'ChromeUtils.import("resource://some/path/to/My.jsm")', + errors: invalidError, + }, + { + code: 'SpecialPowers.ChromeUtils.import("resource://some/path/to/My.jsm")', + errors: invalidError, + }, + { + code: 'ChromeUtils.defineModuleGetter(obj, "My", "resource://some/path/to/My.jsm")', + errors: invalidErrorLazy, + }, + { + code: 'SpecialPowers.ChromeUtils.defineModuleGetter(obj, "My", "resource://some/path/to/My.jsm")', + errors: invalidErrorLazy, + }, + { + code: 'XPCOMUtils.defineLazyModuleGetters(obj, { My: "resource://some/path/to/My.jsm" })', + errors: invalidErrorLazy, + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-eager-module-in-lazy-getter.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-eager-module-in-lazy-getter.js new file mode 100644 index 0000000000..490346b94a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-eager-module-in-lazy-getter.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-eager-module-in-lazy-getter"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, uri) { + return { code, errors: [{ messageId: "eagerModule", data: { uri } }] }; +} + +ruleTester.run("reject-eager-module-in-lazy-getter", rule, { + valid: [ + ` + ChromeUtils.defineModuleGetter( + lazy, "Integration", "resource://gre/modules/Integration.jsm" + ); +`, + ` + XPCOMUtils.defineLazyModuleGetters(lazy, { + Integration: "resource://gre/modules/Integration.jsm", + }); +`, + ` + ChromeUtils.defineESModuleGetters(lazy, { + Integration: "resource://gre/modules/Integration.sys.mjs", + }); +`, + ], + invalid: [ + invalidCode( + ` + ChromeUtils.defineModuleGetter( + lazy, "XPCOMUtils", "resource://gre/modules/XPCOMUtils.jsm" + ); +`, + "resource://gre/modules/XPCOMUtils.jsm" + ), + invalidCode( + ` + XPCOMUtils.defineLazyModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.jsm", + }); +`, + "resource://gre/modules/AppConstants.jsm" + ), + invalidCode( + ` + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`, + "resource://gre/modules/AppConstants.sys.mjs" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-global-this.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-global-this.js new file mode 100644 index 0000000000..ec9ba711dc --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-global-this.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-global-this"); +var RuleTester = require("eslint").RuleTester; + +// class static block is available from ES2022 = 13. +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { + code, + errors: [{ messageId: "avoidGlobalThis", type: "ThisExpression" }], + }; +} + +ruleTester.run("reject-top-level-await", rule, { + valid: [ + "function f() { this; }", + "(function f() { this; });", + "({ foo() { this; } });", + "({ get foo() { this; } })", + "({ set foo(x) { this; } })", + "class X { foo() { this; } }", + "class X { get foo() { this; } }", + "class X { set foo(x) { this; } }", + "class X { static foo() { this; } }", + "class X { static get foo() { this; } }", + "class X { static set foo(x) { this; } }", + "class X { P = this; }", + "class X { #P = this; }", + "class X { static { this; } }", + ], + invalid: [ + invalidCode("this;"), + invalidCode("() => this;"), + + invalidCode("this.foo = 10;"), + invalidCode("ChromeUtils.defineModuleGetter(this, {});"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-globalThis-modification.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-globalThis-modification.js new file mode 100644 index 0000000000..c0244eaeab --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-globalThis-modification.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-globalThis-modification"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCall(code) { + return { + code, + errors: [ + { + messageId: "rejectPassingGlobalThis", + type: "CallExpression", + }, + ], + }; +} + +function invalidAssignment(code) { + return { + code, + errors: [ + { + messageId: "rejectModifyGlobalThis", + type: "AssignmentExpression", + }, + ], + }; +} + +ruleTester.run("reject-globalThis-modification", rule, { + valid: [ + `var x = globalThis.Array;`, + `Array in globalThis;`, + `result.deserialize(globalThis)`, + ], + invalid: [ + invalidAssignment(` + globalThis.foo = 10; +`), + invalidCall(` + Object.defineProperty(globalThis, "foo", { value: 10 }); +`), + invalidCall(` + Object.defineProperties(globalThis, { + foo: { value: 10 }, + }); +`), + invalidCall(` + Object.assign(globalThis, { foo: 10 }); +`), + invalidCall(` + ChromeUtils.defineModuleGetter( + globalThis, "AppConstants", "resource://gre/modules/AppConstants.jsm" + ); +`), + invalidCall(` + ChromeUtils.defineESMGetters(globalThis, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`), + invalidCall(` + XPCOMUtils.defineLazyModuleGetters(globalThis, { + AppConstants: "resource://gre/modules/AppConstants.jsm", + }); +`), + invalidCall(` + someFunction(1, globalThis); +`), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-import-system-module-from-non-system.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-import-system-module-from-non-system.js new file mode 100644 index 0000000000..5b09007626 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-import-system-module-from-non-system.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-import-system-module-from-non-system"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, +}); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +ruleTester.run("reject-import-system-module-from-non-system", rule, { + valid: [ + { + code: `const { AppConstants } = ChromeUtils.importESM("resource://gre/modules/AppConstants.sys.mjs");`, + }, + ], + invalid: [ + { + code: `import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";`, + errors: [{ messageId: "rejectStaticImportSystemModuleFromNonSystem" }], + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-importGlobalProperties.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-importGlobalProperties.js new file mode 100644 index 0000000000..7f796abae7 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-importGlobalProperties.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-importGlobalProperties"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +ruleTester.run("reject-importGlobalProperties", rule, { + valid: [ + { + code: "Cu.something();", + }, + { + options: ["allownonwebidl"], + code: "Cu.importGlobalProperties(['caches'])", + }, + { + options: ["allownonwebidl"], + code: "XPCOMUtils.defineLazyGlobalGetters(this, ['caches'])", + }, + ], + invalid: [ + { + code: "Cu.importGlobalProperties(['fetch'])", + options: ["everything"], + errors: [{ messageId: "unexpectedCall" }], + }, + { + code: "XPCOMUtils.defineLazyGlobalGetters(this, ['fetch'])", + options: ["everything"], + errors: [{ messageId: "unexpectedCall" }], + }, + { + code: "Cu.importGlobalProperties(['TextEncoder'])", + options: ["everything"], + errors: [{ messageId: "unexpectedCall" }], + }, + { + options: ["everything"], + code: "XPCOMUtils.defineLazyGlobalGetters(this, ['TextEncoder'])", + errors: [{ messageId: "unexpectedCallSjs" }], + filename: "foo.sjs", + }, + { + code: "XPCOMUtils.defineLazyGlobalGetters(this, ['TextEncoder'])", + options: ["everything"], + errors: [{ messageId: "unexpectedCall" }], + }, + { + code: "Cu.importGlobalProperties(['TextEncoder'])", + options: ["allownonwebidl"], + errors: [{ messageId: "unexpectedCallCuWebIdl" }], + }, + { + code: "XPCOMUtils.defineLazyGlobalGetters(this, ['TextEncoder'])", + options: ["allownonwebidl"], + errors: [{ messageId: "unexpectedCallXPCOMWebIdl" }], + }, + { + options: ["allownonwebidl"], + code: "Cu.importGlobalProperties(['TextEncoder'])", + errors: [{ messageId: "unexpectedCallCuWebIdl" }], + filename: "foo.js", + }, + { + options: ["allownonwebidl"], + code: "XPCOMUtils.defineLazyGlobalGetters(this, ['TextEncoder'])", + errors: [{ messageId: "unexpectedCallXPCOMWebIdl" }], + filename: "foo.js", + }, + { + options: ["allownonwebidl"], + code: "Cu.importGlobalProperties(['TextEncoder'])", + errors: [{ messageId: "unexpectedCallCuWebIdl" }], + filename: "foo.sjs", + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-lazy-imports-into-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-lazy-imports-into-globals.js new file mode 100644 index 0000000000..2c50c5524d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-lazy-imports-into-globals.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-lazy-imports-into-globals"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, +}); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { code, errors: [{ messageId: "rejectLazyImportsIntoGlobals" }] }; +} + +ruleTester.run("reject-lazy-imports-into-globals", rule, { + valid: [ + ` + const lazy = {}; + ChromeUtils.defineLazyGetter(lazy, "foo", () => {}); + `, + ], + invalid: [ + invalidCode(`ChromeUtils.defineLazyGetter(globalThis, "foo", () => {});`), + invalidCode(`ChromeUtils.defineLazyGetter(window, "foo", () => {});`), + invalidCode( + `XPCOMUtils.defineLazyPreferenceGetter(globalThis, "foo", "foo.bar");` + ), + invalidCode( + `XPCOMUtils.defineLazyServiceGetter(globalThis, "foo", "@foo", "nsIFoo");` + ), + invalidCode(`XPCOMUtils.defineLazyGlobalGetters(globalThis, {});`), + invalidCode(`XPCOMUtils.defineLazyGlobalGetters(window, {});`), + invalidCode(`XPCOMUtils.defineLazyModuleGetters(globalThis, {});`), + invalidCode(`ChromeUtils.defineESModuleGetters(window, {});`), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-mixing-eager-and-lazy.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-mixing-eager-and-lazy.js new file mode 100644 index 0000000000..af0dd99c22 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-mixing-eager-and-lazy.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-mixing-eager-and-lazy"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, +}); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, uri) { + return { code, errors: [{ messageId: "mixedEagerAndLazy", data: { uri } }] }; +} + +ruleTester.run("reject-mixing-eager-and-lazy", rule, { + valid: [ + ` + ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); +`, + ` + ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs"); +`, + ` + import{ AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +`, + ` + ChromeUtils.defineModuleGetter( + lazy, "AppConstants", "resource://gre/modules/AppConstants.jsm" + ); +`, + ` + XPCOMUtils.defineLazyModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.jsm", + }); +`, + ` + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`, + ` + if (some_condition) { + ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + } + XPCOMUtils.defineLazyModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.jsm" + }); +`, + ` + ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + XPCOMUtils.defineLazyModuleGetters(sandbox, { + AppConstants: "resource://gre/modules/AppConstants.jsm", + }); +`, + ], + invalid: [ + invalidCode( + ` + ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + ChromeUtils.defineModuleGetter( + lazy, "AppConstants", "resource://gre/modules/AppConstants.jsm" + ); +`, + "resource://gre/modules/AppConstants.jsm" + ), + invalidCode( + ` + ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + XPCOMUtils.defineLazyModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.jsm", + }); +`, + "resource://gre/modules/AppConstants.jsm" + ), + invalidCode( + ` + ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs"); + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`, + "resource://gre/modules/AppConstants.sys.mjs" + ), + invalidCode( + ` + import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); +`, + "resource://gre/modules/AppConstants.sys.mjs" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-multiple-getters-calls.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-multiple-getters-calls.js new file mode 100644 index 0000000000..a2b88a8652 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-multiple-getters-calls.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-multiple-getters-calls"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { code, errors: [{ messageId: "rejectMultipleCalls" }] }; +} + +ruleTester.run("reject-multiple-getters-calls", rule, { + valid: [ + ` + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + }); + `, + ` + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); + ChromeUtils.defineESModuleGetters(window, { + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + }); + `, + ` + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); + if (cond) { + ChromeUtils.defineESModuleGetters(lazy, { + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + }); + } + `, + ], + invalid: [ + invalidCode(` + ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + }); + ChromeUtils.defineESModuleGetters(lazy, { + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + }); + `), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-relative-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-relative-requires.js new file mode 100644 index 0000000000..a7bad6f992 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-relative-requires.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-relative-requires"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError() { + return [{ messageId: "rejectRelativeRequires", type: "CallExpression" }]; +} + +ruleTester.run("reject-relative-requires", rule, { + valid: [ + 'require("devtools/absolute/path")', + 'require("resource://gre/modules/SomeModule.jsm")', + 'loader.lazyRequireGetter(this, "path", "devtools/absolute/path", true)', + 'loader.lazyRequireGetter(this, "Path", "devtools/absolute/path")', + ], + invalid: [ + { + code: 'require("./relative/path")', + errors: invalidError(), + }, + { + code: 'require("../parent/folder/path")', + errors: invalidError(), + }, + { + code: 'loader.lazyRequireGetter(this, "path", "./relative/path", true)', + errors: invalidError(), + }, + { + code: 'loader.lazyRequireGetter(this, "path", "../parent/folder/path", true)', + errors: invalidError(), + }, + { + code: 'loader.lazyRequireGetter(this, "path", "./relative/path")', + errors: invalidError(), + }, + { + code: 'loader.lazyRequireGetter(this, "path", "../parent/folder/path")', + errors: invalidError(), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-scriptableunicodeconverter.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-scriptableunicodeconverter.js new file mode 100644 index 0000000000..22123bb99c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-scriptableunicodeconverter.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-scriptableunicodeconverter"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError() { + return [ + { messageId: "rejectScriptableUnicodeConverter", type: "MemberExpression" }, + ]; +} + +ruleTester.run("reject-scriptableunicodeconverter", rule, { + valid: ["TextEncoder", "TextDecoder"], + invalid: [ + { + code: "Ci.nsIScriptableUnicodeConverter", + errors: invalidError(), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-some-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-some-requires.js new file mode 100644 index 0000000000..3415946b07 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-some-requires.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-some-requires"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function requirePathError(path) { + return [ + { messageId: "rejectRequire", data: { path }, type: "CallExpression" }, + ]; +} + +const DEVTOOLS_FORBIDDEN_PATH = "^(resource://)?devtools/forbidden"; + +ruleTester.run("reject-some-requires", rule, { + valid: [ + { + code: 'require("devtools/not-forbidden/path")', + options: [DEVTOOLS_FORBIDDEN_PATH], + }, + { + code: 'require("resource://devtools/not-forbidden/path")', + options: [DEVTOOLS_FORBIDDEN_PATH], + }, + ], + invalid: [ + { + code: 'require("devtools/forbidden/path")', + errors: requirePathError("devtools/forbidden/path"), + options: [DEVTOOLS_FORBIDDEN_PATH], + }, + { + code: 'require("resource://devtools/forbidden/path")', + errors: requirePathError("resource://devtools/forbidden/path"), + options: [DEVTOOLS_FORBIDDEN_PATH], + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-top-level-await.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-top-level-await.js new file mode 100644 index 0000000000..4416b42abb --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-top-level-await.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-top-level-await"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, +}); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, messageId) { + return { code, errors: [{ messageId: "rejectTopLevelAwait" }] }; +} + +ruleTester.run("reject-top-level-await", rule, { + valid: [ + "async() => { await bar() }", + "async() => { for await (let x of []) {} }", + ], + invalid: [ + invalidCode("await foo"), + invalidCode("{ await foo }"), + invalidCode("(await foo)"), + invalidCode("for await (let x of []) {}"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/rejects-requires-await.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/rejects-requires-await.js new file mode 100644 index 0000000000..a7e95769c1 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/rejects-requires-await.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/rejects-requires-await"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, messageId) { + return { code, errors: [{ messageId: "rejectRequiresAwait" }] }; +} + +ruleTester.run("reject-requires-await", rule, { + valid: [ + "async() => { await Assert.rejects(foo, /assertion/) }", + "async() => { await Assert.rejects(foo, /assertion/, 'msg') }", + ], + invalid: [ + invalidCode("Assert.rejects(foo)"), + invalidCode("Assert.rejects(foo, 'msg')"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-cc-etc.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-cc-etc.js new file mode 100644 index 0000000000..d588fabc84 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-cc-etc.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-cc-etc"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, originalName, newName, output) { + return { + code, + output, + errors: [ + { + messageId: "useCcEtc", + data: { + shortName: newName, + oldName: originalName, + }, + type: "MemberExpression", + }, + ], + }; +} + +ruleTester.run("use-cc-etc", rule, { + valid: ["Components.Constructor();", "let x = Components.foo;"], + invalid: [ + invalidCode( + "let foo = Components.classes['bar'];", + "classes", + "Cc", + "let foo = Cc['bar'];" + ), + invalidCode( + "let bar = Components.interfaces.bar;", + "interfaces", + "Ci", + "let bar = Ci.bar;" + ), + invalidCode( + "Components.results.NS_ERROR_ILLEGAL_INPUT;", + "results", + "Cr", + "Cr.NS_ERROR_ILLEGAL_INPUT;" + ), + invalidCode( + "Components.utils.reportError('fake');", + "utils", + "Cu", + "Cu.reportError('fake');" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-definelazygetter.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-definelazygetter.js new file mode 100644 index 0000000000..89948f12b7 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-definelazygetter.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-chromeutils-definelazygetter"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError(messageId) { + return [{ messageId, type: "CallExpression" }]; +} + +ruleTester.run("use-chromeutils-definelazygetter", rule, { + valid: [ + `ChromeUtils.defineLazyGetter(lazy, "textEncoder", function () { return new TextEncoder(); });`, + ], + invalid: [ + { + code: `XPCOMUtils.defineLazyGetter(lazy, "textEncoder", function () { return new TextEncoder(); });`, + output: `ChromeUtils.defineLazyGetter(lazy, "textEncoder", function () { return new TextEncoder(); });`, + errors: callError("useChromeUtilsDefineLazyGetter"), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-generateqi.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-generateqi.js new file mode 100644 index 0000000000..ca32d2ac26 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-generateqi.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-chromeutils-generateqi"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function error(messageId, type) { + return [{ messageId, type }]; +} + +/* globals nsIFlug */ +function QueryInterface(iid) { + if ( + iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIMeh) || + iid.equals(nsIFlug) || + iid.equals(Ci.amIFoo) + ) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); +} + +ruleTester.run("use-chromeutils-generateqi", rule, { + valid: [ + `X.prototype.QueryInterface = ChromeUtils.generateQI(["nsIMeh"]);`, + `X.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIMeh"]) }`, + ], + invalid: [ + { + code: `X.prototype.QueryInterface = XPCOMUtils.generateQI(["nsIMeh"]);`, + output: `X.prototype.QueryInterface = ChromeUtils.generateQI(["nsIMeh"]);`, + errors: error("noXpcomUtilsGenerateQI", "CallExpression"), + }, + { + code: `X.prototype = { QueryInterface: XPCOMUtils.generateQI(["nsIMeh"]) };`, + output: `X.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIMeh"]) };`, + errors: error("noXpcomUtilsGenerateQI", "CallExpression"), + }, + { + code: `X.prototype = { QueryInterface: ${QueryInterface} };`, + output: `X.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIMeh", "nsIFlug", "amIFoo"]) };`, + errors: error("noJSQueryInterface", "Property"), + }, + { + code: `X.prototype = { ${String(QueryInterface).replace( + /^function /, + "" + )} };`, + output: `X.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIMeh", "nsIFlug", "amIFoo"]) };`, + errors: error("noJSQueryInterface", "Property"), + }, + { + code: `X.prototype.QueryInterface = ${QueryInterface};`, + output: `X.prototype.QueryInterface = ChromeUtils.generateQI(["nsIMeh", "nsIFlug", "amIFoo"]);`, + errors: error("noJSQueryInterface", "AssignmentExpression"), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-import.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-import.js new file mode 100644 index 0000000000..37976d252e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-import.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-chromeutils-import"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError(messageId) { + return [{ messageId, type: "CallExpression" }]; +} + +ruleTester.run("use-chromeutils-import", rule, { + valid: [ + `ChromeUtils.import("resource://gre/modules/AppConstants.jsm");`, + `ChromeUtils.import("resource://gre/modules/AppConstants.jsm", this);`, + `ChromeUtils.defineModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm");`, + ], + invalid: [ + { + code: `Cu.import("resource://gre/modules/AppConstants.jsm");`, + output: `ChromeUtils.import("resource://gre/modules/AppConstants.jsm");`, + errors: callError("useChromeUtilsImport"), + }, + { + code: `Cu.import("resource://gre/modules/AppConstants.jsm", this);`, + output: `ChromeUtils.import("resource://gre/modules/AppConstants.jsm", this);`, + errors: callError("useChromeUtilsImport"), + }, + { + code: `Components.utils.import("resource://gre/modules/AppConstants.jsm");`, + output: `ChromeUtils.import("resource://gre/modules/AppConstants.jsm");`, + errors: callError("useChromeUtilsImport"), + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-console-createInstance.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-console-createInstance.js new file mode 100644 index 0000000000..bd7fe3a507 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-console-createInstance.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-console-createInstance"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +ruleTester.run("use-console-createInstance", rule, { + valid: ['"resource://gre/modules/Foo.sys.mjs"'], + invalid: [ + { + code: '"resource://gre/modules/Console.sys.mjs"', + errors: [ + { + messageId: "useConsoleRatherThanModule", + data: { module: "Console.sys.mjs" }, + }, + ], + }, + { + code: '"resource://gre/modules/Log.sys.mjs"', + errors: [ + { + messageId: "useConsoleRatherThanModule", + data: { module: "Log.sys.mjs" }, + }, + ], + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-default-preference-values.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-default-preference-values.js new file mode 100644 index 0000000000..a8353ec200 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-default-preference-values.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-default-preference-values"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { + code, + errors: [{ messageId: "provideDefaultValue", type: "TryStatement" }], + }; +} + +let types = ["Bool", "Char", "Float", "Int"]; +let methods = types.map(type => "get" + type + "Pref"); + +ruleTester.run("use-default-preference-values", rule, { + valid: [].concat( + methods.map(m => "blah = branch." + m + "('blah', true);"), + methods.map(m => "blah = branch." + m + "('blah');"), + methods.map( + m => "try { canThrow(); blah = branch." + m + "('blah'); } catch(e) {}" + ) + ), + + invalid: [].concat( + methods.map(m => + invalidCode("try { blah = branch." + m + "('blah'); } catch(e) {}") + ) + ), +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-includes-instead-of-indexOf.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-includes-instead-of-indexOf.js new file mode 100644 index 0000000000..fff826874f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-includes-instead-of-indexOf.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-includes-instead-of-indexOf"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { + code, + errors: [{ messageId: "useIncludes", type: "BinaryExpression" }], + }; +} + +ruleTester.run("use-includes-instead-of-indexOf", rule, { + valid: [ + "let a = foo.includes(bar);", + "let a = foo.indexOf(bar) > 0;", + "let a = foo.indexOf(bar) != 0;", + ], + invalid: [ + invalidCode("let a = foo.indexOf(bar) >= 0;"), + invalidCode("let a = foo.indexOf(bar) != -1;"), + invalidCode("let a = foo.indexOf(bar) !== -1;"), + invalidCode("let a = foo.indexOf(bar) == -1;"), + invalidCode("let a = foo.indexOf(bar) === -1;"), + invalidCode("let a = foo.indexOf(bar) < 0;"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-isInstance.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-isInstance.js new file mode 100644 index 0000000000..0de5e4577c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-isInstance.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-isInstance"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const errors = [ + { + messageId: "preferIsInstance", + type: "BinaryExpression", + }, +]; + +const env = { browser: true }; + +/** + * A test case boilerplate simulating chrome privileged script. + * @param {string} code + */ +function mockChromeScript(code) { + return { + code, + filename: "foo.sys.mjs", + env, + }; +} + +/** + * A test case boilerplate simulating content script. + * @param {string} code + */ +function mockContentScript(code) { + return { + code, + filename: "foo.js", + env, + }; +} + +ruleTester.run("use-isInstance", rule, { + valid: [ + mockChromeScript("(() => {}) instanceof Function;"), + mockChromeScript("({}) instanceof Object;"), + mockChromeScript("Node instanceof Object;"), + mockChromeScript("node instanceof lazy.Node;"), + mockChromeScript("var Node;node instanceof Node;"), + mockChromeScript("file instanceof lazy.File;"), + mockChromeScript("file instanceof OS.File;"), + mockChromeScript("file instanceof OS.File.Error;"), + mockChromeScript("file instanceof lazy.OS.File;"), + mockChromeScript("file instanceof lazy.OS.File.Error;"), + mockChromeScript("file instanceof lazy.lazy.OS.File;"), + mockChromeScript("var File;file instanceof File;"), + mockChromeScript("foo instanceof RandomGlobalThing;"), + mockChromeScript("foo instanceof lazy.RandomGlobalThing;"), + mockContentScript("node instanceof Node;"), + mockContentScript("file instanceof File;"), + mockContentScript( + "SpecialPowers.ChromeUtils.import(''); file instanceof File;" + ), + ], + invalid: [ + { + code: "node instanceof Node", + output: "Node.isInstance(node)", + env, + errors, + filename: "foo.sys.mjs", + }, + { + code: "text instanceof win.Text", + output: "win.Text.isInstance(text)", + errors, + filename: "foo.sys.mjs", + }, + { + code: "target instanceof this.contentWindow.HTMLAudioElement", + output: "this.contentWindow.HTMLAudioElement.isInstance(target)", + errors, + filename: "foo.sys.mjs", + }, + { + code: "target instanceof File", + output: "File.isInstance(target)", + env, + errors, + filename: "foo.sys.mjs", + }, + { + code: "target instanceof win.File", + output: "win.File.isInstance(target)", + errors, + filename: "foo.sys.mjs", + }, + { + code: "window.arguments[0] instanceof window.XULElement", + output: "window.XULElement.isInstance(window.arguments[0])", + errors, + filename: "foo.sys.mjs", + }, + { + code: "ChromeUtils.import(''); node instanceof Node", + output: "ChromeUtils.import(''); Node.isInstance(node)", + env, + errors, + filename: "foo.js", + }, + { + code: "ChromeUtils.import(''); file instanceof File", + output: "ChromeUtils.import(''); File.isInstance(file)", + env, + errors, + filename: "foo.js", + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-ownerGlobal.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-ownerGlobal.js new file mode 100644 index 0000000000..150f0fac63 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-ownerGlobal.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-ownerGlobal"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + return { + code, + errors: [{ messageId: "useOwnerGlobal", type: "MemberExpression" }], + }; +} + +ruleTester.run("use-ownerGlobal", rule, { + valid: [ + "aEvent.target.ownerGlobal;", + "this.DOMPointNode.ownerGlobal.getSelection();", + "windowToMessageManager(node.ownerGlobal);", + ], + invalid: [ + invalidCode("aEvent.target.ownerDocument.defaultView;"), + invalidCode("this.DOMPointNode.ownerDocument.defaultView.getSelection();"), + invalidCode("windowToMessageManager(node.ownerDocument.defaultView);"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-returnValue.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-returnValue.js new file mode 100644 index 0000000000..2c15349139 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-returnValue.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-returnValue"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, methodName) { + return { + code, + errors: [ + { + messageId: "useReturnValue", + data: { + property: methodName, + }, + type: "ExpressionStatement", + }, + ], + }; +} + +ruleTester.run("use-returnValue", rule, { + valid: [ + "a = foo.concat(bar)", + "b = bar.concat([1,3,4])", + "c = baz.concat()", + "d = qux.join(' ')", + "e = quux.slice(1)", + ], + invalid: [ + invalidCode("foo.concat(bar)", "concat"), + invalidCode("bar.concat([1,3,4])", "concat"), + invalidCode("baz.concat()", "concat"), + invalidCode("qux.join(' ')", "join"), + invalidCode("quux.slice(1)", "slice"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-services.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-services.js new file mode 100644 index 0000000000..fc4af03ac1 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-services.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-services"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, serviceName, getterName, type = "CallExpression") { + return { + code, + errors: [ + { messageId: "useServices", data: { serviceName, getterName }, type }, + ], + }; +} + +ruleTester.run("use-services", rule, { + valid: [ + 'Cc["@mozilla.org/fakeservice;1"].getService(Ci.nsIFake)', + 'Components.classes["@mozilla.org/fakeservice;1"].getService(Components.interfaces.nsIFake)', + "Services.wm.addListener()", + ], + invalid: [ + invalidCode( + 'Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);', + "wm", + "getService()" + ), + invalidCode( + 'Components.classes["@mozilla.org/toolkit/app-startup;1"].getService(Components.interfaces.nsIAppStartup);', + "startup", + "getService()" + ), + invalidCode( + `XPCOMUtils.defineLazyServiceGetters(this, { + uuidGen: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"], + });`, + "uuid", + "defineLazyServiceGetters", + "ArrayExpression" + ), + invalidCode( + `XPCOMUtils.defineLazyServiceGetter( + this, + "gELS", + "@mozilla.org/eventlistenerservice;1", + "nsIEventListenerService" + );`, + "els", + "defineLazyServiceGetter" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/use-static-import.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-static-import.js new file mode 100644 index 0000000000..656e83e0e9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-static-import.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/use-static-import"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, +}); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError() { + return [{ messageId: "useStaticImport", type: "VariableDeclaration" }]; +} + +ruleTester.run("use-static-import", rule, { + valid: [ + { + // Already converted, no issues. + code: 'import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";', + filename: "test.sys.mjs", + }, + { + // Inside an if statement. + code: 'if (foo) { const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs") }', + filename: "test.sys.mjs", + }, + { + // Inside a function. + code: 'function foo() { const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs") }', + filename: "test.sys.mjs", + }, + { + // importESModule with two args cannot be converted. + code: 'const { f } = ChromeUtils.importESModule("some/module.sys.mjs", { loadInDevToolsLoader : true });', + filename: "test.sys.mjs", + }, + { + // A non-system file attempting to import a system file should not be + // converted. + code: 'const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs")', + filename: "test.mjs", + }, + ], + invalid: [ + { + // Simple import in system module should be converted. + code: 'const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs")', + errors: callError(), + filename: "test.sys.mjs", + output: + 'import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"', + }, + { + // Should handle rewritten variables as well. + code: 'const { XPCOMUtils: foo } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs")', + errors: callError(), + filename: "test.sys.mjs", + output: + 'import { XPCOMUtils as foo } from "resource://gre/modules/XPCOMUtils.sys.mjs"', + }, + { + // Should handle multiple variables. + code: 'const { foo, XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs")', + errors: callError(), + filename: "test.sys.mjs", + output: + 'import { foo, XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"', + }, + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-ci-uses.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-ci-uses.js new file mode 100644 index 0000000000..0f5fe2eadf --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-ci-uses.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var os = require("os"); +var rule = require("../lib/rules/valid-ci-uses"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, messageId, data) { + return { code, errors: [{ messageId, data }] }; +} + +process.env.MOZ_XPT_ARTIFACTS_DIR = `${__dirname}/xpidl`; + +const tests = { + valid: ["Ci.nsIURIFixup", "Ci.nsIURIFixup.FIXUP_FLAG_NONE"], + invalid: [ + invalidCode("Ci.nsIURIFixup.UNKNOWN_CONSTANT", "unknownProperty", { + interface: "nsIURIFixup", + property: "UNKNOWN_CONSTANT", + }), + invalidCode("Ci.nsIFoo", "unknownInterface", { + interface: "nsIFoo", + }), + ], +}; + +// For ESLint tests, we only have a couple of xpt examples in the xpidl directory. +// Therefore we can pretend that these interfaces no longer exist. +switch (os.platform) { + case "windows": + tests.invalid.push( + invalidCode("Ci.nsIJumpListShortcut", "missingInterface") + ); + break; + case "darwin": + tests.invalid.push( + invalidCode("Ci.nsIMacShellService", "missingInterface") + ); + break; + case "linux": + tests.invalid.push( + invalidCode("Ci.mozISandboxReporter", "missingInterface") + ); +} + +ruleTester.run("valid-ci-uses", rule, tests); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-lazy.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-lazy.js new file mode 100644 index 0000000000..ba0a8dafab --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-lazy.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/valid-lazy"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, +}); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, name, messageId) { + return { code, errors: [{ messageId, data: { name } }] }; +} + +ruleTester.run("valid-lazy", rule, { + // Note: these tests build on top of one another, although lazy gets + // re-declared, it + valid: [ + ` + const lazy = {}; + ChromeUtils.defineLazyGetter(lazy, "foo", () => {}); + if (x) { lazy.foo.bar(); } + `, + ` + const lazy = {}; + ChromeUtils.defineESModuleGetters(lazy, { + foo: "foo.mjs", + }); + if (x) { lazy.foo.bar(); } + `, + ` + const lazy = {}; + Integration.downloads.defineESModuleGetter(lazy, "foo", "foo.sys.mjs"); + if (x) { lazy.foo.bar(); } + `, + ` + const lazy = createLazyLoaders({ foo: () => {}}); + if (x) { lazy.foo.bar(); } + `, + ` + const lazy = {}; + loader.lazyRequireGetter( + lazy, + ["foo1", "foo2"], + "bar", + true + ); + if (x) { + lazy.foo1.bar(); + lazy.foo2.bar(); + } + `, + // Test for top-level unconditional. + ` + const lazy = {}; + ChromeUtils.defineLazyGetter(lazy, "foo", () => {}); + if (x) { lazy.foo.bar(); } + for (;;) { lazy.foo.bar(); } + for (var x in y) { lazy.foo.bar(); } + for (var x of y) { lazy.foo.bar(); } + while (true) { lazy.foo.bar(); } + do { lazy.foo.bar(); } while (true); + switch (x) { case 1: lazy.foo.bar(); } + try { lazy.foo.bar(); } catch (e) {} + function f() { lazy.foo.bar(); } + (function f() { lazy.foo.bar(); }); + () => { lazy.foo.bar(); }; + class C { + constructor() { lazy.foo.bar(); } + foo() { lazy.foo.bar(); } + get x() { lazy.foo.bar(); } + set x(v) { lazy.foo.bar(); } + a = lazy.foo.bar(); + #b = lazy.foo.bar(); + static { + lazy.foo.bar(); + } + } + a && lazy.foo.bar(); + a || lazy.foo.bar(); + a ?? lazy.foo.bar(); + a ? lazy.foo.bar() : b; + a?.b[lazy.foo.bar()]; + a ||= lazy.foo.bar(); + a &&= lazy.foo.bar(); + a ??= lazy.foo.bar(); + var { x = lazy.foo.bar() } = {}; + var [ y = lazy.foo.bar() ] = []; + `, + ` + const lazy = {}; + ChromeUtils.defineLazyGetter(lazy, "foo", () => {}); + export { lazy as Foo }; + `, + ` + const lazy = {}; + if (cond) { + ChromeUtils.defineLazyGetter(lazy, "foo", () => { return 1; }); + } else { + ChromeUtils.defineLazyGetter(lazy, "foo", () => { return 2; }); + } + if (x) { lazy.foo; } + `, + ], + invalid: [ + invalidCode("if (x) { lazy.bar; }", "bar", "unknownProperty"), + invalidCode( + ` + const lazy = {}; + ChromeUtils.defineLazyGetter(lazy, "foo", "foo.jsm"); + ChromeUtils.defineLazyGetter(lazy, "foo", "foo1.jsm"); + if (x) { lazy.foo.bar(); } + `, + "foo", + "duplicateSymbol" + ), + invalidCode( + ` + const lazy = {}; + XPCOMUtils.defineLazyModuleGetters(lazy, { + "foo-bar": "foo.jsm", + }); + if (x) { lazy["foo-bar"].bar(); } + `, + "foo-bar", + "incorrectType" + ), + invalidCode( + `const lazy = {}; + ChromeUtils.defineLazyGetter(lazy, "foo", "foo.jsm"); + `, + "foo", + "unusedProperty" + ), + invalidCode( + `const lazy = {}; + ChromeUtils.defineLazyGetter(lazy, "foo1", () => {}); + lazy.foo1.bar();`, + "foo1", + "topLevelAndUnconditional" + ), + invalidCode( + `const lazy = {}; + ChromeUtils.defineLazyGetter(lazy, "foo1", () => {}); + { x = -f(1 + lazy.foo1.bar()); }`, + "foo1", + "topLevelAndUnconditional" + ), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-services-property.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-services-property.js new file mode 100644 index 0000000000..a9806ccc72 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-services-property.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/valid-services-property"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, messageId, data) { + return { code, errors: [{ messageId, data }] }; +} + +process.env.MOZ_XPT_ARTIFACTS_DIR = `${__dirname}/xpidl`; + +ruleTester.run("valid-services-property", rule, { + valid: [ + "Services.uriFixup.keywordToURI()", + "Services.uriFixup.FIXUP_FLAG_NONE", + ], + invalid: [ + invalidCode("Services.uriFixup.UNKNOWN_CONSTANT", "unknownProperty", { + alias: "uriFixup", + propertyName: "UNKNOWN_CONSTANT", + checkedInterfaces: ["nsIURIFixup"], + }), + invalidCode("Services.uriFixup.foo()", "unknownProperty", { + alias: "uriFixup", + propertyName: "foo", + checkedInterfaces: ["nsIURIFixup"], + }), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-services.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-services.js new file mode 100644 index 0000000000..1453e95b7b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/valid-services.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/valid-services"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, alias) { + return { code, errors: [{ messageId: "unknownProperty", data: { alias } }] }; +} + +ruleTester.run("valid-services", rule, { + valid: ["Services.crashmanager", "lazy.Services.crashmanager"], + invalid: [ + invalidCode("Services.foo", "foo"), + invalidCode("Services.foo()", "foo"), + invalidCode("lazy.Services.foo", "foo"), + invalidCode("Services.foo.bar()", "foo"), + invalidCode("lazy.Services.foo.bar()", "foo"), + ], +}); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/xpidl/docshell.xpt b/tools/lint/eslint/eslint-plugin-mozilla/tests/xpidl/docshell.xpt new file mode 100644 index 0000000000..fc8a45216b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/xpidl/docshell.xpt @@ -0,0 +1,5940 @@ +[ + { + "consts": [ + { + "name": "ePrompt", + "type": { + "tag": "TD_UINT8" + }, + "value": 0 + }, + { + "name": "eDontPromptAndDontUnload", + "type": { + "tag": "TD_UINT8" + }, + "value": 1 + }, + { + "name": "eDontPromptAndUnload", + "type": { + "tag": "TD_UINT8" + }, + "value": 2 + }, + { + "name": "eAllowNavigation", + "type": { + "tag": "TD_UINT8" + }, + "value": 0 + }, + { + "name": "eRequestBlockNavigation", + "type": { + "tag": "TD_UINT8" + }, + "value": 1 + }, + { + "name": "eDelayResize", + "type": { + "tag": "TD_UINT32" + }, + "value": 1 + } + ], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [ + "hidden" + ], + "name": "init", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "container", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocShell", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "container", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShell", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "loadStart", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "headerFile": "mozilla/dom/Document.h", + "name": "Document", + "native": "mozilla::dom::Document", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [], + "name": "loadComplete", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "loadCompleted", + "params": [] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "isStopped", + "params": [] + }, + { + "flags": [ + "hasretval" + ], + "name": "permitUnload", + "params": [ + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_UINT8" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "inPermitUnload", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "dispatchBeforeUnload", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "beforeUnloadFiring", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "pageHide", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "close", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsISHEntry", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "destroy", + "params": [] + }, + { + "flags": [], + "name": "stop", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "DOMDocument", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "headerFile": "mozilla/dom/Document.h", + "name": "Document", + "native": "mozilla::dom::Document", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getDocument", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "setDocument", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "headerFile": "mozilla/dom/Document.h", + "name": "Document", + "native": "mozilla::dom::Document", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getBounds", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "setBounds", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "setBoundsWithFlags", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "previousViewer", + "params": [] + }, + { + "flags": [ + "setter", + "hidden" + ], + "name": "previousViewer", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocumentViewer", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "move", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "show", + "params": [] + }, + { + "flags": [], + "name": "hide", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "sticky", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "sticky", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "open", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISHEntry", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "clearHistoryEntry", + "params": [] + }, + { + "flags": [], + "name": "setPageModeForTesting", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrintSettings", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "setPrintSettingsForSubdocument", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrintSettings", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "historyEntry", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsISHEntry", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isTabModalPromptAllowed", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isHidden", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "isHidden", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "presShell", + "params": [] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "presContext", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "setDocumentInternal", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "headerFile": "mozilla/dom/Document.h", + "name": "Document", + "native": "mozilla::dom::Document", + "tag": "TD_DOMOBJECT" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "findContainerView", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "setNavigationTiming", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "deviceFullZoomForTest", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_FLOAT" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "authorStyleDisabled", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "authorStyleDisabled", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "getContentSize", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "getContentSizeConstrained", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getReloadEncodingAndSource", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "setReloadEncodingAndSource", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "forgetReloadEncoding", + "params": [] + } + ], + "name": "nsIDocumentViewer", + "parent": "nsISupports", + "uuid": "48118355-e9a5-4452-ab18-59cc426fb817" + }, + { + "consts": [ + { + "name": "COPY_IMAGE_TEXT", + "type": { + "tag": "TD_INT32" + }, + "value": 1 + }, + { + "name": "COPY_IMAGE_HTML", + "type": { + "tag": "TD_INT32" + }, + "value": 2 + }, + { + "name": "COPY_IMAGE_DATA", + "type": { + "tag": "TD_INT32" + }, + "value": 4 + }, + { + "name": "COPY_IMAGE_ALL", + "type": { + "tag": "TD_INT32" + }, + "value": -1 + } + ], + "flags": [], + "methods": [ + { + "flags": [], + "name": "clearSelection", + "params": [] + }, + { + "flags": [], + "name": "selectAll", + "params": [] + }, + { + "flags": [], + "name": "copySelection", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "copyable", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "copyLinkLocation", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "inLink", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "copyImage", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "inImage", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "getContents", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "canGetContents", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "setCommandNode", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "headerFile": "nsIContent.h", + "name": "Node", + "native": "nsINode", + "tag": "TD_DOMOBJECT" + } + } + ] + } + ], + "name": "nsIDocumentViewerEdit", + "parent": "nsISupports", + "uuid": "35be2d7e-f29b-48ec-bf7e-80a30a724de3" + }, + { + "consts": [ + { + "name": "ENUMERATE_FORWARDS", + "type": { + "tag": "TD_UINT8" + }, + "value": 0 + }, + { + "name": "ENUMERATE_BACKWARDS", + "type": { + "tag": "TD_UINT8" + }, + "value": 1 + }, + { + "name": "APP_TYPE_UNKNOWN", + "type": { + "tag": "TD_UINT8" + }, + "value": 0 + }, + { + "name": "APP_TYPE_MAIL", + "type": { + "tag": "TD_UINT8" + }, + "value": 1 + }, + { + "name": "APP_TYPE_EDITOR", + "type": { + "tag": "TD_UINT8" + }, + "value": 2 + }, + { + "name": "BUSY_FLAGS_NONE", + "type": { + "tag": "TD_UINT8" + }, + "value": 0 + }, + { + "name": "BUSY_FLAGS_BUSY", + "type": { + "tag": "TD_UINT8" + }, + "value": 1 + }, + { + "name": "BUSY_FLAGS_BEFORE_PAGE_LOAD", + "type": { + "tag": "TD_UINT8" + }, + "value": 2 + }, + { + "name": "BUSY_FLAGS_PAGE_LOADING", + "type": { + "tag": "TD_UINT8" + }, + "value": 4 + }, + { + "name": "LOAD_CMD_NORMAL", + "type": { + "tag": "TD_UINT8" + }, + "value": 1 + }, + { + "name": "LOAD_CMD_RELOAD", + "type": { + "tag": "TD_UINT8" + }, + "value": 2 + }, + { + "name": "LOAD_CMD_HISTORY", + "type": { + "tag": "TD_UINT8" + }, + "value": 4 + }, + { + "name": "LOAD_CMD_PUSHSTATE", + "type": { + "tag": "TD_UINT8" + }, + "value": 8 + }, + { + "name": "META_VIEWPORT_OVERRIDE_DISABLED", + "type": { + "tag": "TD_UINT8" + }, + "value": 0 + }, + { + "name": "META_VIEWPORT_OVERRIDE_ENABLED", + "type": { + "tag": "TD_UINT8" + }, + "value": 1 + }, + { + "name": "META_VIEWPORT_OVERRIDE_NONE", + "type": { + "tag": "TD_UINT8" + }, + "value": 2 + } + ], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [], + "name": "setCancelContentJSEpoch", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "loadURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "jscontext" + ], + "name": "addState", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_JSVAL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "prepareForNewContentModel", + "params": [] + }, + { + "flags": [], + "name": "setCurrentURIForSessionStore", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "firePageHideNotification", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "presContext", + "params": [] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "presShell", + "params": [] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "eldestPresShell", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "docViewer", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocumentViewer", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "outerWindowID", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "chromeEventHandler", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "headerFile": "mozilla/dom/EventTarget.h", + "name": "EventTarget", + "native": "mozilla::dom::EventTarget", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "chromeEventHandler", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "headerFile": "mozilla/dom/EventTarget.h", + "name": "EventTarget", + "native": "mozilla::dom::EventTarget", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "customUserAgent", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "customUserAgent", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "cssErrorReportingEnabled", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "cssErrorReportingEnabled", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowPlugins", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowPlugins", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowMetaRedirects", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowMetaRedirects", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowSubframes", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowSubframes", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowImages", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowImages", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowMedia", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowMedia", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowDNSPrefetch", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowDNSPrefetch", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowWindowControl", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowWindowControl", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowContentRetargeting", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowContentRetargeting", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowContentRetargetingOnChildren", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowContentRetargetingOnChildren", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "getAllDocShellsInSubtree", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT8" + } + }, + { + "flags": [ + "out" + ], + "type": { + "element": { + "name": "nsIDocShell", + "tag": "TD_INTERFACE_TYPE" + }, + "tag": "TD_ARRAY" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "appType", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT8" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "appType", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT8" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "allowAuth", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "allowAuth", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "zoom", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_FLOAT" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "zoom", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_FLOAT" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "tabToTreeOwner", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "busyFlags", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT8" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "loadType", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "loadType", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "defaultLoadFlags", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "defaultLoadFlags", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "isBeingDestroyed", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isExecutingOnLoadHandler", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "layoutHistoryState", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsILayoutHistoryState", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "layoutHistoryState", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsILayoutHistoryState", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "loadURIDelegate", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsILoadURIDelegate", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "suspendRefreshURIs", + "params": [] + }, + { + "flags": [], + "name": "resumeRefreshURIs", + "params": [] + }, + { + "flags": [], + "name": "beginRestore", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocumentViewer", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "finishRestore", + "params": [] + }, + { + "flags": [], + "name": "clearCachedUserAgent", + "params": [] + }, + { + "flags": [], + "name": "clearCachedPlatform", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "restoringDocument", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "useErrorPages", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "useErrorPages", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "displayLoadError", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PWSTRING" + } + }, + { + "flags": [ + "in", + "optional" + ], + "type": { + "name": "nsIChannel", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "failedChannel", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIChannel", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "previousEntryIndex", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "loadedEntryIndex", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "historyPurged", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "currentDocumentChannel", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIChannel", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isInUnload", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "DetachEditorFromWindow", + "params": [] + }, + { + "flags": [], + "name": "exitPrintPreview", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "historyID", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_NSID" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "HistoryID", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isAppTab", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "isAppTab", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "createAboutBlankDocumentViewer", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrincipal", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrincipal", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in", + "optional" + ], + "type": { + "name": "nsIContentSecurityPolicy", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "charset", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_CSTRING" + } + } + ] + }, + { + "flags": [], + "name": "forceEncodingDetection", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "setParentCharset", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrincipal", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getParentCharset", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIPrincipal", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "now", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_DOUBLE" + } + } + ] + }, + { + "flags": [], + "name": "addWeakPrivacyTransitionObserver", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrivacyTransitionObserver", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "addWeakReflowObserver", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIReflowObserver", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "removeWeakReflowObserver", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIReflowObserver", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "notifyReflowObservers", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_DOUBLE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_DOUBLE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "addWeakScrollObserver", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIScrollObserver", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "removeWeakScrollObserver", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIScrollObserver", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "notifyScrollObservers", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isTopLevelContentDocShell", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "asyncPanZoomEnabled", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "pluginsAllowedInCurrentDoc", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "mayEnableCharacterEncodingMenu", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "editor", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIEditor", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "editor", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIEditor", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "editable", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "hasEditingSession", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "makeEditable", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "getCurrentSHEntry", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsISHEntry", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "isCommandEnabled", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "doCommand", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + } + ] + }, + { + "flags": [], + "name": "doCommandWithParams", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsICommandParams", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "IsInvisible", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "SetInvisible", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "GetScriptGlobalObject", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "getExtantDocument", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "deviceSizeIsPageSize", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "deviceSizeIsPageSize", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "hasLoadedNonBlankURI", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "windowDraggingAllowed", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "windowDraggingAllowed", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "currentScrollRestorationIsManual", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "currentScrollRestorationIsManual", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "jscontext", + "hasretval" + ], + "name": "getOriginAttributes", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_JSVAL" + } + } + ] + }, + { + "flags": [ + "jscontext" + ], + "name": "setOriginAttributes", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_JSVAL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "editingSession", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIEditingSession", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "browserChild", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIBrowserChild", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "GetBrowserChild", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "GetCommandManager", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "metaViewportOverride", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT8" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "metaViewportOverride", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT8" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "useTrackingProtection", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "useTrackingProtection", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "dispatchLocationChangeEvent", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "startDelayedAutoplayMediaComponents", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "TakeInitialClientSource", + "params": [] + }, + { + "flags": [], + "name": "setColorMatrix", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "element": { + "tag": "TD_FLOAT" + }, + "tag": "TD_ARRAY" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isForceReloading", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "getColorMatrix", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "element": { + "tag": "TD_FLOAT" + }, + "tag": "TD_ARRAY" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "messageManager", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "headerFile": "mozilla/dom/ContentFrameMessageManager.h", + "name": "ContentFrameMessageManager", + "native": "mozilla::dom::ContentFrameMessageManager", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "getHasTrackingContentBlocked", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_PROMISE" + } + } + ] + }, + { + "flags": [ + "getter", + "hidden", + "hasretval" + ], + "name": "isAttemptingToNavigate", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isNavigating", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "synchronizeLayoutHistoryState", + "params": [] + }, + { + "flags": [], + "name": "persistLayoutHistoryState", + "params": [] + } + ], + "name": "nsIDocShell", + "parent": "nsIDocShellTreeItem", + "uuid": "049234fe-da10-478b-bc5d-bc6f9a1ba63d" + }, + { + "consts": [ + { + "name": "typeChrome", + "type": { + "tag": "TD_INT32" + }, + "value": 0 + }, + { + "name": "typeContent", + "type": { + "tag": "TD_INT32" + }, + "value": 1 + }, + { + "name": "typeContentWrapper", + "type": { + "tag": "TD_INT32" + }, + "value": 2 + }, + { + "name": "typeChromeWrapper", + "type": { + "tag": "TD_INT32" + }, + "value": 3 + }, + { + "name": "typeAll", + "type": { + "tag": "TD_INT32" + }, + "value": 2147483647 + } + ], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [ + "getter", + "hasretval" + ], + "name": "name", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "name", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "nameEquals", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "itemType", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "ItemType", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "parent", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "sameTypeParent", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "rootTreeItem", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "sameTypeRootTreeItem", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "treeOwner", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocShellTreeOwner", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "setTreeOwner", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShellTreeOwner", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "childCount", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "addChild", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "removeChild", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "getChildAt", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "browsingContext", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "headerFile": "mozilla/dom/BrowsingContext.h", + "name": "BrowsingContext", + "native": "mozilla::dom::BrowsingContext", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getBrowsingContext", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "domWindow", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "mozIDOMWindowProxy", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getDocument", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "getWindow", + "params": [] + } + ], + "name": "nsIDocShellTreeItem", + "parent": "nsISupports", + "uuid": "9b7c586f-9214-480c-a2c4-49b526fff1a6" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "contentShellAdded", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "contentShellRemoved", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "primaryContentShell", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "remoteTabAdded", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIRemoteTab", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "remoteTabRemoved", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIRemoteTab", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "primaryRemoteTab", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIRemoteTab", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "primaryContentBrowsingContext", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "headerFile": "mozilla/dom/BrowsingContext.h", + "name": "BrowsingContext", + "native": "mozilla::dom::BrowsingContext", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [], + "name": "sizeShellTo", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShellTreeItem", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "getPrimaryContentSize", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "setPrimaryContentSize", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "getRootShellSize", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "setRootShellSize", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "setPersistence", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "getPersistence", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "tabCount", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "hasPrimaryContent", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + } + ], + "name": "nsIDocShellTreeOwner", + "parent": "nsISupports", + "uuid": "0e3dc4b1-4cea-4a37-af71-79f0afd07574" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "createInstance", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIChannel", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsILoadGroup", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShell", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIStreamListener", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocumentViewer", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "createInstanceForDocument", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "headerFile": "mozilla/dom/Document.h", + "name": "Document", + "native": "mozilla::dom::Document", + "tag": "TD_DOMOBJECT" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIDocumentViewer", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsIDocumentLoaderFactory", + "parent": "nsISupports", + "uuid": "e795239e-9d3c-47c4-b063-9e600fb3b287" + }, + { + "consts": [], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [ + "getter", + "hasretval" + ], + "name": "associatedWindow", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "mozIDOMWindowProxy", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "topWindow", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "mozIDOMWindowProxy", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "topFrameElement", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "headerFile": "mozilla/dom/Element.h", + "name": "Element", + "native": "mozilla::dom::Element", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isContent", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "usePrivateBrowsing", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "usePrivateBrowsing", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "useRemoteTabs", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "useRemoteSubframes", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "useTrackingProtection", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "useTrackingProtection", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "SetPrivateBrowsing", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "SetRemoteTabs", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "SetRemoteSubframes", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "originAttributes", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_JSVAL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "GetOriginAttributes", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + } + ], + "name": "nsILoadContext", + "parent": "nsISupports", + "uuid": "2813a7a3-d084-4d00-acd0-f76620315c02" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "loadURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT16" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrincipal", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "handleLoadError", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT16" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsILoadURIDelegate", + "parent": "nsISupports", + "uuid": "78e42d37-a34c-4d96-b901-25385669aba4" + }, + { + "consts": [], + "flags": [ + "function" + ], + "methods": [ + { + "flags": [], + "name": "privateModeChanged", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + } + ], + "name": "nsIPrivacyTransitionObserver", + "parent": "nsISupports", + "uuid": "b4b1449d-0ef0-47f5-b62e-adc57fd49702" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "reflow", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_DOUBLE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_DOUBLE" + } + } + ] + }, + { + "flags": [], + "name": "reflowInterruptible", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_DOUBLE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_DOUBLE" + } + } + ] + } + ], + "name": "nsIReflowObserver", + "parent": "nsISupports", + "uuid": "832e692c-c4a6-11e2-8fd1-dce678957a39" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "refreshURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrincipal", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [], + "name": "forceRefreshURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIPrincipal", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [], + "name": "cancelRefreshURITimers", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "refreshPending", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + } + ], + "name": "nsIRefreshURI", + "parent": "nsISupports", + "uuid": "a5e61a3c-51bd-45be-ac0c-e87b71860656" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "onShowTooltip", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [], + "name": "onHideTooltip", + "params": [] + } + ], + "name": "nsITooltipListener", + "parent": "nsISupports", + "uuid": "44b78386-1dd2-11b2-9ad2-e4eee2ca1916" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "getNodeText", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "headerFile": "nsIContent.h", + "name": "Node", + "native": "nsINode", + "tag": "TD_DOMOBJECT" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_PWSTRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_PWSTRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + } + ], + "name": "nsITooltipTextProvider", + "parent": "nsISupports", + "uuid": "b128a1e6-44f3-4331-8fbe-5af360ff21ee" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "getter", + "hasretval" + ], + "name": "consumer", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "headerFile": "mozilla/dom/BrowsingContext.h", + "name": "BrowsingContext", + "native": "mozilla::dom::BrowsingContext", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "consumer", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "headerFile": "mozilla/dom/BrowsingContext.h", + "name": "BrowsingContext", + "native": "mozilla::dom::BrowsingContext", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "preferredURI", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "preferredURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "fixedURI", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "fixedURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "keywordProviderName", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "keywordProviderName", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "keywordAsSent", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "keywordAsSent", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "fixupChangedProtocol", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "fixupChangedProtocol", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "fixupCreatedAlternateURI", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "fixupCreatedAlternateURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "originalInput", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UTF8STRING" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "originalInput", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "postData", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIInputStream", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "postData", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIInputStream", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsIURIFixupInfo", + "parent": "nsISupports", + "uuid": "4819f183-b532-4932-ac09-b309cd853be7" + }, + { + "consts": [ + { + "name": "FIXUP_FLAG_NONE", + "type": { + "tag": "TD_UINT32" + }, + "value": 0 + }, + { + "name": "FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP", + "type": { + "tag": "TD_UINT32" + }, + "value": 1 + }, + { + "name": "FIXUP_FLAGS_MAKE_ALTERNATE_URI", + "type": { + "tag": "TD_UINT32" + }, + "value": 2 + }, + { + "name": "FIXUP_FLAG_PRIVATE_CONTEXT", + "type": { + "tag": "TD_UINT32" + }, + "value": 4 + }, + { + "name": "FIXUP_FLAG_FIX_SCHEME_TYPOS", + "type": { + "tag": "TD_UINT32" + }, + "value": 8 + } + ], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "getFixupURIInfo", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + }, + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_UINT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIURIFixupInfo", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "webNavigationFlagsToFixupFlags", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "keywordToURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + }, + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIURIFixupInfo", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "forceHttpFixup", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "name": "nsIURIFixupInfo", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "checkHost", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDNSListener", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_JSVAL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "isDomainKnown", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + } + ], + "name": "nsIURIFixup", + "parent": "nsISupports", + "uuid": "1da7e9d4-620b-4949-849a-1cd6077b1b2d" + }, + { + "consts": [ + { + "name": "LOAD_FLAGS_MASK", + "type": { + "tag": "TD_UINT32" + }, + "value": 65535 + }, + { + "name": "LOAD_FLAGS_NONE", + "type": { + "tag": "TD_UINT32" + }, + "value": 0 + }, + { + "name": "LOAD_FLAGS_IS_REFRESH", + "type": { + "tag": "TD_UINT32" + }, + "value": 16 + }, + { + "name": "LOAD_FLAGS_IS_LINK", + "type": { + "tag": "TD_UINT32" + }, + "value": 32 + }, + { + "name": "LOAD_FLAGS_BYPASS_HISTORY", + "type": { + "tag": "TD_UINT32" + }, + "value": 64 + }, + { + "name": "LOAD_FLAGS_REPLACE_HISTORY", + "type": { + "tag": "TD_UINT32" + }, + "value": 128 + }, + { + "name": "LOAD_FLAGS_BYPASS_CACHE", + "type": { + "tag": "TD_UINT32" + }, + "value": 256 + }, + { + "name": "LOAD_FLAGS_BYPASS_PROXY", + "type": { + "tag": "TD_UINT32" + }, + "value": 512 + }, + { + "name": "LOAD_FLAGS_CHARSET_CHANGE", + "type": { + "tag": "TD_UINT32" + }, + "value": 1024 + }, + { + "name": "LOAD_FLAGS_STOP_CONTENT", + "type": { + "tag": "TD_UINT32" + }, + "value": 2048 + }, + { + "name": "LOAD_FLAGS_FROM_EXTERNAL", + "type": { + "tag": "TD_UINT32" + }, + "value": 4096 + }, + { + "name": "LOAD_FLAGS_FIRST_LOAD", + "type": { + "tag": "TD_UINT32" + }, + "value": 16384 + }, + { + "name": "LOAD_FLAGS_ALLOW_POPUPS", + "type": { + "tag": "TD_UINT32" + }, + "value": 32768 + }, + { + "name": "LOAD_FLAGS_BYPASS_CLASSIFIER", + "type": { + "tag": "TD_UINT32" + }, + "value": 65536 + }, + { + "name": "LOAD_FLAGS_FORCE_ALLOW_COOKIES", + "type": { + "tag": "TD_UINT32" + }, + "value": 131072 + }, + { + "name": "LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL", + "type": { + "tag": "TD_UINT32" + }, + "value": 262144 + }, + { + "name": "LOAD_FLAGS_ERROR_LOAD_CHANGES_RV", + "type": { + "tag": "TD_UINT32" + }, + "value": 524288 + }, + { + "name": "LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP", + "type": { + "tag": "TD_UINT32" + }, + "value": 1048576 + }, + { + "name": "LOAD_FLAGS_FIXUP_SCHEME_TYPOS", + "type": { + "tag": "TD_UINT32" + }, + "value": 2097152 + }, + { + "name": "LOAD_FLAGS_FORCE_ALLOW_DATA_URI", + "type": { + "tag": "TD_UINT32" + }, + "value": 4194304 + }, + { + "name": "LOAD_FLAGS_IS_REDIRECT", + "type": { + "tag": "TD_UINT32" + }, + "value": 8388608 + }, + { + "name": "LOAD_FLAGS_DISABLE_TRR", + "type": { + "tag": "TD_UINT32" + }, + "value": 16777216 + }, + { + "name": "LOAD_FLAGS_FORCE_TRR", + "type": { + "tag": "TD_UINT32" + }, + "value": 33554432 + }, + { + "name": "LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE", + "type": { + "tag": "TD_UINT32" + }, + "value": 67108864 + }, + { + "name": "LOAD_FLAGS_USER_ACTIVATION", + "type": { + "tag": "TD_UINT32" + }, + "value": 134217728 + }, + { + "name": "STOP_NETWORK", + "type": { + "tag": "TD_UINT32" + }, + "value": 1 + }, + { + "name": "STOP_CONTENT", + "type": { + "tag": "TD_UINT32" + }, + "value": 2 + }, + { + "name": "STOP_ALL", + "type": { + "tag": "TD_UINT32" + }, + "value": 3 + } + ], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [ + "getter", + "hasretval" + ], + "name": "canGoBack", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "canGoForward", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "goBack", + "params": [ + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "goForward", + "params": [ + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "gotoIndex", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in", + "optional" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "jscontext" + ], + "name": "loadURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_JSVAL" + } + } + ] + }, + { + "flags": [], + "name": "binaryLoadURI", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [], + "name": "reload", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [], + "name": "stop", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "document", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "headerFile": "mozilla/dom/Document.h", + "name": "Document", + "native": "mozilla::dom::Document", + "tag": "TD_DOMOBJECT" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "currentURI", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIURI", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "sessionHistory", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "resumeRedirectedLoad", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT64" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + } + ], + "name": "nsIWebNavigation", + "parent": "nsISupports", + "uuid": "3ade79d4-8cb9-4952-b18d-4f9b63ca0d31" + }, + { + "consts": [ + { + "name": "UNSUPPORTED", + "type": { + "tag": "TD_UINT32" + }, + "value": 0 + }, + { + "name": "IMAGE", + "type": { + "tag": "TD_UINT32" + }, + "value": 1 + }, + { + "name": "FALLBACK", + "type": { + "tag": "TD_UINT32" + }, + "value": 2 + }, + { + "name": "OTHER", + "type": { + "tag": "TD_UINT32" + }, + "value": 32768 + } + ], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "isTypeSupported", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + } + ], + "name": "nsIWebNavigationInfo", + "parent": "nsISupports", + "uuid": "62a93afb-93a1-465c-84c8-0432264229de" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "loadPageAsViewSource", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDocShell", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "currentDescriptor", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsIWebPageDescriptor", + "parent": "nsISupports", + "uuid": "6f30b676-3710-4c2c-80b1-0395fb26516e" + } +]
\ No newline at end of file diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/xpidl/xpcom_base.xpt b/tools/lint/eslint/eslint-plugin-mozilla/tests/xpidl/xpcom_base.xpt new file mode 100644 index 0000000000..8b3db4bdeb --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/xpidl/xpcom_base.xpt @@ -0,0 +1,3165 @@ +[ + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "unloadTabAsync", + "params": [] + } + ], + "name": "nsITabUnloader", + "parent": "nsISupports", + "uuid": "2e530956-6054-464f-9f4c-0ae6f8de5523" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "registerTabUnloader", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsITabUnloader", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "onUnloadAttemptCompleted", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + } + ], + "name": "nsIAvailableMemoryWatcherBase", + "parent": "nsISupports", + "uuid": "b0b5701e-239d-49db-9009-37e89f86441c" + }, + { + "consts": [], + "flags": [ + "function" + ], + "methods": [ + { + "flags": [], + "name": "observe", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIConsoleMessage", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsIConsoleListener", + "parent": "nsISupports", + "uuid": "35c400a4-5792-438c-b915-65e30d58d557" + }, + { + "consts": [ + { + "name": "debug", + "type": { + "tag": "TD_UINT32" + }, + "value": 0 + }, + { + "name": "info", + "type": { + "tag": "TD_UINT32" + }, + "value": 1 + }, + { + "name": "warn", + "type": { + "tag": "TD_UINT32" + }, + "value": 2 + }, + { + "name": "error", + "type": { + "tag": "TD_UINT32" + }, + "value": 3 + } + ], + "flags": [], + "methods": [ + { + "flags": [ + "getter", + "hasretval" + ], + "name": "logLevel", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "timeStamp", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "microSecondTimeStamp", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "message", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isForwardedFromContentProcess", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "isForwardedFromContentProcess", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "toString", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UTF8STRING" + } + } + ] + } + ], + "name": "nsIConsoleMessage", + "parent": "nsISupports", + "uuid": "3aba9617-10e2-4839-83ae-2e6fc4df428b" + }, + { + "consts": [ + { + "name": "SuppressLog", + "type": { + "tag": "TD_UINT8" + }, + "value": 0 + }, + { + "name": "OutputToLog", + "type": { + "tag": "TD_UINT8" + }, + "value": 1 + } + ], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [], + "name": "logMessage", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIConsoleMessage", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "logMessageWithMode", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIConsoleMessage", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT8" + } + } + ] + }, + { + "flags": [], + "name": "logStringMessage", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PWSTRING" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "getMessageArray", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "element": { + "name": "nsIConsoleMessage", + "tag": "TD_INTERFACE_TYPE" + }, + "tag": "TD_ARRAY" + } + } + ] + }, + { + "flags": [], + "name": "registerListener", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIConsoleListener", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "unregisterListener", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIConsoleListener", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "reset", + "params": [] + }, + { + "flags": [], + "name": "resetWindow", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT64" + } + } + ] + } + ], + "name": "nsIConsoleService", + "parent": "nsISupports", + "uuid": "0eb81d20-c37e-42d4-82a8-ca9ae96bdf52" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "noteRefCountedObject", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + } + ] + }, + { + "flags": [], + "name": "noteGCedObject", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + } + ] + }, + { + "flags": [], + "name": "noteEdge", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + } + ] + }, + { + "flags": [], + "name": "describeRoot", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UINT32" + } + } + ] + }, + { + "flags": [], + "name": "describeGarbage", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + } + ] + } + ], + "name": "nsICycleCollectorHandler", + "parent": "nsISupports", + "uuid": "7f093367-1492-4b89-87af-c01dbc831246" + }, + { + "consts": [], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [ + "hidden" + ], + "name": "open", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [], + "name": "closeGCLog", + "params": [] + }, + { + "flags": [], + "name": "closeCCLog", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "filenameIdentifier", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "filenameIdentifier", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "processIdentifier", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "processIdentifier", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "gcLog", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIFile", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "ccLog", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIFile", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsICycleCollectorLogSink", + "parent": "nsISupports", + "uuid": "3ad9875f-d0e4-4ac2-87e3-f127f6c02ce1" + }, + { + "consts": [], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "allTraces", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsICycleCollectorListener", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "wantAllTraces", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "disableLog", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "disableLog", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "logSink", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsICycleCollectorLogSink", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "logSink", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsICycleCollectorLogSink", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "wantAfterProcessing", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "setter" + ], + "name": "wantAfterProcessing", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "processNext", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsICycleCollectorHandler", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden", + "hasretval" + ], + "name": "asLogger", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + } + ], + "name": "nsICycleCollectorListener", + "parent": "nsISupports", + "uuid": "703b53b6-24f6-40c6-9ea9-aeb2dc53d170" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isDebugBuild", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "assertionCount", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isDebuggerAttached", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "assertion", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "warning", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "break", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "abort", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [], + "name": "rustPanic", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + } + ] + }, + { + "flags": [], + "name": "rustLog", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_PSTRING" + } + } + ] + }, + { + "flags": [], + "name": "crashWithOOM", + "params": [] + } + ], + "name": "nsIDebug2", + "parent": "nsISupports", + "uuid": "9641dc15-10fb-42e3-a285-18be90a5c10b" + }, + { + "consts": [], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "filename", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "name", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "sourceId", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "lineNumber", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "columnNumber", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "sourceLine", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UTF8STRING" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "asyncCause", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "asyncCaller", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIStackFrame", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "caller", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIStackFrame", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "getter", + "jscontext", + "hasretval" + ], + "name": "formattedStack", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "nativeSavedFrame", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_JSVAL" + } + } + ] + }, + { + "flags": [ + "jscontext", + "hasretval" + ], + "name": "toString", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UTF8STRING" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getFilename", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getName", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getSourceId", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getLineNumber", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getColumnNumber", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getAsyncCause", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getAsyncCaller", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getCaller", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getFormattedStack", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "toStringInfallible", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_UTF8STRING" + } + } + ] + } + ], + "name": "nsIStackFrame", + "parent": "nsISupports", + "uuid": "28bfb2a2-5ea6-4738-918b-049dc4d51f0b" + }, + { + "consts": [], + "flags": [ + "builtinclass" + ], + "methods": [], + "name": "nsIException", + "parent": "nsISupports", + "uuid": "4371b5bf-6845-487f-8d9d-3f1e4a9badd2" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "init", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIFile", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "initANSIFileDesc", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + }, + { + "flags": [], + "name": "write", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + } + ] + }, + { + "flags": [], + "name": "finish", + "params": [] + } + ], + "name": "nsIGZFileWriter", + "parent": "nsISupports", + "uuid": "6bd5642c-1b90-4499-ba4b-199f27efaba5" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "getInterface", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_NSID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "iid_is": 0, + "tag": "TD_INTERFACE_IS_TYPE" + } + } + ] + } + ], + "name": "nsIInterfaceRequestor", + "parent": "nsISupports", + "uuid": "033a1470-8b2a-11d3-af88-00a024ffc08c" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "policiesEnabled", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "jscontext", + "hasretval" + ], + "name": "readPreferences", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_JSVAL" + } + } + ] + } + ], + "name": "nsIMacPreferencesReader", + "parent": "nsISupports", + "uuid": "b0f20595-88ce-4738-a1a4-24de78eb8051" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "getter", + "hasretval" + ], + "name": "architecturesInBinary", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isTranslated", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + } + ], + "name": "nsIMacUtils", + "parent": "nsISupports", + "uuid": "5e9072d7-ff95-455e-9466-8af9841a72ec" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "heapMinimize", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hasretval" + ], + "name": "isLowMemoryPlatform", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + } + ], + "name": "nsIMemory", + "parent": "nsISupports", + "uuid": "1e004834-6d8f-425a-bc9c-a2812ed43bb7" + }, + { + "consts": [], + "flags": [ + "function" + ], + "methods": [ + { + "flags": [], + "name": "callback", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsIFinishDumpingCallback", + "parent": "nsISupports", + "uuid": "2dea18fc-fbfa-4bf7-ad45-0efaf5495f5e" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [], + "name": "onDump", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIFile", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIFile", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "onFinish", + "params": [] + } + ], + "name": "nsIDumpGCAndCCLogsCallback", + "parent": "nsISupports", + "uuid": "dc1b2b24-65bd-441b-b6bd-cb5825a7ed14" + }, + { + "consts": [], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [], + "name": "dumpMemoryReportsToNamedFile", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIFinishDumpingCallback", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "dumpMemoryInfoToTempDir", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "dumpGCAndCCLogsToFile", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIDumpGCAndCCLogsCallback", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "dumpGCAndCCLogsToSink", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsICycleCollectorLogSink", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsIMemoryInfoDumper", + "parent": "nsISupports", + "uuid": "48541b74-47ee-4a62-9557-7f4b809bda5c" + }, + { + "consts": [], + "flags": [ + "function" + ], + "methods": [ + { + "flags": [], + "name": "callback", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT32" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT64" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_UTF8STRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsIHandleReportCallback", + "parent": "nsISupports", + "uuid": "62ef0e1c-dbd6-11e3-aa75-3c970e9f4238" + }, + { + "consts": [ + { + "name": "KIND_NONHEAP", + "type": { + "tag": "TD_INT32" + }, + "value": 0 + }, + { + "name": "KIND_HEAP", + "type": { + "tag": "TD_INT32" + }, + "value": 1 + }, + { + "name": "KIND_OTHER", + "type": { + "tag": "TD_INT32" + }, + "value": 2 + }, + { + "name": "UNITS_BYTES", + "type": { + "tag": "TD_INT32" + }, + "value": 0 + }, + { + "name": "UNITS_COUNT", + "type": { + "tag": "TD_INT32" + }, + "value": 1 + }, + { + "name": "UNITS_COUNT_CUMULATIVE", + "type": { + "tag": "TD_INT32" + }, + "value": 2 + }, + { + "name": "UNITS_PERCENTAGE", + "type": { + "tag": "TD_INT32" + }, + "value": 3 + } + ], + "flags": [], + "methods": [ + { + "flags": [], + "name": "collectReports", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIHandleReportCallback", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + } + ], + "name": "nsIMemoryReporter", + "parent": "nsISupports", + "uuid": "92a36db1-46bd-4fe6-988e-47db47236d8b" + }, + { + "consts": [], + "flags": [ + "function" + ], + "methods": [ + { + "flags": [], + "name": "callback", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsIFinishReportingCallback", + "parent": "nsISupports", + "uuid": "548b3909-c04d-4ca6-8466-b8bee3837457" + }, + { + "consts": [], + "flags": [ + "function" + ], + "methods": [ + { + "flags": [], + "name": "callback", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + } + ], + "name": "nsIHeapAllocatedCallback", + "parent": "nsISupports", + "uuid": "1a80cd0f-0d9e-4397-be69-68ad28fe5175" + }, + { + "consts": [], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [], + "name": "init", + "params": [] + }, + { + "flags": [], + "name": "registerStrongReporter", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIMemoryReporter", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "registerStrongAsyncReporter", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIMemoryReporter", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "registerWeakReporter", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIMemoryReporter", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "registerWeakAsyncReporter", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIMemoryReporter", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "unregisterStrongReporter", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIMemoryReporter", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "unregisterWeakReporter", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIMemoryReporter", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "blockRegistrationAndHideExistingReporters", + "params": [] + }, + { + "flags": [], + "name": "unblockRegistrationAndRestoreOriginalReporters", + "params": [] + }, + { + "flags": [], + "name": "registerStrongReporterEvenIfBlocked", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIMemoryReporter", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "getReports", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIHandleReportCallback", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIFinishReportingCallback", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getReportsExtended", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIHandleReportCallback", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIFinishReportingCallback", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_ASTRING" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "getReportsForThisProcessExtended", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIHandleReportCallback", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_BOOL" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsIFinishReportingCallback", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "in" + ], + "type": { + "name": "nsISupports", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "endReport", + "params": [] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "vsize", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "vsizeMaxContiguous", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "resident", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "residentFast", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "residentPeak", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "residentUnique", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "heapAllocated", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "heapOverheadFraction", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "JSMainRuntimeGCHeap", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "JSMainRuntimeTemporaryPeak", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "JSMainRuntimeCompartmentsSystem", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "JSMainRuntimeCompartmentsUser", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "JSMainRuntimeRealmsSystem", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "JSMainRuntimeRealmsUser", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "imagesContentUsedUncompressed", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "storageSQLite", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "lowMemoryEventsPhysical", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "ghostWindows", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "pageFaultsHard", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "hasMozMallocUsableSize", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isDMDEnabled", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [ + "getter", + "hasretval" + ], + "name": "isDMDRunning", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_BOOL" + } + } + ] + }, + { + "flags": [], + "name": "minimizeMemoryUsage", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "nsIRunnable", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + }, + { + "flags": [], + "name": "sizeOfTab", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "name": "mozIDOMWindowProxy", + "tag": "TD_INTERFACE_TYPE" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT64" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_DOUBLE" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_DOUBLE" + } + } + ] + } + ], + "name": "nsIMemoryReporterManager", + "parent": "nsISupports", + "uuid": "2998574d-8993-407a-b1a5-8ad7417653e1" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "QueryInterface", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_NSID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "iid_is": 0, + "tag": "TD_INTERFACE_IS_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "AddRef", + "params": [] + }, + { + "flags": [ + "hidden" + ], + "name": "Release", + "params": [] + } + ], + "name": "nsISupports", + "parent": null, + "uuid": "00000000-0000-0000-c000-000000000046" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "generateUUID", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_NSIDPTR" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "generateUUIDInPlace", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + } + ], + "name": "nsIUUIDGenerator", + "parent": "nsISupports", + "uuid": "138ad1b2-c694-41cc-b201-333ce936d8b8" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "compare", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_CSTRING" + } + }, + { + "flags": [ + "out" + ], + "type": { + "tag": "TD_INT32" + } + } + ] + } + ], + "name": "nsIVersionComparator", + "parent": "nsISupports", + "uuid": "e6cd620a-edbb-41d2-9e42-9a2ffc8107f3" + }, + { + "consts": [], + "flags": [ + "builtinclass" + ], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "QueryReferent", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_NSID" + } + }, + { + "flags": [ + "out" + ], + "type": { + "iid_is": 0, + "tag": "TD_INTERFACE_IS_TYPE" + } + } + ] + }, + { + "flags": [ + "hidden" + ], + "name": "sizeOfOnlyThis", + "params": [ + { + "flags": [ + "in" + ], + "type": { + "tag": "TD_VOID" + } + } + ] + } + ], + "name": "nsIWeakReference", + "parent": "nsISupports", + "uuid": "9188bc85-f92e-11d2-81ef-0060083a0bcf" + }, + { + "consts": [], + "flags": [], + "methods": [ + { + "flags": [ + "hasretval" + ], + "name": "GetWeakReference", + "params": [ + { + "flags": [ + "out" + ], + "type": { + "name": "nsIWeakReference", + "tag": "TD_INTERFACE_TYPE" + } + } + ] + } + ], + "name": "nsISupportsWeakReference", + "parent": "nsISupports", + "uuid": "9188bc86-f92e-11d2-81ef-0060083a0bcf" + } +] diff --git a/tools/lint/eslint/eslint-plugin-mozilla/update.sh b/tools/lint/eslint/eslint-plugin-mozilla/update.sh new file mode 100755 index 0000000000..240cfdb9c2 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/update.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# Script to regenerate the npm packages used for eslint-plugin-mozilla by the builders. +# Requires + +# Force the scripts working directory to be projdir/tools/lint/eslint/eslint-plugin-mozilla. +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd $DIR + +if [ -z "$TASKCLUSTER_ACCESS_TOKEN" -o -z "$TASKCLUSTER_CLIENT_ID" -o -z "$TASKCLUSTER_ROOT_URL" ]; then + echo "Please ensure you have run the taskcluster shell correctly to set" + echo "the TASKCLUSTER_ACCESS_TOKEN, TASKCLUSTER_CLIENT_ID and" + echo "TASKCLUSTER_ROOT_URL environment variables." + echo "See https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint/enabling-rules.html" + exit 1; +fi + +echo "" +echo "Removing node_modules and package-lock.json..." +# Move to the top-level directory. +rm -rf node_modules +rm package-lock.json + +echo "Installing modules for eslint-plugin-mozilla..." +../../../../mach npm install + +echo "Creating eslint-plugin-mozilla.tar.gz..." +tar cvz -f eslint-plugin-mozilla.tar.gz node_modules + +echo "Adding eslint-plugin-mozilla.tar.gz to tooltool..." +rm -f manifest.tt +../../../../python/mozbuild/mozbuild/action/tooltool.py add --visibility public --unpack eslint-plugin-mozilla.tar.gz --url="https://tooltool.mozilla-releng.net/" + +echo "Uploading eslint-plugin-mozilla.tar.gz to tooltool..." +../../../../python/mozbuild/mozbuild/action/tooltool.py upload --message "node_modules folder update for tools/lint/eslint/eslint-plugin-mozilla" --url="https://tooltool.mozilla-releng.net/" + +echo "Cleaning up..." +rm eslint-plugin-mozilla.tar.gz + +echo "" +echo "Update complete, please commit and check in your changes." diff --git a/tools/lint/eslint/eslint-plugin-spidermonkey-js/LICENSE b/tools/lint/eslint/eslint-plugin-spidermonkey-js/LICENSE new file mode 100644 index 0000000000..e87a115e46 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-spidermonkey-js/LICENSE @@ -0,0 +1,363 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + 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/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/environments/self-hosted.js b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/environments/self-hosted.js new file mode 100644 index 0000000000..37ae42bfa3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/environments/self-hosted.js @@ -0,0 +1,180 @@ +/** + * @fileoverview Add environment defaults to SpiderMonkey's self-hosted JS. + * + * 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/. + */ + +"use strict"; + +const path = require("path"); +const fs = require("fs"); + +let gRootDir = null; + +// Copied from `tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js`. +function getRootDir() { + if (!gRootDir) { + function searchUpForIgnore(dirName, filename) { + let parsed = path.parse(dirName); + while (parsed.root !== dirName) { + if (fs.existsSync(path.join(dirName, filename))) { + return dirName; + } + // Move up a level + dirName = parsed.dir; + parsed = path.parse(dirName); + } + return null; + } + + let possibleRoot = searchUpForIgnore( + path.dirname(module.filename), + ".eslintignore" + ); + if (!possibleRoot) { + possibleRoot = searchUpForIgnore(path.resolve(), ".eslintignore"); + } + if (!possibleRoot) { + possibleRoot = searchUpForIgnore(path.resolve(), "package.json"); + } + if (!possibleRoot) { + // We've couldn't find a root from the module or CWD, so lets just go + // for the CWD. We really don't want to throw if possible, as that + // tends to give confusing results when used with ESLint. + possibleRoot = process.cwd(); + } + + gRootDir = possibleRoot; + } + + return gRootDir; +} + +function tryReadFile(filePath) { + let absPath = path.join(getRootDir(), filePath); + if (!fs.existsSync(absPath)) { + // Safely handle the case when the file wasn't found, because throwing + // errors can lead to confusing result when used with ESLint. + return ""; + } + return fs.readFileSync(absPath, "utf-8"); +} + +// Search for top-level declarations, #defines, and #includes. +function addGlobalsFrom(dirName, fileName, globals) { + let filePath = path.join(dirName, fileName); + + // Definitions are separated by line. + let lines = tryReadFile(filePath).split("\n"); + + // We don't have to fully parse the source code, because it's formatted + // through "prettier", which means we know the exact code structure. + // + // |class| is disallowed in self-hosted code, so we don't have to handle it. + for (let line of lines) { + if ( + line.startsWith("function") || + line.startsWith("function*") || + line.startsWith("async function") || + line.startsWith("async function*") + ) { + let m = line.match(/^(?:async )?function(?:\*)?\s+([\w\$]+)\s*\(/); + if (m) { + globals[m[1]] = "readonly"; + } + } else if ( + line.startsWith("var") || + line.startsWith("let") || + line.startsWith("const") + ) { + let m = line.match(/^(?:var|let|const)\s+([\w\$]+)\s*[;=]/); + if (m) { + globals[m[1]] = "readonly"; + } + } else if (line.startsWith("#define")) { + let m = line.match(/^#define (\w+)/); + if (m) { + globals[m[1]] = "readonly"; + } + } else if (line.startsWith("#include")) { + let m = line.match(/^#include \"([\w\.]+)\"$/); + if (m) { + // Also process definitions from includes. + addGlobalsFrom(dirName, m[1], globals); + } + } + } +} + +function selfHostingDefines(dirName = "js/src/builtin/") { + let absDir = path.join(getRootDir(), dirName); + if (!fs.existsSync(absDir)) { + // See |tryReadFile| for why we avoid to throw any errors. + return {}; + } + + // Search sub-directories and js-files within |dirName|. + let dirs = []; + let jsFiles = []; + for (let name of fs.readdirSync(absDir)) { + let stat = fs.statSync(path.join(absDir, name)); + if (stat.isDirectory()) { + dirs.push(name); + } else if (stat.isFile() && name.endsWith(".js")) { + jsFiles.push(name); + } + } + + let globals = Object.create(null); + + // Process each js-file. + for (let jsFile of jsFiles) { + addGlobalsFrom(dirName, jsFile, globals); + } + + // Recursively traverse all sub-directories. + for (let dir of dirs) { + globals = { ...globals, ...selfHostingDefines(path.join(dirName, dir)) }; + } + + return globals; +} + +function selfHostingFunctions() { + // Definitions can be spread across multiple lines and may have extra + // whitespace, so we simply remove all whitespace and match over the complete + // file. + let content = tryReadFile("js/src/vm/SelfHosting.cpp").replace(/\s+/g, ""); + + let globals = Object.create(null); + for (let m of content.matchAll(/(?:JS_FN|JS_INLINABLE_FN)\("(\w+)"/g)) { + globals[m[1]] = "readonly"; + } + return globals; +} + +function errorNumbers() { + // Definitions are separated by line. + let lines = tryReadFile("js/public/friend/ErrorNumbers.msg").split("\n"); + + let globals = Object.create(null); + for (let line of lines) { + let m = line.match(/^MSG_DEF\((\w+),/); + if (m) { + globals[m[1]] = "readonly"; + } + } + return globals; +} + +const globals = { + ...selfHostingDefines(), + ...selfHostingFunctions(), + ...errorNumbers(), +}; + +module.exports = { + globals, +}; diff --git a/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/index.js b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/index.js new file mode 100644 index 0000000000..d9d40af8c6 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/index.js @@ -0,0 +1,20 @@ +/** + * @fileoverview A processor to help parse the spidermonkey js code. + * 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/. + */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Plugin Definition +// ------------------------------------------------------------------------------ +module.exports = { + processors: { + processor: require("../lib/processors/self-hosted"), + }, + environments: { + environment: require("../lib/environments/self-hosted"), + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/processors/self-hosted.js b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/processors/self-hosted.js new file mode 100644 index 0000000000..02201a329b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/processors/self-hosted.js @@ -0,0 +1,129 @@ +/** + * @fileoverview Remove macros from SpiderMonkey's self-hosted JS. + * + * 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/. + */ + +"use strict"; + +const path = require("path"); +const fs = require("fs"); + +const selfHostedRegex = /js\/src\/builtin\/.*?\.js$/; +const macroRegex = + /\s*\#(if|ifdef|else|elif|endif|include|define|undef|error).*/; + +function isSelfHostedFile(filename) { + if (path.win32) { + filename = filename.split(path.sep).join("/"); + } + return selfHostedRegex.test(filename); +} + +function tryReadFile(filePath) { + if (!path.isAbsolute(filePath)) { + return ""; + } + if (!fs.existsSync(filePath)) { + // Safely handle the case when the file wasn't found, because throwing + // errors can lead to confusing result when used with ESLint. + return ""; + } + return fs.readFileSync(filePath, "utf-8"); +} + +// Adjust the range of fixes to match the original source code. +function createFix(lines, message) { + let { line, column, fix } = message; + + // Line and column are 1-based. Make sure we got a valid input. + if (line <= 0 || column <= 0) { + return null; + } + + // Reject to create a fix when the line is out of range for some reason. + if (line > lines.length) { + return null; + } + + // Find the absolute start position of the line in the original file. + let startOfLine = 0; + for (let i = 0; i < line - 1; ++i) { + // Add the length of the line, including its line separator. + startOfLine += lines[i].length + "\n".length; + } + + // Add the 1-based column to the start of line to get the start position. + let start = startOfLine + (column - 1); + + // Add the fix range to get the end position. + let end = start + (fix.range[1] - fix.range[0]); + + // And finally return the new fix object. + return { text: fix.text, range: [start, end] }; +} + +module.exports = { + preprocess(text, filename) { + if (!isSelfHostedFile(filename)) { + return [text]; + } + + let lines = text.split(/\n/); + for (let i = 0; i < lines.length; i++) { + if (!macroRegex.test(lines[i])) { + // No macro here, nothing to do. + continue; + } + + for (; i < lines.length; i++) { + // The macro isn't correctly indented, so we need to instruct + // prettier to ignore them. + lines[i] = "// prettier-ignore -- " + lines[i]; + + // If the line ends with a backslash (\), the next line + // is also part of part of the macro. + if (!lines[i].endsWith("\\")) { + break; + } + } + } + + return [lines.join("\n")]; + }, + + postprocess(messages, filename) { + // Don't attempt to create fixes for any non-selfhosted files. + if (!isSelfHostedFile(filename)) { + return [].concat(...messages); + } + + let lines = null; + + let result = []; + for (let message of messages.flat()) { + if (message.fix) { + if (lines === null) { + lines = tryReadFile(filename).split(/\n/); + } + + let fix = createFix(lines, message); + if (fix) { + message.fix = fix; + } else { + // We couldn't create a fix, so we better remove the passed in fix, + // because its range points to the preprocessor output, but the post- + // processor must translate it into a range of the original source. + delete message.fix; + } + } + + result.push(message); + } + return result; + }, + + supportsAutofix: true, +}; diff --git a/tools/lint/eslint/eslint-plugin-spidermonkey-js/package.json b/tools/lint/eslint/eslint-plugin-spidermonkey-js/package.json new file mode 100644 index 0000000000..7d96b5b1c4 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-spidermonkey-js/package.json @@ -0,0 +1,28 @@ +{ + "name": "eslint-plugin-spidermonkey-js", + "version": "0.1.1", + "description": "A collection of rules that help enforce JavaScript coding standard in the Mozilla SpiderMonkey project.", + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin", + "mozilla", + "spidermonkey" + ], + "bugs": { + "url": "https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=Lint" + }, + "homepage": "http://firefox-source-docs.mozilla.org/tools/lint/linters/eslint-plugin-spidermonkey-js.html", + "repository": { + "type": "hg", + "url": "https://hg.mozilla.org/mozilla-central/" + }, + "author": "Mozilla", + "main": "lib/index.js", + "dependencies": {}, + "devDependencies": {}, + "engines": { + "node": ">=6.9.1" + }, + "license": "MPL-2.0" +} diff --git a/tools/lint/eslint/manifest.tt b/tools/lint/eslint/manifest.tt new file mode 100644 index 0000000000..c1ec658b0e --- /dev/null +++ b/tools/lint/eslint/manifest.tt @@ -0,0 +1,10 @@ +[ + { + "filename": "eslint.tar.gz", + "size": 22086762, + "algorithm": "sha512", + "digest": "abf5caa29669e1f8f889459b7af074f2744c7ba7e51901a1b3ad3e504c3f331d360616a0d9c1f0114034dce33874ca7ed3971a8ae7f38f21ae2a6b4514a0e5eb", + "unpack": true, + "visibility": "public" + } +]
\ No newline at end of file diff --git a/tools/lint/eslint/setup_helper.py b/tools/lint/eslint/setup_helper.py new file mode 100644 index 0000000000..2f2074d2cf --- /dev/null +++ b/tools/lint/eslint/setup_helper.py @@ -0,0 +1,423 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 platform +import re +import subprocess +import sys +from filecmp import dircmp + +from mozbuild.nodeutil import ( + NODE_MIN_VERSION, + NPM_MIN_VERSION, + find_node_executable, + find_npm_executable, +) +from mozfile.mozfile import remove as mozfileremove +from packaging.version import Version + +NODE_MACHING_VERSION_NOT_FOUND_MESSAGE = """ +Could not find Node.js executable later than %s. + +Executing `mach bootstrap --no-system-changes` should +install a compatible version in ~/.mozbuild on most platforms. +""".strip() + +NPM_MACHING_VERSION_NOT_FOUND_MESSAGE = """ +Could not find npm executable later than %s. + +Executing `mach bootstrap --no-system-changes` should +install a compatible version in ~/.mozbuild on most platforms. +""".strip() + +NODE_NOT_FOUND_MESSAGE = """ +nodejs is either not installed or is installed to a non-standard path. + +Executing `mach bootstrap --no-system-changes` should +install a compatible version in ~/.mozbuild on most platforms. +""".strip() + +NPM_NOT_FOUND_MESSAGE = """ +Node Package Manager (npm) is either not installed or installed to a +non-standard path. + +Executing `mach bootstrap --no-system-changes` should +install a compatible version in ~/.mozbuild on most platforms. +""".strip() + + +VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") +CARET_VERSION_RANGE_RE = re.compile(r"^\^((\d+)\.\d+\.\d+)$") + +project_root = None + + +def eslint_maybe_setup(): + """Setup ESLint only if it is needed.""" + has_issues, needs_clobber = eslint_module_needs_setup() + + if has_issues: + eslint_setup(needs_clobber) + + +def eslint_setup(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. + """ + package_setup(get_project_root(), "eslint", should_clobber=should_clobber) + + +def remove_directory(path): + print("Clobbering %s..." % path) + if sys.platform.startswith("win") and have_winrm(): + process = subprocess.Popen(["winrm", "-rf", path]) + process.wait() + else: + mozfileremove(path) + + +def package_setup( + package_root, + package_name, + should_update=False, + should_clobber=False, + no_optional=False, +): + """Ensure `package_name` at `package_root` is installed. + + When `should_update` is true, clobber, install, and produce a new + "package-lock.json" file. + + This populates `package_root/node_modules`. + + """ + orig_project_root = get_project_root() + orig_cwd = os.getcwd() + + if should_update: + should_clobber = True + + try: + set_project_root(package_root) + sys.path.append(os.path.dirname(__file__)) + + # npm sometimes fails to respect cwd when it is run using check_call so + # we manually switch folders here instead. + project_root = get_project_root() + os.chdir(project_root) + + if should_clobber: + remove_directory(os.path.join(project_root, "node_modules")) + + # 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" + ) + ) + + npm_path, _ = find_npm_executable() + if not npm_path: + return 1 + + node_path, _ = find_node_executable() + if not node_path: + return 1 + + extra_parameters = ["--loglevel=error"] + + if no_optional: + extra_parameters.append("--no-optional") + + package_lock_json_path = os.path.join(get_project_root(), "package-lock.json") + + if should_update: + cmd = [npm_path, "install"] + mozfileremove(package_lock_json_path) + else: + cmd = [npm_path, "ci"] + + # On non-Windows, ensure npm is called via node, as node may not be in the + # path. + if platform.system() != "Windows": + cmd.insert(0, node_path) + + cmd.extend(extra_parameters) + + # Ensure that bare `node` and `npm` in scripts, including post-install scripts, finds the + # binary we're invoking with. Without this, it's easy for compiled extensions to get + # mismatched versions of the Node.js extension API. + path = os.environ.get("PATH", "").split(os.pathsep) + node_dir = os.path.dirname(node_path) + if node_dir not in path: + path = [node_dir] + path + + print('Installing %s for mach using "%s"...' % (package_name, " ".join(cmd))) + result = call_process( + package_name, cmd, append_env={"PATH": os.pathsep.join(path)} + ) + + if not result: + return 1 + + bin_path = os.path.join( + get_project_root(), "node_modules", ".bin", package_name + ) + + print("\n%s installed successfully!" % package_name) + print("\nNOTE: Your local %s binary is at %s\n" % (package_name, bin_path)) + + finally: + set_project_root(orig_project_root) + os.chdir(orig_cwd) + + +def call_process(name, cmd, cwd=None, append_env={}): + env = dict(os.environ) + env.update(append_env) + + try: + with open(os.devnull, "w") as fnull: + subprocess.check_call(cmd, cwd=cwd, stdout=fnull, env=env) + except subprocess.CalledProcessError: + if cwd: + print("\nError installing %s in the %s folder, aborting." % (name, cwd)) + else: + print("\nError installing %s, aborting." % name) + + return False + + return True + + +def expected_eslint_modules(): + # Read the expected version of ESLint and external modules + expected_modules_path = os.path.join(get_project_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", {})) + + # 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(): + has_issues = False + needs_clobber = False + node_modules_path = os.path.join(get_project_root(), "node_modules") + + for name, expected_data in expected_eslint_modules().items(): + # expected_eslint_modules returns 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("%s v%s needs to be installed locally." % (name, version_range)) + has_issues = True + continue + data = json.load(open(path, encoding="utf-8")) + + if version_range.startswith("file:"): + # We don't need to check local file installations for versions, as + # these are symlinked, so we'll always pick up the latest. + 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("%s v%s should be v%s." % (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("mach eslint doesn't understand module version %s" % 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("%d.0.0" % (range_major + 1)) + + return range_min <= version < range_max + + return False + + +def get_possible_node_paths_win(): + """ + Return possible nodejs paths on Windows. + """ + if platform.system() != "Windows": + return [] + + return list( + { + "%s\\nodejs" % os.environ.get("SystemDrive"), + os.path.join(os.environ.get("ProgramFiles"), "nodejs"), + os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"), + os.path.join(os.environ.get("PROGRAMFILES"), "nodejs"), + } + ) + + +def get_version(path): + try: + version_str = subprocess.check_output( + [path, "--version"], stderr=subprocess.STDOUT, universal_newlines=True + ) + return version_str + except (subprocess.CalledProcessError, OSError): + return None + + +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. + """ + global project_root + + if root: + 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: + 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. + """ + global project_root + + if not project_root: + set_project_root() + + return project_root + + +def get_eslint_module_path(): + return os.path.join(get_project_root(), "tools", "lint", "eslint") + + +def check_node_executables_valid(): + node_path, version = find_node_executable() + if not node_path: + print(NODE_NOT_FOUND_MESSAGE) + return False + if not version: + print(NODE_MACHING_VERSION_NOT_FOUND_MESSAGE % NODE_MIN_VERSION) + return False + + npm_path, version = find_npm_executable() + if not npm_path: + print(NPM_NOT_FOUND_MESSAGE) + return False + if not version: + print(NPM_MACHING_VERSION_NOT_FOUND_MESSAGE % NPM_MIN_VERSION) + return False + + return True + + +def have_winrm(): + # `winrm -h` should print 'winrm version ...' and exit 1 + try: + p = subprocess.Popen( + ["winrm.exe", "-h"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + return p.wait() == 1 and p.stdout.read().startswith("winrm") + except Exception: + return False diff --git a/tools/lint/eslint/update.sh b/tools/lint/eslint/update.sh new file mode 100755 index 0000000000..025f747135 --- /dev/null +++ b/tools/lint/eslint/update.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# Script to regenerate the npm packages used for ESLint by the builders. +# Requires + +# Force the scripts working directory to be projdir/tools/lint/eslint. +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd $DIR + +if [ -z "$TASKCLUSTER_ACCESS_TOKEN" -o -z "$TASKCLUSTER_CLIENT_ID" -o -z "$TASKCLUSTER_ROOT_URL" ]; then + echo "Please ensure you have run the taskcluster shell correctly to set" + echo "the TASKCLUSTER_ACCESS_TOKEN, TASKCLUSTER_CLIENT_ID and" + echo "TASKCLUSTER_ROOT_URL environment variables." + echo "See https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint/enabling-rules.html" + exit 1; +fi + +echo "" +echo "Removing node_modules and package-lock.json..." +# Move to the top-level directory. +cd ../../../ +rm -rf node_modules/ +rm -rf tools/lint/eslint/eslint-plugin-mozilla/node_modules +rm package-lock.json + +echo "Installing eslint and external plugins..." +# ESLint and all _external_ plugins are listed in this directory's package.json, +# so a regular `npm install` will install them at the specified versions. +# The in-tree eslint-plugin-mozilla is kept out of this tooltool archive on +# purpose so that it can be changed by any developer without requiring tooltool +# access to make changes. +./mach npm install + +echo "Creating eslint.tar.gz..." +tar cvz --exclude=eslint-plugin-mozilla --exclude=eslint-plugin-spidermonkey-js -f eslint.tar.gz node_modules + +echo "Adding eslint.tar.gz to tooltool..." +rm tools/lint/eslint/manifest.tt +./python/mozbuild/mozbuild/action/tooltool.py add --visibility public --unpack eslint.tar.gz + +echo "Uploading eslint.tar.gz to tooltool..." +./python/mozbuild/mozbuild/action/tooltool.py upload --message "node_modules folder update for tools/lint/eslint" + +echo "Cleaning up..." +mv manifest.tt tools/lint/eslint/manifest.tt +rm eslint.tar.gz + +cd $DIR + +echo "" +echo "Update complete, please commit and check in your changes." diff --git a/tools/lint/file-perm.yml b/tools/lint/file-perm.yml new file mode 100644 index 0000000000..566472e7fb --- /dev/null +++ b/tools/lint/file-perm.yml @@ -0,0 +1,56 @@ +--- +file-perm: + description: File permission check + include: + - . + extensions: + - .build + - .c + - .cc + - .cpp + - .flac + - .h + - .html + - .idl + - .js + - .jsm + - .json + - .jsx + - .m + - .m4s + - .md + - .mjs + - .mm + - .mn + - .mozbuild + - .mp4 + - .png + - .rs + - .rst + - .svg + - .toml + - .ttf + - .wasm + - .webidl + - .xhtml + - .xml + - .yaml + - .yml + support-files: + - 'tools/lint/file-perm/**' + type: external + payload: file-perm:lint + +maybe-shebang-file-perm: + description: "File permission check for files that might have `#!` header." + include: + - . + allow-shebang: true + extensions: + - .js + - .py + - .sh + support-files: + - 'tools/lint/file-perm/**' + type: external + payload: file-perm:lint diff --git a/tools/lint/file-perm/__init__.py b/tools/lint/file-perm/__init__.py new file mode 100644 index 0000000000..c5f31c008c --- /dev/null +++ b/tools/lint/file-perm/__init__.py @@ -0,0 +1,45 @@ +# 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 os +import platform + +from mozlint import result +from mozlint.pathutils import expand_exclusions + + +def lint(paths, config, fix=None, **lintargs): + results = [] + fixed = 0 + + if platform.system() == "Windows": + # Windows doesn't have permissions in files + # Exit now + return {"results": results, "fixed": fixed} + + files = list(expand_exclusions(paths, config, lintargs["root"])) + for f in files: + if os.access(f, os.X_OK): + if config.get("allow-shebang"): + with open(f, "r+") as content: + # Some source files have +x permissions + line = content.readline() + if line.startswith("#!"): + # Check if the file doesn't start with a shebang + # if it does, not a warning + continue + + if fix: + # We want to fix it, do it and leave + os.chmod(f, 0o644) + fixed += 1 + continue + + res = { + "path": f, + "message": "Execution permissions on a source file", + "level": "error", + } + results.append(result.from_config(config, **res)) + return {"results": results, "fixed": fixed} diff --git a/tools/lint/file-whitespace.yml b/tools/lint/file-whitespace.yml new file mode 100644 index 0000000000..91a8ed2103 --- /dev/null +++ b/tools/lint/file-whitespace.yml @@ -0,0 +1,176 @@ +--- +file-whitespace: + description: File content sanity check + include: + - . + - tools/lint/python/black_requirements.txt + - tools/lint/python/ruff_requirements.txt + - tools/lint/rst/requirements.txt + - tools/lint/tox/tox_requirements.txt + - tools/lint/spell/codespell_requirements.txt + exclude: + - accessible/tests/crashtests + - accessible/tests/mochitest + - browser/locales/en-US/chrome/browser/uiDensity.properties + - build/pgo/blueprint + - build/pgo/js-input + - devtools/client/debugger/test + - devtools/client/inspector/markup/test + - devtools/client/inspector/rules/test + - devtools/client/inspector/test + # Excluded because of python json output. + - testing/talos/talos/unittests/test_talosconfig_browser_config.json + - testing/talos/talos/unittests/test_talosconfig_test_config.json + # Excluded because tests were failing unexpectedly + - devtools/client/styleeditor/test/sync_with_csp.css + - devtools/client/webconsole/test/browser/test-message-categories-css-parser.css + - devtools/shared/webconsole/test/chrome/test_object_actor_native_getters.html + - docshell/base/crashtests + - docshell/test + - dom/base/crashtests + - dom/base/test + - dom/canvas/crashtests + - dom/canvas/test + - dom/events/crashtests + - dom/events/test + - dom/file/tests/crashtests/1748342.html + - dom/file/tests/file_mozfiledataurl_inner.html + - dom/html/crashtests + - dom/html/reftests + - dom/html/test + - dom/jsurl/crashtests/344996-1.xhtml + - dom/jsurl/test + - dom/media/mediasource/test/crashtests/926665.html + - dom/media/test + - dom/media/tests + - dom/media/webaudio/test + - dom/media/webrtc/transport/nricectx.cpp + - dom/media/webspeech/synth/test + - dom/plugins/test + - dom/smil/crashtests + - dom/smil/test + - dom/security/test + - dom/svg/crashtests + - dom/svg/test + - dom/webauthn/winwebauthn/webauthn.h + - dom/tests/mochitest + - dom/xml/crashtests + - dom/xml/test + - dom/xslt/crashtests + - dom/xslt/tests + - dom/xul/crashtests + - dom/xul/test + - editor/composer/test + - editor/composer/crashtests/removing-editable-xslt.html + - editor/libeditor/tests + - editor/libeditor/crashtests + - editor/reftests + - extensions/universalchardet + - gfx/tests/crashtests + - gfx/vr/nsFxrCommandLineHandler.cpp + - image/test/crashtests + - image/test/mochitest + - image/test/reftest + - intl/components/gtest/TestNumberRangeFormat.cpp + - intl/icu_segmenter_data + - intl/lwbrk/crashtests + - intl/lwbrk/rulebrk.c + - intl/uconv/crashtests + - intl/uconv/tests + - intl/strres/tests/unit/397093.properties + - intl/strres/tests/unit/strres.properties + - js/xpconnect/crashtests + - js/xpconnect/tests + - js/src/builtin/intl/IcuMemoryUsage.java + - js/src/frontend/BytecodeEmitter.cpp + - js/src/frontend/SharedContext.h + - layout/base/crashtests + - layout/base/tests + - layout/forms/crashtests + - layout/forms/test + - layout/generic/crashtests + - layout/generic/test + - layout/inspector/tests + - layout/painting/crashtests/1405881-1.html + - layout/painting/crashtests/1407470-1.html + - layout/reftests + - layout/style/crashtests + - layout/style/test + - layout/svg/crashtests + - layout/tables/test/test_bug337124.html + - layout/tables/crashtests + - layout/xul/crashtests + - layout/xul/reftest + - layout/xul/test + - layout/xul/tree + - modules/libjar/zipwriter/test/unit/data/test_bug399727.html + - netwerk/test/crashtests + - netwerk/test/mochitests/test1.css + - netwerk/test/mochitests/test2.css + - parser/htmlparser/tests + - parser/html/java/named-character-references.html + - parser/html/javasrc + - testing/perfdocs/generated/ + - testing/talos/talos/pageloader/chrome/pageloader.xhtml + - testing/talos/talos/tests + - testing/web-platform/mozilla/tests/editor + - testing/web-platform/mozilla/tests/focus + - testing/web-platform/tests + - testing/web-platform/tests/conformance-checkers + - testing/web-platform/tests/content-security-policy + - testing/web-platform/tests/html + - testing/web-platform/tests/tools/lint/tests/dummy/broken.html + - testing/web-platform/tests/tools/lint/tests/dummy/broken_ignored.html + - toolkit/components/passwordmgr/test/mochitest/ + - toolkit/content/tests/chrome + - toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties + - tools/jprof/README.html + - tools/lint/eslint + - tools/lint/test/test_clang_format.py + - view/crashtests + - widget/cocoa/crashtests + - widget/nsFilePickerProxy.cpp + - widget/tests + - widget/windows/tests/TestUrisToValidate.h + - xpcom/reflect/xptcall/porting.html + - xpcom/tests/test.properties + - xpcom/tests/unit/data/bug121341.properties + # Excluded below files because tests were failing unexpectedly + - dom/bindings/test/test_barewordGetsWindow.html + - devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css + - devtools/client/styleeditor/test/sourcemap-css/contained.css + - devtools/client/styleeditor/test/sourcemap-css/test-stylus.css + - dom/bindings/test/file_barewordGetsWindow_frame1.html + - dom/bindings/test/file_barewordGetsWindow_frame2.html + - devtools/perfdocs/index.rst + - python/mozperftest/perfdocs/running.rst + - python/mozperftest/perfdocs/vision.rst + - python/mozperftest/perfdocs/writing.rst + extensions: + - .c + - .cc + - .cpp + - .css + - .dtd + - .idl + - .ftl + - .h + - .html + - .java + - .json + - .kt + - .md + - .mn + - .properties + - .py + - .rs + - .rst + - .toml + - .webidl + - .yaml + - .yml + - .xhtml + support-files: + - 'tools/lint/file-whitespace/**' + type: external + payload: file-whitespace:lint diff --git a/tools/lint/file-whitespace/__init__.py b/tools/lint/file-whitespace/__init__.py new file mode 100644 index 0000000000..ab7a6944b7 --- /dev/null +++ b/tools/lint/file-whitespace/__init__.py @@ -0,0 +1,124 @@ +# 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/. + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +results = [] + + +def lint(paths, config, fix=None, **lintargs): + files = list(expand_exclusions(paths, config, lintargs["root"])) + log = lintargs["log"] + fixed = 0 + + for f in files: + with open(f, "rb") as open_file: + hasFix = False + content_to_write = [] + + try: + lines = open_file.readlines() + # Check for Empty spaces or newline character at end of file + if lines[:].__len__() != 0 and lines[-1:][0].strip().__len__() == 0: + # return file pointer to first + open_file.seek(0) + if fix: + fixed += 1 + # fix Empty lines at end of file + for i, line in reversed(list(enumerate(open_file))): + # determine if line is empty + if line.strip() != b"": + with open(f, "wb") as write_file: + # determine if file's last line have \n, if not then add a \n + if not lines[i].endswith(b"\n"): + lines[i] = lines[i] + b"\n" + # write content to file + for e in lines[: i + 1]: + write_file.write(e) + # end the loop + break + else: + res = { + "path": f, + "message": "Empty Lines at end of file", + "level": "error", + "lineno": open_file.readlines()[:].__len__(), + } + results.append(result.from_config(config, **res)) + except Exception as ex: + log.debug("Error: " + str(ex) + ", in file: " + f) + + # return file pointer to first + open_file.seek(0) + + lines = open_file.readlines() + # Detect missing newline at the end of the file + if lines[:].__len__() != 0 and not lines[-1].endswith(b"\n"): + if fix: + fixed += 1 + with open(f, "wb") as write_file: + # add a newline character at end of file + lines[-1] = lines[-1] + b"\n" + # write content to file + for e in lines: + write_file.write(e) + else: + res = { + "path": f, + "message": "File does not end with newline character", + "level": "error", + "lineno": lines.__len__(), + } + results.append(result.from_config(config, **res)) + + # return file pointer to first + open_file.seek(0) + + for i, line in enumerate(open_file): + if line.endswith(b" \n"): + # We found a trailing whitespace + if fix: + # We want to fix it, strip the trailing spaces + content_to_write.append(line.rstrip() + b"\n") + fixed += 1 + hasFix = True + else: + res = { + "path": f, + "message": "Trailing whitespace", + "level": "error", + "lineno": i + 1, + } + results.append(result.from_config(config, **res)) + else: + if fix: + content_to_write.append(line) + if hasFix: + # Only update the file when we found a change to make + with open(f, "wb") as open_file_to_write: + open_file_to_write.write(b"".join(content_to_write)) + + # We are still using the same fp, let's return to the first + # line + open_file.seek(0) + # Open it as once as we just need to know if there is + # at least one \r\n + content = open_file.read() + + if b"\r\n" in content: + if fix: + fixed += 1 + content = content.replace(b"\r\n", b"\n") + with open(f, "wb") as open_file_to_write: + open_file_to_write.write(content) + else: + res = { + "path": f, + "message": "Windows line return", + "level": "error", + } + results.append(result.from_config(config, **res)) + + return {"results": results, "fixed": fixed} diff --git a/tools/lint/fluent-lint.yml b/tools/lint/fluent-lint.yml new file mode 100644 index 0000000000..8d8a5d956b --- /dev/null +++ b/tools/lint/fluent-lint.yml @@ -0,0 +1,13 @@ +--- +fluent-lint: + description: Linter for Fluent files + exclude: + - dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl + extensions: ['ftl'] + support-files: + - 'tools/lint/fluent-lint**' + brand-files: + - 'browser/branding/official/locales/en-US/brand.ftl' + - 'toolkit/locales/en-US/toolkit/branding/brandings.ftl' + type: external + payload: fluent-lint:lint diff --git a/tools/lint/fluent-lint/__init__.py b/tools/lint/fluent-lint/__init__.py new file mode 100644 index 0000000000..3b4c3c570b --- /dev/null +++ b/tools/lint/fluent-lint/__init__.py @@ -0,0 +1,531 @@ +# 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 bisect +import os +import re +from html.parser import HTMLParser + +import mozpack.path as mozpath +import yaml +from fluent.syntax import ast, parse, visitor +from mozlint import result +from mozlint.pathutils import expand_exclusions + + +class TextElementHTMLParser(HTMLParser): + """HTML Parser for TextElement. + + TextElements may contain embedded html tags, which can include + quotes in attributes. We only want to check the actual text. + """ + + def __init__(self): + super().__init__() + self.extracted_text = [] + + def handle_data(self, data): + self.extracted_text.append(data) + + +class Linter(visitor.Visitor): + """Fluent linter implementation. + + This subclasses the Fluent AST visitor. Methods are called corresponding + to each type of node in the Fluent AST. It is possible to control + whether a node is recursed into by calling the generic_visit method on + the superclass. + + See the documentation here: + https://www.projectfluent.org/python-fluent/fluent.syntax/stable/usage.html + """ + + def __init__( + self, path, config, exclusions, contents, offsets_and_lines, brand_names=[] + ): + super().__init__() + self.path = path + self.config = config + self.exclusions = exclusions + self.contents = contents + self.offsets_and_lines = offsets_and_lines + + self.results = [] + self.identifier_re = re.compile(r"[a-z0-9-]+") + self.apostrophe_re = re.compile(r"\w'") + self.incorrect_apostrophe_re = re.compile(r"\w\u2018\w") + self.single_quote_re = re.compile(r"'(.+)'") + self.double_quote_re = re.compile(r"\".+\"") + self.ellipsis_re = re.compile(r"\.\.\.") + + self.brand_names = brand_names + self.minimum_id_length = 9 + + self.state = { + # The resource comment should be at the top of the page after the license. + "node_can_be_resource_comment": True, + # Group comments must be followed by a message. Two group comments are not + # allowed in a row. + "can_have_group_comment": True, + # Comment bound to the current message + "comment": "", + # The current group comment + "group_comment": "", + # Variables in the current message + "variables": [], + } + + attributes = [ + "label", + "value", + "accesskey", + "alt", + "title", + "tooltiptext", + "placeholder", + "aria-label", + "aria-description", + "aria-valuetext", + "style", + # For XUL key/command setup. + "key", + "keycode", + # For download filenames: + "download", + # Used in the Firefox prefs + "searchkeywords", + # Used by search-textbox.js + "searchbuttonlabel", + # Used in toolbar customization. + "toolbarname", + # Used in moz-message-bar. + "message", + # Used in dialogs (should be moved to using fluent IDs though) + "buttonlabelaccept", + "buttonaccesskeyaccept", + "buttonlabelcancel", + "buttonaccesskeycancel", + "buttonlabelextra2", + "buttonaccesskeyextra2", + # Used in app menu notifications (should be moved to use fluent IDs) + "buttonlabel", + "buttonaccesskey", + "secondarybuttonlabel", + "secondarybuttonaccesskey", + # Commonly used in Lit-based web components + "heading", + "description", + ] + self.known_attribute_list = [a.lower() for a in attributes] + + # Set this to true to debug print the root node's json. This is useful for + # writing new lint rules, or debugging existing ones. + self.debug_print_json = False + + def generic_visit(self, node): + node_name = type(node).__name__ + self.state["node_can_be_resource_comment"] = self.state[ + "node_can_be_resource_comment" + ] and ( + # This is the root node. + node_name == "Resource" + # Empty space is allowed. + or node_name == "Span" + # Comments are allowed + or node_name == "Comment" + ) + + if self.debug_print_json: + import json + + print(json.dumps(node.to_json(), indent=2)) + # Only debug print the root node. + self.debug_print_json = False + + super(Linter, self).generic_visit(node) + + def visit_Attribute(self, node): + # Only visit values for Attribute nodes, the identifier comes from dom. + super().generic_visit(node.value) + + def visit_FunctionReference(self, node): + # We don't recurse into function references, the identifiers there are + # allowed to be free form. + pass + + def visit_Message(self, node): + # There must be at least one message or term between group comments. + self.state["can_have_group_comment"] = True + self.last_message_id = node.id.name + + super().generic_visit(node) + + # Do this here instead as visit_Attribute doesn't have access to the + # message's comment. + for attr in node.attributes: + if not attr.id.name.lower() in self.known_attribute_list: + comment = self.state["comment"] + self.state["group_comment"] + if not f".{attr.id.name}" in comment: + self.add_error( + attr, + "VA01", + "Use attributes designed for localized content directly." + " If script-based processing is necessary, add a comment" + f" explaining why. The linter didn't recognize: .{attr.id.name}", + "warning", + ) + + # Check if variables are referenced in comments + if self.state["variables"]: + comments = self.state["comment"] + self.state["group_comment"] + missing_references = [ + v for v in self.state["variables"] if f"${v}" not in comments + ] + if missing_references: + self.add_error( + node, + "VC01", + "Messages including variables should have a comment " + "explaining what will replace the variable. " + "Missing references: " + + ", ".join([f"${m}" for m in missing_references]), + ) + + # Reset current comment and variable references after reading the + # message. + self.state["comment"] = "" + self.state["variables"] = [] + + def visit_Term(self, node): + # There must be at least one message or term between group comments. + self.state["can_have_group_comment"] = True + self.last_message_id = None + + super().generic_visit(node) + + # Reset current comment and variable references after reading the term. + self.state["comment"] = "" + self.state["variables"] = [] + + def visit_MessageReference(self, node): + # We don't recurse into message references, the identifiers are either + # checked elsewhere or are attributes and come from DOM. + pass + + def visit_Identifier(self, node): + if ( + self.path not in self.exclusions["ID01"]["files"] + and node.name not in self.exclusions["ID01"]["messages"] + and not self.identifier_re.fullmatch(node.name) + ): + self.add_error( + node, + "ID01", + "Identifiers may only contain lowercase characters and -", + ) + if ( + len(node.name) < self.minimum_id_length + and self.path not in self.exclusions["ID02"]["files"] + and node.name not in self.exclusions["ID02"]["messages"] + ): + self.add_error( + node, + "ID02", + f"Identifiers must be at least {self.minimum_id_length} characters long", + ) + + def visit_TextElement(self, node): + parser = TextElementHTMLParser() + parser.feed(node.value) + for text in parser.extracted_text: + # To check for apostrophes, first remove pairs of straight quotes + # used as delimiters. + cleaned_str = re.sub(self.single_quote_re, "\1", node.value) + if self.apostrophe_re.search(cleaned_str): + self.add_error( + node, + "TE01", + "Strings with apostrophes should use foo\u2019s instead of foo's.", + ) + if self.incorrect_apostrophe_re.search(text): + self.add_error( + node, + "TE02", + "Strings with apostrophes should use foo\u2019s instead of foo\u2018s.", + ) + if self.single_quote_re.search(text): + self.add_error( + node, + "TE03", + "Single-quoted strings should use Unicode \u2018foo\u2019 instead of 'foo'.", + ) + if self.double_quote_re.search(text): + self.add_error( + node, + "TE04", + 'Double-quoted strings should use Unicode \u201cfoo\u201d instead of "foo".', + ) + if self.ellipsis_re.search(text): + self.add_error( + node, + "TE05", + "Strings with an ellipsis should use the Unicode \u2026 character" + " instead of three periods", + ) + + # If part of a message, check for brand names + if ( + self.last_message_id is not None + and self.path not in self.exclusions["CO01"]["files"] + and self.last_message_id not in self.exclusions["CO01"]["messages"] + ): + found_brands = [] + for brand in self.brand_names: + if brand in text: + found_brands.append(brand) + if found_brands: + self.add_error( + node, + "CO01", + "Strings should use the corresponding terms instead of" + f" hard-coded brand names ({', '.join(found_brands)})", + ) + + def visit_ResourceComment(self, node): + # This node is a comment with: "###" + if not self.state["node_can_be_resource_comment"]: + self.add_error( + node, + "RC01", + "Resource comments (###) should be placed at the top of the file, just " + "after the license header. There should only be one resource comment " + "per file.", + ) + return + + lines_after = get_newlines_count_after(node.span, self.contents) + lines_before = get_newlines_count_before(node.span, self.contents) + + if node.span.end == len(self.contents) - 1: + # This file only contains a resource comment. + return + + if lines_after != 2: + self.add_error( + node, + "RC02", + "Resource comments (###) should be followed by one empty line.", + ) + return + + if lines_before != 2: + self.add_error( + node, + "RC03", + "Resource comments (###) should have one empty line above them.", + ) + return + + def visit_SelectExpression(self, node): + # We only want to visit the variant values, the identifiers in selectors + # and keys are allowed to be free form. + for variant in node.variants: + super().generic_visit(variant.value) + + # Store the variable used for the SelectExpression, excluding functions + # like PLATFORM() + if ( + type(node.selector) == ast.VariableReference + and node.selector.id.name not in self.state["variables"] + ): + self.state["variables"].append(node.selector.id.name) + + def visit_Comment(self, node): + # This node is a comment with: "#" + + # Store the comment + self.state["comment"] = node.content + + def visit_GroupComment(self, node): + # This node is a comment with: "##" + + # Store the group comment + self.state["group_comment"] = node.content + + if not self.state["can_have_group_comment"]: + self.add_error( + node, + "GC04", + "Group comments (##) must be followed by at least one message " + "or term. Make sure that a single group comment with multiple " + "paragraphs is not separated by whitespace, as it will be " + "interpreted as two different comments.", + ) + return + + self.state["can_have_group_comment"] = False + + lines_after = get_newlines_count_after(node.span, self.contents) + lines_before = get_newlines_count_before(node.span, self.contents) + + if node.span.end == len(self.contents) - 1: + # The group comment is the last thing in the file. + + if node.content == "": + # Empty comments are allowed at the end of the file. + return + + self.add_error( + node, + "GC01", + "Group comments (##) should not be at the end of the file, they should " + "always be above a message. Only an empty group comment is allowed at " + "the end of a file.", + ) + return + + if lines_after != 2: + self.add_error( + node, + "GC02", + "Group comments (##) should be followed by one empty line.", + ) + return + + if lines_before != 2: + self.add_error( + node, + "GC03", + "Group comments (##) should have an empty line before them.", + ) + return + + def visit_VariableReference(self, node): + # Identifiers are allowed to be free form, but need to store them + # for comment checks. + + if node.id.name not in self.state["variables"]: + self.state["variables"].append(node.id.name) + + def add_error(self, node, rule, msg, level=None): + (col, line) = self.span_to_line_and_col(node.span) + res = { + "path": self.path, + "lineno": line, + "column": col, + "rule": rule, + "message": msg, + } + if level: + res["level"] = level + + self.results.append(result.from_config(self.config, **res)) + + def span_to_line_and_col(self, span): + i = bisect.bisect_left(self.offsets_and_lines, (span.start, 0)) + if i > 0: + col = span.start - self.offsets_and_lines[i - 1][0] + else: + col = 1 + span.start + return (col, self.offsets_and_lines[i][1]) + + +def get_offsets_and_lines(contents): + """Return a list consisting of tuples of (offset, line). + + The Fluent AST contains spans of start and end offsets in the file. + This function returns a list of offsets and line numbers so that errors + can be reported using line and column. + """ + line = 1 + result = [] + for m in re.finditer(r"\n", contents): + result.append((m.start(), line)) + line += 1 + return result + + +def get_newlines_count_after(span, contents): + # Determine the number of newlines. + count = 0 + for i in range(span.end, len(contents)): + assert contents[i] != "\r", "This linter does not handle \\r characters." + if contents[i] != "\n": + break + count += 1 + + return count + + +def get_newlines_count_before(span, contents): + # Determine the range of newline characters. + count = 0 + for i in range(span.start - 1, 0, -1): + assert contents[i] != "\r", "This linter does not handle \\r characters." + if contents[i] != "\n": + break + count += 1 + + return count + + +def get_exclusions(root): + with open( + mozpath.join(root, "tools", "lint", "fluent-lint", "exclusions.yml") + ) as f: + exclusions = list(yaml.safe_load_all(f))[0] + for error_type in exclusions: + exclusions[error_type]["files"] = set( + [mozpath.join(root, x) for x in exclusions[error_type]["files"]] + ) + return exclusions + + +def get_branding_list(root, brand_files): + class MessageExtractor(visitor.Visitor): + def __init__(self): + self.brands = [] + self.last_message_id = None + + def visit_Term(self, node): + self.last_message_id = node.id.name + self.generic_visit(node) + + def visit_TextElement(self, node): + if self.last_message_id: + self.brands += [node.value] + self.last_message_id = None + self.generic_visit(node) + + extractor = MessageExtractor() + + for brand_path in brand_files: + brand_file = mozpath.join(root, brand_path) + if os.path.exists(brand_file): + with open(brand_file, encoding="utf-8") as f: + messages = parse(f.read()) + extractor.visit(messages) + + return list(set(extractor.brands)) + + +def lint(paths, config, fix=None, **lintargs): + root = lintargs["root"] + files = list(expand_exclusions(paths, config, root)) + exclusions = get_exclusions(root) + brand_files = config.get("brand-files") + brand_names = get_branding_list(root, brand_files) + results = [] + for path in files: + contents = open(path, "r", encoding="utf-8").read() + linter = Linter( + path, + config, + exclusions, + contents, + get_offsets_and_lines(contents), + brand_names, + ) + linter.visit(parse(contents)) + results.extend(linter.results) + return results diff --git a/tools/lint/fluent-lint/exclusions.yml b/tools/lint/fluent-lint/exclusions.yml new file mode 100644 index 0000000000..549eb395c2 --- /dev/null +++ b/tools/lint/fluent-lint/exclusions.yml @@ -0,0 +1,186 @@ +# 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/. + +# Warning: Only exclusions for identifiers (ID01) are currently allowed. +--- +# Only add exceptions to this file if the ID is generated programmatically and +# can't easily be changed to follow the naming convention. +# Only lowercase letters and hyphens should be used in Fluent IDs. +ID01: + messages: + - trademarkInfo + - crashed-include-URL-2 + - blocklist-item-moz-std-listName + - blocklist-item-moz-full-listName + - shortcuts-browserAction2 + - shortcuts-pageAction + - shortcuts-sidebarAction + - about-networking-originAttributesSuffix + - size-KB + - size-MB + - size-GB + - state-dd-Disabled + - state-dd-Disabled-block-list-state + - memory-unit-B + - memory-unit-KB + - memory-unit-MB + - memory-unit-GB + - memory-unit-TB + - memory-unit-PB + - memory-unit-EB + - enableSafeBrowsing-label + - about-telemetry-show-in-Firefox-json-viewer + - url-classifier-search-listType + # aboutDialog.ftl: Do not add new exceptions for this file, + # new strings should follow the naming convention. + - aboutDialog-title + - releaseNotes-link + - update-checkForUpdatesButton + - update-updateButton + - update-checkingForUpdates + - update-noUpdatesFound + - update-otherInstanceHandlingUpdates + - warningDesc-version + - bottomLinks-license + - bottomLinks-rights + - bottomLinks-privacy + - aboutDialog-version + - aboutDialog-version-nightly + # certError.ftl: These IDs are generated programmatically + # from certificate error codes. + - connectionFailure-title + - deniedPortAccess-title + - dnsNotFound-title + - fileNotFound-title + - fileAccessDenied-title + - captivePortal-title + - malformedURI-title + - netInterrupt-title + - notCached-title + - netOffline-title + - contentEncodingError-title + - unsafeContentType-title + - netReset-title + - netTimeout-title + - unknownProtocolFound-title + - proxyConnectFailure-title + - proxyResolveFailure-title + - redirectLoop-title + - unknownSocketType-title + - nssFailure2-title + - corruptedContentError-title + - sslv3Used-title + - inadequateSecurityError-title + - blockedByPolicy-title + - clockSkewError-title + - networkProtocolError-title + - nssBadCert-title + - nssBadCert-sts-title + files: + # policies-descriptions.ftl: These IDs are generated programmatically + # from policy names. + - browser/locales/en-US/browser/policies/policies-descriptions.ftl + # The webext-perms-description-* IDs are generated programmatically + # from permission names + - toolkit/locales/en-US/toolkit/global/extensionPermissions.ftl +ID02: + messages: + # browser/components/ion/content/ion.ftl + - ion + # browser/locales/en-US/browser/aboutDialog.ftl + - helpus + # browser/locales/en-US/browser/aboutLogins.ftl + - menu + # browser/locales/en-US/browser/pageInfo.ftl + - copy + - perm-tab + # browser/locales/en-US/browser/tabContextMenu.ftl + - pin-tab + # browser/locales/en-US/browser/touchbar/touchbar.ftl + - back + - forward + - reload + - home + - find + - new-tab + - share + # toolkit/locales/en-US/toolkit/about/aboutServiceWorkers.ftl + - scope + - waiting + # toolkit/locales/en-US/toolkit/about/aboutSupport.ftl + # yaml interprets yes and no as booleans if quotes are not present. + - "yes" + - "no" + - unknown + - found + - missing + - gpu-ram + - apz-none + # toolkit/locales/en-US/toolkit/printing/printDialogs.ftl + - portrait + - scale + - print-bg + - hf-blank + - hf-title + - hf-url + - hf-page + files: [] +# Hard-coded brand names like Firefox or Mozilla should be used only in +# specific cases, in all other cases the corresponding terms should be used. +# Check with the localization team for advice. +CO01: + messages: + # browser/branding/official/locales/en-US/brand.ftl + - trademarkInfo + # toolkit/locales/en-US/toolkit/neterror/certError.ftl + - cert-error-mitm-mozilla + - cert-error-mitm-connection + # browser/locales/en-US/browser/appExtensionFields.ftl + - extension-firefox-alpenglow-name + # browser/locales/en-US/browser/browser.ftl + - identity-custom-root + - identity-description-custom-root2 + # browser/locales/en-US/browser/migrationWizard.ftl + - migration-wizard-migrator-display-name-firefox + # browser/locales/en-US/browser/newtab/onboarding.ftl + - mr1-onboarding-welcome-image-caption + - mr2022-onboarding-gratitude-subtitle + - onboarding-gratitude-security-and-privacy-subtitle + # browser/locales/en-US/browser/policies/policies-descriptions.ftl + - policy-DisableFirefoxScreenshots + # browser/locales/en-US/browser/preferences/preferences.ftl + - sync-engine-addons + - sync-mobile-promo + # devtools/client/locales/en-US/aboutdebugging.ftl + - about-debugging-setup-usb-step-enable-debug-firefox2 + - about-debugging-browser-version-too-old-fennec + - about-debugging-browser-version-too-recent + # devtools/client/locales/en-US/application.ftl + - manifest-loaded-devtools-error + # devtools/shared/locales/en-US/webconsole-commands.ftl + - webconsole-commands-usage-block + # toolkit/locales/en-US/toolkit/about/aboutAddons.ftl + - addon-badge-line3 + - recommended-theme-1 + - plugins-openh264-description + # toolkit/locales/en-US/toolkit/about/aboutGlean.ftl + - about-glean-description + # toolkit/locales/en-US/toolkit/about/aboutRights.ftl + - rights-intro-point-1 + - rights-intro-point-2 + # toolkit/locales/en-US/toolkit/about/aboutSupport.ftl + - app-basics-key-mozilla + - virtual-monitor-disp + # toolkit/locales/en-US/toolkit/about/aboutTelemetry.ftl + - about-telemetry-firefox-data-doc + - about-telemetry-telemetry-client-doc + - about-telemetry-telemetry-dashboard + # toolkit/locales/en-US/toolkit/global/extensionPermissions.ftl + - webext-perms-description-management + # toolkit/locales/en-US/toolkit/global/processTypes.ftl + - process-type-privilegedmozilla + files: + - browser/components/ion/content/ion.ftl + - browser/locales/en-US/browser/profile/default-bookmarks.ftl + - toolkit/locales/en-US/toolkit/about/aboutMozilla.ftl diff --git a/tools/lint/hooks.py b/tools/lint/hooks.py new file mode 100755 index 0000000000..92f8565bf7 --- /dev/null +++ b/tools/lint/hooks.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# 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 os +import shutil +import signal +import subprocess +import sys + +here = os.path.dirname(os.path.realpath(__file__)) +topsrcdir = os.path.join(here, os.pardir, os.pardir) + + +def run_process(cmd): + proc = subprocess.Popen(cmd) + + # ignore SIGINT, the mozlint subprocess should exit quickly and gracefully + orig_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + proc.wait() + signal.signal(signal.SIGINT, orig_handler) + return proc.returncode + + +def run_mozlint(hooktype, args): + if isinstance(hooktype, bytes): + hooktype = hooktype.decode("UTF-8", "replace") + + python = shutil.which("python3") + if not python: + print("error: Python 3 not detected on your system! Please install it.") + sys.exit(1) + + cmd = [python, os.path.join(topsrcdir, "mach"), "lint"] + + if "commit" in hooktype: + # don't prevent commits, just display the lint results + run_process(cmd + ["--workdir=staged"]) + return False + elif "push" in hooktype: + return run_process(cmd + ["--outgoing"] + args) + + print("warning: '{}' is not a valid mozlint hooktype".format(hooktype)) + return False + + +def hg(ui, repo, **kwargs): + hooktype = kwargs["hooktype"] + return run_mozlint(hooktype, kwargs.get("pats", [])) + + +def git(): + hooktype = os.path.basename(__file__) + if hooktype == "hooks.py": + hooktype = "pre-push" + return run_mozlint(hooktype, []) + + +if __name__ == "__main__": + sys.exit(git()) diff --git a/tools/lint/hooks_clang_format.py b/tools/lint/hooks_clang_format.py new file mode 100755 index 0000000000..9adb81b7f0 --- /dev/null +++ b/tools/lint/hooks_clang_format.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# 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 os +import subprocess +import sys +from subprocess import CalledProcessError, check_output + +here = os.path.dirname(os.path.realpath(__file__)) +topsrcdir = os.path.join(here, os.pardir, os.pardir) + +EXTRA_PATHS = ( + "python/mach", + "python/mozbuild", + "python/mozversioncontrol", + "testing/mozbase/mozfile", + "third_party/python/jsmin", + "third_party/python/six", +) +sys.path[:0] = [os.path.join(topsrcdir, p) for p in EXTRA_PATHS] + +from mozversioncontrol import InvalidRepoPath, get_repository_object + + +def run_clang_format(hooktype, changedFiles): + try: + vcs = get_repository_object(topsrcdir) + except InvalidRepoPath: + return + + if not changedFiles: + # No files have been touched + return + + # We have also a copy of this list in: + # python/mozbuild/mozbuild/mach_commands.py + # version-control-tools/hgext/clang-format/__init__.py + # release-services/src/staticanalysis/bot/static_analysis_bot/config.py + # Too heavy to import the full class just for this variable + extensions = (".cpp", ".c", ".cc", ".h", ".m", ".mm") + path_list = [] + for filename in sorted(changedFiles): + # Ignore files unsupported in clang-format + if filename.endswith(extensions): + path_list.append(filename) + + if not path_list: + # No files have been touched + return + + arguments = ["clang-format", "-p"] + path_list + # On windows we need this to call the command in a shell, see Bug 1511594 + if os.name == "nt": + clang_format_cmd = [sys.executable, "mach"] + arguments + else: + clang_format_cmd = [os.path.join(topsrcdir, "mach")] + arguments + if "commit" in hooktype: + # don't prevent commits, just display the clang-format results + subprocess.call(clang_format_cmd) + + vcs.add_remove_files(*path_list) + + return False + print("warning: '{}' is not a valid clang-format hooktype".format(hooktype)) + return False + + +def hg(ui, repo, node, **kwargs): + print( + "warning: this hook has been deprecated. Please use the hg extension instead.\n" + "please add 'clang-format = ~/.mozbuild/version-control-tools/hgext/clang-format'" + " to hgrc\n" + "Or run 'mach bootstrap'" + ) + return False + + +def git(): + hooktype = os.path.basename(__file__) + if hooktype == "hooks_clang_format.py": + hooktype = "pre-push" + + try: + changedFiles = check_output( + ["git", "diff", "--staged", "--diff-filter=d", "--name-only", "HEAD"], + text=True, + ).split() + # TODO we should detect if we are in a "add -p" mode and show a warning + return run_clang_format(hooktype, changedFiles) + + except CalledProcessError: + print("Command to retrieve local files failed") + return 1 + + +if __name__ == "__main__": + sys.exit(git()) diff --git a/tools/lint/hooks_js_format.py b/tools/lint/hooks_js_format.py new file mode 100755 index 0000000000..1b0386685e --- /dev/null +++ b/tools/lint/hooks_js_format.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# 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 os +import subprocess +import sys +from subprocess import CalledProcessError, check_output + +here = os.path.dirname(os.path.realpath(__file__)) +topsrcdir = os.path.join(here, os.pardir, os.pardir) + +EXTRA_PATHS = ( + "python/mach", + "python/mozbuild", + "python/mozversioncontrol", + "testing/mozbase/mozfile", + "third_party/python/jsmin", +) +sys.path[:0] = [os.path.join(topsrcdir, p) for p in EXTRA_PATHS] + +from mozversioncontrol import InvalidRepoPath, get_repository_object + + +def run_js_format(hooktype, changedFiles): + try: + vcs = get_repository_object(topsrcdir) + except InvalidRepoPath: + return + + if not changedFiles: + # No files have been touched + return + + extensions = (".js", ".jsx", ".jsm", ".json", ".mjs", "sjs", "html", "xhtml") + path_list = [] + for filename in sorted(changedFiles): + # Ignore files unsupported in eslint and prettier + if filename.endswith(extensions): + path_list.append(filename) + + if not path_list: + # No files have been touched + return + + arguments = ["eslint", "--fix"] + path_list + # On windows we need this to call the command in a shell, see Bug 1511594 + if os.name == "nt": + js_format_cmd = ["sh", "mach"] + arguments + else: + js_format_cmd = [os.path.join(topsrcdir, "mach")] + arguments + if "commit" in hooktype: + # don't prevent commits, just display the eslint and prettier results + subprocess.call(js_format_cmd) + + vcs.add_remove_files(*path_list) + + return False + print("warning: '{}' is not a valid js-format hooktype".format(hooktype)) + return False + + +def git(): + hooktype = os.path.basename(__file__) + if hooktype == "hooks_js_format.py": + hooktype = "pre-push" + + try: + changedFiles = check_output( + ["git", "diff", "--staged", "--diff-filter=d", "--name-only", "HEAD"], + text=True, + ).split() + # TODO we should detect if we are in a "add -p" mode and show a warning + return run_js_format(hooktype, changedFiles) + + except CalledProcessError: + print("Command to retrieve local files failed") + return 1 + + +if __name__ == "__main__": + sys.exit(git()) diff --git a/tools/lint/l10n.yml b/tools/lint/l10n.yml new file mode 100644 index 0000000000..85ad1aaabd --- /dev/null +++ b/tools/lint/l10n.yml @@ -0,0 +1,37 @@ +--- +l10n: + description: Localization linter + # list of include directories of both + # browser and mobile/android l10n.tomls + include: + - browser/branding/official/locales/en-US + - browser/extensions/formautofill/locales/en-US + - browser/extensions/report-site-issue/locales/en-US + - browser/locales/en-US + - devtools/client/locales/en-US + - devtools/shared/locales/en-US + - devtools/startup/locales/en-US + - dom/locales/en-US + - mobile/android/locales/en-US + - netwerk/locales/en-US + - security/manager/locales/en-US + - toolkit/locales/en-US + - tools/lint/l10n.yml + # files not supported by compare-locales, + # and also not relevant to this linter + exclude: + - browser/locales/en-US/firefox-l10n.js + - mobile/android/locales/en-US/mobile-l10n.js + - toolkit/locales/en-US/chrome/global/intl.css + l10n_configs: + - browser/locales/l10n.toml + - mobile/android/locales/l10n.toml + type: external + payload: python.l10n_lint:lint + setup: python.l10n_lint:gecko_strings_setup + support-files: + - '**/l10n.toml' + - 'third_party/python/compare-locales/**' + - 'third_party/python/fluent/**' + - 'tools/lint/python/l10n_lint.py' + - 'tools/lint/l10n.yml' diff --git a/tools/lint/libpref/__init__.py b/tools/lint/libpref/__init__.py new file mode 100644 index 0000000000..48b347ab03 --- /dev/null +++ b/tools/lint/libpref/__init__.py @@ -0,0 +1,113 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 re +import sys + +import yaml +from mozlint import result +from mozlint.pathutils import expand_exclusions + +# This simple linter checks for duplicates from +# modules/libpref/init/StaticPrefList.yaml against modules/libpref/init/all.js + +# If for any reason a pref needs to appear in both files, add it to this set. +IGNORE_PREFS = { + "devtools.console.stdout.chrome", # Uses the 'sticky' attribute. + "devtools.console.stdout.content", # Uses the 'sticky' attribute. + "fission.autostart", # Uses the 'locked' attribute. + "browser.dom.window.dump.enabled", # Uses the 'sticky' attribute. + "apz.fling_curve_function_y2", # This pref is a part of a series. + "dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled", # NOQA: E501; Uses the 'locked' attribute. + "extensions.backgroundServiceWorkerEnabled.enabled", # NOQA: E501; Uses the 'locked' attribute. +} +PATTERN = re.compile(r"\s*pref\(\s*\"(?P<pref>.+)\"\s*,\s*(?P<val>.+)\)\s*;.*") + + +def get_names(pref_list_filename): + pref_names = {} + # We want to transform patterns like 'foo: @VAR@' into valid yaml. This + # pattern does not happen in 'name', so it's fine to ignore these. + # We also want to evaluate all branches of #ifdefs for pref names, so we + # ignore anything else preprocessor related. + file = open(pref_list_filename).read().replace("@", "") + try: + pref_list = yaml.safe_load(file) + except (IOError, ValueError) as e: + print("{}: error:\n {}".format(pref_list_filename, e), file=sys.stderr) + sys.exit(1) + + for pref in pref_list: + if pref["name"] not in IGNORE_PREFS: + pref_names[pref["name"]] = pref["value"] + + return pref_names + + +# Check the names of prefs against each other, and if the pref is a duplicate +# that has not previously been noted, add that name to the list of errors. +def check_against(path, pref_names): + errors = [] + prefs = read_prefs(path) + for pref in prefs: + if pref["name"] in pref_names: + errors.extend(check_value_for_pref(pref, pref_names[pref["name"]], path)) + return errors + + +def check_value_for_pref(some_pref, some_value, path): + errors = [] + if some_pref["value"] == some_value: + errors.append( + { + "path": path, + "message": some_pref["raw"], + "lineno": some_pref["line"], + "hint": "Remove the duplicate pref or add it to IGNORE_PREFS.", + "level": "error", + } + ) + return errors + + +# The entries in the *.js pref files are regular enough to use simple pattern +# matching to load in prefs. +def read_prefs(path): + prefs = [] + with open(path) as source: + for lineno, line in enumerate(source, start=1): + match = PATTERN.match(line) + if match: + prefs.append( + { + "name": match.group("pref"), + "value": evaluate_pref(match.group("val")), + "line": lineno, + "raw": line, + } + ) + return prefs + + +def evaluate_pref(value): + bools = {"true": True, "false": False} + if value in bools: + return bools[value] + elif value.isdigit(): + return int(value) + return value + + +def checkdupes(paths, config, **kwargs): + results = [] + errors = [] + pref_names = get_names(config["support-files"][0]) + files = list(expand_exclusions(paths, config, kwargs["root"])) + for file in files: + errors.extend(check_against(file, pref_names)) + for error in errors: + results.append(result.from_config(config, **error)) + return results diff --git a/tools/lint/license.yml b/tools/lint/license.yml new file mode 100644 index 0000000000..c1cf7e628e --- /dev/null +++ b/tools/lint/license.yml @@ -0,0 +1,104 @@ +--- +license: + description: License Check + include: + - . + exclude: + # These paths need to be triaged. + - build/pgo/js-input + # License not super clear + - browser/branding/ + # Trademarks + - browser/components/pocket/content/panels/ + - browser/components/newtab/data/content/tippytop/images/ + - toolkit/components/pdfjs/content/web/images/ + # We probably want a specific license + - browser/extensions/webcompat/injections/ + # Copied mostly verbatim from upstream. License is documented in + # Cargo.toml. + - build/rust/windows/src/lib.rs + # Different license + - build/pgo/blueprint/print.css + # Different license + - build/pgo/blueprint/screen.css + # Empty files + - config/external/nspr/_pl_bld.h + - config/external/nspr/_pr_bld.h + # Unknown origin + - gfx/2d/MMIHelpers.h + # might not work with license + - gradle.properties + # might not work with license + - gradle/wrapper/gradle-wrapper.properties + # ICU4X data + - intl/icu_segmenter_data + # Imported code that is dual Apache2 / MIT licensed + - intl/l10n/rust/l10nregistry-rs + - intl/l10n/rust/l10nregistry-tests + # tests + - js/src/devtools/rootAnalysis/t/ + - mobile/android/geckoview/src/main/AndroidManifest_overlay.jinja + - mobile/android/geckoview_example/src/main + - testing/webcompat/interventions/ + - testing/webcompat/shims/ + # might not work with license + - mobile/android/gradle/dotgradle-offline/gradle.properties + # might not work with license + - mobile/android/gradle/dotgradle-online/gradle.properties + # Almost empty file + - modules/libpref/greprefs.js + - parser/html/java/named-character-references.html + - python/mozlint/test/files/ + # By design + - python/mozrelease/mozrelease + - security/mac/hardenedruntime/v1/production/browser.xml + - security/mac/hardenedruntime/v1/developer/browser.xml + - security/mac/hardenedruntime/v2/developer/browser.xml + - security/mac/hardenedruntime/v2/developer/media-plugin-helper.xml + - security/mac/hardenedruntime/v2/developer/plugin-container.xml + - security/mac/hardenedruntime/v2/developer/utility.xml + - security/mac/hardenedruntime/v2/production/nightly.browser.xml + - security/mac/hardenedruntime/v2/production/firefox.browser.xml + - security/mac/hardenedruntime/v2/production/firefoxdeveloperedition.browser.xml + - security/mac/hardenedruntime/v2/production/media-plugin-helper.xml + - security/mac/hardenedruntime/v2/production/plugin-container.xml + - testing/marionette/harness/marionette_harness/www/ + # Browsertime can't handle this script when there's a comment at the top + - testing/raptor/browsertime/browsertime_benchmark.js + - toolkit/components/reputationservice/chromium/chrome/common/safe_browsing/csd.pb.cc + - toolkit/components/reputationservice/chromium/chrome/common/safe_browsing/csd.pb.h + - toolkit/mozapps/update/updater/crctable.h + - tools/lint/eslint/eslint-plugin-mozilla/lib/configs + # template fragments used to generate .js sources. + - toolkit/components/uniffi-bindgen-gecko-js/src/templates/js + # By design + - tools/lint/test/ + extensions: + - .c + - .cc + - .cpp + - .css + - .dtd + - .ftl + - .h + - .html + - .idl + - .java + - .js + - .jsm + - .jsx + - .m + - .mm + - .mjs + - .properties + - .py + - .rs + - .svg + - .webidl + - .xhtml + - .xml + support-files: + - 'tools/lint/license/**' + type: external + payload: license:lint + find-dotfiles: true diff --git a/tools/lint/license/__init__.py b/tools/lint/license/__init__.py new file mode 100644 index 0000000000..e49b3b3dea --- /dev/null +++ b/tools/lint/license/__init__.py @@ -0,0 +1,268 @@ +# 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 https://mozilla.org/MPL/2.0/. + +import os +from glob import glob +from html.parser import HTMLParser + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) +topsrcdir = os.path.join(here, "..", "..", "..") + +# Official source: https://www.mozilla.org/en-US/MPL/headers/ +TEMPLATES = { + "mpl2_license": """ + 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 https://mozilla.org/MPL/2.0/. + """.strip().splitlines(), + "public_domain_license": """ + Any copyright is dedicated to the public domain. + https://creativecommons.org/publicdomain/zero/1.0/ + """.strip().splitlines(), +} +license_list = os.path.join(here, "valid-licenses.txt") + + +def load_valid_license(): + """ + Load the list of license patterns + """ + with open(license_list) as f: + l = f.readlines() + # Remove the empty lines + return list(filter(bool, [x.replace("\n", "") for x in l])) + + +def is_valid_license(licenses, filename): + """ + From a given file, check if we can find the license patterns + in the X first lines of the file + """ + with open(filename, "r", errors="replace") as myfile: + contents = myfile.read() + # Empty files don't need a license. + if not contents: + return True + + for l in licenses: + if l.lower().strip() in contents.lower(): + return True + return False + + +def add_header(log, filename, header): + """ + Add the header to the top of the file + """ + header.append("\n") + with open(filename, "r+") as f: + # lines in list format + try: + lines = f.readlines() + except UnicodeDecodeError as e: + log.debug("Could not read file '{}'".format(f)) + log.debug("Error: {}".format(e)) + return + + i = 0 + if lines: + # if the file isn't empty (__init__.py can be empty files) + if lines[0].startswith("#!") or lines[0].startswith("<?xml "): + i = 1 + + if lines[0].startswith("/* -*- Mode"): + i = 2 + # Insert in the top of the data structure + lines[i:i] = header + f.seek(0, 0) + f.write("".join(lines)) + + +def is_test(f): + """ + is the file a test or not? + """ + if "lint/test/" in f or "lint_license_test_tmp_file.js" in f: + # For the unit tests + return False + return ( + "/tests/" in f + or "/test/" in f + or "/test_" in f + or "/gtest" in f + or "/crashtest" in f + or "/mochitest" in f + or "/reftest" in f + or "/imptest" in f + or "/androidTest" in f + or "/jit-test/" in f + or "jsapi-tests/" in f + ) + + +def fix_me(log, filename): + """ + Add the copyright notice to the top of the file + """ + _, ext = os.path.splitext(filename) + license = [] + + license_template = TEMPLATES["mpl2_license"] + test = False + + if is_test(filename): + license_template = TEMPLATES["public_domain_license"] + test = True + + if ext in [ + ".cpp", + ".c", + ".cc", + ".h", + ".m", + ".mm", + ".rs", + ".java", + ".js", + ".jsm", + ".jsx", + ".mjs", + ".css", + ".idl", + ".webidl", + ]: + for i, l in enumerate(license_template): + start = " " + end = "" + if i == 0: + # first line, we have the /* + start = "/" + if i == len(license_template) - 1: + # Last line, we end by */ + end = " */" + license.append(start + "* " + l.strip() + end + "\n") + + add_header(log, filename, license) + return True + + if ext in [".py", ".ftl", ".properties"]: + for l in license_template: + license.append("# " + l.strip() + "\n") + add_header(log, filename, license) + return True + + if ext in [".xml", ".html", ".xhtml", ".dtd", ".svg"]: + for i, l in enumerate(license_template): + start = " - " + end = "" + if i == 0: + # first line, we have the <!-- + start = "<!-- " + if i == 2 or (i == 1 and test): + # Last line, we end by --> + end = " -->" + license.append(start + l.strip() + end) + if ext != ".svg" or not end: + # When dealing with an svg, we should not have a space between + # the license and the content + license.append("\n") + add_header(log, filename, license) + return True + + # In case we don't know how to handle a specific format. + return False + + +class HTMLParseError(Exception): + def __init__(self, msg, pos): + super().__init__(msg, *pos) + + +class LicenseHTMLParser(HTMLParser): + def __init__(self): + super().__init__() + self.in_code = False + self.invalid_paths = [] + + def handle_starttag(self, tag, attrs): + if tag == "code": + if self.in_code: + raise HTMLParseError("nested code tag", self.getpos()) + self.in_code = True + + def handle_endtag(self, tag): + if tag == "code": + if not self.in_code: + raise HTMLParseError("not started code tag", self.getpos()) + self.in_code = False + + def handle_data(self, data): + if self.in_code: + path = data.strip() + abspath = os.path.join(topsrcdir, path) + if not glob(abspath): + self.invalid_paths.append((path, self.getpos())) + + +def lint_license_html(path): + parser = LicenseHTMLParser() + with open(path) as fd: + content = fd.read() + parser.feed(content) + return parser.invalid_paths + + +def is_html_licence_summary(path): + license_html = os.path.join(topsrcdir, "toolkit", "content", "license.html") + return os.path.samefile(path, license_html) + + +def lint(paths, config, fix=None, **lintargs): + results = [] + log = lintargs["log"] + files = list(expand_exclusions(paths, config, lintargs["root"])) + fixed = 0 + + licenses = load_valid_license() + for f in files: + if is_test(f): + # For now, do not do anything with test (too many) + continue + + if not is_valid_license(licenses, f): + if fix and fix_me(log, f): + fixed += 1 + else: + res = { + "path": f, + "message": "No matching license strings found in tools/lint/license/valid-licenses.txt", # noqa + "level": "error", + } + results.append(result.from_config(config, **res)) + + if is_html_licence_summary(f): + try: + for invalid_path, (lineno, column) in lint_license_html(f): + res = { + "path": f, + "message": "references unknown path {}".format(invalid_path), + "level": "error", + "lineno": lineno, + "column": column, + } + results.append(result.from_config(config, **res)) + except HTMLParseError as err: + res = { + "path": f, + "message": err.args[0], + "level": "error", + "lineno": err.args[1], + "column": err.args[2], + } + results.append(result.from_config(config, **res)) + + return {"results": results, "fixed": fixed} diff --git a/tools/lint/license/valid-licenses.txt b/tools/lint/license/valid-licenses.txt new file mode 100644 index 0000000000..8a1d39a69e --- /dev/null +++ b/tools/lint/license/valid-licenses.txt @@ -0,0 +1,30 @@ +mozilla.org/MPL/ +Licensed under the Apache License, Version 2.0 +copyright is dedicated to the Public Domain. +under the MIT +Redistributions of source code must retain the above copyright +Use of this source code is governed by a BSD-style license +The author disclaims copyright to this source code. +The author hereby disclaims copyright to this source code +Use, Modification and Redistribution (including distribution of any +author grants irrevocable permission to anyone to use, modify, +THIS FILE IS AUTO-GENERATED +Permission is hereby granted, free of charge, to any person obtaining +Permission to use, copy, modify, +License: Public domain. You are free to use this code however you +You are granted a license to use, reproduce and create derivative works +GENERATED FILE, DO NOT EDIT +This code is governed by the BSD license +This Source Code Form is subject to the terms of the Apache License +DO NOT EDIT +This program is made available under an ISC-style license. +under MIT license +License MIT per upstream +DO NOT MODIFY +GNU General Public License +FILE IS GENERATED +Generated by +do not edit +may be protected as a trademark in some jurisdictions +The ASF licenses this file to You under the Apache License, Version 2.0 +Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions diff --git a/tools/lint/lintpref.yml b/tools/lint/lintpref.yml new file mode 100644 index 0000000000..cb58d642bc --- /dev/null +++ b/tools/lint/lintpref.yml @@ -0,0 +1,17 @@ +--- +lintpref: + description: Linter for static prefs. + include: + - 'modules/libpref/init' + - 'browser/app/profile/' + - 'mobile/android/app/' + - 'devtools/client/preferences/' + - 'browser/branding/' + - 'mobile/android/installer/' + - 'mobile/android/locales/' + exclude: [] + extensions: ['js'] + type: external + payload: libpref:checkdupes + support-files: + - 'modules/libpref/init/StaticPrefList.yaml' diff --git a/tools/lint/mach_commands.py b/tools/lint/mach_commands.py new file mode 100644 index 0000000000..c9778e28a0 --- /dev/null +++ b/tools/lint/mach_commands.py @@ -0,0 +1,189 @@ +# 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 argparse +import copy +import os + +from mach.decorators import Command, CommandArgument +from mozbuild.base import BuildEnvironmentNotFoundException +from mozbuild.base import MachCommandConditions as conditions + +here = os.path.abspath(os.path.dirname(__file__)) +EXCLUSION_FILES = [ + os.path.join("tools", "rewriting", "Generated.txt"), + os.path.join("tools", "rewriting", "ThirdPartyPaths.txt"), +] + +EXCLUSION_FILES_OPTIONAL = [] +thunderbird_excludes = os.path.join("comm", "tools", "lint", "GlobalExclude.txt") +if os.path.exists(thunderbird_excludes): + EXCLUSION_FILES_OPTIONAL.append(thunderbird_excludes) + +GLOBAL_EXCLUDES = ["**/node_modules", "tools/lint/test/files", ".hg", ".git"] + +VALID_FORMATTERS = {"black", "clang-format", "rustfmt", "isort"} +VALID_ANDROID_FORMATTERS = {"android-format"} + +# Code-review bot must index issues from the whole codebase when pushing +# to autoland or try repositories. In such cases, output warnings in the +# task's JSON artifact but do not fail if only warnings are found. +REPORT_WARNINGS = os.environ.get("GECKO_HEAD_REPOSITORY", "").rstrip("/") in ( + "https://hg.mozilla.org/mozilla-central", + "https://hg.mozilla.org/integration/autoland", + "https://hg.mozilla.org/try", +) + + +def setup_argument_parser(): + from mozlint import cli + + return cli.MozlintParser() + + +def get_global_excludes(**lintargs): + # exclude misc paths + excludes = GLOBAL_EXCLUDES[:] + topsrcdir = lintargs["root"] + + # exclude top level paths that look like objdirs + excludes.extend( + [ + name + for name in os.listdir(topsrcdir) + if name.startswith("obj") and os.path.isdir(name) + ] + ) + + if lintargs.get("include_thirdparty"): + # For some linters, we want to include the thirdparty code too. + # Example: trojan-source linter should run also on third party code. + return excludes + + for path in EXCLUSION_FILES + EXCLUSION_FILES_OPTIONAL: + with open(os.path.join(topsrcdir, path), "r") as fh: + excludes.extend([f.strip() for f in fh.readlines()]) + + return excludes + + +@Command( + "lint", + category="devenv", + description="Run linters.", + parser=setup_argument_parser, + virtualenv_name="lint", +) +def lint(command_context, *runargs, **lintargs): + """Run linters.""" + command_context.activate_virtualenv() + from mozlint import cli, parser + + try: + buildargs = {} + buildargs["substs"] = copy.deepcopy(dict(command_context.substs)) + buildargs["defines"] = copy.deepcopy(dict(command_context.defines)) + buildargs["topobjdir"] = command_context.topobjdir + lintargs.update(buildargs) + except BuildEnvironmentNotFoundException: + pass + + lintargs.setdefault("root", command_context.topsrcdir) + lintargs["exclude"] = get_global_excludes(**lintargs) + lintargs["config_paths"].insert(0, here) + lintargs["virtualenv_bin_path"] = command_context.virtualenv_manager.bin_path + lintargs["virtualenv_manager"] = command_context.virtualenv_manager + if REPORT_WARNINGS and lintargs.get("show_warnings") is None: + lintargs["show_warnings"] = "soft" + for path in EXCLUSION_FILES: + parser.GLOBAL_SUPPORT_FILES.append( + os.path.join(command_context.topsrcdir, path) + ) + setupargs = { + "mach_command_context": command_context, + } + return cli.run(*runargs, setupargs=setupargs, **lintargs) + + +@Command( + "eslint", + category="devenv", + description="Run eslint or help configure eslint for optimal development.", +) +@CommandArgument( + "paths", + default=None, + nargs="*", + help="Paths to file or directories to lint, like " + "'browser/' Defaults to the " + "current directory if not given.", +) +@CommandArgument( + "-s", + "--setup", + default=False, + action="store_true", + help="Configure eslint for optimal development.", +) +@CommandArgument("-b", "--binary", default=None, help="Path to eslint binary.") +@CommandArgument( + "--fix", + default=False, + action="store_true", + help="Request that eslint automatically fix errors, where possible.", +) +@CommandArgument( + "--rule", + default=[], + dest="rules", + action="append", + help="Specify an additional rule for ESLint to run, e.g. 'no-new-object: error'", +) +@CommandArgument( + "extra_args", + nargs=argparse.REMAINDER, + help="Extra args that will be forwarded to eslint.", +) +def eslint(command_context, paths, extra_args=[], **kwargs): + command_context._mach_context.commands.dispatch( + "lint", + command_context._mach_context, + linters=["eslint"], + paths=paths, + argv=extra_args, + **kwargs + ) + + +@Command( + "format", + category="devenv", + description="Format files, alternative to 'lint --fix' ", + parser=setup_argument_parser, +) +def format_files(command_context, paths, extra_args=[], **kwargs): + linters = kwargs["linters"] + + formatters = VALID_FORMATTERS + if conditions.is_android(command_context): + formatters |= VALID_ANDROID_FORMATTERS + + if not linters: + linters = formatters + else: + invalid_linters = set(linters) - formatters + if invalid_linters: + print( + "error: One or more linters passed are not valid formatters. " + "Note that only the following linters are valid formatters:" + ) + print("\n".join(sorted(formatters))) + return 1 + + kwargs["linters"] = list(linters) + + kwargs["fix"] = True + command_context._mach_context.commands.dispatch( + "lint", command_context._mach_context, paths=paths, argv=extra_args, **kwargs + ) diff --git a/tools/lint/mingw-capitalization.yml b/tools/lint/mingw-capitalization.yml new file mode 100644 index 0000000000..04c9c79e16 --- /dev/null +++ b/tools/lint/mingw-capitalization.yml @@ -0,0 +1,12 @@ +--- +mingw-capitalization: + description: > + "A Windows include file is not lowercase, and may break the MinGW build" + extensions: ['h', 'cpp', 'cc', 'c'] + include: ['.'] + exclude: + # We do not compile WebRTC with MinGW yet + - media/webrtc + type: external + level: error + payload: cpp.mingw-capitalization:lint diff --git a/tools/lint/mscom-init.yml b/tools/lint/mscom-init.yml new file mode 100644 index 0000000000..d818aa4ab9 --- /dev/null +++ b/tools/lint/mscom-init.yml @@ -0,0 +1,74 @@ +# 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/. +--- +forbid-mscom-init: + description: >- + New calls to CoInitialize, CoInitializeEx, OleInitialize, RoInitialize, + CoUninitialize, OleUninitialize, or RoUninitialize are forbidden. If you + have questions, please consult a peer of the IPC: MSCOM module. + level: error + include: ['.'] + type: regex + payload: ([CR]o|Ole)(Uni|I)nitialize(Ex)? + ignore-case: false + extensions: + - h + - c + - cc + - cpp + exclude: + # These files contain utilities for using COM more safely. + - ipc/mscom/ApartmentRegion.h + - ipc/mscom/COMWrappers.cpp + - ipc/mscom/COMWrappers.h + - ipc/mscom/EnsureMTA.cpp + # These files have been reviewed by MSCOM peers, and the use of + # CoInitialize within them has been confirmed to be necessary and + # proper. + - ipc/mscom/ProcessRuntime.cpp + # These files are existing legacy uses of CoInitialize (and so forth) + # that must eventually be fixed -- probably by converting them to use + # ApartmentRegion and moving them to _that_ lint's exception-list. + - browser/components/migration/nsIEHistoryEnumerator.cpp + - browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp + - browser/components/shell/nsWindowsShellService.cpp + - gfx/thebes/gfxWindowsPlatform.cpp + - image/DecodePool.cpp + - ipc/glue/BrowserProcessSubThread.cpp + - netwerk/system/win32/nsNotifyAddrListener.cpp + - toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp + - toolkit/crashreporter/google-breakpad/src/common/windows/pdb_source_line_writer.cc + - toolkit/mozapps/defaultagent/proxy/main.cpp + - uriloader/exthandler/win/nsOSHelperAppService.cpp + - widget/windows/TaskbarPreview.cpp + - widget/windows/WinTaskbar.cpp + - widget/windows/nsAppShell.cpp + - widget/windows/nsWindow.cpp + - widget/windows/nsWindow.h + - widget/windows/tests/TestUriValidation.cpp + - xpcom/io/nsLocalFileWin.cpp + +forbid-apartment-region: + description: >- + New uses of ApartmentRegion, ApartmentRegionT, MTARegion, or STARegion + require approval by a peer of the IPC: MSCOM module. + level: error + include: ['.'] + type: regex + payload: ApartmentRegion(T)?|[MS]TARegion + ignore-case: false + extensions: + - h + - c + - cc + - cpp + exclude: + # ApartmentRegion's definition. + - ipc/mscom/ApartmentRegion.h + # These files have been reviewed and approved by MSCOM peers. + - ipc/mscom/ProcessRuntime.cpp + - ipc/mscom/ProcessRuntime.h + - widget/windows/filedialog/WinFileDialogCommands.cpp + # These files are existing uses that must eventually be fixed. + - widget/windows/LegacyJumpListBuilder.cpp diff --git a/tools/lint/perfdocs.yml b/tools/lint/perfdocs.yml new file mode 100644 index 0000000000..175781ad00 --- /dev/null +++ b/tools/lint/perfdocs.yml @@ -0,0 +1,18 @@ +--- +perfdocs: + description: Performance Documentation linter + # This task handles its own search, so just include cwd + include: [ + 'python/mozperftest', + 'testing/awsy', + 'testing/raptor', + 'testing/talos', + 'testing/performance/fxrecord', + 'testing/performance/mach-try-perf', + 'dom/indexedDB/test', + ] + exclude: [] + extensions: ['rst', 'ini', 'yml'] + support-files: [] + type: structured_log + payload: perfdocs:lint diff --git a/tools/lint/perfdocs/__init__.py b/tools/lint/perfdocs/__init__.py new file mode 100644 index 0000000000..1194d38624 --- /dev/null +++ b/tools/lint/perfdocs/__init__.py @@ -0,0 +1,13 @@ +# 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 os + +from perfdocs import perfdocs + +here = os.path.abspath(os.path.dirname(__file__)) +PERFDOCS_REQUIREMENTS_PATH = os.path.join(here, "requirements.txt") + + +def lint(paths, config, logger, fix=False, **lintargs): + return perfdocs.run_perfdocs(config, logger=logger, paths=paths, generate=fix) diff --git a/tools/lint/perfdocs/doc_helpers.py b/tools/lint/perfdocs/doc_helpers.py new file mode 100644 index 0000000000..709f190204 --- /dev/null +++ b/tools/lint/perfdocs/doc_helpers.py @@ -0,0 +1,85 @@ +# 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/. + + +class MismatchedRowLengthsException(Exception): + """ + This exception is thrown when there is a mismatch between the number of items in a row, + and the number of headers defined. + """ + + pass + + +class TableBuilder(object): + """ + Helper class for building tables. + """ + + def __init__(self, title, widths, header_rows, headers, indent=0): + """ + :param title: str - Title of the table + :param widths: list of str - Widths of each column of the table + :param header_rows: int - Number of header rows + :param headers: 2D list of str - Headers + :param indent: int - Number of spaces to indent table + """ + if not isinstance(title, str): + raise TypeError("TableBuilder attribute title must be a string.") + if not isinstance(widths, list) or not isinstance(widths[0], int): + raise TypeError("TableBuilder attribute widths must be a list of integers.") + if not isinstance(header_rows, int): + raise TypeError("TableBuilder attribute header_rows must be an integer.") + if ( + not isinstance(headers, list) + or not isinstance(headers[0], list) + or not isinstance(headers[0][0], str) + ): + raise TypeError( + "TableBuilder attribute headers must be a two-dimensional list of strings." + ) + if not isinstance(indent, int): + raise TypeError("TableBuilder attribute indent must be an integer.") + + self.title = title + self.widths = widths + self.header_rows = header_rows + self.headers = headers + self.indent = " " * indent + self.table = "" + self._build_table() + + def _build_table(self): + if len(self.widths) != len(self.headers[0]): + raise MismatchedRowLengthsException( + "Number of table headers must match number of column widths." + ) + widths = " ".join(map(str, self.widths)) + self.table += ( + f"{self.indent}.. list-table:: **{self.title}**\n" + f"{self.indent} :widths: {widths}\n" + f"{self.indent} :header-rows: {self.header_rows}\n\n" + ) + self.add_rows(self.headers) + + def add_rows(self, rows): + if type(rows) != list or type(rows[0]) != list or type(rows[0][0]) != str: + raise TypeError("add_rows() requires a two-dimensional list of strings.") + for row in rows: + self.add_row(row) + + def add_row(self, values): + if len(values) != len(self.widths): + raise MismatchedRowLengthsException( + "Number of items in a row must must number of columns defined." + ) + for i, val in enumerate(values): + if i == 0: + self.table += f"{self.indent} * - **{val}**\n" + else: + self.table += f"{self.indent} - {val}\n" + + def finish_table(self): + self.table += "\n" + return self.table diff --git a/tools/lint/perfdocs/framework_gatherers.py b/tools/lint/perfdocs/framework_gatherers.py new file mode 100644 index 0000000000..4c2ca585ad --- /dev/null +++ b/tools/lint/perfdocs/framework_gatherers.py @@ -0,0 +1,571 @@ +# 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 pathlib +import re + +from gecko_taskgraph.util.attributes import match_run_on_projects +from manifestparser import TestManifest +from mozperftest.script import ScriptInfo + +from perfdocs.doc_helpers import TableBuilder +from perfdocs.logger import PerfDocLogger +from perfdocs.utils import read_yaml + +logger = PerfDocLogger() + +BRANCHES = [ + "mozilla-central", + "autoland", + "mozilla-release", + "mozilla-beta", +] + +""" +This file is for framework specific gatherers since manifests +might be parsed differently in each of them. The gatherers +must implement the FrameworkGatherer class. +""" + + +class FrameworkGatherer(object): + """ + Abstract class for framework gatherers. + """ + + def __init__(self, yaml_path, workspace_dir, taskgraph={}): + """ + Generic initialization for a framework gatherer. + """ + self.workspace_dir = workspace_dir + self._yaml_path = yaml_path + self._taskgraph = taskgraph + self._suite_list = {} + self._test_list = {} + self._descriptions = {} + self._manifest_path = "" + self._manifest = None + self.script_infos = {} + self._task_list = {} + self._task_match_pattern = re.compile(r"([\w\W]*/[pgo|opt]*)-([\w\W]*)") + + def get_task_match(self, task_name): + return re.search(self._task_match_pattern, task_name) + + def get_manifest_path(self): + """ + Returns the path to the manifest based on the + manifest entry in the frameworks YAML configuration + file. + + :return str: Path to the manifest. + """ + if self._manifest_path: + return self._manifest_path + + yaml_content = read_yaml(self._yaml_path) + self._manifest_path = pathlib.Path(self.workspace_dir, yaml_content["manifest"]) + return self._manifest_path + + def get_suite_list(self): + """ + Each framework gatherer must return a dictionary with + the following structure. Note that the test names must + be relative paths so that issues can be correctly issued + by the reviewbot. + + :return dict: A dictionary with the following structure: { + "suite_name": [ + 'testing/raptor/test1', + 'testing/raptor/test2' + ] + } + """ + raise NotImplementedError + + def _build_section_with_header(self, title, content, header_type=None): + """ + Adds a section to the documentation with the title as the type mentioned + and paragraph as content mentioned. + :param title: title of the section + :param content: content of section paragraph + :param header_type: type of the title heading + """ + heading_map = {"H2": "*", "H3": "=", "H4": "-", "H5": "^"} + return [title, heading_map.get(header_type, "^") * len(title), content, ""] + + +class RaptorGatherer(FrameworkGatherer): + """ + Gatherer for the Raptor framework. + """ + + def get_suite_list(self): + """ + Returns a dictionary containing a mapping from suites + to the tests they contain. + + :return dict: A dictionary with the following structure: { + "suite_name": [ + 'testing/raptor/test1', + 'testing/raptor/test2' + ] + } + """ + if self._suite_list: + return self._suite_list + + manifest_path = self.get_manifest_path() + + # Get the tests from the manifest + test_manifest = TestManifest([str(manifest_path)], strict=False) + test_list = test_manifest.active_tests(exists=False, disabled=False) + + # Parse the tests into the expected dictionary + for test in test_list: + # Get the top-level suite + s = os.path.basename(test["here"]) + if s not in self._suite_list: + self._suite_list[s] = [] + + # Get the individual test + fpath = re.sub(".*testing", "testing", test["manifest"]) + + if fpath not in self._suite_list[s]: + self._suite_list[s].append(fpath) + + return self._suite_list + + def _get_ci_tasks(self): + for task in self._taskgraph.keys(): + if type(self._taskgraph[task]) == dict: + command = self._taskgraph[task]["task"]["payload"].get("command", []) + run_on_projects = self._taskgraph[task]["attributes"]["run_on_projects"] + else: + command = self._taskgraph[task].task["payload"].get("command", []) + run_on_projects = self._taskgraph[task].attributes["run_on_projects"] + + test_match = re.search(r"[\s']--test[\s=](.+?)[\s']", str(command)) + task_match = self.get_task_match(task) + if test_match and task_match: + test = test_match.group(1) + platform = task_match.group(1) + test_name = task_match.group(2) + + item = {"test_name": test_name, "run_on_projects": run_on_projects} + self._task_list.setdefault(test, {}).setdefault(platform, []).append( + item + ) + + def _get_subtests_from_ini(self, manifest_path, suite_name): + """ + Returns a list of (sub)tests from an ini file containing the test definitions. + + :param str manifest_path: path to the ini file + :return list: the list of the tests + """ + desc_exclusion = ["here", "manifest_relpath", "path", "relpath"] + test_manifest = TestManifest([str(manifest_path)], strict=False) + test_list = test_manifest.active_tests(exists=False, disabled=False) + subtests = {} + for subtest in test_list: + subtests[subtest["name"]] = subtest["manifest"] + + description = {} + for key, value in subtest.items(): + if key not in desc_exclusion: + description[key] = value + + # Prepare alerting metrics for verification + description["metrics"] = [ + metric.strip() + for metric in description.get("alert_on", "").split(",") + if metric.strip() != "" + ] + + subtests[subtest["name"]] = description + self._descriptions.setdefault(suite_name, []).append(description) + + self._descriptions[suite_name].sort(key=lambda item: item["name"]) + + return subtests + + def get_test_list(self): + """ + Returns a dictionary containing the tests in every suite ini file. + + :return dict: A dictionary with the following structure: { + "suite_name": { + 'raptor_test1', + 'raptor_test2' + }, + } + """ + if self._test_list: + return self._test_list + + suite_list = self.get_suite_list() + + # Iterate over each manifest path from suite_list[suite_name] + # and place the subtests into self._test_list under the same key + for suite_name, manifest_paths in suite_list.items(): + if not self._test_list.get(suite_name): + self._test_list[suite_name] = {} + for manifest_path in manifest_paths: + subtest_list = self._get_subtests_from_ini(manifest_path, suite_name) + self._test_list[suite_name].update(subtest_list) + + self._get_ci_tasks() + + return self._test_list + + def build_test_description(self, title, test_description="", suite_name=""): + matcher = [] + browsers = [ + "firefox", + "chrome", + "chromium", + "refbrow", + "fennec68", + "geckoview", + "fenix", + ] + test_name = [f"{title}-{browser}" for browser in browsers] + test_name.append(title) + + for suite, val in self._descriptions.items(): + for test in val: + if test["name"] in test_name and suite_name == suite: + matcher.append(test) + + if len(matcher) == 0: + logger.critical( + "No tests exist for the following name " + "(obtained from config.yml): {}".format(title) + ) + raise Exception( + "No tests exist for the following name " + "(obtained from config.yml): {}".format(title) + ) + + result = f".. dropdown:: {title}\n" + result += f" :class-container: anchor-id-{title}-{suite_name[0]}\n\n" + + for idx, description in enumerate(matcher): + if description["name"] != title: + result += f" {idx+1}. **{description['name']}**\n\n" + if "owner" in description.keys(): + result += f" **Owner**: {description['owner']}\n\n" + + for key in sorted(description.keys()): + if key in ["owner", "name", "manifest", "metrics"]: + continue + sub_title = key.replace("_", " ") + if key == "test_url": + if "<" in description[key] or ">" in description[key]: + description[key] = description[key].replace("<", "\<") + description[key] = description[key].replace(">", "\>") + result += f" * **{sub_title}**: `<{description[key]}>`__\n" + elif key == "secondary_url": + result += f" * **{sub_title}**: `<{description[key]}>`__\n" + elif key in ["playback_pageset_manifest"]: + result += ( + f" * **{sub_title}**: " + f"{description[key].replace('{subtest}', description['name'])}\n" + ) + else: + if "\n" in description[key]: + description[key] = description[key].replace("\n", " ") + result += f" * **{sub_title}**: {description[key]}\n" + + if self._task_list.get(title, []): + result += " * **Test Task**:\n\n" + for platform in sorted(self._task_list[title]): + self._task_list[title][platform].sort(key=lambda x: x["test_name"]) + + table = TableBuilder( + title=platform, + widths=[30] + [15 for x in BRANCHES], + header_rows=1, + headers=[["Test Name"] + BRANCHES], + indent=3, + ) + + for task in self._task_list[title][platform]: + values = [task["test_name"]] + values += [ + "\u2705" + if match_run_on_projects(x, task["run_on_projects"]) + else "\u274C" + for x in BRANCHES + ] + table.add_row(values) + result += f"{table.finish_table()}\n" + + return [result] + + def build_suite_section(self, title, content): + return self._build_section_with_header( + title.capitalize(), content, header_type="H4" + ) + + +class MozperftestGatherer(FrameworkGatherer): + """ + Gatherer for the Mozperftest framework. + """ + + def get_test_list(self): + """ + Returns a dictionary containing the tests that are in perftest.toml manifest. + + :return dict: A dictionary with the following structure: { + "suite_name": { + 'perftest_test1', + 'perftest_test2', + }, + } + """ + for path in list(pathlib.Path(self.workspace_dir).rglob("perftest.toml")): + if "obj-" in str(path): + continue + suite_name = str(path.parent).replace(str(self.workspace_dir), "") + + # If the workspace dir doesn't end with a forward-slash, + # the substitution above won't work completely + if suite_name.startswith("/") or suite_name.startswith("\\"): + suite_name = suite_name[1:] + + # We have to add new paths to the logger as we search + # because mozperftest tests exist in multiple places in-tree + PerfDocLogger.PATHS.append(suite_name) + + # Get the tests from perftest.toml + test_manifest = TestManifest([str(path)], strict=False) + test_list = test_manifest.active_tests(exists=False, disabled=False) + for test in test_list: + si = ScriptInfo(test["path"]) + self.script_infos[si["name"]] = si + self._test_list.setdefault(suite_name.replace("\\", "/"), {}).update( + {si["name"]: {"path": str(path)}} + ) + + return self._test_list + + def build_test_description(self, title, test_description="", suite_name=""): + return [str(self.script_infos[title])] + + def build_suite_section(self, title, content): + return self._build_section_with_header(title, content, header_type="H4") + + +class TalosGatherer(FrameworkGatherer): + def _get_ci_tasks(self): + with open( + pathlib.Path(self.workspace_dir, "testing", "talos", "talos.json") + ) as f: + config_suites = json.load(f)["suites"] + + for task_name in self._taskgraph.keys(): + task = self._taskgraph[task_name] + + if type(task) == dict: + is_talos = task["task"]["extra"].get("suite", []) + command = task["task"]["payload"].get("command", []) + run_on_projects = task["attributes"]["run_on_projects"] + else: + is_talos = task.task["extra"].get("suite", []) + command = task.task["payload"].get("command", []) + run_on_projects = task.attributes["run_on_projects"] + + suite_match = re.search(r"[\s']--suite[\s=](.+?)[\s']", str(command)) + task_match = self.get_task_match(task_name) + if "talos" == is_talos and task_match: + suite = suite_match.group(1) + platform = task_match.group(1) + test_name = task_match.group(2) + item = {"test_name": test_name, "run_on_projects": run_on_projects} + + for test in config_suites[suite]["tests"]: + self._task_list.setdefault(test, {}).setdefault( + platform, [] + ).append(item) + + def get_test_list(self): + from talos import test as talos_test + + test_lists = talos_test.test_dict() + mod = __import__("talos.test", fromlist=test_lists) + + suite_name = "Talos Tests" + + for test in test_lists: + self._test_list.setdefault(suite_name, {}).update({test: {}}) + + klass = getattr(mod, test) + self._descriptions.setdefault(test, klass.__dict__) + + self._get_ci_tasks() + + return self._test_list + + def build_test_description(self, title, test_description="", suite_name=""): + result = f".. dropdown:: {title}\n" + result += f" :class-container: anchor-id-{title}\n\n" + + yml_descriptions = [s.strip() for s in test_description.split("- ") if s] + for description in yml_descriptions: + if "Example Data" in description: + # Example Data for using code block + example_list = [s.strip() for s in description.split("* ")] + result += f" * {example_list[0]}\n" + result += "\n .. code-block::\n\n" + for example in example_list[1:]: + result += f" {example}\n" + result += "\n" + + elif " * " in description: + # Sub List + sub_list = [s.strip() for s in description.split(" * ")] + result += f" * {sub_list[0]}\n" + for sub in sub_list[1:]: + result += f" * {sub}\n" + + else: + # General List + result += f" * {description}\n" + + if title in self._descriptions: + for key in sorted(self._descriptions[title]): + if key.startswith("__") and key.endswith("__"): + continue + elif key == "filters": + continue + + # On windows, we get the paths in the wrong style + value = self._descriptions[title][key] + if isinstance(value, dict): + for k, v in value.items(): + if isinstance(v, str) and "\\" in v: + value[k] = str(v).replace("\\", r"/") + result += r" * " + key + r": " + str(value) + r"\n" + + # Command + result += " * Command\n\n" + result += " .. code-block::\n\n" + result += f" ./mach talos-test -a {title}\n\n" + + if self._task_list.get(title, []): + result += " * **Test Task**:\n\n" + for platform in sorted(self._task_list[title]): + self._task_list[title][platform].sort(key=lambda x: x["test_name"]) + + table = TableBuilder( + title=platform, + widths=[30] + [15 for x in BRANCHES], + header_rows=1, + headers=[["Test Name"] + BRANCHES], + indent=3, + ) + + for task in self._task_list[title][platform]: + values = [task["test_name"]] + values += [ + "\u2705" + if match_run_on_projects(x, task["run_on_projects"]) + else "\u274C" + for x in BRANCHES + ] + table.add_row(values) + result += f"{table.finish_table()}\n" + + return [result] + + def build_suite_section(self, title, content): + return self._build_section_with_header(title, content, header_type="H2") + + +class AwsyGatherer(FrameworkGatherer): + """ + Gatherer for the Awsy framework. + """ + + def _generate_ci_tasks(self): + for task_name in self._taskgraph.keys(): + task = self._taskgraph[task_name] + + if type(task) == dict: + awsy_test = task["task"]["extra"].get("suite", []) + run_on_projects = task["attributes"]["run_on_projects"] + else: + awsy_test = task.task["extra"].get("suite", []) + run_on_projects = task.attributes["run_on_projects"] + + task_match = self.get_task_match(task_name) + + if "awsy" in awsy_test and task_match: + platform = task_match.group(1) + test_name = task_match.group(2) + item = {"test_name": test_name, "run_on_projects": run_on_projects} + self._task_list.setdefault(platform, []).append(item) + + def get_suite_list(self): + self._suite_list = {"Awsy tests": ["tp6", "base", "dmd", "tp5"]} + return self._suite_list + + def get_test_list(self): + self._generate_ci_tasks() + return { + "Awsy tests": { + "tp6": {}, + "base": {}, + "dmd": {}, + "tp5": {}, + } + } + + def build_suite_section(self, title, content): + return self._build_section_with_header( + title.capitalize(), content, header_type="H4" + ) + + def build_test_description(self, title, test_description="", suite_name=""): + dropdown_suite_name = suite_name.replace(" ", "-") + result = f".. dropdown:: {title} ({test_description})\n" + result += f" :class-container: anchor-id-{title}-{dropdown_suite_name}\n\n" + + awsy_data = read_yaml(self._yaml_path)["suites"]["Awsy tests"] + if "owner" in awsy_data.keys(): + result += f" **Owner**: {awsy_data['owner']}\n\n" + result += " * **Test Task**:\n" + + # tp5 tests are represented by awsy-e10s test names + # while the others have their title in test names + search_tag = "awsy-e10s" if title == "tp5" else title + for platform in sorted(self._task_list.keys()): + result += f" * {platform}\n" + for test_dict in sorted( + self._task_list[platform], key=lambda d: d["test_name"] + ): + if search_tag in test_dict["test_name"]: + run_on_project = ": " + ( + ", ".join(test_dict["run_on_projects"]) + if test_dict["run_on_projects"] + else "None" + ) + result += ( + f" * {test_dict['test_name']}{run_on_project}\n" + ) + result += "\n" + + return [result] + + +class StaticGatherer(FrameworkGatherer): + """ + A noop gatherer for frameworks with static-only documentation. + """ + + pass diff --git a/tools/lint/perfdocs/gatherer.py b/tools/lint/perfdocs/gatherer.py new file mode 100644 index 0000000000..828c2f7f2b --- /dev/null +++ b/tools/lint/perfdocs/gatherer.py @@ -0,0 +1,156 @@ +# 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 os +import pathlib + +from perfdocs.framework_gatherers import ( + AwsyGatherer, + MozperftestGatherer, + RaptorGatherer, + StaticGatherer, + TalosGatherer, +) +from perfdocs.logger import PerfDocLogger +from perfdocs.utils import read_yaml + +logger = PerfDocLogger() + +# TODO: Implement decorator/searcher to find the classes. +frameworks = { + "raptor": RaptorGatherer, + "mozperftest": MozperftestGatherer, + "talos": TalosGatherer, + "awsy": AwsyGatherer, +} + +# List of file types allowed to be used as static files +ALLOWED_STATIC_FILETYPES = ("rst", "png") + + +class Gatherer(object): + """ + Gatherer produces the tree of the perfdoc's entries found + and can obtain manifest-based test lists. Used by the Verifier. + """ + + def __init__(self, workspace_dir, taskgraph=None): + """ + Initialzie the Gatherer. + + :param str workspace_dir: Path to the gecko checkout. + """ + self.workspace_dir = workspace_dir + self.taskgraph = taskgraph + self._perfdocs_tree = [] + self._test_list = [] + self.framework_gatherers = {} + + @property + def perfdocs_tree(self): + """ + Returns the perfdocs_tree, and computes it + if it doesn't exist. + + :return dict: The perfdocs tree containing all + framework perfdoc entries. See `fetch_perfdocs_tree` + for information on the data structure. + """ + if self._perfdocs_tree: + return self._perfdocs_tree + else: + self.fetch_perfdocs_tree() + return self._perfdocs_tree + + def fetch_perfdocs_tree(self): + """ + Creates the perfdocs tree with the following structure: + [ + { + "path": Path to the perfdocs directory. + "yml": Name of the configuration YAML file. + "rst": Name of the RST file. + "static": Name of the static file. + }, ... + ] + + This method doesn't return anything. The result can be found in + the perfdocs_tree attribute. + """ + exclude_dir = [ + str(pathlib.Path(self.workspace_dir, ".hg")), + str(pathlib.Path("tools", "lint")), + str(pathlib.Path("testing", "perfdocs")), + ] + + for path in pathlib.Path(self.workspace_dir).rglob("perfdocs"): + if any(d in str(path.resolve()) for d in exclude_dir): + continue + files = [f for f in os.listdir(path)] + matched = {"path": str(path), "yml": "", "rst": "", "static": []} + + for file in files: + # Add the yml/rst/static file to its key if re finds the searched file + if file == "config.yml" or file == "config.yaml": + matched["yml"] = file + elif file == "index.rst": + matched["rst"] = file + elif file.split(".")[-1] in ALLOWED_STATIC_FILETYPES: + matched["static"].append(file) + + # Append to structdocs if all the searched files were found + if all(val for val in matched.values() if not type(val) == list): + self._perfdocs_tree.append(matched) + + logger.log( + "Found {} perfdocs directories in {}".format( + len(self._perfdocs_tree), + [d["path"] for d in self._perfdocs_tree], + ) + ) + + def get_test_list(self, sdt_entry): + """ + Use a perfdocs_tree entry to find the test list for + the framework that was found. + + :return: A framework info dictionary with fields: { + 'yml_path': Path to YAML, + 'yml_content': Content of YAML, + 'name': Name of framework, + 'test_list': Test list found for the framework + } + """ + + # If it was computed before, return it + yaml_path = pathlib.Path(sdt_entry["path"], sdt_entry["yml"]) + for entry in self._test_list: + if entry["yml_path"] == yaml_path: + return entry + + # Set up framework entry with meta data + yaml_content = read_yaml(yaml_path) + framework = { + "yml_content": yaml_content, + "yml_path": yaml_path, + "name": yaml_content["name"], + "test_list": {}, + } + + if yaml_content["static-only"]: + framework_gatherer_cls = StaticGatherer + else: + framework_gatherer_cls = frameworks[framework["name"]] + + # Get and then store the frameworks tests + framework_gatherer = self.framework_gatherers[ + framework["name"] + ] = framework_gatherer_cls( + framework["yml_path"], self.workspace_dir, self.taskgraph + ) + + if not yaml_content["static-only"]: + framework["test_list"] = framework_gatherer.get_test_list() + + self._test_list.append(framework) + return framework diff --git a/tools/lint/perfdocs/generator.py b/tools/lint/perfdocs/generator.py new file mode 100644 index 0000000000..3f3a0acefa --- /dev/null +++ b/tools/lint/perfdocs/generator.py @@ -0,0 +1,281 @@ +# 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 pathlib +import re +import shutil +import tempfile + +from perfdocs.logger import PerfDocLogger +from perfdocs.utils import ( + ON_TRY, + are_dirs_equal, + get_changed_files, + read_file, + read_yaml, + save_file, +) + +logger = PerfDocLogger() + + +class Generator(object): + """ + After each perfdocs directory was validated, the generator uses the templates + for each framework, fills them with the test descriptions in config and saves + the perfdocs in the form index.rst as index file and suite_name.rst for + each suite of tests in the framework. + """ + + def __init__(self, verifier, workspace, generate=False): + """ + Initialize the Generator. + + :param verifier: Verifier object. It should not be a fresh Verifier object, + but an initialized one with validate_tree() method already called + :param workspace: Path to the top-level checkout directory. + :param generate: Flag for generating the documentation + """ + self._workspace = workspace + if not self._workspace: + raise Exception("PerfDocs Generator requires a workspace directory.") + # Template documents without added information reside here + self.templates_path = pathlib.Path( + self._workspace, "tools", "lint", "perfdocs", "templates" + ) + self.perfdocs_path = pathlib.Path( + self._workspace, "testing", "perfdocs", "generated" + ) + + self._generate = generate + self._verifier = verifier + self._perfdocs_tree = self._verifier._gatherer.perfdocs_tree + + def build_perfdocs_from_tree(self): + """ + Builds up a document for each framework that was found. + + :return dict: A dictionary containing a mapping from each framework + to the document that was built for it, i.e: + { + framework_name: framework_document, + ... + } + """ + + # Using the verified `perfdocs_tree`, build up the documentation. + frameworks_info = {} + for framework in self._perfdocs_tree: + yaml_content = read_yaml(pathlib.Path(framework["path"], framework["yml"])) + rst_content = read_file( + pathlib.Path(framework["path"], framework["rst"]), stringify=True + ) + + # Gather all tests and descriptions and format them into + # documentation content + documentation = [] + suites = yaml_content["suites"] + for suite_name in sorted(suites.keys()): + suite_info = suites[suite_name] + + # Add the suite section + documentation.extend( + self._verifier._gatherer.framework_gatherers[ + yaml_content["name"] + ].build_suite_section(suite_name, suite_info["description"]) + ) + + tests = suite_info.get("tests", {}) + for test_name in sorted(tests.keys()): + gatherer = self._verifier._gatherer.framework_gatherers[ + yaml_content["name"] + ] + test_description = gatherer.build_test_description( + test_name, tests[test_name], suite_name + ) + documentation.extend(test_description) + documentation.append("") + + # Insert documentation into `.rst` file + framework_rst = re.sub( + r"{documentation}", "\n".join(documentation), rst_content + ) + frameworks_info[yaml_content["name"]] = { + "dynamic": framework_rst, + "static": [], + } + + # For static `.rst` file + for static_file in framework["static"]: + if static_file.endswith("rst"): + frameworks_info[yaml_content["name"]]["static"].append( + { + "file": static_file, + "content": read_file( + pathlib.Path(framework["path"], static_file), + stringify=True, + ), + } + ) + else: + frameworks_info[yaml_content["name"]]["static"].append( + { + "file": static_file, + "content": pathlib.Path(framework["path"], static_file), + } + ) + + return frameworks_info + + def _create_temp_dir(self): + """ + Create a temp directory as preparation of saving the documentation tree. + :return: str the location of perfdocs_tmpdir + """ + # Build the directory that will contain the final result (a tmp dir + # that will be moved to the final location afterwards) + try: + tmpdir = pathlib.Path(tempfile.mkdtemp()) + perfdocs_tmpdir = pathlib.Path(tmpdir, "generated") + perfdocs_tmpdir.mkdir(parents=True, exist_ok=True) + perfdocs_tmpdir.chmod(0o766) + except OSError as e: + logger.critical("Error creating temp file: {}".format(e)) + + if perfdocs_tmpdir.is_dir(): + return perfdocs_tmpdir + return False + + def _create_perfdocs(self): + """ + Creates the perfdocs documentation. + :return: str path of the temp dir it is saved in + """ + # All directories that are kept in the perfdocs tree are valid, + # so use it to build up the documentation. + framework_docs = self.build_perfdocs_from_tree() + perfdocs_tmpdir = self._create_temp_dir() + + # Save the documentation files + frameworks = [] + for framework_name in sorted(framework_docs.keys()): + frameworks.append(framework_name) + save_file( + framework_docs[framework_name]["dynamic"], + pathlib.Path(perfdocs_tmpdir, framework_name), + ) + + for static_name in framework_docs[framework_name]["static"]: + if static_name["file"].endswith(".rst"): + # XXX Replace this with a shutil.copy call (like below) + save_file( + static_name["content"], + pathlib.Path( + perfdocs_tmpdir, static_name["file"].split(".")[0] + ), + ) + else: + shutil.copy( + static_name["content"], + pathlib.Path(perfdocs_tmpdir, static_name["file"]), + ) + + # Get the main page and add the framework links to it + mainpage = read_file( + pathlib.Path(self.templates_path, "index.rst"), stringify=True + ) + + fmt_frameworks = "\n".join([" * :doc:`%s`" % name for name in frameworks]) + fmt_toctree = "\n".join([" %s" % name for name in frameworks]) + + fmt_mainpage = re.sub(r"{toctree_documentation}", fmt_toctree, mainpage) + fmt_mainpage = re.sub(r"{test_documentation}", fmt_frameworks, fmt_mainpage) + + save_file(fmt_mainpage, pathlib.Path(perfdocs_tmpdir, "index")) + + return perfdocs_tmpdir + + def _save_perfdocs(self, perfdocs_tmpdir): + """ + Copies the perfdocs tree after it was saved into the perfdocs_tmpdir + :param perfdocs_tmpdir: str location of the temp dir where the + perfdocs was saved + """ + # Remove the old docs and copy the new version there without + # checking if they need to be regenerated. + logger.log("Regenerating perfdocs...") + + if self.perfdocs_path.exists(): + shutil.rmtree(str(self.perfdocs_path)) + + try: + saved = shutil.copytree(str(perfdocs_tmpdir), str(self.perfdocs_path)) + if saved: + logger.log( + "Documentation saved to {}/".format( + re.sub(".*testing", "testing", str(self.perfdocs_path)) + ) + ) + except Exception as e: + logger.critical( + "There was an error while saving the documentation: {}".format(e) + ) + + def generate_perfdocs(self): + """ + Generate the performance documentation. + + If `self._generate` is True, then the documentation will be regenerated + without any checks. Otherwise, if it is False, the new documentation will be + prepare and compare with the existing documentation to determine if + it should be regenerated. + + :return bool: True/False - For True, if `self._generate` is True, then the + docs were regenerated. If `self._generate` is False, then True will mean + that the docs should be regenerated, and False means that they do not + need to be regenerated. + """ + + def get_possibly_changed_files(): + """ + Returns files that might have been modified + (used to output a linter warning for regeneration) + :return: list - files that might have been modified + """ + # Returns files that might have been modified + # (used to output a linter warning for regeneration) + files = [] + for entry in self._perfdocs_tree: + files.extend( + [ + pathlib.Path(entry["path"], entry["yml"]), + pathlib.Path(entry["path"], entry["rst"]), + ] + ) + return files + + # Throw a warning if there's no need for generating + if not self.perfdocs_path.exists() and not self._generate: + # If they don't exist and we are not generating, then throw + # a linting error and exit. + logger.warning( + "PerfDocs need to be regenerated.", files=get_possibly_changed_files() + ) + return True + + perfdocs_tmpdir = self._create_perfdocs() + if self._generate: + self._save_perfdocs(perfdocs_tmpdir) + else: + # If we are not generating, then at least check if they + # should be regenerated by comparing the directories. + if not are_dirs_equal(perfdocs_tmpdir, self.perfdocs_path): + logger.warning( + "PerfDocs are outdated, run ./mach lint -l perfdocs --fix .` " + + "to update them. You can also apply the " + + f"{'perfdocs.diff' if ON_TRY else 'diff.txt'} patch file " + + f"{'produced from this reviewbot test ' if ON_TRY else ''}" + + "to fix the issue.", + files=get_changed_files(self._workspace), + restricted=False, + ) diff --git a/tools/lint/perfdocs/logger.py b/tools/lint/perfdocs/logger.py new file mode 100644 index 0000000000..ba02532c32 --- /dev/null +++ b/tools/lint/perfdocs/logger.py @@ -0,0 +1,95 @@ +# 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 pathlib + + +class PerfDocLogger(object): + """ + Logger for the PerfDoc tooling. Handles the warnings by outputting + them into through the StructuredLogger provided by lint. + """ + + TOP_DIR = "" + PATHS = [] + LOGGER = None + FAILED = False + + def __init__(self): + """Initializes the PerfDocLogger.""" + + # Set up class attributes for all logger instances + if not PerfDocLogger.LOGGER: + raise Exception( + "Missing linting LOGGER instance for PerfDocLogger initialization" + ) + if not PerfDocLogger.PATHS: + raise Exception("Missing PATHS for PerfDocLogger initialization") + self.logger = PerfDocLogger.LOGGER + + def log(self, msg): + """ + Log an info message. + + :param str msg: Message to log. + """ + self.logger.info(msg) + + def warning(self, msg, files, restricted=True): + """ + Logs a validation warning message. The warning message is + used as the error message that is output in the reviewbot. + + :param str msg: Message to log, it's also used as the error message + for the issue that is output by the reviewbot. + :param list/str files: The file(s) that this warning is about. + :param boolean restricted: If the param is False, the lint error can be used anywhere. + """ + if type(files) != list: + files = [files] + + if len(files) == 0: + self.logger.info("No file was provided for the warning") + self.logger.lint_error( + message=msg, + lineno=0, + column=None, + path=None, + linter="perfdocs", + rule="Flawless performance docs (unknown file)", + ) + + PerfDocLogger.FAILED = True + return + + # Add a reviewbot error for each file that is given + for file in files: + # Get a relative path (reviewbot can't handle absolute paths) + fpath = str(file).replace(str(PerfDocLogger.TOP_DIR), "") + + # Filter out any issues that do not relate to the paths + # that are being linted + for path in PerfDocLogger.PATHS: + if restricted and str(path) not in str(file): + continue + + # Output error entry + self.logger.lint_error( + message=msg, + lineno=0, + column=None, + path=str(pathlib.PurePosixPath(fpath)), + linter="perfdocs", + rule="Flawless performance docs.", + ) + + PerfDocLogger.FAILED = True + break + + def critical(self, msg): + """ + Log a critical message. + + :param str msg: Message to log. + """ + self.logger.critical(msg) diff --git a/tools/lint/perfdocs/perfdocs.py b/tools/lint/perfdocs/perfdocs.py new file mode 100644 index 0000000000..b41edb1979 --- /dev/null +++ b/tools/lint/perfdocs/perfdocs.py @@ -0,0 +1,95 @@ +# 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 os +import pathlib + + +def run_perfdocs(config, logger=None, paths=None, generate=True): + """ + Build up performance testing documentation dynamically by combining + text data from YAML files that reside in `perfdoc` folders + across the `testing` directory. Each directory is expected to have + an `index.rst` file along with `config.yml` YAMLs defining what needs + to be added to the documentation. + + The YAML must also define the name of the "framework" that should be + used in the main index.rst for the performance testing documentation. + + The testing documentation list will be ordered alphabetically once + it's produced (to avoid unwanted shifts because of unordered dicts + and path searching). + + Note that the suite name headings will be given the H4 (---) style so it + is suggested that you use H3 (===) style as the heading for your + test section. H5 will be used be used for individual tests within each + suite. + + Usage for verification: "./mach lint -l perfdocs ." + Usage for generation: "./mach lint -l perfdocs --fix ." + + For validation, see the Verifier class for a description of how + it works. + + The run will fail if the valid result from validate_tree is not + False, implying some warning/problem was logged. + + :param dict config: The configuration given by mozlint. + :param StructuredLogger logger: The StructuredLogger instance to be used to + output the linting warnings/errors. + :param list paths: The paths that are being tested. Used to filter + out errors from files outside of these paths. + :param bool generate: If true, the docs will be (re)generated. + """ + from perfdocs.logger import PerfDocLogger + + if not os.environ.get("WORKSPACE", None): + floc = pathlib.Path(__file__).absolute() + top_dir = pathlib.Path(str(floc).split("tools")[0]).resolve() + else: + top_dir = pathlib.Path(os.environ.get("WORKSPACE")).resolve() + + PerfDocLogger.LOGGER = logger + PerfDocLogger.TOP_DIR = top_dir + + # Convert all the paths to relative ones + target_dir = [pathlib.Path(path) for path in paths] + rel_paths = [] + for path in target_dir: + try: + rel_paths.append(path.relative_to(top_dir)) + except ValueError: + rel_paths.append(path) + + PerfDocLogger.PATHS = rel_paths + + for path in target_dir: + if not path.exists(): + raise Exception("Cannot locate directory at %s" % str(path)) + + decision_task_id = os.environ.get("DECISION_TASK_ID", None) + if decision_task_id: + from taskgraph.util.taskcluster import get_artifact + + task_graph = get_artifact(decision_task_id, "public/full-task-graph.json") + else: + from tryselect.tasks import generate_tasks + + task_graph = generate_tasks( + params=None, full=True, disable_target_task_filter=True + ).tasks + + # Late import because logger isn't defined until later + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + # Run the verifier first + verifier = Verifier(top_dir, task_graph) + verifier.validate_tree() + + if not PerfDocLogger.FAILED: + # Even if the tree is valid, we need to check if the documentation + # needs to be regenerated, and if it does, we throw a linting error. + # `generate` dictates whether or not the documentation is generated. + generator = Generator(verifier, generate=generate, workspace=top_dir) + generator.generate_perfdocs() diff --git a/tools/lint/perfdocs/templates/index.rst b/tools/lint/perfdocs/templates/index.rst new file mode 100644 index 0000000000..d2d82f6328 --- /dev/null +++ b/tools/lint/perfdocs/templates/index.rst @@ -0,0 +1,86 @@ +################### +Performance Testing +################### + +.. toctree:: + :maxdepth: 2 + :hidden: + :glob: + +{toctree_documentation} + +Performance tests are designed to catch performance regressions before they reach our +end users. At this time, there is no unified approach for these types of tests, +but `mozperftest </testing/perfdocs/mozperftest.html>`_ aims to provide this in the future. + +For more detailed information about each test suite and project, see their documentation: + +{test_documentation} + + +Here are the active PerfTest components/modules and their respective owners: + + * AWFY (Are We Fast Yet) - + - Owner: Beatrice A. + - Description: A public dashboard comparing Firefox and Chrome performance metrics + * AWSY (Are We Slim Yet) + - Owner: Alexandru I. + - Description: Project that tracks memory usage across builds + * Raptor + - Owner: Sparky + - Co-owner: Kash + - Description: Test harness that uses Browsertime (based on webdriver) as the underlying engine to run performance tests + * CondProf (Conditioned profiles) + - Owner: Sparky + - Co-owner: Jmaher + - Description: Provides tooling to build, and obtain profiles that are preconditioned in some way. + * fxrecord + - Owner: Sparky + - Co-owners: Kash, Andrej + - Description: Tool for measuring startup performance for Firefox Desktop + * Infrastructure + - Owner: Sparky + - Co-owners: Kash, Andrej + - Description: All things involving: TaskCluster, Youtube Playback, Bitbar, Mobile Configs, etc... + * Mozperftest + - Owner: Sparky + - Co-owners: Kash, Andrej + - Description: Testing framework used to run performance tests + * Mozperftest Tools + - Owner: Sparky + - Co-owner: Alexandru I. + - Description: Various tools used by performance testing team + * Mozproxy + - Owner: Sparky + - Co-owner: Kash + - Description: An http proxy used to run tests against third-party websites in a reliable and reproducible way + * PerfCompare + - Owner: Carla S. + - Co-owner: Beatrice A. + - Description: Performance comparison tool used to compare performance of different commits within a repository + * PerfDocs + - Owner: Sparky + - Co-owner: Alexandru I. + - Description: Automatically generated performance test engineering documentation + * PerfHerder + - Owner: Beatrice A + - Co-owner: Andra A. + - Description: The framework used by the performance sheriffs to find performance regressions and for storing, and visualizing our performance data. + * Performance Sheriffing + - Owner: Alexandru I. + - Co-owner: Andra A. + - Description: Performance sheriffs are responsible for finding commits that cause performance regressions and getting fixes from devs or backing out the changes + * Talos + - Owner: Sparky + - Co-owner: Andrej + - Description: Testing framework used to run Firefox-specific performance tests + * WebPageTest + - Owner: Andrej + - Co-owner: Sparky + - Description: A test running in the mozperftest framework used as a third party performance benchmark + +You can additionally reach out to our team on +the `#perftest <https://matrix.to/#/#perftest:mozilla.org>`_ channel on matrix + +For more information about the performance testing team, +`visit the wiki page <https://wiki.mozilla.org/TestEngineering/Performance>`_. diff --git a/tools/lint/perfdocs/utils.py b/tools/lint/perfdocs/utils.py new file mode 100644 index 0000000000..1ba7daeb52 --- /dev/null +++ b/tools/lint/perfdocs/utils.py @@ -0,0 +1,157 @@ +# 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 difflib +import filecmp +import os +import pathlib + +import yaml +from mozversioncontrol import get_repository_object + +from perfdocs.logger import PerfDocLogger + +logger = PerfDocLogger() + +ON_TRY = "MOZ_AUTOMATION" in os.environ + + +def save_file(file_content, path, extension="rst"): + """ + Saves data into a file. + + :param str path: Location and name of the file being saved + (without an extension). + :param str data: Content to write into the file. + :param str extension: Extension to save the file as. + """ + new_file = pathlib.Path("{}.{}".format(str(path), extension)) + with new_file.open("wb") as f: + f.write(file_content.encode("utf-8")) + + +def read_file(path, stringify=False): + """ + Opens a file and returns its contents. + + :param str path: Path to the file. + :return list: List containing the lines in the file. + """ + with path.open(encoding="utf-8") as f: + return f.read() if stringify else f.readlines() + + +def read_yaml(yaml_path): + """ + Opens a YAML file and returns the contents. + + :param str yaml_path: Path to the YAML to open. + :return dict: Dictionary containing the YAML content. + """ + contents = {} + try: + with yaml_path.open(encoding="utf-8") as f: + contents = yaml.safe_load(f) + except Exception as e: + logger.warning( + "Error opening file {}: {}".format(str(yaml_path), str(e)), str(yaml_path) + ) + + return contents + + +def are_dirs_equal(dir_1, dir_2): + """ + Compare two directories to see if they are equal. Files in each + directory are assumed to be equal if their names and contents + are equal. + + :param dir_1: First directory path + :param dir_2: Second directory path + :return: True if the directory trees are the same and False otherwise. + """ + + dirs_cmp = filecmp.dircmp(str(dir_1.resolve()), str(dir_2.resolve())) + if dirs_cmp.left_only or dirs_cmp.right_only or dirs_cmp.funny_files: + logger.log("Some files are missing or are funny.") + for file in dirs_cmp.left_only: + logger.log(f"Missing in existing docs: {file}") + for file in dirs_cmp.right_only: + logger.log(f"Missing in new docs: {file}") + for file in dirs_cmp.funny_files: + logger.log(f"The following file is funny: {file}") + return False + + _, mismatch, errors = filecmp.cmpfiles( + str(dir_1.resolve()), str(dir_2.resolve()), dirs_cmp.common_files, shallow=False + ) + + if mismatch or errors: + logger.log(f"Found mismatches: {mismatch}") + + # The root for where to save the diff will be different based on + # whether we are running in CI or not + os_root = pathlib.Path.cwd().anchor + diff_root = pathlib.Path(os_root, "builds", "worker") + if not ON_TRY: + diff_root = pathlib.Path(PerfDocLogger.TOP_DIR, "artifacts") + diff_root.mkdir(parents=True, exist_ok=True) + + diff_path = pathlib.Path(diff_root, "diff.txt") + with diff_path.open("w", encoding="utf-8") as diff_file: + for entry in mismatch: + logger.log(f"Mismatch found on {entry}") + + with pathlib.Path(dir_1, entry).open(encoding="utf-8") as f: + newlines = f.readlines() + with pathlib.Path(dir_2, entry).open(encoding="utf-8") as f: + baselines = f.readlines() + for line in difflib.unified_diff( + baselines, newlines, fromfile="base", tofile="new" + ): + logger.log(line) + + # Here we want to add to diff.txt in a patch format, we use + # the basedir to make the file names/paths relative and this is + # different in CI vs local runs. + basedir = pathlib.Path( + os_root, "builds", "worker", "checkouts", "gecko" + ) + if not ON_TRY: + basedir = diff_root + + relative_path = str(pathlib.Path(dir_2, entry)).split(str(basedir))[-1] + patch = difflib.unified_diff( + baselines, newlines, fromfile=relative_path, tofile=relative_path + ) + + write_header = True + for line in patch: + if write_header: + diff_file.write( + f"diff --git a/{relative_path} b/{relative_path}\n" + ) + write_header = False + diff_file.write(line) + + logger.log(f"Completed diff on {entry}") + + logger.log(f"Saved diff to {diff_path}") + + return False + + for common_dir in dirs_cmp.common_dirs: + subdir_1 = pathlib.Path(dir_1, common_dir) + subdir_2 = pathlib.Path(dir_2, common_dir) + if not are_dirs_equal(subdir_1, subdir_2): + return False + + return True + + +def get_changed_files(top_dir): + """ + Returns the changed files found with duplicates removed. + """ + repo = get_repository_object(top_dir) + return list(set(repo.get_changed_files() + repo.get_outgoing_files())) diff --git a/tools/lint/perfdocs/verifier.py b/tools/lint/perfdocs/verifier.py new file mode 100644 index 0000000000..6603b9acce --- /dev/null +++ b/tools/lint/perfdocs/verifier.py @@ -0,0 +1,601 @@ +# 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 os +import pathlib +import re + +import jsonschema + +from perfdocs.gatherer import Gatherer +from perfdocs.logger import PerfDocLogger +from perfdocs.utils import read_file, read_yaml + +logger = PerfDocLogger() + +""" +Schema for the config.yml file. +Expecting a YAML file with a format such as this: + +name: raptor +manifest: testing/raptor/raptor/raptor.toml +static-only: False +suites: + desktop: + description: "Desktop tests." + tests: + raptor-tp6: "Raptor TP6 tests." + mobile: + description: "Mobile tests" + benchmarks: + description: "Benchmark tests." + tests: + wasm: "All wasm tests." + +""" +CONFIG_SCHEMA = { + "definitions": { + "metrics_schema": { + "metric_name": { + "type": "object", + "properties": { + "aliases": {"type": "array", "items": {"type": "string"}}, + "description": {"type": "string"}, + "matcher": {"type": "string"}, + }, + "required": ["description", "aliases"], + }, + }, + }, + "type": "object", + "properties": { + "name": {"type": "string"}, + "manifest": {"type": "string"}, + "static-only": {"type": "boolean"}, + "metrics": {"$ref": "#/definitions/metrics_schema"}, + "suites": { + "type": "object", + "properties": { + "suite_name": { + "type": "object", + "properties": { + "tests": { + "type": "object", + "properties": { + "test_name": {"type": "string"}, + "metrics": {"$ref": "#/definitions/metrics_schema"}, + }, + }, + "description": {"type": "string"}, + "owner": {"type": "string"}, + "metrics": {"$ref": "#/definitions/metrics_schema"}, + }, + "required": ["description"], + } + }, + }, + }, + "required": ["name", "manifest", "static-only", "suites"], +} + + +class Verifier(object): + """ + Verifier is used for validating the perfdocs folders/tree. In the future, + the generator will make use of this class to obtain a validated set of + descriptions that can be used to build up a document. + """ + + def __init__(self, workspace_dir, taskgraph=None): + """ + Initialize the Verifier. + + :param str workspace_dir: Path to the top-level checkout directory. + """ + self.workspace_dir = workspace_dir + self._gatherer = Gatherer(workspace_dir, taskgraph) + self._compiled_matchers = {} + + def _is_yaml_test_match( + self, target_test_name, test_name, suite="", global_descriptions={} + ): + """Determine if a target name (from a YAML) matches with a test.""" + tb = os.path.basename(target_test_name) + tb = re.sub("\..*", "", tb) + if test_name == tb: + # Found an exact match for the test_name + return True + if test_name in tb: + # Found a 'fuzzy' match for the test_name + # i.e. 'wasm' could exist for all raptor wasm tests + global_descriptions.setdefault(suite, []).append(test_name) + return True + + def _validate_desc_yaml_direction( + self, suite, framework_info, yaml_content, global_descriptions + ): + """Validate the descriptions in the YAML. + + This validation ensures that all tests defined in the YAML exist in the test + harness. Failures here suggest that there's a typo in the YAML or that + a test was removed. + """ + ytests = yaml_content["suites"][suite] + global_descriptions[suite] = [] + if not ytests.get("tests"): + # It's possible a suite entry has no tests + return True + + # Suite found - now check if any tests in YAML + # definitions don't exist + ytests = ytests["tests"] + for test_name in ytests: + foundtest = False + for t in framework_info["test_list"][suite]: + if self._is_yaml_test_match( + t, test_name, suite=suite, global_descriptions=global_descriptions + ): + foundtest = True + break + if not foundtest: + logger.warning( + "Could not find an existing test for {} - bad test name?".format( + test_name + ), + framework_info["yml_path"], + ) + return False + + def _validate_desc_harness_direction( + self, suite, test_list, yaml_content, global_descriptions + ): + """Validate that the tests have a description in the YAML. + + This stage of validation ensures that all the tests have some + form of description, or that global descriptions are available. + Failures here suggest a new test was added, or the config.yml + file was changed. + """ + # If only a description is provided for the suite, assume + # that this is a suite-wide description and don't check for + # it's tests + stests = yaml_content["suites"][suite].get("tests", None) + if not stests: + return + + tests_found = 0 + missing_tests = [] + test_to_manifest = {} + for test_name, test_info in test_list.items(): + manifest_path = test_info.get("path", test_info.get("manifest", "")) + tb = os.path.basename(manifest_path) + tb = re.sub("\..*", "", tb) + if ( + stests.get(tb, None) is not None + or stests.get(test_name, None) is not None + ): + # Test description exists, continue with the next test + tests_found += 1 + continue + test_to_manifest[test_name] = manifest_path + missing_tests.append(test_name) + + # Check if global test descriptions exist (i.e. + # ones that cover all of tp6) for the missing tests + new_mtests = [] + for mt in missing_tests: + found = False + for test_name in global_descriptions[suite]: + # Global test exists for this missing test + if mt.startswith(test_name): + found = True + break + if test_name in mt: + found = True + break + if not found: + new_mtests.append(mt) + + if len(new_mtests): + # Output an error for each manifest with a missing + # test description + for test_name in new_mtests: + logger.warning( + "Could not find a test description for {}".format(test_name), + test_to_manifest[test_name], + ) + + def _match_metrics(self, target_metric_name, target_metric_info, measured_metrics): + """Find all metrics that match the given information. + + It either checks for the metric through a direct equality check, and if + a regex matcher was provided, we will use that afterwards. + """ + verified_metrics = [] + + metric_names = target_metric_info["aliases"] + [target_metric_name] + for measured_metric in measured_metrics: + if measured_metric in metric_names: + verified_metrics.append(measured_metric) + + if target_metric_info.get("matcher", ""): + # Compile the regex separately to capture issues in the regex + # compilation + matcher = self._compiled_matchers.get(target_metric_name, None) + if not matcher: + matcher = re.compile(target_metric_info.get("matcher")) + self._compiled_matchers[target_metric_name] = matcher + + # Search the measured metrics + for measured_metric in measured_metrics: + if matcher.search(measured_metric): + verified_metrics.append(measured_metric) + + return verified_metrics + + def _validate_metrics_yaml_direction( + self, suite, framework_info, yaml_content, global_metrics + ): + """Validate the metric descriptions in the YAML. + + This direction (`yaml_direction`) checks that the YAML definitions exist in + the test harness as real metrics. Failures here suggest that a metric + changed name, is missing an alias, is misnamed, duplicated, or was removed. + """ + yaml_suite = yaml_content["suites"][suite] + suite_metrics = yaml_suite.get("metrics", {}) + + # Check to make sure all the metrics with given descriptions + # are actually being measured. Add the metric to the "verified" field in + # global_metrics to use it later for "global" metrics that can + # have their descriptions removed. Start from the test level. + for test_name, test_info in yaml_suite.get("tests", {}).items(): + if not isinstance(test_info, dict): + continue + test_metrics_info = test_info.get("metrics", {}) + + # Find all tests that match with this name in case they measure + # different things + measured_metrics = [] + for t in framework_info["test_list"][suite]: + if not self._is_yaml_test_match(t, test_name): + # Check to make sure we are checking against the right + # test. Skip the metric check if we can't find the test. + continue + measured_metrics.extend( + framework_info["test_list"][suite][t].get("metrics", []) + ) + + if len(measured_metrics) == 0: + continue + + # Check if all the test metrics documented exist + for metric_name, metric_info in test_metrics_info.items(): + verified_metrics = self._match_metrics( + metric_name, metric_info, measured_metrics + ) + if len(verified_metrics) > 0: + global_metrics["yaml-verified"].extend( + [metric_name] + metric_info["aliases"] + ) + global_metrics["verified"].extend( + [metric_name] + metric_info["aliases"] + verified_metrics + ) + else: + logger.warning( + ( + "Cannot find documented metric `{}` " + "being used in the specified test `{}`." + ).format(metric_name, test_name), + framework_info["yml_path"], + ) + + # Check the suite level now + for suite_metric_name, suite_metric_info in suite_metrics.items(): + measured_metrics = [] + for _, test_info in framework_info["test_list"][suite].items(): + measured_metrics.extend(test_info.get("metrics", [])) + + verified_metrics = self._match_metrics( + suite_metric_name, suite_metric_info, measured_metrics + ) + if len(verified_metrics) > 0: + global_metrics["yaml-verified"].extend( + [suite_metric_name] + suite_metric_info["aliases"] + ) + global_metrics["verified"].extend( + [suite_metric_name] + + suite_metric_info["aliases"] + + verified_metrics + ) + else: + logger.warning( + ( + "Cannot find documented metric `{}` " + "being used in the specified suite `{}`." + ).format(suite_metric_name, suite), + framework_info["yml_path"], + ) + + # Finally check the global level (output failures later) + all_measured_metrics = [] + for _, test_info in framework_info["test_list"][suite].items(): + all_measured_metrics.extend(test_info.get("metrics", [])) + for global_metric_name, global_metric_info in global_metrics["global"].items(): + verified_metrics = self._match_metrics( + global_metric_name, global_metric_info, all_measured_metrics + ) + if global_metric_info.get("verified", False): + # We already verified this global metric, but add any + # extra verified metrics here + global_metrics["verified"].extend(verified_metrics) + continue + if len(verified_metrics) > 0: + global_metric_info["verified"] = True + global_metrics["yaml-verified"].extend( + [global_metric_name] + global_metric_info["aliases"] + ) + global_metrics["verified"].extend( + [global_metric_name] + + global_metric_info["aliases"] + + verified_metrics + ) + + def _validate_metrics_harness_direction( + self, suite, test_list, yaml_content, global_metrics + ): + """Validate that metrics in the harness are documented.""" + # Gather all the metrics being measured + all_measured_metrics = {} + for test_name, test_info in test_list.items(): + metrics = test_info.get("metrics", []) + for metric in metrics: + all_measured_metrics.setdefault(metric, []).append(test_name) + + if len(all_measured_metrics) == 0: + # There are no metrics measured by this suite + return + + for metric, tests in all_measured_metrics.items(): + if metric not in global_metrics["verified"]: + # Log a warning in all files that have this metric + for test in tests: + logger.warning( + "Missing description for the metric `{}` in test `{}`".format( + metric, test + ), + test_list[test].get( + "path", test_list[test].get("manifest", "") + ), + ) + + def validate_descriptions(self, framework_info): + """ + Cross-validate the tests found in the manifests and the YAML + test definitions. This function doesn't return a valid flag. Instead, + the StructDocLogger.VALIDATION_LOG is used to determine validity. + + The validation proceeds as follows: + 1. Check that all tests/suites in the YAML exist in the manifests. + - At the same time, build a list of global descriptions which + define descriptions for groupings of tests. + 2. Check that all tests/suites found in the manifests exist in the YAML. + - For missing tests, check if a global description for them exists. + + As the validation is completed, errors are output into the validation log + for any issues that are found. + + The same is done for the metrics field expect it also has regex matching, + and the definitions cannot be duplicated in a single harness. We make use + of two `*verified` fields to simplify the two stages/directions, and checking + for any duplication. + + :param dict framework_info: Contains information about the framework. See + `Gatherer.get_test_list` for information about its structure. + """ + yaml_content = framework_info["yml_content"] + + # Check for any bad test/suite names in the yaml config file + # TODO: Combine global settings into a single dictionary + global_descriptions = {} + global_metrics = { + "global": yaml_content.get("metrics", {}), + "verified": [], + "yaml-verified": [], + } + for suite, ytests in yaml_content["suites"].items(): + # Find the suite, then check against the tests within it + if framework_info["test_list"].get(suite, None) is None: + logger.warning( + "Could not find an existing suite for {} - bad suite name?".format( + suite + ), + framework_info["yml_path"], + ) + continue + + # Validate descriptions + self._validate_desc_yaml_direction( + suite, framework_info, yaml_content, global_descriptions + ) + + # Validate metrics + self._validate_metrics_yaml_direction( + suite, framework_info, yaml_content, global_metrics + ) + + # The suite and test levels were properly checked, but we can only + # check the global level after all suites were checked. If the metric + # isn't in the verified + for global_metric_name, _ in global_metrics["global"].items(): + if global_metric_name not in global_metrics["verified"]: + logger.warning( + ( + "Cannot find documented metric `{}` " + "being used in the specified harness `{}`." + ).format(global_metric_name, yaml_content["name"]), + framework_info["yml_path"], + ) + + # Check for duplicate metrics/aliases in the verified metrics + unique_metrics = set() + warned = set() + for metric in global_metrics["yaml-verified"]: + if ( + metric in unique_metrics or unique_metrics.add(metric) + ) and metric not in warned: + logger.warning( + "Duplicate definitions found for `{}`.".format(metric), + framework_info["yml_path"], + ) + warned.add(metric) + + # Check for duplicate metrics in the global level + unique_metrics = set() + warned = set() + for metric, metric_info in global_metrics["global"].items(): + if ( + metric in unique_metrics or unique_metrics.add(metric) + ) and metric not in warned: + logger.warning( + "Duplicate definitions found for `{}`.".format(metric), + framework_info["yml_path"], + ) + for alias in metric_info.get("aliases", []): + unique_metrics.add(alias) + warned.add(alias) + warned.add(metric) + + # Check for any missing tests/suites + for suite, test_list in framework_info["test_list"].items(): + if not yaml_content["suites"].get(suite): + # Description doesn't exist for the suite + logger.warning( + "Missing suite description for {}".format(suite), + yaml_content["manifest"], + ) + continue + + self._validate_desc_harness_direction( + suite, test_list, yaml_content, global_descriptions + ) + + self._validate_metrics_harness_direction( + suite, test_list, yaml_content, global_metrics + ) + + def validate_yaml(self, yaml_path): + """ + Validate that the YAML file has all the fields that are + required and parse the descriptions into strings in case + some are give as relative file paths. + + :param str yaml_path: Path to the YAML to validate. + :return bool: True/False => Passed/Failed Validation + """ + + def _get_description(desc): + """ + Recompute the description in case it's a file. + """ + desc_path = pathlib.Path(self.workspace_dir, desc) + + try: + if desc_path.exists() and desc_path.is_file(): + with open(desc_path, "r") as f: + desc = f.readlines() + except OSError: + pass + + return desc + + def _parse_descriptions(content): + for suite, sinfo in content.items(): + desc = sinfo["description"] + sinfo["description"] = _get_description(desc) + + # It's possible that the suite has no tests and + # only a description. If they exist, then parse them. + if "tests" in sinfo: + for test, desc in sinfo["tests"].items(): + sinfo["tests"][test] = _get_description(desc) + + valid = False + yaml_content = read_yaml(yaml_path) + + try: + jsonschema.validate(instance=yaml_content, schema=CONFIG_SCHEMA) + _parse_descriptions(yaml_content["suites"]) + valid = True + except Exception as e: + logger.warning("YAML ValidationError: {}".format(str(e)), yaml_path) + + return valid + + def validate_rst_content(self, rst_path): + """ + Validate that the index file given has a {documentation} entry + so that the documentation can be inserted there. + + :param str rst_path: Path to the RST file. + :return bool: True/False => Passed/Failed Validation + """ + rst_content = read_file(rst_path) + + # Check for a {documentation} entry in some line, + # if we can't find one, then the validation fails. + valid = False + docs_match = re.compile(".*{documentation}.*") + for line in rst_content: + if docs_match.search(line): + valid = True + break + if not valid: + logger.warning( # noqa: PLE1205 + "Cannot find a '{documentation}' entry in the given index file", + rst_path, + ) + + return valid + + def _check_framework_descriptions(self, item): + """ + Helper method for validating descriptions + """ + framework_info = self._gatherer.get_test_list(item) + self.validate_descriptions(framework_info) + + def validate_tree(self): + """ + Validate the `perfdocs` directory that was found. + Returns True if it is good, false otherwise. + + :return bool: True/False => Passed/Failed Validation + """ + found_good = 0 + + # For each framework, check their files and validate descriptions + for matched in self._gatherer.perfdocs_tree: + # Get the paths to the YAML and RST for this framework + matched_yml = pathlib.Path(matched["path"], matched["yml"]) + matched_rst = pathlib.Path(matched["path"], matched["rst"]) + + _valid_files = { + "yml": self.validate_yaml(matched_yml), + "rst": True, + } + if not read_yaml(matched_yml)["static-only"]: + _valid_files["rst"] = self.validate_rst_content(matched_rst) + + # Log independently the errors found for the matched files + for file_format, valid in _valid_files.items(): + if not valid: + logger.log("File validation error: {}".format(file_format)) + if not all(_valid_files.values()): + continue + found_good += 1 + + self._check_framework_descriptions(matched) + + if not found_good: + raise Exception("No valid perfdocs directories found") diff --git a/tools/lint/python/__init__.py b/tools/lint/python/__init__.py new file mode 100644 index 0000000000..c580d191c1 --- /dev/null +++ b/tools/lint/python/__init__.py @@ -0,0 +1,3 @@ +# 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/. diff --git a/tools/lint/python/black.py b/tools/lint/python/black.py new file mode 100644 index 0000000000..1e38dd669c --- /dev/null +++ b/tools/lint/python/black.py @@ -0,0 +1,168 @@ +# 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 os +import platform +import re +import signal +import subprocess +import sys + +import mozpack.path as mozpath +from mozfile import which +from mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) +BLACK_REQUIREMENTS_PATH = os.path.join(here, "black_requirements.txt") + +BLACK_INSTALL_ERROR = """ +Unable to install correct version of black +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + BLACK_REQUIREMENTS_PATH +) + + +def default_bindir(): + # We use sys.prefix to find executables as that gets modified with + # virtualenv's activate_this.py, whereas sys.executable doesn't. + if platform.system() == "Windows": + return os.path.join(sys.prefix, "Scripts") + else: + return os.path.join(sys.prefix, "bin") + + +def get_black_version(binary): + """ + Returns found binary's version + """ + try: + output = subprocess.check_output( + [binary, "--version"], + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + output = e.output + try: + # Accept `black.EXE, version ...` on Windows. + # for old version of black, the output is + # black, version 21.4b2 + # From black 21.11b1, the output is like + # black, 21.11b1 (compiled: no) + return re.match(r"black.*,( version)? (\S+)", output)[2] + except TypeError as e: + print("Could not parse the version '{}'".format(output)) + print("Error: {}".format(e)) + + +def parse_issues(config, output, paths, *, log): + would_reformat = re.compile("^would reformat (.*)$", re.I) + reformatted = re.compile("^reformatted (.*)$", re.I) + cannot_reformat = re.compile("^error: cannot format (.*?): (.*)$", re.I) + results = [] + for l in output.split(b"\n"): + line = l.decode("utf-8").rstrip("\r\n") + if line.startswith("All done!") or line.startswith("Oh no!"): + break + + match = would_reformat.match(line) + if match: + res = {"path": match.group(1), "level": "error"} + results.append(result.from_config(config, **res)) + continue + + match = reformatted.match(line) + if match: + res = {"path": match.group(1), "level": "warning", "message": "reformatted"} + results.append(result.from_config(config, **res)) + continue + + match = cannot_reformat.match(line) + if match: + res = {"path": match.group(1), "level": "error", "message": match.group(2)} + results.append(result.from_config(config, **res)) + continue + + log.debug(f"Unhandled line: {line}") + return results + + +def run_process(config, cmd): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + signal.signal(signal.SIGINT, orig) + try: + output, _ = proc.communicate() + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return output + + +def setup(root, **lintargs): + log = lintargs["log"] + virtualenv_bin_path = lintargs.get("virtualenv_bin_path") + # Using `which` searches multiple directories and handles `.exe` on Windows. + binary = which("black", path=(virtualenv_bin_path, default_bindir())) + + if binary and os.path.exists(binary): + binary = mozpath.normsep(binary) + log.debug("Looking for black at {}".format(binary)) + version = get_black_version(binary) + versions = [ + line.split()[0].strip() + for line in open(BLACK_REQUIREMENTS_PATH).readlines() + if line.startswith("black==") + ] + if ["black=={}".format(version)] == versions: + log.debug("Black is present with expected version {}".format(version)) + return 0 + else: + log.debug("Black is present but unexpected version {}".format(version)) + + log.debug("Black needs to be installed or updated") + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements(BLACK_REQUIREMENTS_PATH, quiet=True) + except subprocess.CalledProcessError: + print(BLACK_INSTALL_ERROR) + return 1 + + +def run_black(config, paths, fix=None, *, log, virtualenv_bin_path): + fixed = 0 + binary = os.path.join(virtualenv_bin_path or default_bindir(), "black") + + log.debug("Black version {}".format(get_black_version(binary))) + + cmd_args = [binary] + if not fix: + cmd_args.append("--check") + + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(base_command))) + output = parse_issues(config, run_process(config, base_command), paths, log=log) + + # black returns an issue for fixed files as well + for eachIssue in output: + if eachIssue.message == "reformatted": + fixed += 1 + + return {"results": output, "fixed": fixed} + + +def lint(paths, config, fix=None, **lintargs): + files = list(expand_exclusions(paths, config, lintargs["root"])) + + return run_black( + config, + files, + fix=fix, + log=lintargs["log"], + virtualenv_bin_path=lintargs.get("virtualenv_bin_path"), + ) diff --git a/tools/lint/python/black_requirements.in b/tools/lint/python/black_requirements.in new file mode 100644 index 0000000000..dfe5a54c7b --- /dev/null +++ b/tools/lint/python/black_requirements.in @@ -0,0 +1,5 @@ +black==23.3.0 +typing-extensions==3.10.0.2 +dataclasses==0.6 +typed-ast==1.4.2; python_version < '3.8' +pkgutil-resolve-name==1.3.10 ; python_version < '3.9' diff --git a/tools/lint/python/black_requirements.txt b/tools/lint/python/black_requirements.txt new file mode 100644 index 0000000000..9e89927775 --- /dev/null +++ b/tools/lint/python/black_requirements.txt @@ -0,0 +1,115 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --config=pyproject.toml --generate-hashes --output-file=tools/lint/python/black_requirements.txt ./tools/lint/python/black_requirements.in +# +black==23.3.0 \ + --hash=sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5 \ + --hash=sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915 \ + --hash=sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326 \ + --hash=sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940 \ + --hash=sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b \ + --hash=sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30 \ + --hash=sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c \ + --hash=sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c \ + --hash=sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab \ + --hash=sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27 \ + --hash=sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2 \ + --hash=sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961 \ + --hash=sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9 \ + --hash=sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb \ + --hash=sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70 \ + --hash=sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331 \ + --hash=sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2 \ + --hash=sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266 \ + --hash=sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d \ + --hash=sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6 \ + --hash=sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b \ + --hash=sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925 \ + --hash=sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8 \ + --hash=sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4 \ + --hash=sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3 + # via -r ./tools/lint/python/black_requirements.in +click==8.0.3 \ + --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ + --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b + # via black +dataclasses==0.6 \ + --hash=sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f \ + --hash=sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84 + # via -r ./tools/lint/python/black_requirements.in +importlib-metadata==6.7.0 \ + --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ + --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 + # via click +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 + # via black +packaging==23.1 \ + --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ + --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f + # via black +pathspec==0.9.0 \ + --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ + --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 + # via black +pkgutil-resolve-name==1.3.10 ; python_version < "3.9" \ + --hash=sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174 \ + --hash=sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e + # via -r ./tools/lint/python/black_requirements.in +platformdirs==2.4.0 \ + --hash=sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2 \ + --hash=sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d + # via black +tomli==1.2.2 \ + --hash=sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee \ + --hash=sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade + # via black +typed-ast==1.4.2 ; python_version < "3.8" \ + --hash=sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1 \ + --hash=sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d \ + --hash=sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6 \ + --hash=sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd \ + --hash=sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37 \ + --hash=sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151 \ + --hash=sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07 \ + --hash=sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440 \ + --hash=sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70 \ + --hash=sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496 \ + --hash=sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea \ + --hash=sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400 \ + --hash=sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc \ + --hash=sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606 \ + --hash=sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc \ + --hash=sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581 \ + --hash=sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412 \ + --hash=sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a \ + --hash=sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2 \ + --hash=sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787 \ + --hash=sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f \ + --hash=sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937 \ + --hash=sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64 \ + --hash=sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487 \ + --hash=sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b \ + --hash=sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41 \ + --hash=sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a \ + --hash=sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3 \ + --hash=sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166 \ + --hash=sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10 + # via + # -r ./tools/lint/python/black_requirements.in + # black +typing-extensions==3.10.0.2 \ + --hash=sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e \ + --hash=sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7 \ + --hash=sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34 + # via + # -r ./tools/lint/python/black_requirements.in + # black + # importlib-metadata +zipp==3.15.0 \ + --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ + --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 + # via importlib-metadata diff --git a/tools/lint/python/l10n_lint.py b/tools/lint/python/l10n_lint.py new file mode 100644 index 0000000000..158cb5f7e6 --- /dev/null +++ b/tools/lint/python/l10n_lint.py @@ -0,0 +1,202 @@ +# 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 os +from datetime import datetime, timedelta + +from compare_locales import parser +from compare_locales.lint.linter import L10nLinter +from compare_locales.lint.util import l10n_base_reference_and_tests +from compare_locales.paths import ProjectFiles, TOMLParser +from mach import util as mach_util +from mozlint import pathutils, result +from mozpack import path as mozpath +from mozversioncontrol import MissingVCSTool +from mozversioncontrol.repoupdate import update_git_repo, update_mercurial_repo + +L10N_SOURCE_NAME = "l10n-source" +L10N_SOURCE_REPO = "https://github.com/mozilla-l10n/firefox-l10n-source.git" + +STRINGS_NAME = "gecko-strings" +STRINGS_REPO = "https://hg.mozilla.org/l10n/gecko-strings" + +PULL_AFTER = timedelta(days=2) + + +# Wrapper to call lint_strings with mozilla-central configuration +# comm-central defines its own wrapper since comm-central strings are +# in separate repositories +def lint(paths, lintconfig, **lintargs): + extra_args = lintargs.get("extra_args") or [] + name = L10N_SOURCE_NAME if "--l10n-git" in extra_args else STRINGS_NAME + return lint_strings(name, paths, lintconfig, **lintargs) + + +def lint_strings(name, paths, lintconfig, **lintargs): + l10n_base = mach_util.get_state_dir() + root = lintargs["root"] + exclude = lintconfig.get("exclude") + extensions = lintconfig.get("extensions") + + # Load l10n.toml configs + l10nconfigs = load_configs(lintconfig, root, l10n_base, name) + + # Check include paths in l10n.yml if it's in our given paths + # Only the l10n.yml will show up here, but if the l10n.toml files + # change, we also get the l10n.yml as the toml files are listed as + # support files. + if lintconfig["path"] in paths: + results = validate_linter_includes(lintconfig, l10nconfigs, lintargs) + paths.remove(lintconfig["path"]) + else: + results = [] + + all_files = [] + for p in paths: + fp = pathutils.FilterPath(p) + if fp.isdir: + for _, fileobj in fp.finder: + all_files.append(fileobj.path) + if fp.isfile: + all_files.append(p) + # Filter again, our directories might have picked up files the + # explicitly excluded in the l10n.yml configuration. + # `browser/locales/en-US/firefox-l10n.js` is a good example. + all_files, _ = pathutils.filterpaths( + lintargs["root"], + all_files, + lintconfig["include"], + exclude=exclude, + extensions=extensions, + ) + # These should be excluded in l10n.yml + skips = {p for p in all_files if not parser.hasParser(p)} + results.extend( + result.from_config( + lintconfig, + level="warning", + path=path, + message="file format not supported in compare-locales", + ) + for path in skips + ) + all_files = [p for p in all_files if p not in skips] + files = ProjectFiles(name, l10nconfigs) + + get_reference_and_tests = l10n_base_reference_and_tests(files) + + linter = MozL10nLinter(lintconfig) + results += linter.lint(all_files, get_reference_and_tests) + return results + + +# Similar to the lint/lint_strings wrapper setup, for comm-central support. +def gecko_strings_setup(**lint_args): + extra_args = lint_args.get("extra_args") or [] + if "--l10n-git" in extra_args: + return source_repo_setup(L10N_SOURCE_REPO, L10N_SOURCE_NAME) + else: + return strings_repo_setup(STRINGS_REPO, STRINGS_NAME) + + +def source_repo_setup(repo: str, name: str): + gs = mozpath.join(mach_util.get_state_dir(), name) + marker = mozpath.join(gs, ".git", "l10n_pull_marker") + try: + last_pull = datetime.fromtimestamp(os.stat(marker).st_mtime) + skip_clone = datetime.now() < last_pull + PULL_AFTER + except OSError: + skip_clone = False + if skip_clone: + return + try: + update_git_repo(repo, gs) + except MissingVCSTool: + if os.environ.get("MOZ_AUTOMATION"): + raise + print("warning: l10n linter requires Git but was unable to find 'git'") + return 1 + with open(marker, "w") as fh: + fh.flush() + + +def strings_repo_setup(repo: str, name: str): + gs = mozpath.join(mach_util.get_state_dir(), name) + marker = mozpath.join(gs, ".hg", "l10n_pull_marker") + try: + last_pull = datetime.fromtimestamp(os.stat(marker).st_mtime) + skip_clone = datetime.now() < last_pull + PULL_AFTER + except OSError: + skip_clone = False + if skip_clone: + return + try: + update_mercurial_repo(repo, gs) + except MissingVCSTool: + if os.environ.get("MOZ_AUTOMATION"): + raise + print("warning: l10n linter requires Mercurial but was unable to find 'hg'") + return 1 + with open(marker, "w") as fh: + fh.flush() + + +def load_configs(lintconfig, root, l10n_base, locale): + """Load l10n configuration files specified in the linter configuration.""" + configs = [] + env = {"l10n_base": l10n_base} + for toml in lintconfig["l10n_configs"]: + cfg = TOMLParser().parse( + mozpath.join(root, toml), env=env, ignore_missing_includes=True + ) + cfg.set_locales([locale], deep=True) + configs.append(cfg) + return configs + + +def validate_linter_includes(lintconfig, l10nconfigs, lintargs): + """Check l10n.yml config against l10n.toml configs.""" + reference_paths = set( + mozpath.relpath(p["reference"].prefix, lintargs["root"]) + for project in l10nconfigs + for config in project.configs + for p in config.paths + ) + # Just check for directories + reference_dirs = sorted(p for p in reference_paths if os.path.isdir(p)) + missing_in_yml = [ + refd for refd in reference_dirs if refd not in lintconfig["include"] + ] + # These might be subdirectories in the config, though + missing_in_yml = [ + d + for d in missing_in_yml + if not any(d.startswith(parent + "/") for parent in lintconfig["include"]) + ] + if missing_in_yml: + dirs = ", ".join(missing_in_yml) + return [ + result.from_config( + lintconfig, + path=lintconfig["path"], + message="l10n.yml out of sync with l10n.toml, add: " + dirs, + ) + ] + return [] + + +class MozL10nLinter(L10nLinter): + """Subclass linter to generate the right result type.""" + + def __init__(self, lintconfig): + super(MozL10nLinter, self).__init__() + self.lintconfig = lintconfig + + def lint(self, files, get_reference_and_tests): + return [ + result.from_config(self.lintconfig, **result_data) + for result_data in super(MozL10nLinter, self).lint( + files, get_reference_and_tests + ) + ] diff --git a/tools/lint/python/ruff.py b/tools/lint/python/ruff.py new file mode 100644 index 0000000000..349320e153 --- /dev/null +++ b/tools/lint/python/ruff.py @@ -0,0 +1,176 @@ +# 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 platform +import re +import signal +import subprocess +import sys +from pathlib import Path + +import mozfile +from mozlint import result + +here = os.path.abspath(os.path.dirname(__file__)) +RUFF_REQUIREMENTS_PATH = os.path.join(here, "ruff_requirements.txt") + +RUFF_NOT_FOUND = """ +Could not find ruff! Install ruff and try again. + + $ pip install -U --require-hashes -r {} +""".strip().format( + RUFF_REQUIREMENTS_PATH +) + + +RUFF_INSTALL_ERROR = """ +Unable to install correct version of ruff! +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + RUFF_REQUIREMENTS_PATH +) + + +def default_bindir(): + # We use sys.prefix to find executables as that gets modified with + # virtualenv's activate_this.py, whereas sys.executable doesn't. + if platform.system() == "Windows": + return os.path.join(sys.prefix, "Scripts") + else: + return os.path.join(sys.prefix, "bin") + + +def get_ruff_version(binary): + """ + Returns found binary's version + """ + try: + output = subprocess.check_output( + [binary, "--version"], + stderr=subprocess.STDOUT, + text=True, + ) + except subprocess.CalledProcessError as e: + output = e.output + + matches = re.match(r"ruff ([0-9\.]+)", output) + if matches: + return matches[1] + print("Error: Could not parse the version '{}'".format(output)) + + +def setup(root, log, **lintargs): + virtualenv_bin_path = lintargs.get("virtualenv_bin_path") + binary = mozfile.which("ruff", path=(virtualenv_bin_path, default_bindir())) + + if binary and os.path.isfile(binary): + log.debug(f"Looking for ruff at {binary}") + version = get_ruff_version(binary) + versions = [ + line.split()[0].strip() + for line in open(RUFF_REQUIREMENTS_PATH).readlines() + if line.startswith("ruff==") + ] + if [f"ruff=={version}"] == versions: + log.debug("ruff is present with expected version {}".format(version)) + return 0 + else: + log.debug("ruff is present but unexpected version {}".format(version)) + + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements(RUFF_REQUIREMENTS_PATH, quiet=True) + except subprocess.CalledProcessError: + print(RUFF_INSTALL_ERROR) + return 1 + + +def run_process(config, cmd, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True + ) + signal.signal(signal.SIGINT, orig) + try: + output, _ = proc.communicate() + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return output + + +def lint(paths, config, log, **lintargs): + fixed = 0 + results = [] + + if not paths: + return {"results": results, "fixed": fixed} + + # Currently ruff only lints non `.py` files if they are explicitly passed + # in. So we need to find any non-py files manually. This can be removed + # after https://github.com/charliermarsh/ruff/issues/3410 is fixed. + exts = [e for e in config["extensions"] if e != "py"] + non_py_files = [] + for path in paths: + p = Path(path) + if not p.is_dir(): + continue + for ext in exts: + non_py_files.extend([str(f) for f in p.glob(f"**/*.{ext}")]) + + args = ["ruff", "check", "--force-exclude"] + paths + non_py_files + + if config["exclude"]: + args.append(f"--extend-exclude={','.join(config['exclude'])}") + + process_kwargs = {"processStderrLine": lambda line: log.debug(line)} + + warning_rules = set(config.get("warning-rules", [])) + if lintargs.get("fix"): + # Do a first pass with --fix-only as the json format doesn't return the + # number of fixed issues. + fix_args = args + ["--fix-only"] + + # Don't fix warnings to limit unrelated changes sneaking into patches. + fix_args.append(f"--extend-ignore={','.join(warning_rules)}") + output = run_process(config, fix_args, **process_kwargs) + matches = re.match(r"Fixed (\d+) errors?.", output) + if matches: + fixed = int(matches[1]) + + log.debug(f"Running with args: {args}") + + output = run_process(config, args + ["--format=json"], **process_kwargs) + if not output: + return [] + + try: + issues = json.loads(output) + except json.JSONDecodeError: + log.error(f"could not parse output: {output}") + return [] + + for issue in issues: + res = { + "path": issue["filename"], + "lineno": issue["location"]["row"], + "column": issue["location"]["column"], + "lineoffset": issue["end_location"]["row"] - issue["location"]["row"], + "message": issue["message"], + "rule": issue["code"], + "level": "error", + } + if any(issue["code"].startswith(w) for w in warning_rules): + res["level"] = "warning" + + if issue["fix"]: + res["hint"] = issue["fix"]["message"] + + results.append(result.from_config(config, **res)) + + return {"results": results, "fixed": fixed} diff --git a/tools/lint/python/ruff_requirements.in b/tools/lint/python/ruff_requirements.in new file mode 100644 index 0000000000..84b2a3cfd0 --- /dev/null +++ b/tools/lint/python/ruff_requirements.in @@ -0,0 +1,2 @@ +ruff +pkgutil-resolve-name==1.3.10 ; python_version < '3.9' diff --git a/tools/lint/python/ruff_requirements.txt b/tools/lint/python/ruff_requirements.txt new file mode 100644 index 0000000000..a9db7c1097 --- /dev/null +++ b/tools/lint/python/ruff_requirements.txt @@ -0,0 +1,29 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile --generate-hashes --output-file=tools/lint/python/ruff_requirements.txt tools/lint/python/ruff_requirements.in +# +pkgutil-resolve-name==1.3.10 ; python_version < "3.9" \ + --hash=sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174 \ + --hash=sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e + # via -r tools/lint/python/ruff_requirements.in +ruff==0.0.254 \ + --hash=sha256:059a380c08e849b6f312479b18cc63bba2808cff749ad71555f61dd930e3c9a2 \ + --hash=sha256:09c764bc2bd80c974f7ce1f73a46092c286085355a5711126af351b9ae4bea0c \ + --hash=sha256:0deb1d7226ea9da9b18881736d2d96accfa7f328c67b7410478cc064ad1fa6aa \ + --hash=sha256:0eb66c9520151d3bd950ea43b3a088618a8e4e10a5014a72687881e6f3606312 \ + --hash=sha256:27d39d697fdd7df1f2a32c1063756ee269ad8d5345c471ee3ca450636d56e8c6 \ + --hash=sha256:2fc21d060a3197ac463596a97d9b5db2d429395938b270ded61dd60f0e57eb21 \ + --hash=sha256:688379050ae05394a6f9f9c8471587fd5dcf22149bd4304a4ede233cc4ef89a1 \ + --hash=sha256:8deba44fd563361c488dedec90dc330763ee0c01ba54e17df54ef5820079e7e0 \ + --hash=sha256:ac1429be6d8bd3db0bf5becac3a38bd56f8421447790c50599cd90fd53417ec4 \ + --hash=sha256:b3f15d5d033fd3dcb85d982d6828ddab94134686fac2c02c13a8822aa03e1321 \ + --hash=sha256:b435afc4d65591399eaf4b2af86e441a71563a2091c386cadf33eaa11064dc09 \ + --hash=sha256:c38291bda4c7b40b659e8952167f386e86ec29053ad2f733968ff1d78b4c7e15 \ + --hash=sha256:d4385cdd30153b7aa1d8f75dfd1ae30d49c918ead7de07e69b7eadf0d5538a1f \ + --hash=sha256:dd58c500d039fb381af8d861ef456c3e94fd6855c3d267d6c6718c9a9fe07be0 \ + --hash=sha256:e15742df0f9a3615fbdc1ee9a243467e97e75bf88f86d363eee1ed42cedab1ec \ + --hash=sha256:ef20bf798ffe634090ad3dc2e8aa6a055f08c448810a2f800ab716cc18b80107 \ + --hash=sha256:f70dc93bc9db15cccf2ed2a831938919e3e630993eeea6aba5c84bc274237885 + # via -r tools/lint/python/ruff_requirements.in diff --git a/tools/lint/rejected-words.yml b/tools/lint/rejected-words.yml new file mode 100644 index 0000000000..5b5d3d696a --- /dev/null +++ b/tools/lint/rejected-words.yml @@ -0,0 +1,324 @@ +--- +avoid-blacklist-and-whitelist: + description: "Use words like 'skip', 'select', 'allow' or 'deny' instead" + level: error + include: ['.'] + type: regex + payload: (black|white)[-_]?list + ignore-case: true + # Based on codespell with idl and webidl added. + extensions: + - js + - jsm + - mjs + - jxs + - idl + - webidl + - xml + - html + - xhtml + - cpp + - c + - h + - configure + - py + - properties + - rst + - md + - ftl + - yml + - java + - kt + exclude: + - '**/.eslintrc.js' + - browser/app/profile/firefox.js + - browser/app/winlauncher/LauncherProcessWin.cpp + - browser/base/content/browser.js + - browser/base/content/contentTheme.js + - browser/base/content/test/general/browser_remoteTroubleshoot.js + - browser/base/content/test/general/browser_tab_drag_drop_perwindow.js + - browser/base/content/test/performance/browser_preferences_usage.js + - browser/base/content/test/protectionsUI/browser_protectionsUI_cryptominers.js + - browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js + - browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js + - browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js + - browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js + - browser/base/content/test/protectionsUI/browser_protectionsUI_state.js + - browser/base/content/test/protectionsUI/browser_protectionsUI_subview_shim.js + - browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js + - browser/base/content/test/tabMediaIndicator/browser_mute_webAudio.js + - browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js + - browser/components/enterprisepolicies/Policies.sys.mjs + - browser/components/migration/ChromeMigrationUtils.sys.mjs + - browser/components/migration/ChromeProfileMigrator.sys.mjs + - browser/components/newtab/data/content/activity-stream.bundle.js + - browser/components/preferences/privacy.inc.xhtml + - browser/components/preferences/privacy.js + - browser/components/resistfingerprinting/test/mochitest/test_bug1354633_media_error.html + - browser/components/safebrowsing/content/test/browser_whitelisted.js + - browser/components/sessionstore/test/browser_crashedTabs.js + - browser/components/uitour/UITourChild.sys.mjs + - browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js + - browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js + - browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js + - browser/components/urlbar/tests/unit/test_search_suggestions.js + - browser/components/urlbar/tests/unit/test_tokenizer.js + - browser/extensions/screenshots/background/main.js + - browser/extensions/webcompat/shims/nielsen.js + - browser/modules/SitePermissions.sys.mjs + - browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs + - build/clang-plugin/CustomMatchers.h + - build/clang-plugin/FopenUsageChecker.cpp + - build/clang-plugin/NaNExprChecker.cpp + - build/clang-plugin/NoPrincipalGetURI.cpp + - build/clang-plugin/tests/TestNANTestingExpr.cpp + - build/compare-mozconfig/compare-mozconfigs.py + - build/moz.configure/bindgen.configure + - build/moz.configure/toolchain.configure + - config/check_vanilla_allocations.py + - devtools/client/debugger/dist/parser-worker.js + - devtools/client/debugger/test/mochitest/examples/big-sourcemap_files/bundle.js + - devtools/client/debugger/test/mochitest/examples/ember/quickstart/dist/assets/vendor.js + - devtools/client/debugger/test/mochitest/examples/react/build/main.js + - devtools/client/debugger/test/mochitest/examples/react/build/service-worker.js + - devtools/client/inspector/markup/test/lib_babel_6.21.0_min.js + - devtools/client/inspector/markup/test/lib_react_dom_15.4.1.js + - docshell/base/nsDocShell.cpp + - docshell/base/URIFixup.sys.mjs + - docshell/test/unit/test_URIFixup_info.js + - dom/base/Document.cpp + - dom/base/MaybeCrossOriginObject.cpp + - dom/base/nsContentUtils.h + - dom/base/nsDataDocumentContentPolicy.cpp + - dom/base/nsGlobalWindowOuter.cpp + - dom/base/nsTreeSanitizer.cpp + - dom/base/nsTreeSanitizer.h + - dom/base/test/browser_multiple_popups.js + - dom/base/test/browser_timeout_throttling_with_audio_playback.js + - dom/base/test/chrome/test_permission_hasValidTransientUserActivation.xhtml + - dom/bindings/Codegen.py + - dom/bindings/parser/WebIDL.py + - dom/bindings/RemoteObjectProxy.cpp + - dom/events/EventStateManager.cpp + - dom/events/KeyboardEvent.cpp + - dom/html/MediaError.cpp + - dom/indexedDB/ActorsParent.cpp + - dom/ipc/ContentParent.cpp + - dom/ipc/URLClassifierParent.cpp + - dom/media/autoplay/AutoplayPolicy.cpp + - dom/media/gmp/GMPChild.cpp + - dom/media/MediaManager.cpp + - dom/media/mp4/MP4Decoder.cpp + - dom/media/platforms/apple/AppleVTDecoder.cpp + - dom/media/platforms/wmf/DXVA2Manager.cpp + - dom/media/platforms/wmf/WMFVideoMFTManager.cpp + - dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html + - dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html + - dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html + - dom/media/webm/WebMDecoder.cpp + - dom/media/webrtc/transport/stun_socket_filter.cpp + - dom/media/webrtc/transport/test/ice_unittest.cpp + - dom/tests/mochitest/dom-level0/idn_child.html + - dom/tests/mochitest/dom-level0/test_setting_document.domain_idn.html + - dom/tests/mochitest/whatwg/test_postMessage_origin.xhtml + - gfx/gl/GLContextProviderWGL.cpp + - gfx/gl/GLUploadHelpers.cpp + - gfx/tests/mochitest/test_font_whitelist.html + - gfx/thebes/gfxFT2FontList.cpp + - gfx/thebes/gfxPlatformFontList.cpp + - gfx/thebes/gfxPlatformFontList.h + - gfx/thebes/gfxUserFontSet.cpp + - gfx/thebes/gfxWindowsPlatform.cpp + - gfx/thebes/SharedFontList.cpp + - intl/strres/nsStringBundle.cpp + - ipc/glue/GeckoChildProcessHost.cpp + - js/src/debugger/DebugAPI.h + - js/src/devtools/rootAnalysis/analyzeHeapWrites.js + - js/src/jit/CodeGenerator.cpp + - js/src/jit-test/tests/auto-regress/bug687399.js + - js/src/jit-test/tests/basic/missingArgTest2.js + - js/src/tests/non262/regress/regress-450369.js + - js/xpconnect/src/Sandbox.cpp + - js/xpconnect/src/XPCJSRuntime.cpp + - js/xpconnect/src/xpcpublic.h + - js/xpconnect/src/XPCWrappedNativeScope.cpp + - js/xpconnect/tests/unit/head_watchdog.js + - js/xpconnect/wrappers/FilteringWrapper.cpp + - js/xpconnect/wrappers/XrayWrapper.cpp + - layout/base/PositionedEventTargeting.cpp + - layout/base/PresShell.cpp + - layout/base/PresShell.h + - layout/reftests/css-placeholder/css-restrictions.html + - layout/style/test/test_computed_style_difference.html + - layout/tools/reftest/mach_commands.py + - layout/tools/reftest/mach_test_package_commands.py + - layout/tools/reftest/reftestcommandline.py + - layout/tools/reftest/runreftest.py + - layout/tools/reftest/selftest/conftest.py + - mobile/android/app/geckoview-prefs.js + - mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java + - mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentBlocking.java + - mobile/android/geckoview/src/main/java/org/mozilla/geckoview/CrashReporter.java + - mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebAuthnTokenManager.java + - modules/libpref/Preferences.cpp + - modules/libpref/init/all.js + - netwerk/base/nsIPermissionManager.idl + - netwerk/base/nsIProtocolHandler.idl + - netwerk/base/nsIOService.cpp + - netwerk/base/nsIURI.idl + - netwerk/base/nsURLHelper.cpp + - netwerk/cookie/CookieCommons.h + - netwerk/dns/nsHostRecord.cpp + - netwerk/dns/nsIDNService.cpp + - netwerk/dns/nsIIDNService.idl + - netwerk/dns/TRR.cpp + - netwerk/ipc/DocumentLoadListener.cpp + - netwerk/protocol/http/HttpBaseChannel.cpp + - netwerk/protocol/http/nsHttpChannel.cpp + - netwerk/protocol/http/nsHttpConnectionMgr.cpp + - netwerk/protocol/viewsource/nsViewSourceChannel.cpp + - netwerk/protocol/websocket/BaseWebSocketChannel.cpp + - netwerk/socket/nsSOCKSSocketProvider.cpp + - netwerk/test/unit/test_bug464591.js + - netwerk/test/unit/test_cookie_blacklist.js + - netwerk/test/unit/test_idn_blacklist.js + - netwerk/url-classifier/UrlClassifierCommon.cpp + - netwerk/url-classifier/UrlClassifierFeatureCryptominingAnnotation.cpp + - netwerk/url-classifier/UrlClassifierFeatureCryptominingProtection.cpp + - netwerk/url-classifier/UrlClassifierFeatureFingerprintingAnnotation.cpp + - netwerk/url-classifier/UrlClassifierFeatureFingerprintingProtection.cpp + - netwerk/url-classifier/UrlClassifierFeatureSocialTrackingAnnotation.cpp + - netwerk/url-classifier/UrlClassifierFeatureSocialTrackingProtection.cpp + - netwerk/url-classifier/UrlClassifierFeatureTrackingAnnotation.cpp + - netwerk/url-classifier/UrlClassifierFeatureTrackingProtection.cpp + - python/mozbuild/mozbuild/backend/recursivemake.py + - python/mozbuild/mozbuild/configure/options.py + - python/mozbuild/mozbuild/vendor/vendor_rust.py + - remote/cdp/Protocol.sys.mjs + - security/manager/ssl/tests/unit/test_intermediate_preloads.js + - security/sandbox/linux/broker/SandboxBroker.cpp + - security/sandbox/linux/broker/SandboxBroker.h + - security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp + - security/sandbox/linux/glue/SandboxPrefBridge.cpp + - security/sandbox/linux/gtest/TestBroker.cpp + - security/sandbox/linux/Sandbox.cpp + - security/sandbox/linux/SandboxFilter.cpp + - security/sandbox/linux/SandboxFilterUtil.h + - security/sandbox/linux/Sandbox.h + - taskcluster/ci/docker-image/kind.yml + - taskcluster/gecko_taskgraph/actions/create_interactive.py + - taskcluster/gecko_taskgraph/transforms/test/other.py + - taskcluster/gecko_taskgraph/try_option_syntax.py + - testing/condprofile/condprof/client.py + - testing/condprofile/condprof/tests/profile/prefs.js + - testing/condprofile/condprof/tests/test_client.py + - testing/firefox-ui/tests/functional/safebrowsing/test_initial_download.py + - testing/marionette/client/marionette_driver/wait.py + - testing/mochitest/browser-test.js + - testing/mochitest/mach_test_package_commands.py + - testing/mochitest/mochitest_options.py + - testing/mochitest/runtests.py + - testing/mozbase/mozprofile/mozprofile/profile.py + - testing/mozharness/configs/unittests/linux_unittest.py + - testing/mozharness/configs/unittests/mac_unittest.py + - testing/mozharness/configs/unittests/win_unittest.py + - testing/profiles/unittest-required/user.js + - testing/raptor/browsertime/browsertime_scenario.js + - testing/web-platform/tests/tools/manifest/tests/test_manifest.py + - toolkit/actors/RemotePageChild.sys.mjs + - toolkit/actors/WebChannelChild.sys.mjs + - toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js + - toolkit/components/reputationservice/ApplicationReputation.cpp + - toolkit/components/reputationservice/chromium/chrome/common/safe_browsing/csd.pb.h + - toolkit/components/reputationservice/test/unit/head_download_manager.js + - toolkit/components/reputationservice/test/unit/test_app_rep.js + - toolkit/components/reputationservice/test/unit/test_app_rep_maclinux.js + - toolkit/components/reputationservice/test/unit/test_app_rep_windows.js + - toolkit/components/satchel/test/test_form_autocomplete.html + - toolkit/components/telemetry/docs/data/environment.rst + - toolkit/components/url-classifier/nsUrlClassifierUtils.cpp + - toolkit/components/url-classifier/SafeBrowsing.sys.mjs + - toolkit/components/url-classifier/tests/mochitest/features.js + - toolkit/components/url-classifier/tests/mochitest/good.js + - toolkit/components/url-classifier/tests/mochitest/test_annotation_vs_TP.html + - toolkit/components/url-classifier/tests/mochitest/test_classified_annotations.html + - toolkit/components/url-classifier/tests/mochitest/test_classify_by_default.html + - toolkit/components/url-classifier/tests/mochitest/test_classify_ping.html + - toolkit/components/url-classifier/tests/mochitest/test_cryptomining_annotate.html + - toolkit/components/url-classifier/tests/mochitest/test_cryptomining.html + - toolkit/components/url-classifier/tests/mochitest/test_fingerprinting_annotate.html + - toolkit/components/url-classifier/tests/mochitest/test_fingerprinting.html + - toolkit/components/url-classifier/tests/mochitest/test_safebrowsing_bug1272239.html + - toolkit/components/url-classifier/tests/mochitest/test_socialtracking_annotate.html + - toolkit/components/url-classifier/tests/mochitest/test_socialtracking.html + - toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_bug1580416.html + - toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_whitelist.html + - toolkit/components/url-classifier/tests/unit/head_urlclassifier.js + - toolkit/components/url-classifier/tests/unit/test_digest256.js + - toolkit/components/url-classifier/tests/unit/test_platform_specific_threats.js + - toolkit/components/url-classifier/tests/UrlClassifierTestUtils.sys.mjs + - toolkit/content/aboutTelemetry.js + - toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js + - toolkit/modules/PermissionsUtils.sys.mjs + - toolkit/modules/tests/browser/browser_AsyncPrefs.js + - toolkit/modules/tests/browser/browser_web_channel.js + - toolkit/modules/tests/xpcshell/test_PermissionsUtils.js + - toolkit/modules/third_party/jsesc/jsesc.mjs + - toolkit/modules/Troubleshoot.sys.mjs + - toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs + - toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js + - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js + - toolkit/mozapps/extensions/test/xpcshell/test_permissions.js + - toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js + - toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js + - toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js + - toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js + - toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js + - toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js + - toolkit/mozapps/extensions/test/xpinstall/head.js + - tools/fuzzing/messagemanager/MessageManagerFuzzer.cpp + - tools/fuzzing/messagemanager/MessageManagerFuzzer.h + - tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js + - tools/lint/rejected-words.yml + - widget/android/GfxInfo.cpp + - widget/GfxInfoBase.cpp + - widget/gtk/IMContextWrapper.cpp + - widget/gtk/nsAppShell.cpp + - widget/windows/GfxInfo.cpp + - widget/windows/WinUtils.cpp + - widget/windows/WinUtils.h + - xpcom/io/FilePreferences.cpp + - xpcom/tests/gtest/TestFilePreferencesUnix.cpp + +# --- +# Disable for now. Needs some dev to handle this +# avoid-master-and-slave: +# description: "Use words like 'controller', 'worker' instead" +--- +avoid-gobbledygook: + description: "American English colloquialism. Use 'nonsense' instead." + level: error + include: ['.'] + type: regex + payload: \b(gobbledy)?-?gook + ignore-case: true + exclude: + - extensions/spellcheck/locales/en-US/hunspell/dictionary-sources/orig/en_US-custom.dic + - extensions/spellcheck/locales/en-US/hunspell/dictionary-sources/utf8/en-US-utf8.dic + - extensions/spellcheck/locales/en-US/hunspell/en-US.dic + - tools/lint/rejected-words.yml diff --git a/tools/lint/rst.yml b/tools/lint/rst.yml new file mode 100644 index 0000000000..3f35fe7def --- /dev/null +++ b/tools/lint/rst.yml @@ -0,0 +1,11 @@ +--- +rst: + description: RST linter + include: [.] + extensions: + - rst + support-files: + - 'tools/lint/rst/**' + type: external + payload: rst:lint + setup: rst:setup diff --git a/tools/lint/rst/__init__.py b/tools/lint/rst/__init__.py new file mode 100644 index 0000000000..7151c09a59 --- /dev/null +++ b/tools/lint/rst/__init__.py @@ -0,0 +1,116 @@ +# 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 os +import re +import subprocess + +from mozfile import which +from mozlint import result +from mozlint.pathutils import expand_exclusions + +# Error Levels +# (0, 'debug') +# (1, 'info') +# (2, 'warning') +# (3, 'error') +# (4, 'severe') + +abspath = os.path.abspath(os.path.dirname(__file__)) +rstcheck_requirements_file = os.path.join(abspath, "requirements.txt") + +results = [] + +RSTCHECK_NOT_FOUND = """ +Could not find rstcheck! Install rstcheck and try again. + + $ pip install -U --require-hashes -r {} +""".strip().format( + rstcheck_requirements_file +) + +RSTCHECK_INSTALL_ERROR = """ +Unable to install required version of rstcheck +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + rstcheck_requirements_file +) + +RSTCHECK_FORMAT_REGEX = re.compile(r"(.*):(.*): \(.*/([0-9]*)\) (.*)$") +IGNORE_NOT_REF_LINK_UPSTREAM_BUG = re.compile( + r"Hyperlink target (.*) is not referenced." +) + + +def setup(root, **lintargs): + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements( + rstcheck_requirements_file, quiet=True + ) + except subprocess.CalledProcessError: + print(RSTCHECK_INSTALL_ERROR) + return 1 + + +def get_rstcheck_binary(): + """ + Returns the path of the first rstcheck binary available + if not found returns None + """ + binary = os.environ.get("RSTCHECK") + if binary: + return binary + + return which("rstcheck") + + +def parse_with_split(errors): + match = RSTCHECK_FORMAT_REGEX.match(errors) + filename, lineno, level, message = match.groups() + + return filename, lineno, level, message + + +def lint(files, config, **lintargs): + log = lintargs["log"] + config["root"] = lintargs["root"] + paths = expand_exclusions(files, config, config["root"]) + paths = list(paths) + chunk_size = 50 + binary = get_rstcheck_binary() + rstcheck_options = [ + "--ignore-language=cpp,json", + "--ignore-roles=searchfox", + ] + + while paths: + cmdargs = [which("python"), binary] + rstcheck_options + paths[:chunk_size] + log.debug("Command: {}".format(" ".join(cmdargs))) + + proc = subprocess.Popen( + cmdargs, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=os.environ, + universal_newlines=True, + ) + all_errors = proc.communicate()[1] + for errors in all_errors.split("\n"): + if len(errors) > 1: + filename, lineno, level, message = parse_with_split(errors) + if not IGNORE_NOT_REF_LINK_UPSTREAM_BUG.match(message): + # Ignore an upstream bug + # https://github.com/myint/rstcheck/issues/19 + res = { + "path": filename, + "message": message, + "lineno": lineno, + "level": "error" if int(level) >= 2 else "warning", + } + results.append(result.from_config(config, **res)) + paths = paths[chunk_size:] + + return results diff --git a/tools/lint/rst/requirements.in b/tools/lint/rst/requirements.in new file mode 100644 index 0000000000..e6b6022a47 --- /dev/null +++ b/tools/lint/rst/requirements.in @@ -0,0 +1,20 @@ +alabaster==0.7.13 +charset-normalizer==2.0.12 +docutils==0.17.1 +idna==2.10 +imagesize==1.4.1 +importlib-metadata==6.0.0 +markupsafe==2.0.1 +packaging==21.0 +requests==2.27.1 +snowballstemmer==2.2.0 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-mermaid==0.8.1 +rstcheck==3.5.0 +Pygments==2.14.0 +pytz==2022.7.1 +urllib3==1.26.9 +# We need sphinx to avoid some rstcheck errors and warnings +Sphinx==5.3.0 +pkgutil-resolve-name==1.3.10 ; python_version < '3.9' diff --git a/tools/lint/rst/requirements.txt b/tools/lint/rst/requirements.txt new file mode 100644 index 0000000000..ded13595c3 --- /dev/null +++ b/tools/lint/rst/requirements.txt @@ -0,0 +1,220 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --config=pyproject.toml --generate-hashes --output-file=tools/lint/rst/requirements.txt ./tools/lint/rst/requirements.in +# +alabaster==0.7.13 \ + --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \ + --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2 + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +babel==2.12.1 \ + --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \ + --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455 + # via sphinx +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 + # via requests +charset-normalizer==2.0.12 \ + --hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \ + --hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df + # via + # -r ./tools/lint/rst/requirements.in + # requests +docutils==0.17.1 \ + --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ + --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 + # via + # -r ./tools/lint/rst/requirements.in + # rstcheck + # sphinx +idna==2.10 \ + --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ + --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 + # via + # -r ./tools/lint/rst/requirements.in + # requests +imagesize==1.4.1 \ + --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ + --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +importlib-metadata==6.0.0 \ + --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \ + --hash=sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +jinja2==3.1.2 \ + --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ + --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 + # via sphinx +markupsafe==2.0.1 \ + --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ + --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ + --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ + --hash=sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194 \ + --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ + --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ + --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ + --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ + --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ + --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ + --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ + --hash=sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a \ + --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ + --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ + --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ + --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ + --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ + --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ + --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ + --hash=sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047 \ + --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ + --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ + --hash=sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b \ + --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ + --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ + --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ + --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a \ + --hash=sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1 \ + --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ + --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ + --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ + --hash=sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee \ + --hash=sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f \ + --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ + --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ + --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ + --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ + --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ + --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ + --hash=sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86 \ + --hash=sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6 \ + --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ + --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ + --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ + --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ + --hash=sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e \ + --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ + --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ + --hash=sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f \ + --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ + --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ + --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ + --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ + --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ + --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ + --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ + --hash=sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a \ + --hash=sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207 \ + --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ + --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ + --hash=sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd \ + --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ + --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ + --hash=sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9 \ + --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ + --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ + --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ + --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ + --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 + # via + # -r ./tools/lint/rst/requirements.in + # jinja2 +packaging==21.0 \ + --hash=sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7 \ + --hash=sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14 + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +pkgutil-resolve-name==1.3.10 ; python_version < "3.9" \ + --hash=sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174 \ + --hash=sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e + # via -r ./tools/lint/rst/requirements.in +pygments==2.14.0 \ + --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \ + --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717 + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +pyparsing==3.0.9 \ + --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ + --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc + # via packaging +pytz==2022.7.1 \ + --hash=sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0 \ + --hash=sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a + # via + # -r ./tools/lint/rst/requirements.in + # babel +requests==2.27.1 \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +rstcheck==3.5.0 \ + --hash=sha256:30c36768c4bd617a85ab93c31facaf410582e53803fde624845eb00c1430070c \ + --hash=sha256:d4b035300b7d898403544f38c3a4980171ce85f487d25e188347bbafb6ee58c0 + # via -r ./tools/lint/rst/requirements.in +snowballstemmer==2.2.0 \ + --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ + --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +sphinx==5.3.0 \ + --hash=sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d \ + --hash=sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5 + # via -r ./tools/lint/rst/requirements.in +sphinxcontrib-applehelp==1.0.2 \ + --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ + --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +sphinxcontrib-devhelp==1.0.2 \ + --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ + --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 + # via sphinx +sphinxcontrib-htmlhelp==2.0.0 \ + --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \ + --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 + # via + # -r ./tools/lint/rst/requirements.in + # sphinx +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 + # via sphinx +sphinxcontrib-mermaid==0.8.1 \ + --hash=sha256:15491c24ec78cf1626b1e79e797a9ce87cb7959cf38f955eb72dd5512aeb6ce9 \ + --hash=sha256:fa3e5325d4ba395336e6137d113f55026b1a03ccd115dc54113d1d871a580466 + # via -r ./tools/lint/rst/requirements.in +sphinxcontrib-qthelp==1.0.3 \ + --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ + --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 \ + --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ + --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 + # via sphinx +typing-extensions==4.7.1 \ + --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ + --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 + # via importlib-metadata +urllib3==1.26.9 \ + --hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \ + --hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e + # via + # -r ./tools/lint/rst/requirements.in + # requests +zipp==3.15.0 \ + --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ + --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 + # via importlib-metadata diff --git a/tools/lint/ruff.yml b/tools/lint/ruff.yml new file mode 100644 index 0000000000..59ae2a1350 --- /dev/null +++ b/tools/lint/ruff.yml @@ -0,0 +1,17 @@ +--- +ruff: + description: An extremely fast Python linter, written in Rust + # Excludes should be added to topsrcdir/pyproject.toml + exclude: [] + # The configure option is used by the build system + extensions: ["configure", "py"] + support-files: + - "**/.ruff.toml" + - "**/ruff.toml" + - "**/pyproject.toml" + - "tools/lint/python/ruff.py" + # Rules that should result in warnings rather than errors. + warning-rules: [PLR, PLW] + type: external + payload: python.ruff:lint + setup: python.ruff:setup diff --git a/tools/lint/rust/__init__.py b/tools/lint/rust/__init__.py new file mode 100644 index 0000000000..148aeeb648 --- /dev/null +++ b/tools/lint/rust/__init__.py @@ -0,0 +1,171 @@ +# 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 os +import re +import signal +import subprocess +from collections import namedtuple + +from mozboot.util import get_tools_dir +from mozfile import which +from mozlint import result +from mozlint.pathutils import expand_exclusions +from packaging.version import Version + +RUSTFMT_NOT_FOUND = """ +Could not find rustfmt! Install rustfmt and try again. + + $ rustup component add rustfmt + +And make sure that it is in the PATH +""".strip() + + +RUSTFMT_INSTALL_ERROR = """ +Unable to install correct version of rustfmt +Try to install it manually with: + $ rustup component add rustfmt +""".strip() + + +RUSTFMT_WRONG_VERSION = """ +You are probably using an old version of rustfmt. +Expected version is {version}. +Try to update it: + $ rustup update stable +""".strip() + + +def parse_issues(config, output, paths): + RustfmtDiff = namedtuple("RustfmtDiff", ["file", "line", "diff"]) + issues = [] + diff_line = re.compile("^Diff in (.*) at line ([0-9]*):") + file = "" + line_no = 0 + diff = "" + for line in output.split(b"\n"): + processed_line = ( + line.decode("utf-8", "replace") if isinstance(line, bytes) else line + ).rstrip("\r\n") + match = diff_line.match(processed_line) + if match: + if diff: + issues.append(RustfmtDiff(file, line_no, diff.rstrip("\n"))) + diff = "" + file, line_no = match.groups() + else: + diff += processed_line + "\n" + # the algorithm above will always skip adding the last issue + issues.append(RustfmtDiff(file, line_no, diff)) + file = os.path.normcase(os.path.normpath(file)) + results = [] + for issue in issues: + # rustfmt can not be supplied the paths to the files we want to analyze + # therefore, for each issue detected, we check if any of the the paths + # supplied are part of the file name. + # This just filters out the issues that are not part of paths. + if any([os.path.normcase(os.path.normpath(path)) in file for path in paths]): + res = { + "path": issue.file, + "diff": issue.diff, + "level": "warning", + "lineno": issue.line, + } + results.append(result.from_config(config, **res)) + return {"results": results, "fixed": 0} + + +def run_process(config, cmd): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + signal.signal(signal.SIGINT, orig) + + try: + output, _ = proc.communicate() + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return output + + +def get_rustfmt_binary(): + """ + Returns the path of the first rustfmt binary available + if not found returns None + """ + binary = os.environ.get("RUSTFMT") + if binary: + return binary + + rust_path = os.path.join(get_tools_dir(), "rustc", "bin") + return which("rustfmt", path=os.pathsep.join([rust_path, os.environ["PATH"]])) + + +def get_rustfmt_version(binary): + """ + Returns found binary's version + """ + try: + output = subprocess.check_output( + [binary, "--version"], + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + output = e.output + + version = re.findall(r"\d.\d+.\d+", output)[0] + return Version(version) + + +def lint(paths, config, fix=None, **lintargs): + log = lintargs["log"] + paths = list(expand_exclusions(paths, config, lintargs["root"])) + + # An empty path array can occur when the user passes in `-n`. If we don't + # return early in this case, rustfmt will attempt to read stdin and hang. + if not paths: + return [] + + binary = get_rustfmt_binary() + + if not binary: + print(RUSTFMT_NOT_FOUND) + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + min_version_str = config.get("min_rustfmt_version") + min_version = Version(min_version_str) + actual_version = get_rustfmt_version(binary) + log.debug( + "Found version: {}. Minimal expected version: {}".format( + actual_version, min_version + ) + ) + + if actual_version < min_version: + print(RUSTFMT_WRONG_VERSION.format(version=min_version_str)) + return 1 + + cmd_args = [binary] + cmd_args.append("--check") + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + output = run_process(config, base_command) + + issues = parse_issues(config, output, paths) + + if fix: + issues["fixed"] = len(issues["results"]) + issues["results"] = [] + cmd_args.remove("--check") + + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + output = run_process(config, base_command) + + return issues diff --git a/tools/lint/rustfmt.yml b/tools/lint/rustfmt.yml new file mode 100644 index 0000000000..e317088812 --- /dev/null +++ b/tools/lint/rustfmt.yml @@ -0,0 +1,22 @@ +--- +rust: + description: Reformat rust + min_rustfmt_version: 1.4.12 + include: + - '.' + exclude: + - build/rust/windows/src/lib.rs + - dom/webauthn/libudev-sys/ + - gfx/wr/peek-poke/ + - gfx/wr/webrender_build/ + - gfx/wr/wr_malloc_size_of/ + - intl/icu_segmenter_data/ + - media/mp4parse-rust/ + - servo/ + - '**/*.mako.rs' + extensions: + - rs + support-files: + - 'tools/lint/rust/**' + type: external + payload: rust:lint diff --git a/tools/lint/shell/__init__.py b/tools/lint/shell/__init__.py new file mode 100644 index 0000000000..b75dc2d159 --- /dev/null +++ b/tools/lint/shell/__init__.py @@ -0,0 +1,148 @@ +# 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 +from json.decoder import JSONDecodeError + +import mozpack.path as mozpath +from mozfile import which +from mozlint import result +from mozlint.util.implementation import LintProcess +from mozpack.files import FileFinder + +SHELLCHECK_NOT_FOUND = """ +Unable to locate shellcheck, please ensure it is installed and in +your PATH or set the SHELLCHECK environment variable. + +https://shellcheck.net or your system's package manager. +""".strip() + +results = [] + + +class ShellcheckProcess(LintProcess): + def process_line(self, line): + try: + data = json.loads(line) + except JSONDecodeError as e: + print("Unable to load shellcheck output ({}): {}".format(e, line)) + return + + for entry in data: + res = { + "path": entry["file"], + "message": entry["message"], + "level": "error", + "lineno": entry["line"], + "column": entry["column"], + "rule": entry["code"], + } + results.append(result.from_config(self.config, **res)) + + +def determine_shell_from_script(path): + """Returns a string identifying the shell used. + + Returns None if not identifiable. + + Copes with the following styles: + #!bash + #!/bin/bash + #!/usr/bin/env bash + """ + with open(path, "r") as f: + head = f.readline() + + if not head.startswith("#!"): + return + + # allow for parameters to the shell + shebang = head.split()[0] + + # if the first entry is a variant of /usr/bin/env + if "env" in shebang: + shebang = head.split()[1] + + if shebang.endswith("sh"): + # Strip first to avoid issues with #!bash + return shebang.strip("#!").split("/")[-1] + # make it clear we return None, rather than fall through. + return + + +def find_shell_scripts(config, paths): + found = dict() + + root = config["root"] + exclude = [mozpath.join(root, e) for e in config.get("exclude", [])] + + if config.get("extensions"): + pattern = "**/*.{}".format(config.get("extensions")[0]) + else: + pattern = "**/*.sh" + + files = [] + for path in paths: + path = mozpath.normsep(path) + ignore = [ + e[len(path) :].lstrip("/") + for e in exclude + if mozpath.commonprefix((path, e)) == path + ] + finder = FileFinder(path, ignore=ignore) + files.extend([os.path.join(path, p) for p, f in finder.find(pattern)]) + + for filename in files: + shell = determine_shell_from_script(filename) + if shell: + found[filename] = shell + return found + + +def run_process(config, cmd): + proc = ShellcheckProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + +def get_shellcheck_binary(): + """ + Returns the path of the first shellcheck binary available + if not found returns None + """ + binary = os.environ.get("SHELLCHECK") + if binary: + return binary + + return which("shellcheck") + + +def lint(paths, config, **lintargs): + log = lintargs["log"] + binary = get_shellcheck_binary() + + if not binary: + print(SHELLCHECK_NOT_FOUND) + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + config["root"] = lintargs["root"] + + files = find_shell_scripts(config, paths) + + base_command = [binary, "-f", "json"] + if config.get("excludecodes"): + base_command.extend(["-e", ",".join(config.get("excludecodes"))]) + + for f in files: + cmd = list(base_command) + cmd.extend(["-s", files[f], f]) + log.debug("Command: {}".format(cmd)) + run_process(config, cmd) + return results diff --git a/tools/lint/shellcheck.yml b/tools/lint/shellcheck.yml new file mode 100644 index 0000000000..0100e3d5cc --- /dev/null +++ b/tools/lint/shellcheck.yml @@ -0,0 +1,15 @@ +--- +shellcheck: + description: Shell script linter + include: + - extensions/spellcheck/locales/en-US/hunspell/dictionary-sources/ + - taskcluster/docker/ + exclude: [] + # 1090: https://github.com/koalaman/shellcheck/wiki/SC1090 + # 'Can't follow a non-constant source' + extensions: ['sh'] + support-files: + - 'tools/lint/shell/**' + excludecodes: ['1090', '1091'] + type: external + payload: shell:lint diff --git a/tools/lint/spell/__init__.py b/tools/lint/spell/__init__.py new file mode 100644 index 0000000000..65712acdd7 --- /dev/null +++ b/tools/lint/spell/__init__.py @@ -0,0 +1,168 @@ +# 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 os +import re +import subprocess + +# py2-compat +try: + from json.decoder import JSONDecodeError +except ImportError: + JSONDecodeError = ValueError + +from mozfile import which +from mozlint import result +from mozlint.util.implementation import LintProcess + +here = os.path.abspath(os.path.dirname(__file__)) +CODESPELL_REQUIREMENTS_PATH = os.path.join(here, "codespell_requirements.txt") + +CODESPELL_NOT_FOUND = """ +Could not find codespell! Install codespell and try again. + + $ pip install -U --require-hashes -r {} +""".strip().format( + CODESPELL_REQUIREMENTS_PATH +) + + +CODESPELL_INSTALL_ERROR = """ +Unable to install correct version of codespell +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + CODESPELL_REQUIREMENTS_PATH +) + +results = [] + +CODESPELL_FORMAT_REGEX = re.compile(r"(.*):(.*): (.*) ==> (.*)$") + + +class CodespellProcess(LintProcess): + fixed = 0 + _fix = None + + def process_line(self, line): + try: + match = CODESPELL_FORMAT_REGEX.match(line) + abspath, line, typo, correct = match.groups() + except AttributeError: + if "FIXED: " not in line: + print("Unable to match regex against output: {}".format(line)) + return + + if CodespellProcess._fix: + CodespellProcess.fixed += 1 + + # Ignore false positive like aParent (which would be fixed to apparent) + # See https://github.com/lucasdemarchi/codespell/issues/314 + m = re.match(r"^[a-z][A-Z][a-z]*", typo) + if m: + return + res = { + "path": abspath, + "message": typo.strip() + " ==> " + correct, + "level": "error", + "lineno": line, + } + results.append(result.from_config(self.config, **res)) + + +def run_process(config, cmd): + proc = CodespellProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + +def get_codespell_binary(): + """ + Returns the path of the first codespell binary available + if not found returns None + """ + binary = os.environ.get("CODESPELL") + if binary: + return binary + + return which("codespell") + + +def setup(root, **lintargs): + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements( + CODESPELL_REQUIREMENTS_PATH, quiet=True + ) + except subprocess.CalledProcessError: + print(CODESPELL_INSTALL_ERROR) + return 1 + + +def get_codespell_version(binary): + return subprocess.check_output( + [which("python"), binary, "--version"], + universal_newlines=True, + stderr=subprocess.STDOUT, + ) + + +def get_ignored_words_file(config): + config_root = os.path.dirname(config["path"]) + return os.path.join(config_root, "spell", "exclude-list.txt") + + +def lint(paths, config, fix=None, **lintargs): + log = lintargs["log"] + binary = get_codespell_binary() + if not binary: + print(CODESPELL_NOT_FOUND) + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + config["root"] = lintargs["root"] + + exclude_list = get_ignored_words_file(config) + cmd_args = [ + which("python"), + binary, + "--disable-colors", + # Silence some warnings: + # 1: disable warnings about wrong encoding + # 2: disable warnings about binary file + # 4: shut down warnings about automatic fixes + # that were disabled in dictionary. + "--quiet-level=7", + "--ignore-words=" + exclude_list, + ] + + if "exclude" in config: + cmd_args.append("--skip=*.dic,{}".format(",".join(config["exclude"]))) + + log.debug("Command: {}".format(" ".join(cmd_args))) + log.debug("Version: {}".format(get_codespell_version(binary))) + + if fix: + CodespellProcess._fix = True + + base_command = cmd_args + paths + run_process(config, base_command) + + if fix: + global results + results = [] + cmd_args.append("--write-changes") + log.debug("Command: {}".format(" ".join(cmd_args))) + log.debug("Version: {}".format(get_codespell_version(binary))) + base_command = cmd_args + paths + run_process(config, base_command) + CodespellProcess.fixed = CodespellProcess.fixed - len(results) + else: + CodespellProcess.fixed = 0 + + return {"results": results, "fixed": CodespellProcess.fixed} diff --git a/tools/lint/spell/codespell_requirements.in b/tools/lint/spell/codespell_requirements.in new file mode 100644 index 0000000000..4b1d259fbe --- /dev/null +++ b/tools/lint/spell/codespell_requirements.in @@ -0,0 +1,2 @@ +codespell==2.2.6 +pkgutil-resolve-name==1.3.10 ; python_version < '3.9' diff --git a/tools/lint/spell/codespell_requirements.txt b/tools/lint/spell/codespell_requirements.txt new file mode 100644 index 0000000000..9b09c43897 --- /dev/null +++ b/tools/lint/spell/codespell_requirements.txt @@ -0,0 +1,10 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --generate-hashes --output-file=tools/lint/spell/codespell_requirements.txt tools/lint/spell/codespell_requirements.in +# +codespell==2.2.6 \ + --hash=sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07 \ + --hash=sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9 + # via -r tools/lint/spell/codespell_requirements.in diff --git a/tools/lint/spell/exclude-list.txt b/tools/lint/spell/exclude-list.txt new file mode 100644 index 0000000000..7682d1a177 --- /dev/null +++ b/tools/lint/spell/exclude-list.txt @@ -0,0 +1,25 @@ +cas +optin +aparent +acount +te +wasn +incrementall +aare +whats +crate +files' +thru +referer +dur +ue +tring +delink +warmup +aNumber +falsy +rduce +complies +ehr +inout +manuel diff --git a/tools/lint/stylelint.yml b/tools/lint/stylelint.yml new file mode 100644 index 0000000000..b1b9c94dec --- /dev/null +++ b/tools/lint/stylelint.yml @@ -0,0 +1,15 @@ +--- +stylelint: + description: CSS linter + # Stylelint infra handles its own path filtering, so just include cwd + include: ['.'] + exclude: [] + extensions: ['css', 'scss'] + support-files: + - 'package.json' + - '**/.stylelintrc.js' + - '.stylelintignore' + - 'tools/lint/stylelint/**' + type: external + payload: stylelint:lint + setup: stylelint:setup diff --git a/tools/lint/stylelint/__init__.py b/tools/lint/stylelint/__init__.py new file mode 100644 index 0000000000..417ee836a2 --- /dev/null +++ b/tools/lint/stylelint/__init__.py @@ -0,0 +1,195 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 setup_helper +from mozbuild.nodeutil import 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 setup_helper.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 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 [] + 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))) + + result = run(cmd_args, config, fix) + if result == 1: + return result + + return result + + +def run(cmd_args, config, fix): + shell = False + if 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 + ) + signal.signal(signal.SIGINT, orig) + + try: + output, errors = proc.communicate() + except KeyboardInterrupt: + proc.kill() + return {"results": [], "fixed": 0} + + if errors: + errors = errors.decode(encoding, "replace") + print(STYLELINT_ERROR_MESSAGE.format(errors)) + + # 0 is success, 2 is there was at least 1 rule violation. Anything else + # is more serious. + if proc.returncode != 0 and proc.returncode != 2: + if proc.returncode == 78: + print("Stylelint reported an issue with its configuration file.") + print(output) + return 1 + + if not output: + return {"results": [], "fixed": 0} # no output means success + output = output.decode(encoding, "replace") + try: + jsonresult = json.loads(output) + except ValueError: + print(STYLELINT_ERROR_MESSAGE.format(output)) + 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} + + +def is_windows(): + return ( + os.environ.get("MSYSTEM") in ("MINGW32", "MINGW64") + or "MOZILLABUILD" in os.environ + ) diff --git a/tools/lint/test-manifest-alpha.yml b/tools/lint/test-manifest-alpha.yml new file mode 100644 index 0000000000..328da4cb1b --- /dev/null +++ b/tools/lint/test-manifest-alpha.yml @@ -0,0 +1,19 @@ +--- +test-manifest-alpha: + description: Mochitest manifest tests should be in alphabetical order. + exclude: + - "**/application.ini" + - "**/l10n.ini" + - "**/xpcshell.ini" + - "**/python.ini" + - "**/manifest.ini" + - dom/canvas/test/webgl-conf/mochitest-errata.toml + - python/mozbuild/mozbuild/test/backend/data + - testing/mozbase/manifestparser/tests + - testing/web-platform + - xpcom/tests/unit/data + extensions: ['ini'] + type: external + payload: test-manifest-alpha:lint + support-files: + - 'tools/lint/test-manifest-alpha/error-level-manifests.yml' diff --git a/tools/lint/test-manifest-alpha/__init__.py b/tools/lint/test-manifest-alpha/__init__.py new file mode 100644 index 0000000000..87d6ce5c5d --- /dev/null +++ b/tools/lint/test-manifest-alpha/__init__.py @@ -0,0 +1,77 @@ +# 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 difflib +import os +import sys + +import yaml +from manifestparser import TestManifest +from mozlint import result +from mozlint.pathutils import expand_exclusions +from mozpack import path as mozpath + +# Since this linter matches on all files with .ini extensions, we can omit +# those extensions from this allowlist, which makes it easier to match +# against variants like "mochitest-serviceworker.ini". +FILENAME_ALLOWLIST = ["mochitest", "browser", "chrome", "a11y"] + +here = os.path.abspath(os.path.dirname(__file__)) +ERROR_LEVEL_MANIFESTS_PATH = os.path.join(here, "error-level-manifests.yml") + + +def lint(paths, config, fix=None, **lintargs): + try: + with open(ERROR_LEVEL_MANIFESTS_PATH) as f: + error_level_manifests = yaml.safe_load(f) + except (IOError, ValueError) as e: + print("{}: error:\n {}".format(ERROR_LEVEL_MANIFESTS_PATH, e), file=sys.stderr) + sys.exit(1) + + topsrcdir = lintargs["root"] + + results = [] + file_names = list(expand_exclusions(paths, config, lintargs["root"])) + + for file_name in file_names: + name = os.path.basename(file_name) + if not any(name.startswith(allowed) for allowed in FILENAME_ALLOWLIST): + continue + + manifest = TestManifest(manifests=(file_name,), strict=False) + + test_names = [test["name"] for test in manifest.tests] + sorted_test_names = sorted(test_names) + + if test_names != sorted_test_names: + rel_file_path = mozpath.relpath(file_name, topsrcdir) + level = "warning" + + if (mozpath.normsep(rel_file_path) in error_level_manifests) or ( + any( + mozpath.match(rel_file_path, e) + for e in error_level_manifests + if "*" in e + ) + ): + level = "error" + + diff_instance = difflib.Differ() + diff_result = diff_instance.compare(test_names, sorted_test_names) + diff_list = list(diff_result) + + res = { + "path": rel_file_path, + "lineno": 0, + "column": 0, + "message": ( + "The mochitest test manifest is not in alphabetical order. " + "Expected ordering: \n\n%s\n\n" % "\n".join(sorted_test_names) + ), + "level": level, + "diff": "\n".join(diff_list), + } + results.append(result.from_config(config, **res)) + + return {"results": results, "fixed": 0} diff --git a/tools/lint/test-manifest-alpha/error-level-manifests.yml b/tools/lint/test-manifest-alpha/error-level-manifests.yml new file mode 100644 index 0000000000..f2491ab97d --- /dev/null +++ b/tools/lint/test-manifest-alpha/error-level-manifests.yml @@ -0,0 +1,8 @@ +--- +# This file contains a list of manifest files or directory patterns that have +# have been put into alphabetical order already. Items in this list will +# cause the test-manifest-alpha linter to use the Error level rather than +# the Warning level. + +- browser/** +- mobile/** diff --git a/tools/lint/test-manifest-disable.yml b/tools/lint/test-manifest-disable.yml new file mode 100644 index 0000000000..404ffe6c09 --- /dev/null +++ b/tools/lint/test-manifest-disable.yml @@ -0,0 +1,16 @@ +--- +no-comment-disable: + description: > + "Use 'disabled=<reason>' to disable a test instead of a + comment" + include: ['.'] + exclude: + - "**/application.ini" + - "**/l10n.ini" + - dom/canvas/test/webgl-conf/mochitest-errata.toml + - testing/mozbase/manifestparser/tests + - testing/web-platform + - xpcom/tests/unit/data + extensions: ['ini'] + type: regex + payload: ^[ \t]*(#|;)[ \t]*\[ diff --git a/tools/lint/test-manifest-skip-if.yml b/tools/lint/test-manifest-skip-if.yml new file mode 100644 index 0000000000..393beb42e2 --- /dev/null +++ b/tools/lint/test-manifest-skip-if.yml @@ -0,0 +1,18 @@ +--- +multiline-skip-if: + description: Conditions joined by || should be on separate lines + hint: | + skip-if = + <condition one> # reason + <condition two> # reason + exclude: + - "**/application.ini" + - "**/l10n.ini" + - dom/canvas/test/webgl-conf/mochitest-errata.toml + - testing/mozbase/manifestparser/tests + - testing/web-platform + - xpcom/tests/unit/data + extensions: ['ini'] + level: warning + type: regex + payload: '^\s*(skip|fail)-if\s*=[^(]*\|\|' diff --git a/tools/lint/test-manifest-toml.yml b/tools/lint/test-manifest-toml.yml new file mode 100644 index 0000000000..d1e4b7df94 --- /dev/null +++ b/tools/lint/test-manifest-toml.yml @@ -0,0 +1,32 @@ +--- +test-manifest-toml: + description: ManifestParser TOML linter. + exclude: + - 'intl/icu/source/test/testdata/codepointtrie/' + - 'python/mozbuild/mozbuild/test/' + - 'testing/marionette/harness/marionette_harness/tests/unit-tests.toml' + - 'testing/mozbase/manifestparser/tests/' + - 'third_party/rust/' + - 'toolkit/components/featuregates/test/python/data/' + - '**/.*ruff.toml' + - '**/Cargo.toml' + - '**/Cross.toml' + - '**/Features.toml' + - '**/ServoBindings.toml' + - '**/askama.toml' + - '**/audits.toml' + - '**/cbindgen.toml' + - '**/clippy.toml' + - '**/config-lock.toml' + - '**/config.toml' + - '**/cram.toml' + - '**/empty.toml' + - '**/generated-mochitest.toml' + - '**/l10n.toml' + - '**/labels.toml' + - '**/pyproject.toml' + - '**/rustfmt.toml' + - '**/uniffi.toml' + extensions: ['toml'] + type: external + payload: test-manifest-toml:lint diff --git a/tools/lint/test-manifest-toml/__init__.py b/tools/lint/test-manifest-toml/__init__.py new file mode 100644 index 0000000000..08f0e4ed93 --- /dev/null +++ b/tools/lint/test-manifest-toml/__init__.py @@ -0,0 +1,135 @@ +# 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 io +import os +import re + +from manifestparser import TestManifest +from manifestparser.toml import DEFAULT_SECTION, alphabetize_toml_str, sort_paths +from mozlint import result +from mozlint.pathutils import expand_exclusions +from mozpack import path as mozpath +from tomlkit.items import Array, Table + +SECTION_REGEX = r"^\[.*\]$" +DISABLE_REGEX = r"^[ \t]*#[ \t]*\[.*\]" + + +def make_result(path, message, is_error=False): + if is_error: + level = "error" + else: + level = "warning" + result = { + "path": path, + "lineno": 0, # tomlkit does not report lineno/column + "column": 0, + "message": message, + "level": level, + } + return result + + +def lint(paths, config, fix=None, **lintargs): + results = [] + fixed = 0 + topsrcdir = lintargs["root"] + file_names = list(expand_exclusions(paths, config, topsrcdir)) + file_names = [os.path.normpath(f) for f in file_names] + section_rx = re.compile(SECTION_REGEX, flags=re.M) + disable_rx = re.compile(DISABLE_REGEX, flags=re.M) + + for file_name in file_names: + path = mozpath.relpath(file_name, topsrcdir) + os.path.basename(file_name) + parser = TestManifest(use_toml=True, document=True) + + try: + parser.read(file_name) + except Exception: + r = make_result(path, "The manifest is not valid TOML.", True) + results.append(result.from_config(config, **r)) + continue + + manifest = parser.source_documents[file_name] + manifest_str = io.open(file_name, "r", encoding="utf-8").read() + + if not DEFAULT_SECTION in manifest: + r = make_result( + path, f"The manifest does not start with a [{DEFAULT_SECTION}] section." + ) + if fix: + fixed += 1 + results.append(result.from_config(config, **r)) + + sections = [k for k in manifest.keys() if k != DEFAULT_SECTION] + sorted_sections = sort_paths(sections) + if sections != sorted_sections: + r = make_result( + path, "The manifest sections are not in alphabetical order." + ) + if fix: + fixed += 1 + results.append(result.from_config(config, **r)) + + m = section_rx.findall(manifest_str) + if len(m) > 0: + for section_match in m: + section = section_match[1:-1] + if section == DEFAULT_SECTION: + continue + if not section.startswith('"'): + r = make_result( + path, f"The section name must be double quoted: [{section}]" + ) + if fix: + fixed += 1 + results.append(result.from_config(config, **r)) + + m = disable_rx.findall(manifest_str) + if len(m) > 0: + for disabled_section in m: + r = make_result( + path, + f"Use 'disabled = \"<reason>\"' to disable a test instead of a comment: {disabled_section}", + True, + ) + results.append(result.from_config(config, **r)) + + for section, keyvals in manifest.body: + if section is None: + continue + if not isinstance(keyvals, Table): + r = make_result( + path, f"Bad assignment in preamble: {section} = {keyvals}", True + ) + results.append(result.from_config(config, **r)) + else: + for k, v in keyvals.items(): + if k.endswith("-if"): + if not isinstance(v, Array): + r = make_result( + path, + f'Value for conditional must be an array: {k} = "{v}"', + True, + ) + results.append(result.from_config(config, **r)) + else: + for e in v: + if e.find("||") > 0 and e.find("&&") < 0: + r = make_result( + path, + f'Value for conditional must not include explicit ||, instead put on multiple lines: {k} = [ ... "{e}" ... ]', + True, + ) + results.append(result.from_config(config, **r)) + + if fix: + manifest_str = alphabetize_toml_str(manifest) + fp = io.open(file_name, "w", encoding="utf-8", newline="\n") + fp.write(manifest_str) + fp.close() + + return {"results": results, "fixed": fixed} diff --git a/tools/lint/test/conftest.py b/tools/lint/test/conftest.py new file mode 100644 index 0000000000..ad88f8aa97 --- /dev/null +++ b/tools/lint/test/conftest.py @@ -0,0 +1,305 @@ +import logging +import os +import pathlib +import sys +from collections import defaultdict + +import pytest +from mozbuild.base import MozbuildObject +from mozlint.parser import Parser +from mozlint.pathutils import findobject +from mozlint.result import ResultSummary +from mozlog.structuredlog import StructuredLogger +from mozpack import path + +here = path.abspath(path.dirname(__file__)) +build = MozbuildObject.from_environment(cwd=here, virtualenv_name="python-test") + +lintdir = path.dirname(here) +sys.path.insert(0, lintdir) +logger = logging.getLogger("mozlint") + + +def pytest_generate_tests(metafunc): + """Finds, loads and returns the config for the linter name specified by the + LINTER global variable in the calling module. + + This implies that each test file (that uses this fixture) should only be + used to test a single linter. If no LINTER variable is defined, the test + will fail. + """ + if "config" in metafunc.fixturenames: + if not hasattr(metafunc.module, "LINTER"): + pytest.fail( + "'config' fixture used from a module that didn't set the LINTER variable" + ) + + name = metafunc.module.LINTER + config_path = path.join(lintdir, "{}.yml".format(name)) + parser = Parser(build.topsrcdir) + configs = parser.parse(config_path) + config_names = {config["name"] for config in configs} + + marker = metafunc.definition.get_closest_marker("lint_config") + if marker: + config_name = marker.kwargs["name"] + if config_name not in config_names: + pytest.fail(f"lint config {config_name} not present in {name}.yml") + configs = [ + config for config in configs if config["name"] == marker.kwargs["name"] + ] + + ids = [config["name"] for config in configs] + metafunc.parametrize("config", configs, ids=ids) + + +@pytest.fixture(scope="module") +def root(request): + """Return the root directory for the files of the linter under test. + + For example, with LINTER=flake8 this would be tools/lint/test/files/flake8. + """ + if not hasattr(request.module, "LINTER"): + pytest.fail( + "'root' fixture used from a module that didn't set the LINTER variable" + ) + return path.join(here, "files", request.module.LINTER) + + +@pytest.fixture(scope="module") +def paths(root): + """Return a function that can resolve file paths relative to the linter + under test. + + Can be used like `paths('foo.py', 'bar/baz')`. This will return a list of + absolute paths under the `root` files directory. + """ + + def _inner(*paths): + if not paths: + return [root] + return [path.normpath(path.join(root, p)) for p in paths] + + return _inner + + +@pytest.fixture(autouse=True) +def run_setup(config): + """Make sure that if the linter named in the LINTER global variable has a + setup function, it gets called before running the tests. + """ + if "setup" not in config: + return + + if config["name"] == "clang-format": + # Skip the setup for the clang-format linter, as it requires a Mach context + # (which we may not have if pytest is invoked directly). + return + + log = logging.LoggerAdapter( + logger, {"lintname": config.get("name"), "pid": os.getpid()} + ) + + func = findobject(config["setup"]) + func( + build.topsrcdir, + virtualenv_manager=build.virtualenv_manager, + virtualenv_bin_path=build.virtualenv_manager.bin_path, + log=log, + ) + + +@pytest.fixture +def lint(config, root, request): + """Find and return the 'lint' function for the external linter named in the + LINTER global variable. + + This will automatically pass in the 'config' and 'root' arguments if not + specified. + """ + + if hasattr(request.module, "fixed"): + request.module.fixed = 0 + + try: + func = findobject(config["payload"]) + except (ImportError, ValueError): + pytest.fail( + "could not resolve a lint function from '{}'".format(config["payload"]) + ) + + ResultSummary.root = root + + def wrapper(paths, config=config, root=root, collapse_results=False, **lintargs): + logger.setLevel(logging.DEBUG) + lintargs["log"] = logging.LoggerAdapter( + logger, {"lintname": config.get("name"), "pid": os.getpid()} + ) + + results = func(paths, config, root=root, **lintargs) + if hasattr(request.module, "fixed") and isinstance(results, dict): + request.module.fixed += results["fixed"] + + if isinstance(results, dict): + results = results["results"] + + if isinstance(results, (list, tuple)): + results = sorted(results) + + if not collapse_results: + return results + + ret = defaultdict(list) + for r in results: + ret[r.relpath].append(r) + return ret + + return wrapper + + +@pytest.fixture +def structuredlog_lint(config, root, logger=None): + """Find and return the 'lint' function for the external linter named in the + LINTER global variable. This variant of the lint function is for linters that + use the 'structuredlog' type. + + This will automatically pass in the 'config' and 'root' arguments if not + specified. + """ + try: + func = findobject(config["payload"]) + except (ImportError, ValueError): + pytest.fail( + "could not resolve a lint function from '{}'".format(config["payload"]) + ) + + ResultSummary.root = root + + if not logger: + logger = structured_logger() + + def wrapper( + paths, + config=config, + root=root, + logger=logger, + collapse_results=False, + **lintargs, + ): + lintargs["log"] = logging.LoggerAdapter( + logger, {"lintname": config.get("name"), "pid": os.getpid()} + ) + results = func(paths, config, root=root, logger=logger, **lintargs) + if not collapse_results: + return results + + ret = defaultdict(list) + for r in results: + ret[r.path].append(r) + return ret + + return wrapper + + +@pytest.fixture +def global_lint(config, root, request): + try: + func = findobject(config["payload"]) + except (ImportError, ValueError): + pytest.fail( + "could not resolve a lint function from '{}'".format(config["payload"]) + ) + + ResultSummary.root = root + + def wrapper(config=config, root=root, collapse_results=False, **lintargs): + logger.setLevel(logging.DEBUG) + lintargs["log"] = logging.LoggerAdapter( + logger, {"lintname": config.get("name"), "pid": os.getpid()} + ) + results = func(config, root=root, **lintargs) + if hasattr(request.module, "fixed") and isinstance(results, dict): + request.module.fixed += results["fixed"] + + if isinstance(results, dict): + results = results["results"] + + if isinstance(results, (list, tuple)): + results = sorted(results) + + if not collapse_results: + return results + + ret = defaultdict(list) + for r in results: + ret[r.relpath].append(r) + return ret + + return wrapper + + +@pytest.fixture +def create_temp_file(tmpdir): + def inner(contents, name=None): + name = name or "temp.py" + path = tmpdir.join(name) + path.write(contents) + return path.strpath + + return inner + + +@pytest.fixture +def structured_logger(): + return StructuredLogger("logger") + + +@pytest.fixture +def perfdocs_sample(): + from test_perfdocs import ( + DYNAMIC_SAMPLE_CONFIG, + SAMPLE_CONFIG, + SAMPLE_INI, + SAMPLE_TEST, + temp_dir, + temp_file, + ) + + with temp_dir() as tmpdir: + suite_dir = pathlib.Path(tmpdir, "suite") + raptor_dir = pathlib.Path(tmpdir, "raptor") + raptor_suitedir = pathlib.Path(tmpdir, "raptor", "suite") + raptor_another_suitedir = pathlib.Path(tmpdir, "raptor", "another_suite") + perfdocs_dir = pathlib.Path(tmpdir, "perfdocs") + + perfdocs_dir.mkdir(parents=True, exist_ok=True) + suite_dir.mkdir(parents=True, exist_ok=True) + raptor_dir.mkdir(parents=True, exist_ok=True) + raptor_suitedir.mkdir(parents=True, exist_ok=True) + raptor_another_suitedir.mkdir(parents=True, exist_ok=True) + + with temp_file( + "perftest.toml", tempdir=suite_dir, content='["perftest_sample.js"]' + ) as tmpmanifest, temp_file( + "raptor_example1.ini", tempdir=raptor_suitedir, content=SAMPLE_INI + ) as tmpexample1manifest, temp_file( + "raptor_example2.ini", tempdir=raptor_another_suitedir, content=SAMPLE_INI + ) as tmpexample2manifest, temp_file( + "perftest_sample.js", tempdir=suite_dir, content=SAMPLE_TEST + ) as tmptest, temp_file( + "config.yml", tempdir=perfdocs_dir, content=SAMPLE_CONFIG + ) as tmpconfig, temp_file( + "config_2.yml", tempdir=perfdocs_dir, content=DYNAMIC_SAMPLE_CONFIG + ) as tmpconfig_2, temp_file( + "index.rst", tempdir=perfdocs_dir, content="{documentation}" + ) as tmpindex: + yield { + "top_dir": tmpdir, + "manifest": {"path": tmpmanifest}, + "example1_manifest": tmpexample1manifest, + "example2_manifest": tmpexample2manifest, + "test": tmptest, + "config": tmpconfig, + "config_2": tmpconfig_2, + "index": tmpindex, + } diff --git a/tools/lint/test/files/android-format/Bad.java b/tools/lint/test/files/android-format/Bad.java new file mode 100644 index 0000000000..13aa5d49d5 --- /dev/null +++ b/tools/lint/test/files/android-format/Bad.java @@ -0,0 +1,8 @@ +package org.mozilla.geckoview; + +import java.util.Arrays; + +public class Bad { + public static void main() { + } +} diff --git a/tools/lint/test/files/android-format/Main.kt b/tools/lint/test/files/android-format/Main.kt new file mode 100644 index 0000000000..a172cf71ee --- /dev/null +++ b/tools/lint/test/files/android-format/Main.kt @@ -0,0 +1,7 @@ +package org.mozilla.geckoview + +import java.util.Arrays; + +fun main() { +println("Hello") +} diff --git a/tools/lint/test/files/android-format/build.gradle b/tools/lint/test/files/android-format/build.gradle new file mode 100644 index 0000000000..6d2aff6d60 --- /dev/null +++ b/tools/lint/test/files/android-format/build.gradle @@ -0,0 +1 @@ +buildDir "${topobjdir}/gradle/build/tools/lint/test/files/android-format" diff --git a/tools/lint/test/files/black/bad.py b/tools/lint/test/files/black/bad.py new file mode 100644 index 0000000000..0a50df4dd9 --- /dev/null +++ b/tools/lint/test/files/black/bad.py @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +print ( + "test" + ) diff --git a/tools/lint/test/files/black/invalid.py b/tools/lint/test/files/black/invalid.py new file mode 100644 index 0000000000..079ecbad30 --- /dev/null +++ b/tools/lint/test/files/black/invalid.py @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +print( diff --git a/tools/lint/test/files/clang-format/bad/bad.cpp b/tools/lint/test/files/clang-format/bad/bad.cpp new file mode 100644 index 0000000000..f08a83f795 --- /dev/null +++ b/tools/lint/test/files/clang-format/bad/bad.cpp @@ -0,0 +1,6 @@ +int main ( ) { + +return 0; + + +} diff --git a/tools/lint/test/files/clang-format/bad/bad2.c b/tools/lint/test/files/clang-format/bad/bad2.c new file mode 100644 index 0000000000..9792e85071 --- /dev/null +++ b/tools/lint/test/files/clang-format/bad/bad2.c @@ -0,0 +1,8 @@ +#include "bad2.h" + + +int bad2() { + int a =2; + return a; + +} diff --git a/tools/lint/test/files/clang-format/bad/bad2.h b/tools/lint/test/files/clang-format/bad/bad2.h new file mode 100644 index 0000000000..a35d49d7e7 --- /dev/null +++ b/tools/lint/test/files/clang-format/bad/bad2.h @@ -0,0 +1 @@ +int bad2(void ); diff --git a/tools/lint/test/files/clang-format/bad/good.cpp b/tools/lint/test/files/clang-format/bad/good.cpp new file mode 100644 index 0000000000..76e8197013 --- /dev/null +++ b/tools/lint/test/files/clang-format/bad/good.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tools/lint/test/files/clang-format/good/foo.cpp b/tools/lint/test/files/clang-format/good/foo.cpp new file mode 100644 index 0000000000..76e8197013 --- /dev/null +++ b/tools/lint/test/files/clang-format/good/foo.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tools/lint/test/files/clippy/test1/Cargo.toml b/tools/lint/test/files/clippy/test1/Cargo.toml new file mode 100644 index 0000000000..92d5072eca --- /dev/null +++ b/tools/lint/test/files/clippy/test1/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "hello_world" # the name of the package +version = "0.1.0" # the current version, obeying semver +authors = ["Alice <a@example.com>", "Bob <b@example.com>"] + +[[bin]] +name ="good" +path = "good.rs" + +[[bin]] +name ="bad" +path = "bad.rs" + +[[bin]] +name ="bad2" +path = "bad2.rs" + diff --git a/tools/lint/test/files/clippy/test1/bad.rs b/tools/lint/test/files/clippy/test1/bad.rs new file mode 100644 index 0000000000..c403fba603 --- /dev/null +++ b/tools/lint/test/files/clippy/test1/bad.rs @@ -0,0 +1,14 @@ +fn main() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); + // Clippy detects this as a swap and considers this as an error + let mut a=1; + let mut b=1; + + a = b; + b = a; + + +} diff --git a/tools/lint/test/files/clippy/test1/bad2.rs b/tools/lint/test/files/clippy/test1/bad2.rs new file mode 100644 index 0000000000..bf488bbe72 --- /dev/null +++ b/tools/lint/test/files/clippy/test1/bad2.rs @@ -0,0 +1,14 @@ +fn main() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); + let mut a; + let mut b=1; + let mut vec = vec![1, 2]; + + for x in 5..10 - 5 { + a = x; + } + + } diff --git a/tools/lint/test/files/clippy/test1/good.rs b/tools/lint/test/files/clippy/test1/good.rs new file mode 100644 index 0000000000..9bcaee67b7 --- /dev/null +++ b/tools/lint/test/files/clippy/test1/good.rs @@ -0,0 +1,6 @@ +fn main() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); +} diff --git a/tools/lint/test/files/clippy/test2/Cargo.lock b/tools/lint/test/files/clippy/test2/Cargo.lock new file mode 100644 index 0000000000..6b2bc69eeb --- /dev/null +++ b/tools/lint/test/files/clippy/test2/Cargo.lock @@ -0,0 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "hello_world_2" +version = "0.2.0" diff --git a/tools/lint/test/files/clippy/test2/Cargo.toml b/tools/lint/test/files/clippy/test2/Cargo.toml new file mode 100644 index 0000000000..b0ac992088 --- /dev/null +++ b/tools/lint/test/files/clippy/test2/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello_world_2" # the name of the package +version = "0.2.0" # the current version, obeying semver +authors = ["Alice <a@example.com>", "Bob <b@example.com>"] + +[[bin]] +name = "fake_lib1" +path = "src/bad_1.rs" diff --git a/tools/lint/test/files/clippy/test2/src/bad_1.rs b/tools/lint/test/files/clippy/test2/src/bad_1.rs new file mode 100644 index 0000000000..2fe0630202 --- /dev/null +++ b/tools/lint/test/files/clippy/test2/src/bad_1.rs @@ -0,0 +1,15 @@ +mod bad_2; + +fn main() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); + // Clippy detects this as a swap and considers this as an error + let mut a=1; + let mut b=1; + + a = b; + b = a; + +} diff --git a/tools/lint/test/files/clippy/test2/src/bad_2.rs b/tools/lint/test/files/clippy/test2/src/bad_2.rs new file mode 100644 index 0000000000..f77de330b4 --- /dev/null +++ b/tools/lint/test/files/clippy/test2/src/bad_2.rs @@ -0,0 +1,17 @@ +fn foo() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); + let mut a; + let mut b=1; + let mut vec = Vec::new(); + vec.push(1); + vec.push(2); + + + for x in 5..10 - 5 { + a = x; + } + + } diff --git a/tools/lint/test/files/codespell/ignore.rst b/tools/lint/test/files/codespell/ignore.rst new file mode 100644 index 0000000000..1371d07054 --- /dev/null +++ b/tools/lint/test/files/codespell/ignore.rst @@ -0,0 +1,5 @@ +This is a file with some typos and informations. +But also testing false positive like optin (because this isn't always option) +or stuff related to our coding style like: +aparent (aParent). +but detects mistakes like mozila diff --git a/tools/lint/test/files/condprof-addons/browsertime.yml b/tools/lint/test/files/condprof-addons/browsertime.yml new file mode 100644 index 0000000000..7f065809d9 --- /dev/null +++ b/tools/lint/test/files/condprof-addons/browsertime.yml @@ -0,0 +1,10 @@ +--- +firefox-addons: + description: "fixture for the expected firefox-addons.tar ci fetch config" + fetch: + type: static-url + artifact-name: firefox-addons.tar.zst + add-prefix: firefox-addons/ + url: https://localhost/fake-firefox-addons.tar + sha256: 20372ff1d58fc33d1568f8922fe66e2e2e01c77663820344d2a364a8ddd68281 + size: 3584000 diff --git a/tools/lint/test/files/condprof-addons/fake-condprof-config.json b/tools/lint/test/files/condprof-addons/fake-condprof-config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-condprof-config.json @@ -0,0 +1 @@ +{} diff --git a/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-01.json b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-01.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-01.json @@ -0,0 +1 @@ +{} diff --git a/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-02.json b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-02.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-02.json @@ -0,0 +1 @@ +{} diff --git a/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-03.json b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-03.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-03.json @@ -0,0 +1 @@ +{} diff --git a/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-01.xpi b/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-01.xpi new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-01.xpi diff --git a/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-02.xpi b/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-02.xpi new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-02.xpi diff --git a/tools/lint/test/files/condprof-addons/firefox-addons-fake.tar b/tools/lint/test/files/condprof-addons/firefox-addons-fake.tar Binary files differnew file mode 100644 index 0000000000..2b7e13b82c --- /dev/null +++ b/tools/lint/test/files/condprof-addons/firefox-addons-fake.tar diff --git a/tools/lint/test/files/condprof-addons/with-missing-xpi.json b/tools/lint/test/files/condprof-addons/with-missing-xpi.json new file mode 100644 index 0000000000..ae44833a70 --- /dev/null +++ b/tools/lint/test/files/condprof-addons/with-missing-xpi.json @@ -0,0 +1,5 @@ +{ + "addons": { + "non-existing": "http://localhost/non-existing.xpi" + } +} diff --git a/tools/lint/test/files/eslint/good.js b/tools/lint/test/files/eslint/good.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tools/lint/test/files/eslint/good.js diff --git a/tools/lint/test/files/eslint/import/bad_import.js b/tools/lint/test/files/eslint/import/bad_import.js new file mode 100644 index 0000000000..e2a8ec8de1 --- /dev/null +++ b/tools/lint/test/files/eslint/import/bad_import.js @@ -0,0 +1 @@ +/* import-globals-from notpresent/notpresent.js */ diff --git a/tools/lint/test/files/eslint/nolint/foo.txt b/tools/lint/test/files/eslint/nolint/foo.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tools/lint/test/files/eslint/nolint/foo.txt diff --git a/tools/lint/test/files/eslint/subdir/bad.js b/tools/lint/test/files/eslint/subdir/bad.js new file mode 100644 index 0000000000..9d2dd18f39 --- /dev/null +++ b/tools/lint/test/files/eslint/subdir/bad.js @@ -0,0 +1,2 @@ +// Missing semicolon +let foo = "bar" diff --git a/tools/lint/test/files/eslint/testprettierignore b/tools/lint/test/files/eslint/testprettierignore new file mode 100644 index 0000000000..c2df665174 --- /dev/null +++ b/tools/lint/test/files/eslint/testprettierignore @@ -0,0 +1 @@ +# Intentionally empty file. diff --git a/tools/lint/test/files/file-perm/maybe-shebang/bad.js b/tools/lint/test/files/file-perm/maybe-shebang/bad.js new file mode 100755 index 0000000000..1a0b4c5fd6 --- /dev/null +++ b/tools/lint/test/files/file-perm/maybe-shebang/bad.js @@ -0,0 +1,2 @@ +# Nothing too + diff --git a/tools/lint/test/files/file-perm/maybe-shebang/good.js b/tools/lint/test/files/file-perm/maybe-shebang/good.js new file mode 100755 index 0000000000..8149c0d4f3 --- /dev/null +++ b/tools/lint/test/files/file-perm/maybe-shebang/good.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + + +# Nothing + diff --git a/tools/lint/test/files/file-perm/no-shebang/bad-shebang.c b/tools/lint/test/files/file-perm/no-shebang/bad-shebang.c new file mode 100755 index 0000000000..7151678efa --- /dev/null +++ b/tools/lint/test/files/file-perm/no-shebang/bad-shebang.c @@ -0,0 +1,2 @@ +#!/bin/bash +int main() { return 0; } diff --git a/tools/lint/test/files/file-perm/no-shebang/bad.c b/tools/lint/test/files/file-perm/no-shebang/bad.c new file mode 100755 index 0000000000..76e8197013 --- /dev/null +++ b/tools/lint/test/files/file-perm/no-shebang/bad.c @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tools/lint/test/files/file-perm/no-shebang/bad.png b/tools/lint/test/files/file-perm/no-shebang/bad.png Binary files differnew file mode 100755 index 0000000000..db3a5fda7e --- /dev/null +++ b/tools/lint/test/files/file-perm/no-shebang/bad.png diff --git a/tools/lint/test/files/file-perm/no-shebang/good.c b/tools/lint/test/files/file-perm/no-shebang/good.c new file mode 100644 index 0000000000..76e8197013 --- /dev/null +++ b/tools/lint/test/files/file-perm/no-shebang/good.c @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tools/lint/test/files/file-whitespace/bad-newline.c b/tools/lint/test/files/file-whitespace/bad-newline.c new file mode 100644 index 0000000000..3746a0add3 --- /dev/null +++ b/tools/lint/test/files/file-whitespace/bad-newline.c @@ -0,0 +1,3 @@ +int main() { return 0; } + +
\ No newline at end of file diff --git a/tools/lint/test/files/file-whitespace/bad-windows.c b/tools/lint/test/files/file-whitespace/bad-windows.c new file mode 100644 index 0000000000..70d4c697b9 --- /dev/null +++ b/tools/lint/test/files/file-whitespace/bad-windows.c @@ -0,0 +1,3 @@ +int main(){
+ return 42;
+}
diff --git a/tools/lint/test/files/file-whitespace/bad.c b/tools/lint/test/files/file-whitespace/bad.c new file mode 100644 index 0000000000..4309b1f55d --- /dev/null +++ b/tools/lint/test/files/file-whitespace/bad.c @@ -0,0 +1,3 @@ +int main() { +return 0; +} diff --git a/tools/lint/test/files/file-whitespace/bad.js b/tools/lint/test/files/file-whitespace/bad.js new file mode 100644 index 0000000000..3441696ef1 --- /dev/null +++ b/tools/lint/test/files/file-whitespace/bad.js @@ -0,0 +1,3 @@ +# Nothing too + + diff --git a/tools/lint/test/files/file-whitespace/good.c b/tools/lint/test/files/file-whitespace/good.c new file mode 100644 index 0000000000..76e8197013 --- /dev/null +++ b/tools/lint/test/files/file-whitespace/good.c @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tools/lint/test/files/file-whitespace/good.js b/tools/lint/test/files/file-whitespace/good.js new file mode 100644 index 0000000000..8149c0d4f3 --- /dev/null +++ b/tools/lint/test/files/file-whitespace/good.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + + +# Nothing + diff --git a/tools/lint/test/files/fluent-lint/bad.ftl b/tools/lint/test/files/fluent-lint/bad.ftl new file mode 100644 index 0000000000..ebc0e1a602 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/bad.ftl @@ -0,0 +1,44 @@ +Blah-blah = Uppercase letters in identifiers are not permitted. +blah-blah = This is a legal identifier. +blah_blah = Underscores in identifiers are not permitted. + +bad-apostrophe-1 = The bee's knees +bad-apostrophe-end-1 = The bees' knees +bad-apostrophe-2 = The bee‘s knees +bad-single-quote = 'The bee’s knees' +ok-apostrophe = The bee’s knees +ok-single-quote = ‘The bee’s knees’ +bad-double-quote = "The bee’s knees" +good-double-quote = “The bee’s knees” +bad-ellipsis = The bee’s knees... +good-ellipsis = The bee’s knees… + +embedded-tag = Read more about <a data-l10n-name="privacy-policy"> our privacy policy </a>. +bad-embedded-tag = Read more about <a data-l10n-name="privacy-policy"> our 'privacy' policy </a>. + +Invalid_Id = This identifier is in the exclusions file and will not cause an error. + +good-has-attributes = + .accessKey = Attribute identifiers are not checked. + +bad-has-attributes = + .accessKey = Attribute 'values' are checked. + +good-function-call = Last modified: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") } + +# $engineName (String) - The engine name that will currently be used for the private window. +good-variable-identifier = { $engineName } is your default search engine in Private Windows + +short-id = I am too short + +identifiers-in-selectors-should-be-ignored = + .label = { $tabCount -> + [1] Send Tab to Device + [UPPERCASE] Send Tab to Device + *[other] Send { $tabCount } Tabs to Device + } + +this-message-reference-is-ignored = + .label = { menu-quit.label } + +ok-message-with-html-and-var = This is a <a href="{ $url }">link</a> diff --git a/tools/lint/test/files/fluent-lint/brand-names-excluded.ftl b/tools/lint/test/files/fluent-lint/brand-names-excluded.ftl new file mode 100644 index 0000000000..9f3afa28b8 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/brand-names-excluded.ftl @@ -0,0 +1,2 @@ +# Comment +bad-firefox1 = Welcome to Firefox diff --git a/tools/lint/test/files/fluent-lint/brand-names.ftl b/tools/lint/test/files/fluent-lint/brand-names.ftl new file mode 100644 index 0000000000..c338d920ca --- /dev/null +++ b/tools/lint/test/files/fluent-lint/brand-names.ftl @@ -0,0 +1,30 @@ +bad-firefox1 = Welcome to Firefox + +# Comment should be ignored when displaying the offset of the error +bad-firefox2 = Welcome to Firefox again +bad-firefox2b = <span>Welcome to Firefox<span> again +bad-firefox3 = <b>Firefox</b> +bad-firefox-excluded = <b>Firefox</b> + +bad-mozilla1 = Welcome to Mozilla +bad-mozilla2 = Welcome to Mozilla again +bad-mozilla2b = <span>Welcome to Mozilla</span> again +bad-mozilla3 = <b>Mozilla</b> + +bad-thunderbird1 = Welcome to Thunderbird +bad-thunderbird2 = <span>Welcome to Thunderbird</span> again +bad-thunderbird3 = <b>Thunderbird</b> + +good-firefox1 = Welcome to { -brand-firefox } +good-firefox2 = Welcome to { firefox-message } + +good-mozilla1 = Welcome to { -brand-mozilla } +good-mozilla2 = Welcome to { mozilla-message } + +good-thunderbird1 = Welcome to { -brand-thunderbird } +good-thunderbird2 = Welcome to { thunderbird-message } + +# There are no brand checks on terms +-brand-firefox = Firefox + +bland-message = No brands here. diff --git a/tools/lint/test/files/fluent-lint/comment-group1.ftl b/tools/lint/test/files/fluent-lint/comment-group1.ftl new file mode 100644 index 0000000000..32c19dc441 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-group1.ftl @@ -0,0 +1,35 @@ +# 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/. + +### Test group comments. + +fake-identifier-1 = Fake text + +## Pass: This group comment has proper spacing. + +fake-identifier-2 = Fake text +## Fail: (GC03) Group comments should have an empty line before them. + +fake-identifier-3 = Fake text + +## Fail: (GC02) Group comments should have an empty line after them. +fake-identifier-4 = Fake text + +## Pass: A single comment is fine. + +## Fail: (GC04) A group comment must be followed by at least one message. + +fake-identifier-5 = Fake text + + +## Fail: (GC03) Only allow 1 line above. + +fake-identifier-6 = Fake text + +## Fail: (GC02) Only allow 1 line below. + + +fake-identifier-6 = Fake text + +## Fail: (GC01) Group comments should not be at the end of a file. diff --git a/tools/lint/test/files/fluent-lint/comment-group2.ftl b/tools/lint/test/files/fluent-lint/comment-group2.ftl new file mode 100644 index 0000000000..47d29fa211 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-group2.ftl @@ -0,0 +1,15 @@ +# 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/. + +### Test group comments. + +## Pass: This group comment is followed by a term + +-fake-term = Fake text + +## Pass: The last group comment is allowed to be an empty ## + +fake-identifier-1 = Fake text + +## diff --git a/tools/lint/test/files/fluent-lint/comment-resource1.ftl b/tools/lint/test/files/fluent-lint/comment-resource1.ftl new file mode 100644 index 0000000000..f5d5e53d59 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-resource1.ftl @@ -0,0 +1,11 @@ +# 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/. + +### Pass: This is a resource comment with proper spacing. + +fake-identifier-1 = Fake text + +### Fail: (RC01) There should not be more than one resource comment + +fake-identifier-2 = Fake text diff --git a/tools/lint/test/files/fluent-lint/comment-resource2.ftl b/tools/lint/test/files/fluent-lint/comment-resource2.ftl new file mode 100644 index 0000000000..44a77f4e73 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-resource2.ftl @@ -0,0 +1,6 @@ +# 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/. +### Fail: (RC03) There should be an empty line preceeding. + +fake-identifier-1 = Fake text diff --git a/tools/lint/test/files/fluent-lint/comment-resource3.ftl b/tools/lint/test/files/fluent-lint/comment-resource3.ftl new file mode 100644 index 0000000000..b261404380 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-resource3.ftl @@ -0,0 +1,6 @@ +# 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/. + +### Fail: (RC02) There should be an empty line following. +fake-identifier-1 = Fake text diff --git a/tools/lint/test/files/fluent-lint/comment-resource4.ftl b/tools/lint/test/files/fluent-lint/comment-resource4.ftl new file mode 100644 index 0000000000..c24e8887f8 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-resource4.ftl @@ -0,0 +1,8 @@ +# 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/. + + +### Fail: (RC03) There should be only one space above. + +fake-identifier-1 = Fake text diff --git a/tools/lint/test/files/fluent-lint/comment-resource5.ftl b/tools/lint/test/files/fluent-lint/comment-resource5.ftl new file mode 100644 index 0000000000..60d8e8c264 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-resource5.ftl @@ -0,0 +1,8 @@ +# 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/. + +### Fail: (RC02) There should be only one space below. + + +fake-identifier-1 = Fake text diff --git a/tools/lint/test/files/fluent-lint/comment-resource6.ftl b/tools/lint/test/files/fluent-lint/comment-resource6.ftl new file mode 100644 index 0000000000..a2ca9abfe7 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-resource6.ftl @@ -0,0 +1,4 @@ +### Pass: Check two conditions. +### 1. This is an edge case, but we shouldn't error if there is only a resource comment. +### 2. Make sure this linter does not error if there is no license header. The license is +### checked with `mach lint license`. diff --git a/tools/lint/test/files/fluent-lint/comment-variables1.ftl b/tools/lint/test/files/fluent-lint/comment-variables1.ftl new file mode 100644 index 0000000000..10de9de195 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-variables1.ftl @@ -0,0 +1,60 @@ +# 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/. + +### Test group comments. +### $var doesn't count towards commented placeables + +message-without-comment = This string has a { $var } + +## Variables: +## $foo-group (String): group level comment + +# Variables: +# $foo1 (String): just text +message-with-comment = This string has a { $foo1 } + +message-with-group-comment = This string has a { $foo-group } + +select-without-comment1 = { + $select1 -> + [t] Foo + *[s] Bar +} + +select-without-comment2 = { + $select2 -> + [t] Foo { $select2 } + *[s] Bar +} + +## Variables: +## $select4 (Integer): a number + +# Variables: +# $select3 (Integer): a number +select-with-comment1 = { + $select3 -> + [t] Foo + *[s] Bar +} + +select-with-group-comment1 = { + $select4 -> + [t] Foo { $select4 } + *[s] Bar +} + +message-attribute-without-comment = + .label = This string as { $attr } + +# Variables: +# $attr2 (String): just text +message-attribute-with-comment = + .label = This string as { $attr2 } + +message-selection-function = + { PLATFORM() -> + [macos] foo + *[other] bar + } diff --git a/tools/lint/test/files/fluent-lint/comment-variables2.ftl b/tools/lint/test/files/fluent-lint/comment-variables2.ftl new file mode 100644 index 0000000000..2e9ae8e684 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/comment-variables2.ftl @@ -0,0 +1,27 @@ +# 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/. + +## Terms are not checked for variables, no need for comments. + +-term-without-comment1 = { $term1 } +-term-without-comment2 = { + $select2 -> + [t] Foo { $term2 } + *[s] Bar +} + +# Testing that variable references from terms are not kept around when analyzing +# standard messages (see bug 1812568). +# +# Variables: +# $message1 (String) - Just text +message-with-comment = This string has a { $message1 } + +# This comment is not necessary, just making sure it doesn't get carried over to +# the following message which uses the same variable. +# +# Variables: +# $term-message (string) - Text +-term-with-variable = { $term-message } +message-without-comment = This string has a { $term-message } diff --git a/tools/lint/test/files/fluent-lint/excluded.ftl b/tools/lint/test/files/fluent-lint/excluded.ftl new file mode 100644 index 0000000000..79fe509ad6 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/excluded.ftl @@ -0,0 +1,6 @@ +# This file is used to test excluding paths from tests. +Blah-blah = Uppercase letters in identifiers are not permitted. +blah-blah = This is a legal identifier. +blah_blah = Underscores in identifiers are not permitted. + +bad-apostrophe-1 = The bee's knees diff --git a/tools/lint/test/files/fluent-lint/test-brands.ftl b/tools/lint/test/files/fluent-lint/test-brands.ftl new file mode 100644 index 0000000000..1aa6e9d0e8 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/test-brands.ftl @@ -0,0 +1,5 @@ +# These are brands used in the fluent-lint test + +-brand-first = Firefox +-brand-second = Thunderbird +-brand-third = Mozilla diff --git a/tools/lint/test/files/fluent-lint/tools/lint/fluent-lint/exclusions.yml b/tools/lint/test/files/fluent-lint/tools/lint/fluent-lint/exclusions.yml new file mode 100644 index 0000000000..1aecf8cedd --- /dev/null +++ b/tools/lint/test/files/fluent-lint/tools/lint/fluent-lint/exclusions.yml @@ -0,0 +1,17 @@ +# 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/. +--- +ID01: + messages: + - Invalid_Id + files: + - excluded.ftl +ID02: + messages: [] + files: [] +CO01: + messages: + - bad-firefox-excluded + files: + - brand-names-excluded.ftl diff --git a/tools/lint/test/files/fluent-lint/valid-attributes.ftl b/tools/lint/test/files/fluent-lint/valid-attributes.ftl new file mode 100644 index 0000000000..c308fbd5b3 --- /dev/null +++ b/tools/lint/test/files/fluent-lint/valid-attributes.ftl @@ -0,0 +1,21 @@ +# 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/. + +### Test for valid attributes. + +message-with-known-attribute = + .label = Foo + +# String has a known label but with different case, not a warning +message-with-known-attribute-case = + .Label = Foo + +# Warning: unknown attribute +message-with-unknown-attribute = + .extralabel = Foo + +# NO warning: unknown attribute, but commented +# .extralabel is known +message-with-unknown-attribute-commented = + .extralabel = Foo diff --git a/tools/lint/test/files/license/.eslintrc.js b/tools/lint/test/files/license/.eslintrc.js new file mode 100644 index 0000000000..0449fdfa33 --- /dev/null +++ b/tools/lint/test/files/license/.eslintrc.js @@ -0,0 +1,5 @@ + +// Dot file to verify that it works +// without license + +"use strict"; diff --git a/tools/lint/test/files/license/bad.c b/tools/lint/test/files/license/bad.c new file mode 100644 index 0000000000..76e8197013 --- /dev/null +++ b/tools/lint/test/files/license/bad.c @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tools/lint/test/files/license/bad.js b/tools/lint/test/files/license/bad.js new file mode 100644 index 0000000000..5de1a72f1f --- /dev/null +++ b/tools/lint/test/files/license/bad.js @@ -0,0 +1,6 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Pulic Unknown License as published by + * the Free Software Foundation, version 3. + * + */ diff --git a/tools/lint/test/files/license/good-other.h b/tools/lint/test/files/license/good-other.h new file mode 100644 index 0000000000..fb915e9b26 --- /dev/null +++ b/tools/lint/test/files/license/good-other.h @@ -0,0 +1,9 @@ +/* +Permission to use, copy, modify, distribute and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation. Samphan Raruenrom makes no +representations about the suitability of this software for any +purpose. It is provided "as is" without express or implied warranty. +*/ diff --git a/tools/lint/test/files/license/good.c b/tools/lint/test/files/license/good.c new file mode 100644 index 0000000000..d1a6827fb1 --- /dev/null +++ b/tools/lint/test/files/license/good.c @@ -0,0 +1,8 @@ + +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +int main() { return 0; } diff --git a/tools/lint/test/files/license/good.js b/tools/lint/test/files/license/good.js new file mode 100644 index 0000000000..d10ae3a8d5 --- /dev/null +++ b/tools/lint/test/files/license/good.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +/* 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/. */ + +# Nothing + diff --git a/tools/lint/test/files/lintpref/bad.js b/tools/lint/test/files/lintpref/bad.js new file mode 100644 index 0000000000..ab55ba5dad --- /dev/null +++ b/tools/lint/test/files/lintpref/bad.js @@ -0,0 +1,2 @@ +// Real test pref, matching value. +pref("dom.webidl.test1", true); diff --git a/tools/lint/test/files/lintpref/good.js b/tools/lint/test/files/lintpref/good.js new file mode 100644 index 0000000000..0bf81c8f58 --- /dev/null +++ b/tools/lint/test/files/lintpref/good.js @@ -0,0 +1,6 @@ +// Fake prefs. +pref("foo.bar", 1); +pref("foo.baz", "1.234"); + +// Real test pref, different value. +pref("dom.webidl.test1", false); diff --git a/tools/lint/test/files/rst/.dotfile.rst b/tools/lint/test/files/rst/.dotfile.rst new file mode 100644 index 0000000000..be24e1d161 --- /dev/null +++ b/tools/lint/test/files/rst/.dotfile.rst @@ -0,0 +1,11 @@ +============ +Coding style +========== + +foo bar +~~~~~ + + +This file has error but should not be there +as we don't analyze dot files + diff --git a/tools/lint/test/files/rst/bad.rst b/tools/lint/test/files/rst/bad.rst new file mode 100644 index 0000000000..c9b60f613e --- /dev/null +++ b/tools/lint/test/files/rst/bad.rst @@ -0,0 +1,20 @@ +============ +Coding style +============ + + +This document attempts to explain the basic styles and patterns used in +the Mozilla codebase. New code should try to conform to these standards, +so it is as easy to maintain as existing code. There are exceptions, but +it's still important to know the rules! + + +Whitespace +~~~~~~~~ + +Line length +~~~~~~~~~~~ + +Line length +~~~~~~~~~~~ + diff --git a/tools/lint/test/files/rst/bad2.rst b/tools/lint/test/files/rst/bad2.rst new file mode 100644 index 0000000000..81c35dde06 --- /dev/null +++ b/tools/lint/test/files/rst/bad2.rst @@ -0,0 +1,4 @@ +==== +Test +=== + diff --git a/tools/lint/test/files/rst/bad3.rst b/tools/lint/test/files/rst/bad3.rst new file mode 100644 index 0000000000..b7e66e5c92 --- /dev/null +++ b/tools/lint/test/files/rst/bad3.rst @@ -0,0 +1,6 @@ + +.. _When_Should_I_Use_a_Hashtable.3F: + +When Should I Use a Hashtable? +------------------------------ + diff --git a/tools/lint/test/files/rst/good.rst b/tools/lint/test/files/rst/good.rst new file mode 100644 index 0000000000..fd12da85d3 --- /dev/null +++ b/tools/lint/test/files/rst/good.rst @@ -0,0 +1,11 @@ +============ +Coding style +============ + + +This document attempts to explain the basic styles and patterns used in +the Mozilla codebase. New code should try to conform to these standards, +so it is as easy to maintain as existing code. There are exceptions, but +it's still important to know the rules! + + diff --git a/tools/lint/test/files/ruff/bad.py b/tools/lint/test/files/ruff/bad.py new file mode 100644 index 0000000000..0015d7e7f9 --- /dev/null +++ b/tools/lint/test/files/ruff/bad.py @@ -0,0 +1,4 @@ +import distutils + +if not "foo" in "foobar": + print("oh no!") diff --git a/tools/lint/test/files/ruff/ruff.toml b/tools/lint/test/files/ruff/ruff.toml new file mode 100644 index 0000000000..34f5ca74a4 --- /dev/null +++ b/tools/lint/test/files/ruff/ruff.toml @@ -0,0 +1 @@ +# Empty config to force ruff to ignore the global one. diff --git a/tools/lint/test/files/rustfmt/subdir/bad.rs b/tools/lint/test/files/rustfmt/subdir/bad.rs new file mode 100644 index 0000000000..fb1746fafd --- /dev/null +++ b/tools/lint/test/files/rustfmt/subdir/bad.rs @@ -0,0 +1,16 @@ +fn main() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); + // Clippy detects this as a swap and considers this as an error + let mut a = + 1; + let mut b=1; + + a = + b; + b = a; + + +} diff --git a/tools/lint/test/files/rustfmt/subdir/bad2.rs b/tools/lint/test/files/rustfmt/subdir/bad2.rs new file mode 100644 index 0000000000..a4236a2de7 --- /dev/null +++ b/tools/lint/test/files/rustfmt/subdir/bad2.rs @@ -0,0 +1,17 @@ +fn main() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); + let mut a; + let mut b=1; + let mut vec = Vec::new(); + vec.push(1); + vec.push(2); + + + for x in 5..10 - 5 { + a = x; + } + + } diff --git a/tools/lint/test/files/rustfmt/subdir/good.rs b/tools/lint/test/files/rustfmt/subdir/good.rs new file mode 100644 index 0000000000..9bcaee67b7 --- /dev/null +++ b/tools/lint/test/files/rustfmt/subdir/good.rs @@ -0,0 +1,6 @@ +fn main() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); +} diff --git a/tools/lint/test/files/shellcheck/bad.sh b/tools/lint/test/files/shellcheck/bad.sh new file mode 100644 index 0000000000..b2eb195558 --- /dev/null +++ b/tools/lint/test/files/shellcheck/bad.sh @@ -0,0 +1,3 @@ +#!/bin/sh +hello="Hello world" +echo $1 diff --git a/tools/lint/test/files/shellcheck/good.sh b/tools/lint/test/files/shellcheck/good.sh new file mode 100644 index 0000000000..e61d501955 --- /dev/null +++ b/tools/lint/test/files/shellcheck/good.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "Hello world" diff --git a/tools/lint/test/files/stylelint/nolint/foo.txt b/tools/lint/test/files/stylelint/nolint/foo.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tools/lint/test/files/stylelint/nolint/foo.txt diff --git a/tools/lint/test/files/stylelint/subdir/bad.css b/tools/lint/test/files/stylelint/subdir/bad.css new file mode 100644 index 0000000000..70004c1fb2 --- /dev/null +++ b/tools/lint/test/files/stylelint/subdir/bad.css @@ -0,0 +1,5 @@ +#foo { + /* Duplicate property: */ + font-size: 12px; + font-size: 12px; +} diff --git a/tools/lint/test/files/test-manifest-alpha/mochitest-in-order.ini b/tools/lint/test/files/test-manifest-alpha/mochitest-in-order.ini new file mode 100644 index 0000000000..9de566702a --- /dev/null +++ b/tools/lint/test/files/test-manifest-alpha/mochitest-in-order.ini @@ -0,0 +1,29 @@ +[DEFAULT] +support-files = + click-event-helper.js + head.js + test-button-overlay.html + ../../../../dom/media/test/gizmo.mp4 + ../../../../dom/media/test/owl.mp3 + +prefs = + media.videocontrols.picture-in-picture.display-text-tracks.enabled=false + +[browser_AAA_run_first_firstTimePiPToggleEvents.js] +[browser_aaa_run_first_firstTimePiPToggleEvents.js] +[browser_backgroundTab.js] +[browser_cannotTriggerFromContent.js] +[browser_closePipPause.js] +skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205 +[browser_closePip_pageNavigationChanges.js] +[browser_closePlayer.js] +[browser_closeTab.js] +[browser_close_unpip_focus.js] +[browser_contextMenu.js] +[browser_cornerSnapping.js] +run-if = os == "mac" +[browser_dblclickFullscreen.js] +[browser_flipIconWithRTL.js] +skip-if = + os == "linux" && ccov # Bug 1678091 + tsan # Bug 1678091 diff --git a/tools/lint/test/files/test-manifest-alpha/mochitest-mostly-in-order.ini b/tools/lint/test/files/test-manifest-alpha/mochitest-mostly-in-order.ini new file mode 100644 index 0000000000..01f2551bd4 --- /dev/null +++ b/tools/lint/test/files/test-manifest-alpha/mochitest-mostly-in-order.ini @@ -0,0 +1,30 @@ +[DEFAULT] +support-files = + click-event-helper.js + head.js + test-button-overlay.html + ../../../../dom/media/test/gizmo.mp4 + ../../../../dom/media/test/owl.mp3 + +prefs = + media.videocontrols.picture-in-picture.display-text-tracks.enabled=false + +[browser_AAA_run_first_firstTimePiPToggleEvents.js] +[browser_aaa_run_first_firstTimePiPToggleEvents.js] +[browser_backgroundTab.js] +[browser_cannotTriggerFromContent.js] +[browser_closePipPause.js] +skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205 +[browser_closePip_pageNavigationChanges.js] +[browser_closePlayer.js] +[browser_closeTab.js] +[browser_close_unpip_focus.js] +[browser_contextMenu.js] +[browser_cornerSnapping.js] +run-if = os == "mac" +[browser_dblclickFullscreen.js] +[browser_flipIconWithRTL.js] +skip-if = + os == "linux" && ccov # Bug 1678091 + tsan # Bug 1678091 +[browser_a_new_test.js] diff --git a/tools/lint/test/files/test-manifest-alpha/mochitest-very-out-of-order.ini b/tools/lint/test/files/test-manifest-alpha/mochitest-very-out-of-order.ini new file mode 100644 index 0000000000..45bfdd776b --- /dev/null +++ b/tools/lint/test/files/test-manifest-alpha/mochitest-very-out-of-order.ini @@ -0,0 +1,29 @@ +[DEFAULT] +support-files = + click-event-helper.js + head.js + test-button-overlay.html + ../../../../dom/media/test/gizmo.mp4 + ../../../../dom/media/test/owl.mp3 + +prefs = + media.videocontrols.picture-in-picture.display-text-tracks.enabled=false + +[browser_contextMenu.js] +[browser_aaa_run_first_firstTimePiPToggleEvents.js] +[browser_AAA_run_first_firstTimePiPToggleEvents.js] +[browser_backgroundTab.js] +[browser_cannotTriggerFromContent.js] +[browser_close_unpip_focus.js] +[browser_closePip_pageNavigationChanges.js] +[browser_closePipPause.js] +skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205 +[browser_cornerSnapping.js] +run-if = os == "mac" +[browser_closePlayer.js] +[browser_closeTab.js] +[browser_dblclickFullscreen.js] +[browser_flipIconWithRTL.js] +skip-if = + os == "linux" && ccov # Bug 1678091 + tsan # Bug 1678091 diff --git a/tools/lint/test/files/test-manifest-alpha/other-ini-very-out-of-order.ini b/tools/lint/test/files/test-manifest-alpha/other-ini-very-out-of-order.ini new file mode 100644 index 0000000000..45bfdd776b --- /dev/null +++ b/tools/lint/test/files/test-manifest-alpha/other-ini-very-out-of-order.ini @@ -0,0 +1,29 @@ +[DEFAULT] +support-files = + click-event-helper.js + head.js + test-button-overlay.html + ../../../../dom/media/test/gizmo.mp4 + ../../../../dom/media/test/owl.mp3 + +prefs = + media.videocontrols.picture-in-picture.display-text-tracks.enabled=false + +[browser_contextMenu.js] +[browser_aaa_run_first_firstTimePiPToggleEvents.js] +[browser_AAA_run_first_firstTimePiPToggleEvents.js] +[browser_backgroundTab.js] +[browser_cannotTriggerFromContent.js] +[browser_close_unpip_focus.js] +[browser_closePip_pageNavigationChanges.js] +[browser_closePipPause.js] +skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205 +[browser_cornerSnapping.js] +run-if = os == "mac" +[browser_closePlayer.js] +[browser_closeTab.js] +[browser_dblclickFullscreen.js] +[browser_flipIconWithRTL.js] +skip-if = + os == "linux" && ccov # Bug 1678091 + tsan # Bug 1678091 diff --git a/tools/lint/test/files/test-manifest-toml/comment-section.toml b/tools/lint/test/files/test-manifest-toml/comment-section.toml new file mode 100644 index 0000000000..fa7289ee90 --- /dev/null +++ b/tools/lint/test/files/test-manifest-toml/comment-section.toml @@ -0,0 +1,5 @@ +[DEFAULT] + +# ["aaa.js"] + + # ["bbb.js"] diff --git a/tools/lint/test/files/test-manifest-toml/invalid.toml b/tools/lint/test/files/test-manifest-toml/invalid.toml new file mode 100644 index 0000000000..d9e8ef6463 --- /dev/null +++ b/tools/lint/test/files/test-manifest-toml/invalid.toml @@ -0,0 +1,2 @@ +# Invalid TOML +& = @ diff --git a/tools/lint/test/files/test-manifest-toml/no-default.toml b/tools/lint/test/files/test-manifest-toml/no-default.toml new file mode 100644 index 0000000000..b891d4ffb8 --- /dev/null +++ b/tools/lint/test/files/test-manifest-toml/no-default.toml @@ -0,0 +1 @@ +# this Manifest has no DEFAULT section diff --git a/tools/lint/test/files/test-manifest-toml/non-double-quote-sections.toml b/tools/lint/test/files/test-manifest-toml/non-double-quote-sections.toml new file mode 100644 index 0000000000..abfa25eb68 --- /dev/null +++ b/tools/lint/test/files/test-manifest-toml/non-double-quote-sections.toml @@ -0,0 +1,6 @@ +[DEFAULT] + +[aaa.js] +# Every non DEFAULT section must be double quoted + +['bbb.js'] diff --git a/tools/lint/test/files/test-manifest-toml/skip-if-explicit-or.toml b/tools/lint/test/files/test-manifest-toml/skip-if-explicit-or.toml new file mode 100644 index 0000000000..082baca3c3 --- /dev/null +++ b/tools/lint/test/files/test-manifest-toml/skip-if-explicit-or.toml @@ -0,0 +1,7 @@ +[DEFAULT] + +["aaa.js"] +skip-if = ["asan || verify"] # should be two lines + +["bbb.js"] +skip-if = ["(asan || verify) && os == 'linux'"] # OK diff --git a/tools/lint/test/files/test-manifest-toml/skip-if-not-array.toml b/tools/lint/test/files/test-manifest-toml/skip-if-not-array.toml new file mode 100644 index 0000000000..e6f7460fb9 --- /dev/null +++ b/tools/lint/test/files/test-manifest-toml/skip-if-not-array.toml @@ -0,0 +1,4 @@ +[DEFAULT] + +["aaa.js"] +run-if = "os == 'linux'" diff --git a/tools/lint/test/files/test-manifest-toml/unsorted.toml b/tools/lint/test/files/test-manifest-toml/unsorted.toml new file mode 100644 index 0000000000..5e949daa04 --- /dev/null +++ b/tools/lint/test/files/test-manifest-toml/unsorted.toml @@ -0,0 +1,10 @@ +[DEFAULT] +# unsorted sections + +["ccc.js"] + +["aaa.js"] + +["bug_10.js"] + +["bug_2.js"] diff --git a/tools/lint/test/files/test-manifest-toml/valid.toml b/tools/lint/test/files/test-manifest-toml/valid.toml new file mode 100644 index 0000000000..20ff24979d --- /dev/null +++ b/tools/lint/test/files/test-manifest-toml/valid.toml @@ -0,0 +1,2 @@ +[DEFAULT] +# a minimal valid ManifestParser TOML file diff --git a/tools/lint/test/files/trojan-source/README b/tools/lint/test/files/trojan-source/README new file mode 100644 index 0000000000..343a9d0c3c --- /dev/null +++ b/tools/lint/test/files/trojan-source/README @@ -0,0 +1,5 @@ +These examples are taken from trojan source: +https://github.com/nickboucher/trojan-source + +The examples are published under the MIT license. + diff --git a/tools/lint/test/files/trojan-source/commenting-out.cpp b/tools/lint/test/files/trojan-source/commenting-out.cpp new file mode 100644 index 0000000000..d67df70ce1 --- /dev/null +++ b/tools/lint/test/files/trojan-source/commenting-out.cpp @@ -0,0 +1,9 @@ +#include <iostream> + +int main() { + bool isAdmin = false; + /* } if (isAdmin) begin admins only */ + std::cout << "You are an admin.\n"; + /* end admins only { */ + return 0; +}
\ No newline at end of file diff --git a/tools/lint/test/files/trojan-source/early-return.py b/tools/lint/test/files/trojan-source/early-return.py new file mode 100644 index 0000000000..2797d8ae9f --- /dev/null +++ b/tools/lint/test/files/trojan-source/early-return.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +bank = { 'alice': 100 } + +def subtract_funds(account: str, amount: int): + ''' Subtract funds from bank account then ''' ;return + bank[account] -= amount + return + +subtract_funds('alice', 50)
\ No newline at end of file diff --git a/tools/lint/test/files/trojan-source/invisible-function.rs b/tools/lint/test/files/trojan-source/invisible-function.rs new file mode 100644 index 0000000000..b32efb0372 --- /dev/null +++ b/tools/lint/test/files/trojan-source/invisible-function.rs @@ -0,0 +1,15 @@ +fn isAdmin() { + return false; +} + +fn isAdmin() { + return true; +} + +fn main() { + if isAdmin() { + printf("You are an admin\n"); + } else { + printf("You are NOT an admin.\n"); + } +}
\ No newline at end of file diff --git a/tools/lint/test/files/updatebot/.yamllint b/tools/lint/test/files/updatebot/.yamllint new file mode 100644 index 0000000000..4f11bbd6c5 --- /dev/null +++ b/tools/lint/test/files/updatebot/.yamllint @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +# Explicity default .yamllint to isolate tests from tree-wide yamlint config. +--- +extends: default diff --git a/tools/lint/test/files/updatebot/cargo-mismatch.yaml b/tools/lint/test/files/updatebot/cargo-mismatch.yaml new file mode 100644 index 0000000000..ac18d2b87c --- /dev/null +++ b/tools/lint/test/files/updatebot/cargo-mismatch.yaml @@ -0,0 +1,44 @@ +--- +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Core + component: "Graphics: WebGPU" + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: wgpu + + description: A cross-platform pure-Rust graphics API + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://github.com/gfx-rs/wgpu + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: commit 32af4f56 + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) + revision: idontmatchanything + + license: ['MIT', 'Apache-2.0'] + +updatebot: + maintainer-phab: jimb + maintainer-bz: jimb@mozilla.com + tasks: + - type: vendoring + enabled: true + frequency: 1 week + +vendoring: + url: https://github.com/gfx-rs/wgpu + source-hosting: github + vendor-directory: gfx/wgpu_bindings/ + flavor: rust diff --git a/tools/lint/test/files/updatebot/good1.yaml b/tools/lint/test/files/updatebot/good1.yaml new file mode 100644 index 0000000000..f57d2c5b4c --- /dev/null +++ b/tools/lint/test/files/updatebot/good1.yaml @@ -0,0 +1,44 @@ +--- +schema: 1 + +bugzilla: + product: Core + component: Graphics + +origin: + name: angle + + description: ANGLE - Almost Native Graphics Layer Engine + + url: https://chromium.googlesource.com/angle/angle + + # Note that while the vendoring information here, including revision, + # release, and upstream repo locations refer to the third party upstream, + # Angle is vendored from a mozilla git repository that pulls from + # upstream and mainntains local patches there. + release: commit 018f85dea11fd5e41725750c6958695a6b8e8409 + revision: 018f85dea11fd5e41725750c6958695a6b8e8409 + + license: BSD-3-Clause + +updatebot: + maintainer-phab: jgilbert + maintainer-bz: jgilbert@mozilla.com + tasks: + - type: commit-alert + enabled: true + branch: chromium/4515 + needinfo: ["jgilbert@mozilla.com"] + +vendoring: + url: https://chromium.googlesource.com/angle/angle + tracking: tag + source-hosting: angle + vendor-directory: gfx/angle/checkout + skip-vendoring-steps: ["fetch", "update-moz-build"] + + update-actions: + - action: run-script + script: '{yaml_dir}/auto-update-angle.sh' + args: ['{revision}'] + cwd: '{cwd}' diff --git a/tools/lint/test/files/updatebot/good2.yaml b/tools/lint/test/files/updatebot/good2.yaml new file mode 100644 index 0000000000..0161d28b11 --- /dev/null +++ b/tools/lint/test/files/updatebot/good2.yaml @@ -0,0 +1,74 @@ +--- +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Core + component: "Audio/Video: Playback" + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: dav1d + + description: dav1d, a fast AV1 decoder + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://code.videolan.org/videolan/dav1d + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: ffb59680356fd210816cf9e46d9d023ade1f4d5a + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) + revision: ffb59680356fd210816cf9e46d9d023ade1f4d5a + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: BSD-2-Clause + + license-file: COPYING + +updatebot: + maintainer-phab: chunmin + maintainer-bz: cchang@mozilla.com + tasks: + - type: vendoring + enabled: true + frequency: release + +vendoring: + url: https://code.videolan.org/videolan/dav1d + source-hosting: gitlab + vendor-directory: third_party/dav1d + + exclude: + - build/.gitattributes + - build/.gitignore + - doc + - examples + - package + - tools + + generated: + - '{yaml_dir}/vcs_version.h' + - '{yaml_dir}/version.h' + + update-actions: + - action: copy-file + from: include/vcs_version.h.in + to: '{yaml_dir}/vcs_version.h' + - action: replace-in-file + pattern: '@VCS_TAG@' + with: '{revision}' + file: '{yaml_dir}/vcs_version.h' + - action: run-script + script: '{yaml_dir}/update-version.sh' + cwd: '{vendor_dir}' + args: ['{yaml_dir}/version.h'] diff --git a/tools/lint/test/files/updatebot/no-revision.yaml b/tools/lint/test/files/updatebot/no-revision.yaml new file mode 100644 index 0000000000..4d581508d8 --- /dev/null +++ b/tools/lint/test/files/updatebot/no-revision.yaml @@ -0,0 +1,43 @@ +--- +schema: 1 + +bugzilla: + product: Core + component: Graphics + +origin: + name: angle + + description: ANGLE - Almost Native Graphics Layer Engine + + url: https://chromium.googlesource.com/angle/angle + + # Note that while the vendoring information here, including revision, + # release, and upstream repo locations refer to the third party upstream, + # Angle is vendored from a mozilla git repository that pulls from + # upstream and mainntains local patches there. + release: commit 018f85dea11fd5e41725750c6958695a6b8e8409 + + license: BSD-3-Clause + +updatebot: + maintainer-phab: jgilbert + maintainer-bz: jgilbert@mozilla.com + tasks: + - type: commit-alert + enabled: true + branch: chromium/4515 + needinfo: ["jgilbert@mozilla.com"] + +vendoring: + url: https://chromium.googlesource.com/angle/angle + tracking: tag + source-hosting: angle + vendor-directory: gfx/angle/checkout + skip-vendoring-steps: ["fetch", "update-moz-build"] + + update-actions: + - action: run-script + script: '{yaml_dir}/auto-update-angle.sh' + args: ['{revision}'] + cwd: '{cwd}' diff --git a/tools/lint/test/files/yaml/.yamllint b/tools/lint/test/files/yaml/.yamllint new file mode 100644 index 0000000000..4f11bbd6c5 --- /dev/null +++ b/tools/lint/test/files/yaml/.yamllint @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +# Explicity default .yamllint to isolate tests from tree-wide yamlint config. +--- +extends: default diff --git a/tools/lint/test/files/yaml/bad.yml b/tools/lint/test/files/yaml/bad.yml new file mode 100644 index 0000000000..195ac7b030 --- /dev/null +++ b/tools/lint/test/files/yaml/bad.yml @@ -0,0 +1,8 @@ +--- +yamllint: + description: YAML linteraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaax + include: + - .cron.yml + - browser/config/ + - wrong + application:bar diff --git a/tools/lint/test/files/yaml/good.yml b/tools/lint/test/files/yaml/good.yml new file mode 100644 index 0000000000..b30941b797 --- /dev/null +++ b/tools/lint/test/files/yaml/good.yml @@ -0,0 +1,6 @@ +--- +yamllint: + description: YAML linter + include: + - .cron.yml + - browser/config/ diff --git a/tools/lint/test/python.toml b/tools/lint/test/python.toml new file mode 100644 index 0000000000..65036449a8 --- /dev/null +++ b/tools/lint/test/python.toml @@ -0,0 +1,60 @@ +[DEFAULT] +subsuite = "mozlint" + +["test_android_format.py"] + +["test_black.py"] +requirements = "tools/lint/python/black_requirements.txt" + +["test_clang_format.py"] + +["test_codespell.py"] + +["test_condprof_addons.py"] + +["test_eslint.py"] +skip-if = ["os == 'win'"] # busts the tree for subsequent tasks on the same worker (bug 1708591) +# Setup conflicts with stylelint setup so this should run sequentially. +sequential = true + +["test_file_license.py"] + +["test_file_perm.py"] +skip-if = ["os == 'win'"] + +["test_file_whitespace.py"] + +["test_fluent_lint.py"] + +["test_lintpref.py"] + +["test_manifest_alpha.py"] + +["test_manifest_toml.py"] + +["test_perfdocs.py"] + +["test_perfdocs_generation.py"] + +["test_perfdocs_helpers.py"] + +["test_rst.py"] +requirements = "tools/lint/rst/requirements.txt" + +["test_ruff.py"] +requirements = "tools/lint/python/ruff_requirements.txt" + +["test_rustfmt.py"] + +["test_shellcheck.py"] + +["test_stylelint.py"] +skip-if = ["os == 'win'"] # busts the tree for subsequent tasks on the same worker (bug 1708591) +# Setup conflicts with eslint setup so this should run sequentially. +sequential = true + +["test_trojan_source.py"] + +["test_updatebot.py"] + +["test_yaml.py"] diff --git a/tools/lint/test/test_android_format.py b/tools/lint/test/test_android_format.py new file mode 100644 index 0000000000..70cd1ea02e --- /dev/null +++ b/tools/lint/test/test_android_format.py @@ -0,0 +1,38 @@ +import mozunit +from conftest import build + +LINTER = "android-format" + + +def test_basic(global_lint, config): + substs = { + "GRADLE_ANDROID_FORMAT_LINT_CHECK_TASKS": [ + "spotlessJavaCheck", + "spotlessKotlinCheck", + ], + "GRADLE_ANDROID_FORMAT_LINT_FIX_TASKS": [ + "spotlessJavaApply", + "spotlessKotlinApply", + ], + "GRADLE_ANDROID_FORMAT_LINT_FOLDERS": ["tools/lint/test/files/android-format"], + } + results = global_lint( + config=config, + topobjdir=build.topobjdir, + root=build.topsrcdir, + substs=substs, + extra_args=["-PandroidFormatLintTest"], + ) + print(results) + + # When first task (spotlessJavaCheck) hits error, we won't check next Kotlin error. + # So results length will be 1. + assert len(results) == 1 + assert results[0].level == "error" + + # Since android-format is global lint, fix=True overrides repository files directly. + # No way to add this test. + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_black.py b/tools/lint/test/test_black.py new file mode 100644 index 0000000000..df0e792e68 --- /dev/null +++ b/tools/lint/test/test_black.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# 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 mozunit + +LINTER = "black" +fixed = 0 + + +def test_lint_fix(lint, create_temp_file): + contents = """def is_unique( + s + ): + s = list(s + ) + s.sort() + + + for i in range(len(s) - 1): + if s[i] == s[i + 1]: + return 0 + else: + return 1 + + +if __name__ == "__main__": + print( + is_unique(input()) + ) """ + + path = create_temp_file(contents, "bad.py") + lint([path], fix=True) + assert fixed == 1 + + +def test_lint_black(lint, paths): + results = lint(paths()) + assert len(results) == 2 + + assert results[0].level == "error" + assert results[0].relpath == "bad.py" + + assert "EOF" in results[1].message + assert results[1].level == "error" + assert results[1].relpath == "invalid.py" + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_clang_format.py b/tools/lint/test/test_clang_format.py new file mode 100644 index 0000000000..d32e000131 --- /dev/null +++ b/tools/lint/test/test_clang_format.py @@ -0,0 +1,138 @@ +import mozunit +from conftest import build + +LINTER = "clang-format" +fixed = 0 + + +def test_good(lint, config, paths): + results = lint(paths("good/"), root=build.topsrcdir, use_filters=False) + print(results) + assert len(results) == 0 + + results = lint(paths("good/"), root=build.topsrcdir, use_filters=False, fix=True) + assert fixed == len(results) + + +def test_basic(lint, config, paths): + results = lint(paths("bad/bad.cpp"), root=build.topsrcdir, use_filters=False) + print(results) + assert len(results) == 1 + + assert "Reformat C/C++" in results[0].message + assert results[0].level == "warning" + assert results[0].lineno == 1 + assert results[0].column == 0 + assert "bad.cpp" in results[0].path + assert ( + results[0].diff + == """\ +-int main ( ) { +- +-return 0; +- +- +-} ++int main() { return 0; } +""" # noqa + ) + + +def test_dir(lint, config, paths): + results = lint(paths("bad/"), root=build.topsrcdir, use_filters=False) + print(results) + assert len(results) == 5 + + assert "Reformat C/C++" in results[0].message + assert results[0].level == "warning" + assert results[0].lineno == 1 + assert results[0].column == 0 + assert "bad.cpp" in results[0].path + assert ( + results[0].diff + == """\ +-int main ( ) { +- +-return 0; +- +- +-} ++int main() { return 0; } +""" # noqa + ) + + assert "Reformat C/C++" in results[1].message + assert results[1].level == "warning" + assert results[1].lineno == 1 + assert results[1].column == 0 + assert "bad2.c" in results[1].path + assert ( + results[1].diff + == """\ +-#include "bad2.h" +- +- +-int bad2() { ++#include "bad2.h" ++ ++int bad2() { +""" + ) + + assert "Reformat C/C++" in results[2].message + assert results[2].level == "warning" + assert results[2].lineno == 5 + assert results[2].column == 0 + assert "bad2.c" in results[2].path + assert ( + results[2].diff + == """\ +- int a =2; ++ int a = 2; +""" + ) + + assert "Reformat C/C++" in results[3].message + assert results[3].level == "warning" + assert results[3].lineno == 6 + assert results[3].column == 0 + assert "bad2.c" in results[3].path + assert ( + results[3].diff + == """\ +- return a; +- +-} ++ return a; ++} +""" + ) + + assert "Reformat C/C++" in results[4].message + assert results[4].level == "warning" + assert results[4].lineno == 1 + assert results[4].column == 0 + assert "bad2.h" in results[4].path + assert ( + results[4].diff + == """\ +-int bad2(void ); ++int bad2(void); +""" + ) + + +def test_fixed(lint, create_temp_file): + contents = """int main ( ) { \n +return 0; \n + +}""" + + path = create_temp_file(contents, "ignore.cpp") + lint([path], use_filters=False, fix=True) + + assert fixed == 1 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_codespell.py b/tools/lint/test/test_codespell.py new file mode 100644 index 0000000000..8baae66b41 --- /dev/null +++ b/tools/lint/test/test_codespell.py @@ -0,0 +1,37 @@ +import mozunit + +LINTER = "codespell" +fixed = 0 + + +def test_lint_codespell_fix(lint, create_temp_file): + contents = """This is a file with some typos and informations. +But also testing false positive like optin (because this isn't always option) +or stuff related to our coding style like: +aparent (aParent). +but detects mistakes like mozila +""".lstrip() + + path = create_temp_file(contents, "ignore.rst") + lint([path], fix=True) + + assert fixed == 2 + + +def test_lint_codespell(lint, paths): + results = lint(paths()) + assert len(results) == 2 + + assert results[0].message == "informations ==> information" + assert results[0].level == "error" + assert results[0].lineno == 1 + assert results[0].relpath == "ignore.rst" + + assert results[1].message == "mozila ==> mozilla" + assert results[1].level == "error" + assert results[1].lineno == 5 + assert results[1].relpath == "ignore.rst" + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_condprof_addons.py b/tools/lint/test/test_condprof_addons.py new file mode 100644 index 0000000000..e1401a7119 --- /dev/null +++ b/tools/lint/test/test_condprof_addons.py @@ -0,0 +1,285 @@ +import importlib +import tempfile +from pathlib import Path +from unittest import mock + +import mozunit +import requests + +LINTER = "condprof-addons" + + +def linter_module_mocks( + customizations_path=".", browsertime_fetches_path="browsertime.yml", **othermocks +): + return mock.patch.multiple( + LINTER, + CUSTOMIZATIONS_PATH=Path(customizations_path), + BROWSERTIME_FETCHES_PATH=Path(browsertime_fetches_path), + **othermocks, + ) + + +def linter_class_mocks(**mocks): + return mock.patch.multiple( + f"{LINTER}.CondprofAddonsLinter", + **mocks, + ) + + +# Sanity check (make sure linter message includes the xpi filename). +def test_get_missing_xpi_msg(lint, paths): + condprof_addons = importlib.import_module("condprof-addons") + with linter_class_mocks( + get_firefox_addons_tar_names=mock.Mock(return_value=list()), + ): + instance = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=mock.Mock() + ) + assert instance.get_missing_xpi_msg("test.xpi").startswith( + "test.xpi is missing" + ) + + +def test_xpi_missing_from_firefox_addons_tar(lint, paths): + fixture_customizations = paths("with-missing-xpi.json") + with linter_module_mocks(), linter_class_mocks( + get_firefox_addons_tar_names=mock.Mock(return_value=list()), + ): + logger_mock = mock.Mock() + lint(fixture_customizations, logger=logger_mock) + assert logger_mock.lint_error.call_count == 1 + assert Path(fixture_customizations[0]).samefile( + logger_mock.lint_error.call_args.kwargs["path"] + ) + importlib.import_module("condprof-addons") + assert "non-existing.xpi" in logger_mock.lint_error.call_args.args[0] + + +def test_xpi_all_found_in_firefox_addons_tar(lint, paths): + get_tarnames_mock = mock.Mock( + return_value=["an-extension.xpi", "another-extension.xpi"] + ) + read_json_mock = mock.Mock( + return_value={ + "addons": { + "an-extension": "http://localhost/ext/an-extension.xpi", + "another-extension": "http://localhost/ext/another-extension.xpi", + } + } + ) + + with linter_module_mocks(), linter_class_mocks( + get_firefox_addons_tar_names=get_tarnames_mock, read_json=read_json_mock + ): + logger_mock = mock.Mock() + # Compute a fake condprof customization path, the content is + # going to be the read_json_mock.return_value and so the + # fixture file does not actually exists. + fixture_customizations = paths("fake-condprof-config.json") + lint( + fixture_customizations, + logger=logger_mock, + config={"include": paths(), "extensions": ["json", "yml"]}, + ) + assert read_json_mock.call_count == 1 + assert get_tarnames_mock.call_count == 1 + assert logger_mock.lint_error.call_count == 0 + + +def test_lint_error_on_missing_or_invalid_firefoxaddons_fetch_task( + lint, + paths, +): + read_json_mock = mock.Mock(return_value=dict()) + read_yaml_mock = mock.Mock(return_value=dict()) + # Verify that an explicit linter error is reported if the fetch task is not found. + with linter_module_mocks(), linter_class_mocks( + read_json=read_json_mock, read_yaml=read_yaml_mock + ): + logger_mock = mock.Mock() + fixture_customizations = paths("fake-condprof-config.json") + condprof_addons = importlib.import_module("condprof-addons") + + def assert_linter_error(yaml_mock_value, expected_msg): + logger_mock.reset_mock() + read_yaml_mock.return_value = yaml_mock_value + lint(fixture_customizations, logger=logger_mock) + assert logger_mock.lint_error.call_count == 1 + expected_path = condprof_addons.BROWSERTIME_FETCHES_PATH + assert logger_mock.lint_error.call_args.kwargs["path"] == expected_path + assert logger_mock.lint_error.call_args.args[0] == expected_msg + + # Mock a yaml file that is not including the expected firefox-addons fetch task. + assert_linter_error( + yaml_mock_value=dict(), expected_msg=condprof_addons.ERR_FETCH_TASK_MISSING + ) + # Mock a yaml file where firefox-addons is missing the fetch attribute. + assert_linter_error( + yaml_mock_value={"firefox-addons": {}}, + expected_msg=condprof_addons.ERR_FETCH_TASK_MISSING, + ) + # Mock a yaml file where firefox-addons add-prefix is missing. + assert_linter_error( + yaml_mock_value={"firefox-addons": {"fetch": {}}}, + expected_msg=condprof_addons.ERR_FETCH_TASK_ADDPREFIX, + ) + # Mock a yaml file where firefox-addons add-prefix is invalid. + assert_linter_error( + yaml_mock_value={ + "firefox-addons": {"fetch": {"add-prefix": "invalid-subdir-name/"}} + }, + expected_msg=condprof_addons.ERR_FETCH_TASK_ADDPREFIX, + ) + + +def test_get_xpi_list_from_fetch_dir(lint, paths): + # Verify that when executed on the CI, the helper method looks for the xpi files + # in the MOZ_FETCHES_DIR subdir where they are expected to be unpacked by the + # fetch task. + with linter_module_mocks( + MOZ_AUTOMATION=1, MOZ_FETCHES_DIR=paths("fake-fetches-dir")[0] + ): + condprof_addons = importlib.import_module("condprof-addons") + logger_mock = mock.Mock() + Path(paths("browsertime.yml")[0]) + + linter = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=logger_mock + ) + results = linter.tar_xpi_filenames + + results.sort() + assert results == ["fake-ext-01.xpi", "fake-ext-02.xpi"] + + +def test_get_xpi_list_from_downloaded_tar(lint, paths): + def mocked_download_tar(firefox_addons_tar_url, tar_tmp_path): + tar_tmp_path.write_bytes(Path(paths("firefox-addons-fake.tar")[0]).read_bytes()) + + download_firefox_addons_tar_mock = mock.Mock() + download_firefox_addons_tar_mock.side_effect = mocked_download_tar + + # Verify that when executed locally on a developer machine, the tar archive is downloaded + # and the list of xpi files included in it returned by the helper method. + with tempfile.TemporaryDirectory() as tempdir, linter_module_mocks( + MOZ_AUTOMATION=0, + tempdir=tempdir, + ), linter_class_mocks( + download_firefox_addons_tar=download_firefox_addons_tar_mock, + ): + condprof_addons = importlib.import_module("condprof-addons") + logger_mock = mock.Mock() + Path(paths("browsertime.yml")[0]) + + linter = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=logger_mock + ) + results = linter.tar_xpi_filenames + assert len(results) > 0 + print("List of addons found in the downloaded file archive:", results) + assert all(filename.endswith(".xpi") for filename in results) + assert download_firefox_addons_tar_mock.call_count == 1 + + +@mock.patch("requests.get") +def test_error_on_downloading_tar(requests_get_mock, lint, paths): + # Verify that when executed locally and the tar archive fails to download + # the linter does report an explicit linting error with the http error included. + with tempfile.TemporaryDirectory() as tempdir, linter_module_mocks( + MOZ_AUTOMATION=0, tempdir=tempdir + ): + condprof_addons = importlib.import_module("condprof-addons") + logger_mock = mock.Mock() + response_mock = mock.Mock() + response_mock.raise_for_status.side_effect = requests.exceptions.HTTPError( + "MOCK_ERROR" + ) + requests_get_mock.return_value = response_mock + Path(paths("browsertime.yml")[0]) + + linter = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=logger_mock + ) + + assert ( + logger_mock.lint_error.call_args.kwargs["path"] + == condprof_addons.BROWSERTIME_FETCHES_PATH + ) + assert ( + logger_mock.lint_error.call_args.args[0] + == f"{condprof_addons.ERR_FETCH_TASK_ARCHIVE}, MOCK_ERROR" + ) + assert requests_get_mock.call_count == 1 + assert len(linter.tar_xpi_filenames) == 0 + + +@mock.patch("requests.get") +def test_error_on_opening_tar(requests_get_mock, lint, paths): + # Verify that when executed locally and the tar archive fails to open + # the linter does report an explicit linting error with the tarfile error included. + with tempfile.TemporaryDirectory() as tempdir, linter_module_mocks( + MOZ_AUTOMATION=0, tempdir=tempdir + ): + condprof_addons = importlib.import_module("condprof-addons") + logger_mock = mock.Mock() + response_mock = mock.Mock() + response_mock.raise_for_status.return_value = None + + def mock_iter_content(chunk_size): + yield b"fake tar content" + yield b"expected to trigger tarfile.ReadError" + + response_mock.iter_content.side_effect = mock_iter_content + requests_get_mock.return_value = response_mock + Path(paths("browsertime.yml")[0]) + + linter = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=logger_mock + ) + + assert ( + logger_mock.lint_error.call_args.kwargs["path"] + == condprof_addons.BROWSERTIME_FETCHES_PATH + ) + actual_msg = logger_mock.lint_error.call_args.args[0] + print("Got linter error message:", actual_msg) + assert actual_msg.startswith( + f"{condprof_addons.ERR_FETCH_TASK_ARCHIVE}, file could not be opened successfully" + ) + assert requests_get_mock.call_count == 1 + assert len(linter.tar_xpi_filenames) == 0 + + +def test_lint_all_customization_files_when_linting_browsertime_yml( + lint, + paths, +): + get_tarnames_mock = mock.Mock(return_value=["an-extension.xpi"]) + read_json_mock = mock.Mock( + return_value={ + "addons": {"an-extension": "http://localhost/ext/an-extension.xpi"} + } + ) + with linter_module_mocks( + customizations_path="fake-customizations-dir", + ), linter_class_mocks( + get_firefox_addons_tar_names=get_tarnames_mock, + read_json=read_json_mock, + ): + logger_mock = mock.Mock() + importlib.import_module("condprof-addons") + # When mozlint detects a change to the ci fetch browser.yml support file, + # condprof-addons linter is called for the entire customizations dir path + # and we expect that to be expanded to the list of the json customizations + # files from that directory path. + lint(paths("fake-customizations-dir"), logger=logger_mock) + # Expect read_json_mock to be called once per each of the json files + # found in the fixture dir. + assert read_json_mock.call_count == 3 + assert get_tarnames_mock.call_count == 1 + assert logger_mock.lint_error.call_count == 0 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_eslint.py b/tools/lint/test/test_eslint.py new file mode 100644 index 0000000000..b4fda2fb35 --- /dev/null +++ b/tools/lint/test/test_eslint.py @@ -0,0 +1,99 @@ +import mozunit +import pytest +from conftest import build + +LINTER = "eslint" +fixed = 0 + + +@pytest.fixture +def eslint(lint): + def inner(*args, **kwargs): + # --no-ignore is for ESLint to avoid the .eslintignore file. + # --ignore-path is because Prettier doesn't have the --no-ignore option + # and therefore needs to be given an empty file for the tests to work. + kwargs["extra_args"] = [ + "--no-ignore", + "--ignore-path=tools/lint/test/files/eslint/testprettierignore", + ] + return lint(*args, **kwargs) + + return inner + + +def test_lint_with_global_exclude(lint, config, paths): + config["exclude"] = ["subdir", "import"] + # This uses lint directly as we need to not ignore the excludes. + results = lint(paths(), config=config, root=build.topsrcdir) + assert len(results) == 0 + + +def test_no_files_to_lint(eslint, config, paths): + # A directory with no files to lint. + results = eslint(paths("nolint"), root=build.topsrcdir) + assert results == [] + + # Errors still show up even when a directory with no files is passed in. + results = eslint(paths("nolint", "subdir/bad.js"), root=build.topsrcdir) + assert len(results) == 1 + + +def test_bad_import(eslint, config, paths): + results = eslint(paths("import"), config=config, root=build.topsrcdir) + assert results == 1 + + +def test_eslint_rule(eslint, config, create_temp_file): + contents = """var re = /foo bar/; +var re = new RegExp("foo bar"); +""" + path = create_temp_file(contents, "bad.js") + results = eslint( + [path], config=config, root=build.topsrcdir, rules=["no-regex-spaces: error"] + ) + + assert len(results) == 2 + + +def test_eslint_fix(eslint, config, create_temp_file): + contents = """/*eslint no-regex-spaces: "error"*/ + +var re = /foo bar/; +var re = new RegExp("foo bar"); + +var re = /foo bar/; +var re = new RegExp("foo bar"); + +var re = /foo bar/; +var re = new RegExp("foo bar"); +""" + path = create_temp_file(contents, "bad.js") + eslint([path], config=config, root=build.topsrcdir, fix=True) + + # ESLint returns counts of files fixed, not errors fixed. + assert fixed == 1 + + +def test_prettier_rule(eslint, config, create_temp_file): + contents = """var re = /foobar/; + var re = "foo"; +""" + path = create_temp_file(contents, "bad.js") + results = eslint([path], config=config, root=build.topsrcdir) + + assert len(results) == 1 + + +def test_prettier_fix(eslint, config, create_temp_file): + contents = """var re = /foobar/; + var re = "foo"; +""" + path = create_temp_file(contents, "bad.js") + eslint([path], config=config, root=build.topsrcdir, fix=True) + + # Prettier returns counts of files fixed, not errors fixed. + assert fixed == 1 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_file_license.py b/tools/lint/test/test_file_license.py new file mode 100644 index 0000000000..250d4a0778 --- /dev/null +++ b/tools/lint/test/test_file_license.py @@ -0,0 +1,33 @@ +import mozunit + +LINTER = "license" +fixed = 0 + + +def test_lint_license(lint, paths): + results = lint(paths()) + print(results) + assert len(results) == 3 + + assert ".eslintrc.js" in results[0].relpath + + assert "No matching license strings" in results[1].message + assert results[1].level == "error" + assert "bad.c" in results[1].relpath + + assert "No matching license strings" in results[2].message + assert results[2].level == "error" + assert "bad.js" in results[2].relpath + + +def test_lint_license_fix(lint, paths, create_temp_file): + contents = """let foo = 0;""" + path = create_temp_file(contents, "lint_license_test_tmp_file.js") + results = lint([path], fix=True) + + assert len(results) == 0 + assert fixed == 1 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_file_perm.py b/tools/lint/test/test_file_perm.py new file mode 100644 index 0000000000..08d6a20eef --- /dev/null +++ b/tools/lint/test/test_file_perm.py @@ -0,0 +1,35 @@ +import mozunit +import pytest + +LINTER = "file-perm" + + +@pytest.mark.lint_config(name="file-perm") +def test_lint_file_perm(lint, paths): + results = lint(paths("no-shebang"), collapse_results=True) + + assert results.keys() == { + "no-shebang/bad.c", + "no-shebang/bad-shebang.c", + "no-shebang/bad.png", + } + + for path, issues in results.items(): + for issue in issues: + assert "permissions on a source" in issue.message + assert issue.level == "error" + + +@pytest.mark.lint_config(name="maybe-shebang-file-perm") +def test_lint_shebang_file_perm(config, lint, paths): + results = lint(paths("maybe-shebang")) + + assert len(results) == 1 + + assert "permissions on a source" in results[0].message + assert results[0].level == "error" + assert results[0].relpath == "maybe-shebang/bad.js" + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_file_whitespace.py b/tools/lint/test/test_file_whitespace.py new file mode 100644 index 0000000000..7dc815d6ca --- /dev/null +++ b/tools/lint/test/test_file_whitespace.py @@ -0,0 +1,50 @@ +import mozunit + +LINTER = "file-whitespace" +fixed = 0 + + +def test_lint_file_whitespace(lint, paths): + results = lint(paths()) + print(results) + assert len(results) == 5 + + assert "File does not end with newline character" in results[1].message + assert results[1].level == "error" + assert "bad-newline.c" in results[1].relpath + + assert "Empty Lines at end of file" in results[0].message + assert results[0].level == "error" + assert "bad-newline.c" in results[0].relpath + + assert "Windows line return" in results[2].message + assert results[2].level == "error" + assert "bad-windows.c" in results[2].relpath + + assert "Trailing whitespace" in results[3].message + assert results[3].level == "error" + assert "bad.c" in results[3].relpath + assert results[3].lineno == 1 + + assert "Trailing whitespace" in results[4].message + assert results[4].level == "error" + assert "bad.c" in results[4].relpath + assert results[4].lineno == 2 + + +def test_lint_file_whitespace_fix(lint, paths, create_temp_file): + contents = """int main() { \n + return 0; \n +} + + +""" + + path = create_temp_file(contents, "bad.cpp") + lint([path], fix=True) + # Gives a different answer on Windows. Probably because of Windows CR + assert fixed == 3 or fixed == 2 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_fluent_lint.py b/tools/lint/test/test_fluent_lint.py new file mode 100644 index 0000000000..0bbd4305d5 --- /dev/null +++ b/tools/lint/test/test_fluent_lint.py @@ -0,0 +1,164 @@ +# 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 mozunit + +LINTER = "fluent-lint" + + +def test_lint_exclusions(lint, paths): + results = lint(paths("excluded.ftl")) + assert len(results) == 1 + assert results[0].rule == "TE01" + assert results[0].lineno == 6 + assert results[0].column == 20 + + +def test_lint_single_file(lint, paths): + results = lint(paths("bad.ftl")) + assert len(results) == 13 + assert results[0].rule == "ID01" + assert results[0].lineno == 1 + assert results[0].column == 1 + assert results[1].rule == "ID01" + assert results[1].lineno == 3 + assert results[1].column == 1 + assert results[2].rule == "TE01" + assert results[2].lineno == 5 + assert results[2].column == 20 + assert results[3].rule == "TE01" + assert results[3].lineno == 6 + assert results[3].column == 24 + assert results[4].rule == "TE02" + assert results[4].lineno == 7 + assert results[4].column == 20 + assert results[5].rule == "TE03" + assert results[5].lineno == 8 + assert results[5].column == 20 + assert results[6].rule == "TE04" + assert results[6].lineno == 11 + assert results[6].column == 20 + assert results[7].rule == "TE05" + assert results[7].lineno == 13 + assert results[7].column == 16 + assert results[8].rule == "TE03" + assert results[8].lineno == 17 + assert results[8].column == 20 + assert results[9].rule == "TE03" + assert results[9].lineno == 25 + assert results[9].column == 18 + assert results[10].rule == "ID02" + assert results[10].lineno == 32 + assert results[10].column == 1 + assert results[11].rule == "VC01" + assert "$tabCount" in results[11].message + assert results[12].rule == "VC01" + assert "$url" in results[12].message + + +def test_comment_group(lint, paths): + results = lint(paths("comment-group1.ftl")) + assert len(results) == 6 + assert results[0].rule == "GC03" + assert results[0].lineno == 12 + assert results[0].column == 1 + assert results[1].rule == "GC02" + assert results[1].lineno == 16 + assert results[1].column == 1 + assert results[2].rule == "GC04" + assert results[2].lineno == 21 + assert results[2].column == 1 + assert results[3].rule == "GC03" + assert results[3].lineno == 26 + assert results[3].column == 1 + assert results[4].rule == "GC02" + assert results[4].lineno == 30 + assert results[4].column == 1 + assert results[5].rule == "GC01" + assert results[5].lineno == 35 + assert results[5].column == 1 + + results = lint(paths("comment-group2.ftl")) + assert (len(results)) == 0 + + +def test_comment_resource(lint, paths): + results = lint(paths("comment-resource1.ftl")) + assert len(results) == 1 + assert results[0].rule == "RC01" + assert results[0].lineno == 9 + assert results[0].column == 1 + + results = lint(paths("comment-resource2.ftl")) + assert len(results) == 1 + assert results[0].rule == "RC03" + assert results[0].lineno == 4 + assert results[0].column == 1 + + results = lint(paths("comment-resource3.ftl")) + assert len(results) == 1 + assert results[0].rule == "RC02" + assert results[0].lineno == 5 + assert results[0].column == 1 + + results = lint(paths("comment-resource4.ftl")) + assert len(results) == 1 + assert results[0].rule == "RC03" + assert results[0].lineno == 6 + assert results[0].column == 1 + + results = lint(paths("comment-resource5.ftl")) + assert len(results) == 1 + assert results[0].rule == "RC02" + assert results[0].lineno == 5 + assert results[0].column == 1 + + results = lint(paths("comment-resource6.ftl")) + assert len(results) == 0 + + +def test_brand_names(lint, paths): + results = lint(paths("brand-names.ftl"), {"brand-files": ["test-brands.ftl"]}) + assert len(results) == 11 + assert results[0].rule == "CO01" + assert results[0].lineno == 1 + assert results[0].column == 16 + assert "Firefox" in results[0].message + assert "Mozilla" not in results[0].message + assert "Thunderbird" not in results[0].message + assert results[1].rule == "CO01" + assert results[1].lineno == 4 + assert results[1].column == 16 + + results = lint(paths("brand-names-excluded.ftl")) + assert len(results) == 0 + + +def test_comment_variables(lint, paths): + results = lint(paths("comment-variables1.ftl")) + assert len(results) == 4 + assert results[0].rule == "VC01" + assert "$var" in results[0].message + assert results[1].rule == "VC01" + assert "$select1" in results[1].message + assert results[2].rule == "VC01" + assert "$select2" in results[2].message + assert results[3].rule == "VC01" + assert "$attr" in results[3].message + + results = lint(paths("comment-variables2.ftl")) + assert len(results) == 1 + assert results[0].rule == "VC01" + assert "$term-message" in results[0].message + + +def test_valid_attributes(lint, paths): + results = lint(paths("valid-attributes.ftl")) + print(results) + assert len(results) == 1 + assert results[0].rule == "VA01" + assert ".extralabel" in results[0].message + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_lintpref.py b/tools/lint/test/test_lintpref.py new file mode 100644 index 0000000000..3e75b1675e --- /dev/null +++ b/tools/lint/test/test_lintpref.py @@ -0,0 +1,16 @@ +import mozunit + +LINTER = "lintpref" + + +def test_lintpref(lint, paths): + results = lint(paths()) + assert len(results) == 1 + assert results[0].level == "error" + assert 'pref("dom.webidl.test1", true);' in results[0].message + assert "bad.js" in results[0].relpath + assert results[0].lineno == 2 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_manifest_alpha.py b/tools/lint/test/test_manifest_alpha.py new file mode 100644 index 0000000000..2e8e1a6c77 --- /dev/null +++ b/tools/lint/test/test_manifest_alpha.py @@ -0,0 +1,33 @@ +# 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 mozunit + +LINTER = "test-manifest-alpha" + + +def test_very_out_of_order(lint, paths): + results = lint(paths("mochitest-very-out-of-order.ini")) + assert len(results) == 1 + assert results[0].diff + + +def test_in_order(lint, paths): + results = lint(paths("mochitest-in-order.ini")) + assert len(results) == 0 + + +def test_mostly_in_order(lint, paths): + results = lint(paths("mochitest-mostly-in-order.ini")) + assert len(results) == 1 + assert results[0].diff + + +def test_other_ini_very_out_of_order(lint, paths): + """Test that an .ini file outside of the allowlist is ignored.""" + results = lint(paths("other-ini-very-out-of-order.ini")) + assert len(results) == 0 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_manifest_toml.py b/tools/lint/test/test_manifest_toml.py new file mode 100644 index 0000000000..f205a1cd1f --- /dev/null +++ b/tools/lint/test/test_manifest_toml.py @@ -0,0 +1,78 @@ +# 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 mozunit + +LINTER = "test-manifest-toml" +fixed = 0 + + +def test_valid(lint, paths): + results = lint(paths("valid.toml")) + assert len(results) == 0 + + +def test_invalid(lint, paths): + results = lint(paths("invalid.toml")) + assert len(results) == 1 + assert results[0].message == "The manifest is not valid TOML." + + +def test_no_default(lint, paths): + """Test verifying [DEFAULT] section.""" + results = lint(paths("no-default.toml")) + assert len(results) == 1 + assert results[0].message == "The manifest does not start with a [DEFAULT] section." + + +def test_no_default_fix(lint, paths, create_temp_file): + """Test fixing missing [DEFAULT] section.""" + contents = "# this Manifest has no DEFAULT section\n" + path = create_temp_file(contents, "no-default.toml") + results = lint([path], fix=True) + assert len(results) == 1 + assert results[0].message == "The manifest does not start with a [DEFAULT] section." + assert fixed == 1 + + +def test_non_double_quote_sections(lint, paths): + """Test verifying [DEFAULT] section.""" + results = lint(paths("non-double-quote-sections.toml")) + assert len(results) == 2 + assert results[0].message.startswith("The section name must be double quoted:") + + +def test_unsorted(lint, paths): + """Test sections in alpha order.""" + results = lint(paths("unsorted.toml")) + assert len(results) == 1 + assert results[0].message == "The manifest sections are not in alphabetical order." + + +def test_comment_section(lint, paths): + """Test for commented sections.""" + results = lint(paths("comment-section.toml")) + assert len(results) == 2 + assert results[0].message.startswith( + "Use 'disabled = \"<reason>\"' to disable a test instead of a comment:" + ) + + +def test_skip_if_not_array(lint, paths): + """Test for non-array skip-if value.""" + results = lint(paths("skip-if-not-array.toml")) + assert len(results) == 1 + assert results[0].message.startswith("Value for conditional must be an array:") + + +def test_skip_if_explicit_or(lint, paths): + """Test for explicit || in skip-if.""" + results = lint(paths("skip-if-explicit-or.toml")) + assert len(results) == 1 + assert results[0].message.startswith( + "Value for conditional must not include explicit ||, instead put on multiple lines:" + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_perfdocs.py b/tools/lint/test/test_perfdocs.py new file mode 100644 index 0000000000..4ee834ad68 --- /dev/null +++ b/tools/lint/test/test_perfdocs.py @@ -0,0 +1,858 @@ +import contextlib +import os +import pathlib +import shutil +import tempfile +from unittest import mock + +import mozunit +import pytest + +LINTER = "perfdocs" + + +class PerfDocsLoggerMock: + LOGGER = None + PATHS = [] + FAILED = True + + +""" +This is a sample mozperftest test that we use for testing +the verification process. +""" +SAMPLE_TEST = """ +"use strict"; + +async function setUp(context) { + context.log.info("setUp example!"); +} + +async function test(context, commands) { + context.log.info("Test with setUp/tearDown example!"); + await commands.measure.start("https://www.sitespeed.io/"); + await commands.measure.start("https://www.mozilla.org/en-US/"); +} + +async function tearDown(context) { + context.log.info("tearDown example!"); +} + +module.noexport = {}; + +module.exports = { + setUp, + tearDown, + test, + owner: "Performance Testing Team", + name: "Example", + description: "The description of the example test.", + longDescription: ` + This is a longer description of the test perhaps including information + about how it should be run locally or links to relevant information. + ` +}; +""" + + +SAMPLE_CONFIG = """ +name: mozperftest +manifest: None +static-only: False +suites: + suite: + description: "Performance tests from the 'suite' folder." + tests: + Example: "" +""" + + +DYNAMIC_SAMPLE_CONFIG = """ +name: {} +manifest: None +static-only: False +suites: + suite: + description: "Performance tests from the 'suite' folder." + tests: + Example: "Performance test Example from suite." + another_suite: + description: "Performance tests from the 'another_suite' folder." + tests: + Example: "Performance test Example from another_suite." +""" + + +SAMPLE_METRICS_CONFIG = """ +name: raptor +manifest: "None"{} +static-only: False +suites: + suite: + description: "Performance tests from the 'suite' folder."{} + tests: + Example: "Performance test Example from another_suite." + another_suite: + description: "Performance tests from the 'another_suite' folder." + tests: + Example: "Performance test Example from another_suite." +""" + + +SAMPLE_INI = """ +[Example] +test_url = Example_url +alert_on = fcp +""" + +SAMPLE_METRICS_INI = """ +[Example] +test_url = Example_url +alert_on = fcp,SpeedIndex +""" + + +@contextlib.contextmanager +def temp_file(name="temp", tempdir=None, content=None): + if tempdir is None: + tempdir = tempfile.mkdtemp() + path = pathlib.Path(tempdir, name) + if content is not None: + with path.open("w", newline="\n") as f: + f.write(content) + try: + yield path + finally: + try: + shutil.rmtree(str(tempdir)) + except FileNotFoundError: + pass + + +@contextlib.contextmanager +def temp_dir(): + tempdir = pathlib.Path(tempfile.mkdtemp()) + try: + yield tempdir + finally: + try: + shutil.rmtree(str(tempdir)) + except FileNotFoundError: + pass + + +def setup_sample_logger(logger, structured_logger, top_dir): + from perfdocs.logger import PerfDocLogger + + PerfDocLogger.LOGGER = structured_logger + PerfDocLogger.PATHS = ["perfdocs"] + PerfDocLogger.TOP_DIR = top_dir + + import perfdocs.gatherer as gt + import perfdocs.generator as gn + import perfdocs.verifier as vf + + gt.logger = logger + vf.logger = logger + gn.logger = logger + + +@mock.patch("taskgraph.util.taskcluster.get_artifact") +@mock.patch("tryselect.tasks.generate_tasks") +@mock.patch("perfdocs.generator.Generator") +@mock.patch("perfdocs.verifier.Verifier") +@mock.patch("perfdocs.logger.PerfDocLogger", new=PerfDocsLoggerMock) +def test_perfdocs_start_and_fail( + verifier, + generator, + get_artifact_mock, + gen_tasks_mock, + structured_logger, + config, + paths, +): + from perfdocs.perfdocs import run_perfdocs + + with temp_file("bad", content="foo") as temp: + run_perfdocs( + config, logger=structured_logger, paths=[str(temp)], generate=False + ) + assert PerfDocsLoggerMock.LOGGER == structured_logger + assert PerfDocsLoggerMock.PATHS == [temp] + assert PerfDocsLoggerMock.FAILED + + assert verifier.call_count == 1 + assert mock.call().validate_tree() in verifier.mock_calls + assert generator.call_count == 0 + + +@mock.patch("taskgraph.util.taskcluster.get_artifact") +@mock.patch("tryselect.tasks.generate_tasks") +@mock.patch("perfdocs.generator.Generator") +@mock.patch("perfdocs.verifier.Verifier") +@mock.patch("perfdocs.logger.PerfDocLogger", new=PerfDocsLoggerMock) +def test_perfdocs_start_and_pass(verifier, generator, structured_logger, config, paths): + from perfdocs.perfdocs import run_perfdocs + + PerfDocsLoggerMock.FAILED = False + with temp_file("bad", content="foo") as temp: + run_perfdocs( + config, logger=structured_logger, paths=[str(temp)], generate=False + ) + assert PerfDocsLoggerMock.LOGGER == structured_logger + assert PerfDocsLoggerMock.PATHS == [temp] + assert not PerfDocsLoggerMock.FAILED + + assert verifier.call_count == 1 + assert mock.call().validate_tree() in verifier.mock_calls + assert generator.call_count == 1 + assert mock.call().generate_perfdocs() in generator.mock_calls + + +@mock.patch("perfdocs.logger.PerfDocLogger", new=PerfDocsLoggerMock) +def test_perfdocs_bad_paths(structured_logger, config, paths): + from perfdocs.perfdocs import run_perfdocs + + with pytest.raises(Exception): + run_perfdocs(config, logger=structured_logger, paths=["bad"], generate=False) + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_gatherer_fetch_perfdocs_tree( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.gatherer import Gatherer + + gatherer = Gatherer(top_dir) + assert not gatherer._perfdocs_tree + + gatherer.fetch_perfdocs_tree() + + expected = "Found 1 perfdocs directories" + args, _ = logger.log.call_args + + assert expected in args[0] + assert logger.log.call_count == 1 + assert gatherer._perfdocs_tree + + expected = ["path", "yml", "rst", "static"] + for i, key in enumerate(gatherer._perfdocs_tree[0].keys()): + assert key == expected[i] + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_gatherer_get_test_list(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.gatherer import Gatherer + + gatherer = Gatherer(top_dir) + gatherer.fetch_perfdocs_tree() + framework = gatherer.get_test_list(gatherer._perfdocs_tree[0]) + + expected = ["name", "test_list", "yml_content", "yml_path"] + for i, key in enumerate(sorted(framework.keys())): + assert key == expected[i] + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verification(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + # Make sure that we had no warnings + assert logger.warning.call_count == 0 + assert logger.log.call_count == 1 + assert len(logger.mock_calls) == 1 + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_validate_yaml_pass( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + yaml_path = perfdocs_sample["config"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.verifier import Verifier + + valid = Verifier(top_dir).validate_yaml(pathlib.Path(yaml_path)) + + assert valid + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_invalid_yaml(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + yaml_path = perfdocs_sample["config"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.verifier import Verifier + + verifier = Verifier("top_dir") + with open(yaml_path, "r", newline="\n") as f: + lines = f.readlines() + print(lines) + with open(yaml_path, "w", newline="\n") as f: + f.write("\n".join(lines[2:])) + valid = verifier.validate_yaml(yaml_path) + + expected = ("YAML ValidationError: 'name' is a required property\n", yaml_path) + args, _ = logger.warning.call_args + + assert logger.warning.call_count == 1 + assert expected[0] in args[0] + assert not valid + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_validate_rst_pass( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + rst_path = perfdocs_sample["index"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.verifier import Verifier + + valid = Verifier(top_dir).validate_rst_content(pathlib.Path(rst_path)) + + assert valid + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_invalid_rst(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + rst_path = perfdocs_sample["index"] + setup_sample_logger(logger, structured_logger, top_dir) + + # Replace the target string to invalid Keyword for test + with open(rst_path, "r") as file: + filedata = file.read() + + filedata = filedata.replace("documentation", "Invalid Keyword") + + with open(rst_path, "w", newline="\n") as file: + file.write(filedata) + + from perfdocs.verifier import Verifier + + verifier = Verifier("top_dir") + valid = verifier.validate_rst_content(rst_path) + + expected = ( + "Cannot find a '{documentation}' entry in the given index file", + rst_path, + ) + args, _ = logger.warning.call_args + + assert logger.warning.call_count == 1 + assert args == expected + assert not valid + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_validate_descriptions_pass( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier._check_framework_descriptions(verifier._gatherer.perfdocs_tree[0]) + + assert logger.warning.call_count == 0 + assert logger.log.call_count == 1 + assert len(logger.mock_calls) == 1 + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_not_existing_suite_in_test_list( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + manifest_path = perfdocs_sample["manifest"]["path"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + os.remove(manifest_path) + verifier._check_framework_descriptions(verifier._gatherer.perfdocs_tree[0]) + + expected = ( + "Could not find an existing suite for suite - bad suite name?", + perfdocs_sample["config"], + ) + args, _ = logger.warning.call_args + + assert logger.warning.call_count == 1 + assert args == expected + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_not_existing_tests_in_suites( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + with open(perfdocs_sample["config"], "r") as file: + filedata = file.read() + filedata = filedata.replace("Example", "DifferentName") + with open(perfdocs_sample["config"], "w", newline="\n") as file: + file.write(filedata) + + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier._check_framework_descriptions(verifier._gatherer.perfdocs_tree[0]) + + expected = [ + "Could not find an existing test for DifferentName - bad test name?", + "Could not find a test description for Example", + ] + + assert logger.warning.call_count == 2 + for i, call in enumerate(logger.warning.call_args_list): + args, _ = call + assert args[0] == expected[i] + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_missing_contents_in_suite( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + with open(perfdocs_sample["config"], "r") as file: + filedata = file.read() + filedata = filedata.replace("suite:", "InvalidSuite:") + with open(perfdocs_sample["config"], "w", newline="\n") as file: + file.write(filedata) + + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier._check_framework_descriptions(verifier._gatherer.perfdocs_tree[0]) + + expected = ( + "Could not find an existing suite for InvalidSuite - bad suite name?", + "Missing suite description for suite", + ) + + assert logger.warning.call_count == 2 + for i, call in enumerate(logger.warning.call_args_list): + args, _ = call + assert args[0] == expected[i] + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_invalid_dir(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.verifier import Verifier + + verifier = Verifier("invalid_path") + with pytest.raises(Exception) as exceinfo: + verifier.validate_tree() + + assert str(exceinfo.value) == "No valid perfdocs directories found" + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_file_invalidation( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.verifier import Verifier + + with mock.patch("perfdocs.verifier.Verifier.validate_yaml", return_value=False): + verifier = Verifier(top_dir) + with pytest.raises(Exception): + verifier.validate_tree() + + # Check if "File validation error" log is called + # and Called with a log inside perfdocs_tree(). + assert logger.log.call_count == 2 + assert len(logger.mock_calls) == 2 + + +@pytest.mark.parametrize( + "manifest, metric_definitions, expected", + [ + [ + SAMPLE_INI, + """ +metrics: + "FirstPaint": + aliases: + - fcp + description: "Example" """, + 1, + ], + [ + SAMPLE_METRICS_INI, + """ +metrics: + FirstPaint: + aliases: + - fcp + description: Example + SpeedIndex: + aliases: + - speedindex + - si + description: Example + """, + 2, + ], + ], +) +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_nonexistent_documented_metrics( + logger, structured_logger, perfdocs_sample, manifest, metric_definitions, expected +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + with open(perfdocs_sample["config"], "w", newline="\n") as f: + f.write(SAMPLE_METRICS_CONFIG.format(metric_definitions, "")) + with open(perfdocs_sample["manifest"]["path"], "w", newline="\n") as f: + f.write(manifest) + + sample_gatherer_result = { + "suite": {"Example": {}}, + "another_suite": {"Example": {}}, + } + + from perfdocs.verifier import Verifier + + with mock.patch("perfdocs.framework_gatherers.RaptorGatherer.get_test_list") as m: + m.return_value = sample_gatherer_result + verifier = Verifier(top_dir) + verifier.validate_tree() + + assert len(logger.warning.call_args_list) == expected + for args, _ in logger.warning.call_args_list: + assert "Cannot find documented metric" in args[0] + assert "being used" in args[0] + + +@pytest.mark.parametrize( + "manifest, metric_definitions", + [ + [ + SAMPLE_INI, + """ +metrics: + "FirstPaint": + aliases: + - fcp + description: "Example" """, + ], + [ + SAMPLE_METRICS_INI, + """ +metrics: + SpeedIndex: + aliases: + - speedindex + - si + description: Example + """, + ], + ], +) +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_undocumented_metrics( + logger, structured_logger, perfdocs_sample, manifest, metric_definitions +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + with open(perfdocs_sample["config"], "w", newline="\n") as f: + f.write(SAMPLE_METRICS_CONFIG.format(metric_definitions, "")) + with open(perfdocs_sample["manifest"]["path"], "w", newline="\n") as f: + f.write(manifest) + + sample_gatherer_result = { + "suite": {"Example": {"metrics": ["fcp", "SpeedIndex"]}}, + "another_suite": {"Example": {}}, + } + + from perfdocs.verifier import Verifier + + with mock.patch("perfdocs.framework_gatherers.RaptorGatherer.get_test_list") as m: + m.return_value = sample_gatherer_result + verifier = Verifier(top_dir) + verifier.validate_tree() + + assert len(logger.warning.call_args_list) == 1 + for args, _ in logger.warning.call_args_list: + assert "Missing description for the metric" in args[0] + + +@pytest.mark.parametrize( + "manifest, metric_definitions, expected", + [ + [ + SAMPLE_INI, + """ +metrics: + "FirstPaint": + aliases: + - fcp + - SpeedIndex + description: "Example" """, + 3, + ], + [ + SAMPLE_METRICS_INI, + """ +metrics: + FirstPaint: + aliases: + - fcp + description: Example + SpeedIndex: + aliases: + - speedindex + - si + description: Example + """, + 5, + ], + ], +) +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_duplicate_metrics( + logger, structured_logger, perfdocs_sample, manifest, metric_definitions, expected +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + with open(perfdocs_sample["config"], "w", newline="\n") as f: + indented_defs = "\n".join( + [(" " * 8) + metric_line for metric_line in metric_definitions.split("\n")] + ) + f.write(SAMPLE_METRICS_CONFIG.format(metric_definitions, indented_defs)) + with open(perfdocs_sample["manifest"]["path"], "w", newline="\n") as f: + f.write(manifest) + + sample_gatherer_result = { + "suite": {"Example": {"metrics": ["fcp", "SpeedIndex"]}}, + "another_suite": {"Example": {}}, + } + + from perfdocs.verifier import Verifier + + with mock.patch("perfdocs.framework_gatherers.RaptorGatherer.get_test_list") as m: + m.return_value = sample_gatherer_result + verifier = Verifier(top_dir) + verifier.validate_tree() + + assert len(logger.warning.call_args_list) == expected + for args, _ in logger.warning.call_args_list: + assert "Duplicate definitions found for " in args[0] + + +@pytest.mark.parametrize( + "manifest, metric_definitions", + [ + [ + SAMPLE_INI, + """ +metrics: + "FirstPaint": + aliases: + - fcp + - SpeedIndex + description: "Example" """, + ], + [ + SAMPLE_METRICS_INI, + """ +metrics: + FirstPaint: + aliases: + - fcp + description: Example + SpeedIndex: + aliases: + - speedindex + - si + description: Example + """, + ], + ], +) +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_valid_metrics( + logger, structured_logger, perfdocs_sample, manifest, metric_definitions +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + with open(perfdocs_sample["config"], "w", newline="\n") as f: + f.write(SAMPLE_METRICS_CONFIG.format(metric_definitions, "")) + with open(perfdocs_sample["manifest"]["path"], "w", newline="\n") as f: + f.write(manifest) + + sample_gatherer_result = { + "suite": {"Example": {"metrics": ["fcp", "SpeedIndex"]}}, + "another_suite": {"Example": {}}, + } + + from perfdocs.verifier import Verifier + + with mock.patch("perfdocs.framework_gatherers.RaptorGatherer.get_test_list") as m: + m.return_value = sample_gatherer_result + verifier = Verifier(top_dir) + verifier.validate_tree() + + assert len(logger.warning.call_args_list) == 0 + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_framework_gatherers(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + # Check to make sure that every single framework + # gatherer that has been implemented produces a test list + # in every suite that contains a test with an associated + # manifest. + from perfdocs.gatherer import frameworks + + for framework, gatherer in frameworks.items(): + with open(perfdocs_sample["config"], "w", newline="\n") as f: + f.write(DYNAMIC_SAMPLE_CONFIG.format(framework)) + + fg = gatherer(perfdocs_sample["config"], top_dir) + if getattr(fg, "get_test_list", None) is None: + # Skip framework gatherers that have not + # implemented a method to build a test list. + continue + + # Setup some framework-specific things here if needed + if framework == "raptor": + fg._manifest_path = perfdocs_sample["manifest"]["path"] + fg._get_subtests_from_ini = mock.Mock() + fg._get_subtests_from_ini.return_value = { + "Example": perfdocs_sample["manifest"], + } + + if framework == "talos": + fg._get_ci_tasks = mock.Mock() + for suite, suitetests in fg.get_test_list().items(): + assert suite == "Talos Tests" + assert suitetests + continue + + if framework == "awsy": + for suite, suitetests in fg.get_test_list().items(): + assert suite == "Awsy tests" + assert suitetests + continue + + for suite, suitetests in fg.get_test_list().items(): + assert suite == "suite" + for test, manifest in suitetests.items(): + assert test == "Example" + assert ( + pathlib.Path(manifest["path"]) + == perfdocs_sample["manifest"]["path"] + ) + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_framework_gatherers_urls(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.gatherer import frameworks + from perfdocs.generator import Generator + from perfdocs.utils import read_yaml + from perfdocs.verifier import Verifier + + # This test is only for raptor + gatherer = frameworks["raptor"] + with open(perfdocs_sample["config"], "w", newline="\n") as f: + f.write(DYNAMIC_SAMPLE_CONFIG.format("raptor")) + + fg = gatherer(perfdocs_sample["config_2"], top_dir) + fg.get_suite_list = mock.Mock() + fg.get_suite_list.return_value = { + "suite": [perfdocs_sample["example1_manifest"]], + "another_suite": [perfdocs_sample["example2_manifest"]], + } + + v = Verifier(top_dir) + gn = Generator(v, generate=True, workspace=top_dir) + + # Check to make sure that if a test is present under multiple + # suties the urls are generated correctly for the test under + # every suite + for suite, suitetests in fg.get_test_list().items(): + url = fg._descriptions.get(suite) + assert url is not None + assert url[0]["name"] == "Example" + assert url[0]["test_url"] == "Example_url" + + perfdocs_tree = gn._perfdocs_tree[0] + yaml_content = read_yaml( + pathlib.Path( + os.path.join(os.path.join(perfdocs_tree["path"], perfdocs_tree["yml"])) + ) + ) + suites = yaml_content["suites"] + + # Check that the sections for each suite are generated correctly + for suite_name, suite_details in suites.items(): + gn._verifier._gatherer = mock.Mock(framework_gatherers={"raptor": gatherer}) + section = gn._verifier._gatherer.framework_gatherers[ + "raptor" + ].build_suite_section(fg, suite_name, suites.get(suite_name)["description"]) + assert suite_name.capitalize() == section[0] + assert suite_name in section[2] + + tests = suites.get(suite_name).get("tests", {}) + for test_name in tests.keys(): + desc = gn._verifier._gatherer.framework_gatherers[ + "raptor" + ].build_test_description(fg, test_name, tests[test_name], suite_name) + assert f"**test url**: `<{url[0]['test_url']}>`__" in desc[0] + assert f"**expected**: {url[0]['expected']}" in desc[0] + assert test_name in desc[0] + + +def test_perfdocs_logger_failure(config, paths): + from perfdocs.logger import PerfDocLogger + + PerfDocLogger.LOGGER = None + with pytest.raises(Exception): + PerfDocLogger() + + PerfDocLogger.PATHS = [] + with pytest.raises(Exception): + PerfDocLogger() + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_perfdocs_generation.py b/tools/lint/test/test_perfdocs_generation.py new file mode 100644 index 0000000000..b9b540d234 --- /dev/null +++ b/tools/lint/test/test_perfdocs_generation.py @@ -0,0 +1,350 @@ +import os +import pathlib +from unittest import mock + +import mozunit + +LINTER = "perfdocs" + + +def setup_sample_logger(logger, structured_logger, top_dir): + from perfdocs.logger import PerfDocLogger + + PerfDocLogger.LOGGER = structured_logger + PerfDocLogger.PATHS = ["perfdocs"] + PerfDocLogger.TOP_DIR = top_dir + + import perfdocs.gatherer as gt + import perfdocs.generator as gn + import perfdocs.utils as utls + import perfdocs.verifier as vf + + gt.logger = logger + vf.logger = logger + gn.logger = logger + utls.logger = logger + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_generate_perfdocs_pass( + logger, structured_logger, perfdocs_sample +): + from test_perfdocs import temp_file + + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates") + templates_dir.mkdir(parents=True, exist_ok=True) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"): + generator.generate_perfdocs() + + assert logger.warning.call_count == 0 + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_needed_regeneration( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=False, workspace=top_dir) + generator.generate_perfdocs() + + expected = "PerfDocs need to be regenerated." + args, _ = logger.warning.call_args + + assert logger.warning.call_count == 1 + assert args[0] == expected + + +@mock.patch("perfdocs.generator.get_changed_files", new=lambda x: []) +@mock.patch("perfdocs.generator.ON_TRY", new=True) +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_needed_update(logger, structured_logger, perfdocs_sample): + from test_perfdocs import temp_file + + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates") + templates_dir.mkdir(parents=True, exist_ok=True) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + # Initializing perfdocs + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"): + generator.generate_perfdocs() + + # Removed file for testing and run again + generator._generate = False + files = [f for f in os.listdir(generator.perfdocs_path)] + for f in files: + os.remove(str(pathlib.Path(generator.perfdocs_path, f))) + + generator.generate_perfdocs() + + expected = ( + "PerfDocs are outdated, run ./mach lint -l perfdocs --fix .` to update them. " + "You can also apply the perfdocs.diff patch file produced from this " + "reviewbot test to fix the issue." + ) + args, _ = logger.warning.call_args + + assert logger.warning.call_count == 1 + assert args[0] == expected + + # Check to ensure a diff was produced + assert logger.log.call_count == 6 + + logs = [v[0][0] for v in logger.log.call_args_list] + for failure_log in ( + "Some files are missing or are funny.", + "Missing in existing docs: index.rst", + "Missing in existing docs: mozperftest.rst", + ): + assert failure_log in logs + + +@mock.patch("perfdocs.generator.get_changed_files", new=lambda x: []) +@mock.patch("perfdocs.generator.ON_TRY", new=True) +def test_perfdocs_generator_update_with_no_changes(structured_logger, perfdocs_sample): + """This test ensures that when no changed files exist, we'll still trigger a failure.""" + from perfdocs.logger import PerfDocLogger + from test_perfdocs import temp_file + + top_dir = perfdocs_sample["top_dir"] + + logger_mock = mock.MagicMock() + PerfDocLogger.LOGGER = logger_mock + PerfDocLogger.PATHS = ["perfdocs"] + PerfDocLogger.TOP_DIR = top_dir + logger = PerfDocLogger() + + setup_sample_logger(logger, logger_mock, top_dir) + + templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates") + templates_dir.mkdir(parents=True, exist_ok=True) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + # Initializing perfdocs + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"): + generator.generate_perfdocs() + + # Removed file for testing and run again + generator._generate = False + files = [f for f in os.listdir(generator.perfdocs_path)] + for f in files: + os.remove(str(pathlib.Path(generator.perfdocs_path, f))) + + generator.generate_perfdocs() + + expected = ( + "PerfDocs are outdated, run ./mach lint -l perfdocs --fix .` to update them. " + "You can also apply the perfdocs.diff patch file produced from this " + "reviewbot test to fix the issue." + ) + assert logger.LOGGER.lint_error.call_args is not None + _, msg = logger.LOGGER.lint_error.call_args + + assert logger.FAILED + assert logger.LOGGER.lint_error.call_count == 1 + assert msg["message"] == expected + assert msg["rule"] == "Flawless performance docs (unknown file)" + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_created_perfdocs( + logger, structured_logger, perfdocs_sample +): + from test_perfdocs import temp_file + + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates") + templates_dir.mkdir(parents=True, exist_ok=True) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"): + perfdocs_tmpdir = generator._create_perfdocs() + + files = [f for f in os.listdir(perfdocs_tmpdir)] + files.sort() + expected_files = ["index.rst", "mozperftest.rst"] + + for i, file in enumerate(files): + assert file == expected_files[i] + + with pathlib.Path(perfdocs_tmpdir, expected_files[0]).open() as f: + filedata = f.readlines() + assert "".join(filedata) == " * :doc:`mozperftest`" + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_build_perfdocs(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + frameworks_info = generator.build_perfdocs_from_tree() + + expected = ["dynamic", "static"] + + for framework in sorted(frameworks_info.keys()): + for i, framework_info in enumerate(frameworks_info[framework]): + assert framework_info == expected[i] + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_create_temp_dir(logger, structured_logger, perfdocs_sample): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + tmpdir = generator._create_temp_dir() + + assert pathlib.Path(tmpdir).is_dir() + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_create_temp_dir_fail( + logger, structured_logger, perfdocs_sample +): + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + with mock.patch("perfdocs.generator.pathlib") as path_mock: + path_mock.Path().mkdir.side_effect = OSError() + path_mock.Path().is_dir.return_value = False + tmpdir = generator._create_temp_dir() + + expected = "Error creating temp file: " + args, _ = logger.critical.call_args + + assert not tmpdir + assert logger.critical.call_count == 1 + assert args[0] == expected + + +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_save_perfdocs_pass( + logger, structured_logger, perfdocs_sample +): + from test_perfdocs import temp_file + + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates") + templates_dir.mkdir(parents=True, exist_ok=True) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + + assert not generator.perfdocs_path.is_dir() + + with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"): + perfdocs_tmpdir = generator._create_perfdocs() + + generator._save_perfdocs(perfdocs_tmpdir) + + expected = ["index.rst", "mozperftest.rst"] + files = [f for f in os.listdir(generator.perfdocs_path)] + files.sort() + + for i, file in enumerate(files): + assert file == expected[i] + + +@mock.patch("perfdocs.generator.shutil") +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_generator_save_perfdocs_fail( + logger, shutil, structured_logger, perfdocs_sample +): + from test_perfdocs import temp_file + + top_dir = perfdocs_sample["top_dir"] + setup_sample_logger(logger, structured_logger, top_dir) + + templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates") + templates_dir.mkdir(parents=True, exist_ok=True) + + from perfdocs.generator import Generator + from perfdocs.verifier import Verifier + + verifier = Verifier(top_dir) + verifier.validate_tree() + + generator = Generator(verifier, generate=True, workspace=top_dir) + with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"): + perfdocs_tmpdir = generator._create_perfdocs() + + shutil.copytree = mock.Mock(side_effect=Exception()) + generator._save_perfdocs(perfdocs_tmpdir) + + expected = "There was an error while saving the documentation: " + args, _ = logger.critical.call_args + + assert logger.critical.call_count == 1 + assert args[0] == expected + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_perfdocs_helpers.py b/tools/lint/test/test_perfdocs_helpers.py new file mode 100644 index 0000000000..02c1abfecc --- /dev/null +++ b/tools/lint/test/test_perfdocs_helpers.py @@ -0,0 +1,206 @@ +import mozunit +import pytest + +LINTER = "perfdocs" + +testdata = [ + { + "table_specifications": { + "title": ["not a string"], + "widths": [10, 10, 10, 10], + "header_rows": 1, + "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]], + "indent": 2, + }, + "error_msg": "TableBuilder attribute title must be a string.", + }, + { + "table_specifications": { + "title": "I've got a lovely bunch of coconuts", + "widths": ("not", "a", "list"), + "header_rows": 1, + "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]], + "indent": 2, + }, + "error_msg": "TableBuilder attribute widths must be a list of integers.", + }, + { + "table_specifications": { + "title": "There they are all standing in a row", + "widths": ["not an integer"], + "header_rows": 1, + "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]], + "indent": 2, + }, + "error_msg": "TableBuilder attribute widths must be a list of integers.", + }, + { + "table_specifications": { + "title": "Big ones, small ones", + "widths": [10, 10, 10, 10], + "header_rows": "not an integer", + "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]], + "indent": 2, + }, + "error_msg": "TableBuilder attribute header_rows must be an integer.", + }, + { + "table_specifications": { + "title": "Some as big as your head!", + "widths": [10, 10, 10, 10], + "header_rows": 1, + "headers": ("not", "a", "list"), + "indent": 2, + }, + "error_msg": "TableBuilder attribute headers must be a two-dimensional list of strings.", + }, + { + "table_specifications": { + "title": "(And bigger)", + "widths": [10, 10, 10, 10], + "header_rows": 1, + "headers": ["not", "two", "dimensional"], + "indent": 2, + }, + "error_msg": "TableBuilder attribute headers must be a two-dimensional list of strings.", + }, + { + "table_specifications": { + "title": "Give 'em a twist, a flick of the wrist'", + "widths": [10, 10, 10, 10], + "header_rows": 1, + "headers": [[1, 2, 3]], + "indent": 2, + }, + "error_msg": "TableBuilder attribute headers must be a two-dimensional list of strings.", + }, + { + "table_specifications": { + "title": "That's what the showman said!", + "widths": [10, 10, 10, 10], + "header_rows": 1, + "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]], + "indent": "not an integer", + }, + "error_msg": "TableBuilder attribute indent must be an integer.", + }, +] + +table_specifications = { + "title": "I've got a lovely bunch of coconuts", + "widths": [10, 10, 10], + "header_rows": 1, + "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]], + "indent": 2, +} + + +@pytest.mark.parametrize("testdata", testdata) +def test_table_builder_invalid_attributes(testdata): + from perfdocs.doc_helpers import TableBuilder + + table_specifications = testdata["table_specifications"] + error_msg = testdata["error_msg"] + + with pytest.raises(TypeError) as error: + TableBuilder( + table_specifications["title"], + table_specifications["widths"], + table_specifications["header_rows"], + table_specifications["headers"], + table_specifications["indent"], + ) + + assert str(error.value) == error_msg + + +def test_table_builder_mismatched_columns(): + from perfdocs.doc_helpers import MismatchedRowLengthsException, TableBuilder + + table_specifications = { + "title": "I've got a lovely bunch of coconuts", + "widths": [10, 10, 10, 42], + "header_rows": 1, + "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]], + "indent": 2, + } + + with pytest.raises(MismatchedRowLengthsException) as error: + TableBuilder( + table_specifications["title"], + table_specifications["widths"], + table_specifications["header_rows"], + table_specifications["headers"], + table_specifications["indent"], + ) + assert ( + str(error.value) + == "Number of table headers must match number of column widths." + ) + + +def test_table_builder_add_row_too_long(): + from perfdocs.doc_helpers import MismatchedRowLengthsException, TableBuilder + + table = TableBuilder( + table_specifications["title"], + table_specifications["widths"], + table_specifications["header_rows"], + table_specifications["headers"], + table_specifications["indent"], + ) + with pytest.raises(MismatchedRowLengthsException) as error: + table.add_row( + ["big ones", "small ones", "some as big as your head!", "(and bigger)"] + ) + assert ( + str(error.value) + == "Number of items in a row must must number of columns defined." + ) + + +def test_table_builder_add_rows_type_error(): + from perfdocs.doc_helpers import TableBuilder + + table = TableBuilder( + table_specifications["title"], + table_specifications["widths"], + table_specifications["header_rows"], + table_specifications["headers"], + table_specifications["indent"], + ) + with pytest.raises(TypeError) as error: + table.add_rows( + ["big ones", "small ones", "some as big as your head!", "(and bigger)"] + ) + assert str(error.value) == "add_rows() requires a two-dimensional list of strings." + + +def test_table_builder_validate(): + from perfdocs.doc_helpers import TableBuilder + + table = TableBuilder( + table_specifications["title"], + table_specifications["widths"], + table_specifications["header_rows"], + table_specifications["headers"], + table_specifications["indent"], + ) + table.add_row(["big ones", "small ones", "some as big as your head!"]) + table.add_row( + ["Give 'em a twist", "A flick of the wrist", "That's what the showman said!"] + ) + table = table.finish_table() + print(table) + assert ( + table == " .. list-table:: **I've got a lovely bunch of coconuts**\n" + " :widths: 10 10 10\n :header-rows: 1\n\n" + " * - **Coconut 1**\n - Coconut 2\n - Coconut 3\n" + " * - **big ones**\n - small ones\n - some as big as your head!\n" + " * - **Give 'em a twist**\n - A flick of the wrist\n" + " - That's what the showman said!\n\n" + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_rst.py b/tools/lint/test/test_rst.py new file mode 100644 index 0000000000..e540081a94 --- /dev/null +++ b/tools/lint/test/test_rst.py @@ -0,0 +1,25 @@ +import mozunit +import pytest +from mozfile import which + +LINTER = "rst" +pytestmark = pytest.mark.skipif( + not which("rstcheck"), reason="rstcheck is not installed" +) + + +def test_basic(lint, paths): + results = lint(paths()) + assert len(results) == 2 + + assert "Title underline too short" in results[0].message + assert results[0].level == "error" + assert results[0].relpath == "bad.rst" + + assert "Title overline & underline mismatch" in results[1].message + assert results[1].level == "error" + assert results[1].relpath == "bad2.rst" + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_ruff.py b/tools/lint/test/test_ruff.py new file mode 100644 index 0000000000..fbb483780e --- /dev/null +++ b/tools/lint/test/test_ruff.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# 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/. + +from pprint import pprint +from textwrap import dedent + +import mozunit + +LINTER = "ruff" +fixed = 0 + + +def test_lint_fix(lint, create_temp_file): + contents = dedent( + """ + import distutils + print("hello!") + """ + ) + + path = create_temp_file(contents, "bad.py") + lint([path], fix=True) + assert fixed == 1 + + +def test_lint_ruff(lint, paths): + results = lint(paths()) + pprint(results, indent=2) + assert len(results) == 2 + assert results[0].level == "error" + assert results[0].relpath == "bad.py" + assert "`distutils` imported but unused" in results[0].message + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_rustfmt.py b/tools/lint/test/test_rustfmt.py new file mode 100644 index 0000000000..f1793be383 --- /dev/null +++ b/tools/lint/test/test_rustfmt.py @@ -0,0 +1,69 @@ +import mozunit + +LINTER = "rustfmt" +fixed = 0 + + +def test_good(lint, config, paths): + results = lint(paths("subdir/good.rs")) + print(results) + assert len(results) == 0 + + +def test_basic(lint, config, paths): + results = lint(paths("subdir/bad.rs")) + print(results) + assert len(results) >= 1 + + assert "Reformat rust" in results[0].message + assert results[0].level == "warning" + assert results[0].lineno == 4 + assert "bad.rs" in results[0].path + assert "Print text to the console" in results[0].diff + + +def test_dir(lint, config, paths): + results = lint(paths("subdir/")) + print(results) + assert len(results) >= 4 + + assert "Reformat rust" in results[0].message + assert results[0].level == "warning" + assert results[0].lineno == 4 + assert "bad.rs" in results[0].path + assert "Print text to the console" in results[0].diff + + assert "Reformat rust" in results[1].message + assert results[1].level == "warning" + assert results[1].lineno == 4 + assert "bad2.rs" in results[1].path + assert "Print text to the console" in results[1].diff + + +def test_fix(lint, create_temp_file): + contents = """fn main() { + // Statements here are executed when the compiled binary is called + + // Print text to the console + println!("Hello World!"); + let mut a; + let mut b=1; + let mut vec = Vec::new(); + vec.push(1); + vec.push(2); + + + for x in 5..10 - 5 { + a = x; + } + + } +""" + + path = create_temp_file(contents, "bad.rs") + lint([path], fix=True) + assert fixed == 3 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_shellcheck.py b/tools/lint/test/test_shellcheck.py new file mode 100644 index 0000000000..1b41e298bd --- /dev/null +++ b/tools/lint/test/test_shellcheck.py @@ -0,0 +1,26 @@ +import mozunit +import pytest +from mozfile import which + +LINTER = "shellcheck" +pytestmark = pytest.mark.skipif( + not which("shellcheck"), reason="shellcheck is not installed" +) + + +def test_basic(lint, paths): + results = lint(paths()) + print(results) + assert len(results) == 2 + + assert "hello appears unused" in results[0].message + assert results[0].level == "error" + assert results[0].relpath == "bad.sh" + + assert "Double quote to prevent" in results[1].message + assert results[1].level == "error" + assert results[1].relpath == "bad.sh" + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_stylelint.py b/tools/lint/test/test_stylelint.py new file mode 100644 index 0000000000..5d758ad318 --- /dev/null +++ b/tools/lint/test/test_stylelint.py @@ -0,0 +1,65 @@ +import mozunit +import pytest +from conftest import build + +LINTER = "stylelint" +fixed = 0 + + +@pytest.fixture +def stylelint(lint): + def inner(*args, **kwargs): + # --ignore-path is because stylelint doesn't have the --no-ignore option + # and therefore needs to be given an empty file for the tests to work. + kwargs["extra_args"] = [ + "--ignore-path=tools/lint/test/files/eslint/testprettierignore", + ] + return lint(*args, **kwargs) + + return inner + + +def test_lint_with_global_exclude(lint, config, paths): + config["exclude"] = ["subdir", "import"] + # This uses lint directly as we need to not ignore the excludes. + results = lint(paths(), config=config, root=build.topsrcdir) + assert len(results) == 0 + + +def test_no_files_to_lint(stylelint, config, paths): + # A directory with no files to lint. + results = stylelint(paths("nolint"), root=build.topsrcdir) + assert results == [] + + # Errors still show up even when a directory with no files is passed in. + results = stylelint(paths("nolint", "subdir/bad.css"), root=build.topsrcdir) + assert len(results) == 1 + + +def test_stylelint(stylelint, config, create_temp_file): + contents = """#foo { + font-size: 12px; + font-size: 12px; +} +""" + path = create_temp_file(contents, "bad.css") + results = stylelint([path], config=config, root=build.topsrcdir) + + assert len(results) == 1 + + +def test_stylelint_fix(stylelint, config, create_temp_file): + contents = """#foo { + font-size: 12px; + font-size: 12px; +} +""" + path = create_temp_file(contents, "bad.css") + stylelint([path], config=config, root=build.topsrcdir, fix=True) + + # stylelint returns counts of files fixed, not errors fixed. + assert fixed == 1 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_trojan_source.py b/tools/lint/test/test_trojan_source.py new file mode 100644 index 0000000000..64a3789c37 --- /dev/null +++ b/tools/lint/test/test_trojan_source.py @@ -0,0 +1,25 @@ +import mozunit + +LINTER = "trojan-source" + + +def test_lint_trojan_source(lint, paths): + results = lint(paths()) + print(results) + assert len(results) == 3 + + assert "disallowed characters" in results[0].message + assert results[0].level == "error" + assert "commenting-out.cpp" in results[0].relpath + + assert "disallowed characters" in results[1].message + assert results[1].level == "error" + assert "early-return.py" in results[1].relpath + + assert "disallowed characters" in results[2].message + assert results[2].level == "error" + assert "invisible-function.rs" in results[2].relpath + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_updatebot.py b/tools/lint/test/test_updatebot.py new file mode 100644 index 0000000000..5763cc1a7a --- /dev/null +++ b/tools/lint/test/test_updatebot.py @@ -0,0 +1,44 @@ +import os + +import mozunit + +LINTER = "updatebot" + + +def test_basic(lint, paths): + results = [] + + for p in paths(): + for root, dirs, files in os.walk(p): + for f in files: + if f == ".yamllint": + continue + + filepath = os.path.join(root, f) + result = lint(filepath, testing=True) + if result: + results.append(result) + + assert len(results) == 2 + + expected_results = 0 + + for r in results: + if "no-revision.yaml" in r[0].path: + expected_results += 1 + assert "no-revision.yaml" in r[0].path + assert ( + 'If "vendoring" is present, "revision" must be present in "origin"' + in r[0].message + ) + + if "cargo-mismatch.yaml" in r[0].path: + expected_results += 1 + assert "cargo-mismatch.yaml" in r[0].path + assert "wasn't found in Cargo.lock" in r[0].message + + assert expected_results == 2 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_yaml.py b/tools/lint/test/test_yaml.py new file mode 100644 index 0000000000..63b678d152 --- /dev/null +++ b/tools/lint/test/test_yaml.py @@ -0,0 +1,28 @@ +import mozunit + +LINTER = "yaml" + + +def test_basic(lint, paths): + results = lint(paths()) + + assert len(results) == 3 + + assert "line too long (122 > 80 characters)" in results[0].message + assert results[0].level == "error" + assert "bad.yml" in results[0].relpath + assert results[0].lineno == 3 + + assert "wrong indentation: expected 4 but found 8" in results[1].message + assert results[1].level == "error" + assert "bad.yml" in results[1].relpath + assert results[0].lineno == 3 + + assert "could not find expected" in results[2].message + assert results[2].level == "error" + assert "bad.yml" in results[2].relpath + assert results[2].lineno == 9 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/tox/tox_requirements.txt b/tools/lint/tox/tox_requirements.txt new file mode 100644 index 0000000000..00cec3ff5d --- /dev/null +++ b/tools/lint/tox/tox_requirements.txt @@ -0,0 +1,10 @@ +pluggy==0.13.1 --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d +importlib-metadata==0.23 --hash=sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af +more-itertools==7.2.0 --hash=sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4 +zipp==0.6.0 --hash=sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335 +py==1.11.0 --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 +tox==2.7.0 --hash=sha256:0f37ea637ead4a5bbae91531b0bf8fd327c7152e20255e5960ee180598228d21 +virtualenv==20.24.7 --hash=sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd +distlib==0.3.7 --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 +filelock==3.13.1 --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c +platformdirs==4.0.0 --hash=sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b diff --git a/tools/lint/trojan-source.yml b/tools/lint/trojan-source.yml new file mode 100644 index 0000000000..9b25962b2e --- /dev/null +++ b/tools/lint/trojan-source.yml @@ -0,0 +1,27 @@ +--- +trojan-source: + description: Trojan Source attack - CVE-2021-42572 + include: + - . + exclude: + - intl/lwbrk/rulebrk.c + - testing/web-platform/tests/conformance-checkers/tools/ins-del-datetime.py + - modules/freetype2/src/autofit/afblue.c + - modules/freetype2/builds/amiga/include/config/ftconfig.h + - modules/freetype2/builds/amiga/include/config/ftmodule.h + - modules/freetype2/builds/amiga/src/base/ftsystem.c + - third_party/rust/chardetng/src/data.rs + - third_party/rust/error-chain/tests/tests.rs + - third_party/rust/unicode-width/src/tests.rs + - security/nss/gtests/mozpkix_gtest/pkixnames_tests.cpp + extensions: + - .c + - .cc + - .cpp + - .h + - .py + - .rs + support-files: + - 'tools/lint/trojan-source/**' + type: external + payload: trojan-source:lint diff --git a/tools/lint/trojan-source/__init__.py b/tools/lint/trojan-source/__init__.py new file mode 100644 index 0000000000..a20c10203d --- /dev/null +++ b/tools/lint/trojan-source/__init__.py @@ -0,0 +1,67 @@ +# 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 sys +import unicodedata + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +# Code inspired by Red Hat +# https://github.com/siddhesh/find-unicode-control/ +# published under the 'BSD 3-Clause' license +# https://access.redhat.com/security/vulnerabilities/RHSB-2021-007 + +results = [] + +disallowed = set( + chr(c) for c in range(sys.maxunicode) if unicodedata.category(chr(c)) == "Cf" +) + + +def getfiletext(config, filename): + # Make a text string from a file, attempting to decode from latin1 if necessary. + # Other non-utf-8 locales are not supported at the moment. + with open(filename, "rb") as infile: + try: + return infile.read().decode("utf-8") + except Exception as e: + res = { + "path": filename, + "message": "Could not open file as utf-8 - maybe an encoding error: %s" + % e, + "level": "error", + } + results.append(result.from_config(config, **res)) + return None + + return None + + +def analyze_text(filename, text, disallowed): + line = 0 + for t in text.splitlines(): + line = line + 1 + subset = [c for c in t if chr(ord(c)) in disallowed] + if subset: + return (subset, line) + + return ("", 0) + + +def lint(paths, config, **lintargs): + files = list(expand_exclusions(paths, config, lintargs["root"])) + for f in files: + text = getfiletext(config, f) + if text: + (subset, line) = analyze_text(f, text, disallowed) + if subset: + res = { + "path": f, + "lineno": line, + "message": "disallowed characters: %s" % subset, + "level": "error", + } + results.append(result.from_config(config, **res)) + + return {"results": results, "fixed": 0} diff --git a/tools/lint/updatebot.yml b/tools/lint/updatebot.yml new file mode 100644 index 0000000000..90f13e8cf4 --- /dev/null +++ b/tools/lint/updatebot.yml @@ -0,0 +1,9 @@ +--- +updatebot: + description: > + "Ensure moz.yaml files are valid" + extensions: ['yaml'] + include: ['.'] + type: external-file + level: error + payload: updatebot.validate_yaml:lint diff --git a/tools/lint/updatebot/__init__.py b/tools/lint/updatebot/__init__.py new file mode 100644 index 0000000000..c580d191c1 --- /dev/null +++ b/tools/lint/updatebot/__init__.py @@ -0,0 +1,3 @@ +# 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/. diff --git a/tools/lint/updatebot/validate_yaml.py b/tools/lint/updatebot/validate_yaml.py new file mode 100644 index 0000000000..802a9033fa --- /dev/null +++ b/tools/lint/updatebot/validate_yaml.py @@ -0,0 +1,52 @@ +# 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/. + +from mozbuild.vendor.moz_yaml import load_moz_yaml +from mozlint import result +from mozlint.pathutils import expand_exclusions + + +class UpdatebotValidator: + def lint_file(self, path, **kwargs): + if not kwargs.get("testing", False) and not path.endswith("moz.yaml"): + # When testing, process all files provided + return None + if not kwargs.get("testing", False) and "test/files/updatebot" in path: + # When not testing, ignore the test files + return None + + try: + yaml = load_moz_yaml(path) + + if "vendoring" in yaml and yaml["vendoring"].get("flavor", None) == "rust": + yaml_revision = yaml["origin"]["revision"] + + with open("Cargo.lock", "r") as f: + for line in f: + if yaml_revision in line: + return None + + return f"Revision {yaml_revision} specified in {path} wasn't found in Cargo.lock" + + return None + except Exception as e: + return f"Could not load {path} according to schema in moz_yaml.py: {e}" + + +def lint(paths, config, **lintargs): + # expand_exclusions expects a list, and will convert a string + # into it if it doesn't receive one + if not isinstance(paths, list): + paths = [paths] + + errors = [] + files = list(expand_exclusions(paths, config, lintargs["root"])) + + m = UpdatebotValidator() + for f in files: + message = m.lint_file(f, **lintargs) + if message: + errors.append(result.from_config(config, path=f, message=message)) + + return errors diff --git a/tools/lint/wpt.yml b/tools/lint/wpt.yml new file mode 100644 index 0000000000..dd3c0dd042 --- /dev/null +++ b/tools/lint/wpt.yml @@ -0,0 +1,10 @@ +--- +wpt: + description: web-platform-tests lint + include: + - testing/web-platform/tests + exclude: [] + support-files: + - tools/lint/wpt/wpt.py + type: external + payload: wpt.wpt:lint diff --git a/tools/lint/wpt/__init__.py b/tools/lint/wpt/__init__.py new file mode 100644 index 0000000000..c580d191c1 --- /dev/null +++ b/tools/lint/wpt/__init__.py @@ -0,0 +1,3 @@ +# 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/. diff --git a/tools/lint/wpt/wpt.py b/tools/lint/wpt/wpt.py new file mode 100644 index 0000000000..6f4f40492a --- /dev/null +++ b/tools/lint/wpt/wpt.py @@ -0,0 +1,64 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 subprocess +import sys + +from mozlint import result + +results = [] + + +def lint(files, config, **kwargs): + log = kwargs["log"] + tests_dir = os.path.join(kwargs["root"], "testing", "web-platform", "tests") + + def process_line(line): + try: + data = json.loads(line) + except ValueError: + print( + f"Got non-JSON output: {line}", + file=sys.stderr, + ) + return + + data["level"] = "error" + data["path"] = os.path.relpath( + os.path.join(tests_dir, data["path"]), kwargs["root"] + ) + data.setdefault("lineno", 0) + results.append(result.from_config(config, **data)) + + if files == [tests_dir]: + print( + "No specific files specified, running the full wpt lint" " (this is slow)", + file=sys.stderr, + ) + files = ["--all"] + cmd = ["python3", os.path.join(tests_dir, "wpt"), "lint", "--json"] + files + log.debug("Command: {}".format(" ".join(cmd))) + + proc = subprocess.Popen( + cmd, env=os.environ, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) + try: + for line in proc.stdout: + process_line(line.rstrip("\r\n")) + proc.wait() + if proc.returncode != 0: + results.append( + result.from_config( + config, + message="Lint process exited with return code %s" % proc.returncode, + ) + ) + except KeyboardInterrupt: + proc.kill() + + return results diff --git a/tools/lint/yaml.yml b/tools/lint/yaml.yml new file mode 100644 index 0000000000..47cde80177 --- /dev/null +++ b/tools/lint/yaml.yml @@ -0,0 +1,19 @@ +--- +yamllint: + description: YAML linter + include: + - .cron.yml + - .taskcluster.yml + - browser/config/ + - python/mozlint/ + - security/nss/.taskcluster.yml + - taskcluster + - testing/mozharness + - tools + - build/cargo + extensions: ['yml', 'yaml'] + support-files: + - '**/.yamllint' + - 'tools/lint/yamllint_/**' + type: external + payload: yamllint_:lint diff --git a/tools/lint/yamllint_/__init__.py b/tools/lint/yamllint_/__init__.py new file mode 100644 index 0000000000..2244fabd3f --- /dev/null +++ b/tools/lint/yamllint_/__init__.py @@ -0,0 +1,101 @@ +# 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 os +import re +import sys +from collections import defaultdict + +from mozbuild.base import MozbuildObject + +topsrcdir = MozbuildObject.from_environment().topsrcdir + +from mozlint import result +from mozlint.pathutils import get_ancestors_by_name +from mozlint.util.implementation import LintProcess + +YAMLLINT_FORMAT_REGEX = re.compile("(.*):(.*):(.*): \[(error|warning)\] (.*) \((.*)\)$") + +results = [] + + +class YAMLLintProcess(LintProcess): + def process_line(self, line): + try: + match = YAMLLINT_FORMAT_REGEX.match(line) + abspath, line, col, level, message, code = match.groups() + except AttributeError: + print("Unable to match yaml regex against output: {}".format(line)) + return + + res = { + "path": os.path.relpath(str(abspath), self.config["root"]), + "message": str(message), + "level": "error", + "lineno": line, + "column": col, + "rule": code, + } + + results.append(result.from_config(self.config, **res)) + + +def get_yamllint_version(): + from yamllint import APP_VERSION + + return APP_VERSION + + +def run_process(config, cmd): + proc = YAMLLintProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + +def gen_yamllint_args(cmdargs, paths=None, conf_file=None): + args = cmdargs[:] + if isinstance(paths, str): + paths = [paths] + if conf_file and conf_file != "default": + return args + ["-c", conf_file] + paths + return args + paths + + +def lint(files, config, **lintargs): + log = lintargs["log"] + + log.debug("Version: {}".format(get_yamllint_version())) + + cmdargs = [ + sys.executable, + os.path.join(topsrcdir, "mach"), + "python", + "--", + "-m", + "yamllint", + "-f", + "parsable", + ] + log.debug("Command: {}".format(" ".join(cmdargs))) + + config = config.copy() + config["root"] = lintargs["root"] + + # Run any paths with a .yamllint file in the directory separately so + # it gets picked up. This means only .yamllint files that live in + # directories that are explicitly included will be considered. + paths_by_config = defaultdict(list) + for f in files: + conf_files = get_ancestors_by_name(".yamllint", f, config["root"]) + paths_by_config[conf_files[0] if conf_files else "default"].append(f) + + for conf_file, paths in paths_by_config.items(): + run_process( + config, gen_yamllint_args(cmdargs, conf_file=conf_file, paths=paths) + ) + + return results |