diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /tools/lint | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tools/lint')
259 files changed, 20755 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-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..617df22e78 --- /dev/null +++ b/tools/lint/android/lints.py @@ -0,0 +1,345 @@ +# -*- 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 itertools +import json +import os +import re +import six +import subprocess +import sys + +import xml.etree.ElementTree as ET + +from mozpack.files import FileFinder +import mozpack.path as mozpath +from mozlint import result + + +# 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(six.moves.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 + + +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": mozpath.relpath(r["file"], topsrcdir), + "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": mozpath.relpath(r["file"], topsrcdir), + "lineno": int(r["line"]), + "column": int(r.get("column") or 0), + "message": "Unexpected api change. Please run ./gradlew {} 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: + issue["path"] = issue["path"].replace(lintargs["root"], "") + # We want warnings to be errors for linting purposes. + issue["level"] = "error" + 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] + err = { + "level": issue.get("severity").lower(), + "rule": issue.get("id"), + "message": issue.get("message"), + "path": location.get("file").replace(lintargs["root"], ""), + "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"): + sourcepath = file.get("name").replace(topsrcdir + "/", "") + + 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": sourcepath, + "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("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..79fdee1342 --- /dev/null +++ b/tools/lint/black.yml @@ -0,0 +1,19 @@ +--- +black: + description: Reformat python + exclude: + - gfx/harfbuzz/src/meson.build + - layout/style/ServoCSSPropList.mako.py + - python/mozbuild/mozbuild/fork_interpose.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 + - 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..0545612c10 --- /dev/null +++ b/tools/lint/clang-format.yml @@ -0,0 +1,11 @@ +--- +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 diff --git a/tools/lint/clang-format/__init__.py b/tools/lint/clang-format/__init__.py new file mode 100644 index 0000000000..fb0ed9a654 --- /dev/null +++ b/tools/lint/clang-format/__init__.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 os +import signal +import re + +from buildconfig import substs +from mozboot.util import get_state_dir +from mozlint import result +from mozlint.pathutils import expand_exclusions +from mozprocess import ProcessHandler + +CLANG_FORMAT_NOT_FOUND = """ +Could not find clang-format! Install clang-format with: + + $ ./mach bootstrap + +And make sure that it is in the PATH +""".strip() + + +def parse_issues(config, output, paths, log): + + diff_line = re.compile("^(.*):(.*):(.*): warning: .*;(.*);(.*)") + results = [] + for line in output: + match = diff_line.match(line) + file, line_no, col, diff, diff2 = match.groups() + log.debug( + "file={} line={} col={} diff={} diff2={}".format( + file, line_no, col, diff, diff2 + ) + ) + d = diff + "\n" + diff2 + res = { + "path": file, + "diff": d, + "level": "warning", + "lineno": line_no, + "column": col, + } + results.append(result.from_config(config, **res)) + + return results + + +class ClangFormatProcess(ProcessHandler): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["stream"] = False + kwargs["universal_newlines"] = True + ProcessHandler.__init__(self, *args, **kwargs) + + def run(self, *args, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + ProcessHandler.run(self, *args, **kwargs) + signal.signal(signal.SIGINT, orig) + + +def run_process(config, cmd): + proc = ClangFormatProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.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_state_dir(), "clang-tools") + bin_path = os.path.join(clang_tools_path, "clang-tidy", "bin") + return os.path.join(bin_path, "clang-format" + substs.get("HOST_BIN_SUFFIX", "")) + + +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 line in fh: + # In case it starts with a space + line = line.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) + log.debug("Version: {}".format(version)) + + if fix: + cmd_args.append("-i") + else: + cmd_args.append("--dry-run") + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + output = run_process(config, base_command) + output_list = [] + + if len(output) % 3 != 0: + raise Exception( + "clang-format output should be a multiple of 3. Output: %s" % output + ) + + for i in range(0, len(output), 3): + # Merge the element 3 by 3 (clang-format output) + line = output[i] + line += ";" + output[i + 1] + line += ";" + output[i + 2] + output_list.append(line) + + if fix: + # clang-format is able to fix all issues so don't bother parsing the output. + return [] + return parse_issues(config, output_list, paths, log) diff --git a/tools/lint/clippy.yml b/tools/lint/clippy.yml new file mode 100644 index 0000000000..fa736265a4 --- /dev/null +++ b/tools/lint/clippy.yml @@ -0,0 +1,117 @@ +--- +clippy: + description: Lint rust + include: + - build/workspace-hack/ + - dom/media/gtest/ + - dom/webauthn/libudev-sys/ + - gfx/webrender_bindings/ + - gfx/wr/direct-composition/ + - gfx/wr/example-compositor/compositor-windows/ + - gfx/wr/peek-poke/ + - gfx/wr/peek-poke/peek-poke-derive/ + - gfx/wr/webrender_build/ + - gfx/wr/wr_malloc_size_of/ + - js/src/ + - js/src/frontend/smoosh/ + - js/src/rust/shared/ + - js/src/wasm/cranelift/ + - media/audioipc/ + - modules/libpref/init/static_prefs/ + - mozglue/static/rust/ + - netwerk/base/mozurl/ + - security/manager/ssl/osclientcerts/ + - servo/components/derive_common/ + - servo/components/selectors/ + - servo/components/servo_arc/ + - servo/components/size_of_test/ + - 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/components/kvstore/ + - toolkit/components/glean/ + - toolkit/components/xulstore/tests/gtest/ + - toolkit/library/rust/ + - tools/fuzzing/rust/ + - 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/ + - gfx/wr/webrender_api/ + - gfx/wr/wrench/ + - gfx/wgpu/wgpu-core/ + - 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 + - js/rust/ + - storage/rust/ + - storage/variant/ + # nsstring + - toolkit/components/xulstore/ + - 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/ + - media/audioipc/client/ + - media/audioipc/audioipc/ + - media/audioipc/server/ + - tools/lint/test/files/clippy/ + - servo/components/hashglobe/ + - servo/ports/geckolib/ + - servo/ports/geckolib/tests/ + - servo/tests/unit/malloc_size_of/ + - servo/components/malloc_size_of/ + - dom/media/webrtc/sdp/rsdparsa_capi/ + - servo/components/fallible/ + - testing/geckodriver/marionette/ + - toolkit/components/bitsdownload/bits_client/ + - gfx/wr/example-compositor/compositor/ + - toolkit/components/bitsdownload/bits_client/bits/ + extensions: + - rs + support-files: + - 'tools/lint/clippy/**' + # the version of clippy is: + # clippy 0.0.212 (d4092ac 2020-05-11) + # but 0.0.212 isn't updated. we use the date instead + # replacing - by . because Python StrictVersion expects this + min_clippy_version: 2020.05.11 + 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..75dbcd42fc --- /dev/null +++ b/tools/lint/clippy/__init__.py @@ -0,0 +1,252 @@ +# 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 six +import subprocess + +from distutils.version import StrictVersion +from mozfile import which +from mozlint import result +from mozlint.pathutils import get_ancestors_by_name +from mozprocess import ProcessHandler + + +CLIPPY_WRONG_VERSION = """ +You are probably using an old version of clippy. +Expected version is {version}. + +To install it: + $ rustup component add clippy + +Or to update it: + $ rustup update + +And make sure that 'cargo' is in the PATH +""".strip() + + +CARGO_NOT_FOUND = """ +Could not find cargo! Install cargo. + +And make sure that it is in the PATH +""".strip() + + +def parse_issues(log, config, issues, path, onlyIn): + results = [] + for issue in issues: + + try: + detail = json.loads(six.ensure_text(issue)) + 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)) + continue + # 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) + ) + continue + + l = detail["spans"][0] + if onlyIn and onlyIn not in p: + # Case when we have a .rs in the include list in the yaml file + log.debug( + "{} is not part of the list of files '{}'".format(p, onlyIn) + ) + continue + 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"], + } + results.append(result.from_config(config, **res)) + + except json.decoder.JSONDecodeError: + log.debug("Could not parse the output:") + log.debug("clippy output: {}".format(issue)) + continue + + return results + + +def get_cargo_binary(log): + """ + Returns the path of the first rustfmt binary available + if not found returns None + """ + cargo_home = os.environ.get("CARGO_HOME") + if cargo_home: + log.debug("Found CARGO_HOME in {}".format(cargo_home)) + cargo_bin = os.path.join(cargo_home, "bin", "cargo") + if os.path.exists(cargo_bin): + return cargo_bin + log.debug("Did not find {} in CARGO_HOME".format(cargo_bin)) + return None + return which("cargo") + + +def get_clippy_version(log, binary): + """ + Check if we are running the deprecated rustfmt + """ + try: + output = subprocess.check_output( + [binary, "clippy", "--version"], + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + except subprocess.CalledProcessError: + # --version failed, clippy isn't installed. + return False + + log.debug("Found version: {}".format(output)) + + version = re.findall(r"(\d+-\d+-\d+)", output)[0].replace("-", ".") + version = StrictVersion(version) + return version + + +class clippyProcess(ProcessHandler): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["stream"] = False + ProcessHandler.__init__(self, *args, **kwargs) + + def run(self, *args, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + ProcessHandler.run(self, *args, **kwargs) + signal.signal(signal.SIGINT, orig) + + +def run_process(log, config, cmd): + log.debug("Command: {}".format(cmd)) + proc = clippyProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.output + + +def lint(paths, config, fix=None, **lintargs): + log = lintargs["log"] + cargo = get_cargo_binary(log) + + if not cargo: + print(CARGO_NOT_FOUND) + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + min_version_str = config.get("min_clippy_version") + min_version = StrictVersion(min_version_str) + actual_version = get_clippy_version(log, cargo) + log.debug( + "Found version: {}. Minimal expected version: {}".format( + actual_version, min_version + ) + ) + + if actual_version < min_version: + print(CLIPPY_WRONG_VERSION.format(version=min_version_str)) + return 1 + + cmd_args_clean = [cargo] + cmd_args_clean.append("clean") + + cmd_args_common = ["--manifest-path"] + cmd_args_clippy = [cargo] + + if fix: + cmd_args_clippy += ["+nightly"] + + cmd_args_clippy += [ + "clippy", + "--message-format=json", + ] + + if fix: + cmd_args_clippy += ["--fix", "-Z", "unstable-options"] + + lock_files_to_delete = [] + for p in paths: + lock_file = os.path.join(p, "Cargo.lock") + if not os.path.exists(lock_file): + lock_files_to_delete.append(lock_file) + + results = [] + for p in paths: + # Quick sanity check of the paths + if p.endswith("Cargo.toml"): + print("Error: expects a directory or a rs file") + print("Found {}".format(p)) + return 1 + + for p in paths: + onlyIn = [] + path_conf = p + log.debug("Path = {}".format(p)) + if os.path.isfile(p): + # We are dealing with a file. We remove the filename from the path + # to find the closest Cargo file + # We also store the name of the file to be able to filter out other + # files built by the cargo + p = os.path.dirname(p) + onlyIn = path_conf + + if os.path.isdir(p): + # Sometimes, clippy reports issues from other crates + # Make sure that we don't display that either + onlyIn = p + + cargo_files = get_ancestors_by_name("Cargo.toml", p, lintargs["root"]) + p = cargo_files[0] + + log.debug("Path translated to = {}".format(p)) + # Needs clean because of https://github.com/rust-lang/rust-clippy/issues/2604 + clean_command = cmd_args_clean + cmd_args_common + [p] + run_process(log, config, clean_command) + + # Create the actual clippy command + base_command = cmd_args_clippy + cmd_args_common + [p] + output = run_process(log, config, base_command) + + # Remove build artifacts created by clippy + run_process(log, config, clean_command) + results += parse_issues(log, config, output, p, onlyIn) + + # Remove Cargo.lock files created by clippy + for lock_file in lock_files_to_delete: + if os.path.exists(lock_file): + os.remove(lock_file) + + return sorted(results, key=lambda issue: issue.path) diff --git a/tools/lint/codespell.yml b/tools/lint/codespell.yml new file mode 100644 index 0000000000..f82c0759b6 --- /dev/null +++ b/tools/lint/codespell.yml @@ -0,0 +1,85 @@ +--- +codespell: + description: Check code for common misspellings + include: + - browser/base/content/docs/ + - browser/branding/ + - browser/components/newtab/docs/ + - browser/components/newtab/content-src/asrouter/docs/ + - 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/ + - js/src/doc/ + - layout/tools/layout-debug/ui/locale/en-US/ + - mobile/android/branding/ + - mobile/android/docs/ + - mobile/android/locales/en-US/ + - mobile/locales/en-US/ + - netwerk/locales/en-US/ + - python/docs/ + - python/mach/docs/ + - python/mozlint/ + - remote/doc/ + - security/manager/locales/en-US/ + - services/sync/locales/en-US/ + - taskcluster/docs/ + - testing/geckodriver/doc/ + - testing/marionette/doc/ + - testing/mozbase/docs/ + - toolkit/components/extensions/docs/ + - toolkit/components/normandy/docs/ + - toolkit/components/search/docs/ + - toolkit/components/telemetry/docs/ + - toolkit/crashreporter/docs/ + - toolkit/docs/ + - toolkit/locales/en-US/ + - toolkit/modules/docs/ + - tools/code-coverage/docs/ + - tools/fuzzing/docs/ + - tools/moztreedocs/ + - tools/lint/ + - tools/sanitizer/docs/ + - tools/tryselect/ + - uriloader/docs/ + exclude: + - tools/lint/test/test_codespell.py + # List of extensions coming from: + # tools/lint/{flake8,eslint}.yml + # tools/mach_commands.py (clang-format) + # + documentation + # + localization files + extensions: + - js + - jsm + - jxs + - 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/cpp-virtual-final.yml b/tools/lint/cpp-virtual-final.yml new file mode 100644 index 0000000000..20c5837f36 --- /dev/null +++ b/tools/lint/cpp-virtual-final.yml @@ -0,0 +1,25 @@ +--- +cpp-virtual-final: + description: "Virtual function declarations should specify only one of + `virtual`, `final`, or `override`" + level: error + include: ['.'] + extensions: ['cc', 'cpp', 'h', 'mm'] + type: regex + # + # This lint warns about: + # + # virtual void Bad1() final + # void Bad2() final override + # void Bad3() override final + # + # Caveats: This lint ... + # + # * Doesn't warn about `virtual void NotBad() override` at this time + # because there are currently 6963 instances of this pattern. + # + # * Doesn't warn about function declarations that span multiple lines + # because the regex can't match across line breaks. + # + # virtual ) final | final override | override final + payload: ^ *virtual .+\).+\bfinal\b|\bfinal +override\b|\boverride +final\b 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 100755 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..627cd1a658 --- /dev/null +++ b/tools/lint/eslint.yml @@ -0,0 +1,30 @@ +--- +eslint: + description: JavaScript linter + # ESLint infra handles its own path filtering, so just include cwd + include: ['.'] + exclude: [] + extensions: ['js', 'jsm', 'jsx', 'xul', 'html', 'xhtml'] + support-files: + - '**/.eslintrc.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' + - 'toolkit/modules/Services.jsm' + 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..48285a03d0 --- /dev/null +++ b/tools/lint/eslint/.eslintrc.js @@ -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/. */ + +"use strict"; + +module.exports = { + // 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: { + ecmaVersion: 10, + }, + + 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..cf1b29e64a --- /dev/null +++ b/tools/lint/eslint/__init__.py @@ -0,0 +1,144 @@ +# -*- 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/. + +from __future__ import absolute_import, print_function + +import json +import os +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 + +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() + + +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, 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"])] + ) + + 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", + ] + + extra_args + + exclude_args + + paths + ) + log.debug("Command: {}".format(" ".join(cmd_args))) + + # eslint requires that --fix be set before the --ext argument. + if fix: + cmd_args.insert(2, "--fix") + + shell = False + if os.environ.get("MSYSTEM") in ("MINGW32", "MINGW64"): + # 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 [] + + if errors: + errors = errors.decode(encoding, "replace") + print(ESLINT_ERROR_MESSAGE.format(errors)) + + if proc.returncode >= 2: + return 1 + + if not output: + return [] # 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 = [] + for obj in jsonresult: + errors = obj["messages"] + + 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 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..596815c850 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js @@ -0,0 +1,64 @@ +// 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, + PromiseDebugging: false, + SpecialPowers: false, + TestUtils: false, + XPCNativeWrapper: false, + addLoadEvent: 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, + todo: false, + todo_is: false, + todo_isnot: false, + waitForClipboard: false, + waitForExplicitFinish: false, + waitForFocus: false, + }, + + plugins: ["mozilla"], + + rules: { + "mozilla/import-content-task-globals": "error", + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + "mozilla/no-arbitrary-setTimeout": "error", + }, +}; 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..d097f8ebdf --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/chrome-test.js @@ -0,0 +1,40 @@ +// 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"], + + rules: { + "mozilla/import-content-task-globals": "error", + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + }, +}; 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..af71955a4b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/mochitest-test.js @@ -0,0 +1,39 @@ +// 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, + XPCNativeWrapper: 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"], + + rules: { + "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", + "no-shadow": "error", + }, +}; 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..72023fd0b9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js @@ -0,0 +1,305 @@ +/* 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/ + */ +module.exports = { + env: { + browser: true, + es2021: true, + "mozilla/privileged": true, + }, + + extends: ["eslint:recommended", "plugin:prettier/recommended"], + + globals: { + Cc: false, + // Specific to Firefox (Chrome code only). + ChromeUtils: false, + Ci: false, + Components: false, + Cr: false, + Cu: false, + Debugger: false, + InstallTrigger: false, + // Specific to Firefox + // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/InternalError + InternalError: true, + Intl: false, + SharedArrayBuffer: false, + StopIteration: false, + dump: true, + // Override the "browser" env definition of "location" to allow writing as it + // is a writeable property. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1509270#c1 for more information. + location: true, + openDialog: false, + saveStack: false, + sizeToContent: false, + // Specific to Firefox + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/uneval + uneval: false, + }, + + overrides: [ + { + // We don't have the general browser environment for jsm files, but we do + // have our own special environments for them. + env: { + browser: false, + "mozilla/jsm": true, + }, + files: ["**/*.jsm", "**/*.jsm.js"], + rules: { + "mozilla/mark-exported-symbols-as-used": "error", + // TODO: Bug 1575506 turn `builtinGlobals` on here. + // We can enable builtinGlobals for jsms due to their scopes. + "no-redeclare": ["error", { builtinGlobals: false }], + // JSM modules 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", + }, + ], + }, + }, + ], + + parserOptions: { + ecmaVersion: 12, + }, + + // When adding items to this file please check for effects on sub-directories. + plugins: ["html", "fetch-options", "no-unsanitized"], + + // When adding items to this file please check for effects on all of toolkit + // and browser + rules: { + // 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-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-chromeutils-import-null": "error", + "mozilla/reject-importGlobalProperties": ["error", "allownonwebidl"], + "mozilla/rejects-requires-await": "error", + "mozilla/use-cc-etc": "error", + "mozilla/use-chromeutils-generateqi": "error", + "mozilla/use-chromeutils-import": "error", + "mozilla/use-default-preference-values": "error", + "mozilla/use-includes-instead-of-indexOf": "error", + "mozilla/use-ownerGlobal": "error", + "mozilla/use-returnValue": "error", + "mozilla/use-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", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow constant expressions in conditions + "no-constant-condition": "off", + + // No duplicate keys in object declarations + "no-dupe-keys": "error", + + // 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 assignments to native objects or read-only global variables + "no-global-assign": "error", + + // 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", + + // Use {} instead of new Object() + "no-new-object": "error", + + // Disallow use of new wrappers + "no-new-wrappers": "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"], + + // Disallows unnecessary `return await ...`. + "no-return-await": "error", + + // No unnecessary comparisons + "no-self-compare": "error", + + // No comma sequenced statements + "no-sequences": "error", + + // No declaring variables from an outer scope + // "no-shadow": "error", + + // No declaring variables that hide things like arguments + "no-shadow-restricted-names": "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", + + // No using with + "no-with": "error", + + // Require object-literal shorthand with ES6 method syntax + "object-shorthand": ["error", "always", { avoidQuotes: true }], + + // This generates too many false positives that are not easy to work around, + // and false positives seem to be inherent in the rule. + "require-atomic-updates": "off", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Require generator functions to contain yield + "require-yield": "off", + }, + + // To avoid bad interactions of the html plugin with the xml preprocessor in + // eslint-plugin-mozilla, we turn off processing of the html plugin for .xml + // files. + settings: { + "html/xml-extensions": [".xhtml"], + }, +}; 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..4cff7fcdc3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/xpcshell-test.js @@ -0,0 +1,35 @@ +// Parent config file for all xpcshell files. +"use strict"; + +module.exports = { + env: { + "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", + }, + ], + }, + }, + ], + + 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", + }, +}; 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..76e03f2d49 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js @@ -0,0 +1,108 @@ +/** + * @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/places/content/editBookmark.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 }, +]; + +// 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", + "places-tree.js": "browser/components/places/content/places-tree.js", + "places-menupopup.js": + "browser/components/places/content/places-menupopup.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/", "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-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..5f3cb199af --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js @@ -0,0 +1,43 @@ +/** + * @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: { + addMessageListener: false, + addWeakMessageListener: false, + atob: false, + btoa: false, + chromeOuterWindowID: false, + content: false, + docShell: false, + processMessageManager: false, + removeMessageListener: false, + removeWeakMessageListener: false, + sendAsyncMessage: false, + sendSyncMessage: false, + tabEventTarget: false, + RPMGetAppBuildID: false, + RPMGetInnerMostURI: false, + RPMGetIntPref: false, + RPMGetStringPref: false, + RPMGetBoolPref: false, + RPMSetBoolPref: false, + RPMPrefIsLocked: false, + RPMGetFormatURLPref: false, + RPMIsWindowPrivate: false, + RPMSendAsyncMessage: false, + RPMSendQuery: false, + RPMAddMessageListener: false, + RPMRecordTelemetryEvent: false, + RPMAddToHistogram: false, + RPMRemoveMessageListener: false, + RPMGetHttpResponseHeader: 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..858494b8d4 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/jsm.js @@ -0,0 +1,24 @@ +/** + * @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/ed212c79cfe86357e9a5740082b9364e7f6e526f/js/xpconnect/loader/mozJSComponentLoader.cpp#134-140 + atob: false, + btoa: false, + debug: 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..12d9e0096d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js @@ -0,0 +1,776 @@ +/** + * @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: { + // 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, + AudioDestinationNode: 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, + 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, + 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, + 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, + 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, + FrameLoader: false, + GainNode: false, + Gamepad: false, + GamepadAxisMoveEvent: false, + GamepadButton: false, + GamepadButtonEvent: false, + GamepadEvent: false, + GamepadHapticActuator: false, + GamepadPose: false, + GamepadServiceTest: 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, + HiddenPluginEvent: false, + History: false, + IDBCursor: false, + IDBCursorWithValue: false, + IDBDatabase: false, + IDBFactory: false, + IDBFileHandle: false, + IDBFileRequest: false, + IDBIndex: false, + IDBKeyRange: false, + IDBLocaleAwareKeyRange: 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, + 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, + 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, + PlacesBookmarkRemoved: false, + PlacesEvent: false, + PlacesHistoryCleared: false, + PlacesObservers: false, + PlacesRanking: false, + PlacesVisit: 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, + 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, + 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, + 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, + 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, + 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, + mozRTCIceCandidate: false, + mozRTCPeerConnection: false, + mozRTCSessionDescription: 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/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..570119b6e9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/xpcshell.js @@ -0,0 +1,43 @@ +/** + * @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"; + +var { getScriptGlobals } = require("./utils"); + +const extraGlobals = [ + // Assert.jsm globals. + "setReporter", + "report", + "ok", + "equal", + "notEqual", + "deepEqual", + "notDeepEqual", + "strictEqual", + "notStrictEqual", + "throws", + "rejects", + "greater", + "greaterOrEqual", + "less", + "lessOrEqual", + // TestingFunctions.cpp globals + "allocationMarker", + "byteSize", + "gc", + "gczeal", +]; + +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..cb1b0d5f66 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js @@ -0,0 +1,359 @@ +/** + * @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"); + +/** + * 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 xul 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 = {}; + +/** + * 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) { + this.path = filePath; + this.dirname = path.dirname(this.path); +} + +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; + } + + let filePath = match[1].trim(); + + if (!path.isAbsolute(filePath)) { + filePath = path.resolve(this.dirname, filePath); + } + globals = globals.concat(module.exports.getGlobalsForFile(filePath)); + } + + return globals; + }, + + ExpressionStatement(node, parents, globalScope) { + let isGlobal = helpers.getIsGlobalScope(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 = helpers.convertThisAssignmentExpressionToGlobals( + node, + isGlobal + ); + } else if (node.expression.type === "CallExpression") { + globals = helpers.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 (globalScope && globalScope.set.get("importScripts")) { + let workerDetails = helpers.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 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; + } + let scriptName; + if (script.src.includes("http:")) { + // We don't handle this currently as the paths are complex to match. + } else if (script.src.includes("chrome")) { + // This is one way of referencing test files. + script.src = script.src.replace("chrome://mochikit/content/", "/"); + scriptName = path.join( + helpers.rootDir, + "testing", + "mochitest", + script.src + ); + } else if (script.src.includes("SimpleTest")) { + // This is another way of referencing test files... + scriptName = path.join( + helpers.rootDir, + "testing", + "mochitest", + script.src + ); + } else if (script.src.startsWith("/tests/")) { + scriptName = path.join(helpers.rootDir, script.src.substring(7)); + } else { + // Fallback to hoping this is a relative path. + scriptName = path.join(dir, script.src); + } + if (scriptName && fs.existsSync(scriptName)) { + globals.push( + ...module.exports.getGlobalsForFile(scriptName, { + ecmaVersion: helpers.getECMAVersion(), + sourceType: script.type, + }) + ); + } + } + + 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..a3a5fcf8e7 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js @@ -0,0 +1,881 @@ +/** + * @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("babel-eslint"); +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 ini = require("multi-ini"); +const recommendedConfig = require("./configs/recommended"); + +var gModules = null; +var gRootDir = null; +var directoryManifests = new Map(); + +const callExpressionDefinitions = [ + /^loader\.lazyGetter\(this, "(\w+)"/, + /^loader\.lazyImporter\(this, "(\w+)"/, + /^loader\.lazyServiceGetter\(this, "(\w+)"/, + /^loader\.lazyRequireGetter\(this, "(\w+)"/, + /^XPCOMUtils\.defineLazyGetter\(this, "(\w+)"/, + /^XPCOMUtils\.defineLazyModuleGetter\(this, "(\w+)"/, + /^ChromeUtils\.defineModuleGetter\(this, "(\w+)"/, + /^XPCOMUtils\.defineLazyPreferenceGetter\(this, "(\w+)"/, + /^XPCOMUtils\.defineLazyProxy\(this, "(\w+)"/, + /^XPCOMUtils\.defineLazyScriptGetter\(this, "(\w+)"/, + /^XPCOMUtils\.defineLazyServiceGetter\(this, "(\w+)"/, + /^XPCOMUtils\.defineConstant\(this, "(\w+)"/, + /^DevToolsUtils\.defineLazyModuleGetter\(this, "(\w+)"/, + /^DevToolsUtils\.defineLazyGetter\(this, "(\w+)"/, + /^Object\.defineProperty\(this, "(\w+)"/, + /^Reflect\.defineProperty\(this, "(\w+)"/, + /^this\.__defineGetter__\("(\w+)"/, +]; + +const callExpressionMultiDefinitions = [ + "XPCOMUtils.defineLazyGlobalGetters(this,", + "XPCOMUtils.defineLazyModuleGetters(this,", + "XPCOMUtils.defineLazyServiceGetters(this,", + "loader.lazyRequireGetter(this,", +]; + +const imports = [ + /^(?:Cu|Components\.utils|ChromeUtils)\.import\(".*\/((.*?)\.jsm?)", this\)/, +]; + +const workerImportFilenameMatch = /(.*\/)*((.*?)\.jsm?)/; + +module.exports = { + get iniParser() { + if (!this._iniParser) { + this._iniParser = new ini.Parser(); + } + return this._iniParser; + }, + + get modulesGlobalData() { + if (!gModules) { + if (this.isMozillaCentralBased()) { + gModules = require(path.join( + this.rootDir, + "tools", + "lint", + "eslint", + "modules.json" + )); + } else { + gModules = require("./modules.json"); + } + } + + return gModules; + }, + + get servicesData() { + return require("./services.json"); + }, + + /** + * 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(). + * + * @return {Object} + * Returns an object containing `ast`, `scopeManager` and + * `visitorKeys` + */ + parseCode(sourceText, astOptions = {}) { + // Use a permissive config file to allow parsing of anything that Espree + // can parse. + let config = { ...this.getPermissiveConfig(), ...astOptions }; + + let parseResult = + "parseForESLint" in parser + ? parser.parseForESLint(sourceText, config) + : { ast: parser.parse(sourceText, config) }; + + let visitorKeys = parseResult.visitorKeys || defaultVisitorKeys; + visitorKeys.ExperimentalRestProperty = visitorKeys.RestElement; + visitorKeys.ExperimentalSpreadProperty = visitorKeys.SpreadElement; + + return { + ast: parseResult.ast, + scopeManager: parseResult.scopeManager || analyze(parseResult.ast), + 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) + ); + 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 == 0) { + throw new Error("Left more nodes than entered."); + } + parents.pop(); + }, + + keys: visitorKeys, + }); + if (parents.length) { + throw new Error("Entered more nodes than left."); + } + }, + + /** + * 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. + */ + convertWorkerExpressionToGlobals(node, isGlobal, dirname) { + var getGlobalsForFile = require("./globals").getGlobalsForFile; + + let globalModules = this.modulesGlobalData; + + 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 = getGlobalsForFile(filePath); + results = results.concat(additionalGlobals); + } + } else if (match[2] in globalModules) { + results = results.concat( + globalModules[match[2]].map(name => { + return { name, writable: true }; + }) + ); + } else { + results.push({ name: match[3], writable: true, explicit: true }); + } + } + } + } + + return results; + }, + + /** + * 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. + */ + 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 CallExpressions that look like module imports + * into global variable definitions, using modules.json data if appropriate. + * + * @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. + */ + 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 = this.getASTSource(node); + } catch (e) { + return []; + } + + for (let reg of imports) { + let match = source.match(reg); + if (match) { + // The two argument form is only acceptable in the global scope + if (node.expression.arguments.length > 1 && !isGlobal) { + return []; + } + + let globalModules = this.modulesGlobalData; + + if (match[1] in globalModules) { + // XXX We mark as explicit when there is only one exported symbol from + // the module. For now this avoids no-unused-vars complaining in the + // cases where we import everything from a module but only use one + // of them. + let explicit = globalModules[match[1]].length == 1; + return globalModules[match[1]].map(name => ({ + name, + writable: true, + explicit, + })); + } + + return [{ name: match[2], writable: true, explicit: true }]; + } + } + + // 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 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 []; + }, + + /** + * 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({ node, name: { name } }); + 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. + * + * @return {Object} + * Espree compatible permissive config. + */ + getPermissiveConfig() { + 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 a node is a function. + * + * @param {Object} node + * The AST node to check + * + * @return {Boolean} + * True or false + */ + getIsFunctionNode(node) { + switch (node.type) { + case "ArrowFunctionExpression": + case "FunctionDeclaration": + case "FunctionExpression": + return true; + } + return false; + }, + + /** + * Check whether the context is the global scope. + * + * @param {Array} ancestors + * The parents of the current node. + * + * @return {Boolean} + * True or false + */ + getIsGlobalScope(ancestors) { + for (let parent of ancestors) { + if (this.getIsFunctionNode(parent)) { + 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(".ini")) { + continue; + } + + try { + let manifest = this.iniParser.parse( + fs.readFileSync(path.join(dir, name), "utf8").split("\n") + ); + manifests.push({ + file: path.join(dir, name), + manifest, + }); + } catch (e) {} + } + + 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); + }, + + /** + * 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.callee.property.name == "lazyImporter") && + node.arguments.length >= 3 && + node.arguments[2].type == "Literal" + ) { + return node.arguments[2].value; + } + 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..c05cb9a089 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js @@ -0,0 +1,72 @@ +/** + * @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"), + "xpcshell-test": require("../lib/configs/xpcshell-test"), + }, + environments: { + "browser-window": require("../lib/environments/browser-window.js"), + "chrome-worker": require("../lib/environments/chrome-worker.js"), + "frame-script": require("../lib/environments/frame-script.js"), + jsm: require("../lib/environments/jsm.js"), + simpletest: require("../lib/environments/simpletest.js"), + privileged: require("../lib/environments/privileged.js"), + xpcshell: require("../lib/environments/xpcshell.js"), + }, + processors: { + ".xul": require("../lib/processors/xul"), + }, + 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"), + "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-arbitrary-setTimeout": require("../lib/rules/no-arbitrary-setTimeout"), + "no-compare-against-boolean-literals": require("../lib/rules/no-compare-against-boolean-literals"), + "no-define-cc-etc": require("../lib/rules/no-define-cc-etc"), + "no-task": require("../lib/rules/no-task"), + "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-chromeutils-import-null": require("../lib/rules/reject-chromeutils-import-null"), + "reject-importGlobalProperties": require("../lib/rules/reject-importGlobalProperties"), + "reject-relative-requires": require("../lib/rules/reject-relative-requires"), + "reject-some-requires": require("../lib/rules/reject-some-requires"), + "rejects-requires-await": require("../lib/rules/rejects-requires-await"), + "use-cc-etc": require("../lib/rules/use-cc-etc"), + "use-chromeutils-generateqi": require("../lib/rules/use-chromeutils-generateqi"), + "use-chromeutils-import": require("../lib/rules/use-chromeutils-import"), + "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-returnValue": require("../lib/rules/use-returnValue"), + "use-services": require("../lib/rules/use-services"), + "var-only-at-top-level": require("../lib/rules/var-only-at-top-level"), + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js new file mode 100644 index 0000000000..0f56debe1b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js @@ -0,0 +1,120 @@ +/* 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"; + +let sax = require("sax"); + +// Converts sax's error message to something that eslint will understand +let errorRegex = /(.*)\nLine: (\d+)\nColumn: (\d+)\nChar: (.*)/; +function parseError(err) { + let matches = err.message.match(errorRegex); + if (!matches) { + return null; + } + + return { + fatal: true, + message: matches[1], + line: parseInt(matches[2]) + 1, + column: parseInt(matches[3]), + }; +} + +let entityRegex = /&[\w][\w-\.]*;/g; + +// A simple sax listener that generates a tree of element information +function XMLParser(text) { + // Non-strict allows us to ignore many errors from entities and + // preprocessing at the expense of failing to report some XML errors. + // Unfortunately it also throws away the case of tagnames and attributes + let parser = sax.parser(false, { + lowercase: true, + xmlns: true, + }); + + parser.onerror = function(err) { + this.lastError = parseError(err); + }; + + this.parser = parser; + parser.onopentag = this.onOpenTag.bind(this); + parser.onclosetag = this.onCloseTag.bind(this); + parser.ontext = this.onText.bind(this); + parser.onopencdata = this.onOpenCDATA.bind(this); + parser.oncdata = this.onCDATA.bind(this); + parser.oncomment = this.onComment.bind(this); + + this.document = { + local: "#document", + uri: null, + children: [], + comments: [], + }; + this._currentNode = this.document; + + parser.write(text); +} + +XMLParser.prototype = { + parser: null, + + lastError: null, + + onOpenTag(tag) { + let node = { + parentNode: this._currentNode, + local: tag.local, + namespace: tag.uri, + attributes: {}, + children: [], + comments: [], + textContent: "", + textLine: this.parser.line, + textColumn: this.parser.column, + textEndLine: this.parser.line, + }; + + for (let attr of Object.keys(tag.attributes)) { + if (tag.attributes[attr].uri == "") { + node.attributes[attr] = tag.attributes[attr].value; + } + } + + this._currentNode.children.push(node); + this._currentNode = node; + }, + + onCloseTag(tagname) { + this._currentNode.textEndLine = this.parser.line; + this._currentNode = this._currentNode.parentNode; + }, + + addText(text) { + this._currentNode.textContent += text; + }, + + onText(text) { + // Replace entities with some valid JS token. + this.addText(text.replace(entityRegex, "null")); + }, + + onOpenCDATA() { + // Turn the CDATA opening tag into whitespace for indent alignment + this.addText(" ".repeat("<![CDATA[".length)); + }, + + onCDATA(text) { + this.addText(text); + }, + + onComment(text) { + this._currentNode.comments.push(text); + }, +}; + +module.exports = { + XMLParser, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js new file mode 100644 index 0000000000..c486b38e06 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js @@ -0,0 +1,262 @@ +/** + * @fileoverview Converts inline attributes from XUL into JS + * 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"; + +let path = require("path"); +let fs = require("fs"); + +let XMLParser = require("./processor-helpers").XMLParser; + +// Stores any XML parse error +let xmlParseError = null; + +// Stores the lines of JS code generated from the XUL. +let scriptLines = []; +// Stores a map from the synthetic line number to the real line number +// and column offset. +let lineMap = []; + +let includedRanges = []; + +// Deal with ifdefs. This is the state we pretend to have: +const kIfdefStateForLinting = { + MOZ_UPDATER: true, + XP_WIN: true, + MOZ_BUILD_APP_IS_BROWSER: true, + MOZ_SERVICES_SYNC: true, + MOZ_DATA_REPORTING: true, + MOZ_TELEMETRY_REPORTING: true, + MOZ_CRASHREPORTER: true, + MOZ_NORMANDY: true, + MOZ_MAINTENANCE_SERVICE: true, + HAVE_SHELL_SERVICE: true, + MENUBAR_CAN_AUTOHIDE: true, + MOZILLA_OFFICIAL: true, +}; + +// Anything not in the above list is assumed false. +function dealWithIfdefs(text, filename) { + function stripIfdefsFromLines(input, innerFile) { + let outputLines = []; + let inSkippingIfdef = [false]; + for (let i = 0; i < input.length; i++) { + let line = input[i]; + let shouldSkip = inSkippingIfdef.some(x => x); + if (!line.startsWith("#")) { + outputLines.push(shouldSkip ? "" : line); + } else { + if ( + line.startsWith("# ") || + line.startsWith("#filter") || + line == "#" || + line.startsWith("#define") + ) { + outputLines.push(""); + continue; + } + // if this isn't just a comment (which we skip), figure out what to do: + let term = ""; + let negate = false; + if (line.startsWith("#ifdef")) { + term = line.match(/^#ifdef *([A-Z_]+)/); + } else if (line.startsWith("#ifndef")) { + term = line.match(/^#ifndef *([A-Z_]+)/); + negate = true; + } else if (line.startsWith("#if ")) { + term = line.match(/^defined\(([A-Z_]+)\)/); + } else if (line.startsWith("#elifdef")) { + // Replace the old one: + inSkippingIfdef.pop(); + term = line.match(/^#elifdef *([A-Z_]+)/); + } else if (line.startsWith("#else")) { + // Switch the last one around: + let old = inSkippingIfdef.pop(); + inSkippingIfdef.push(!old); + outputLines.push(""); + } else if (line.startsWith("#endif")) { + inSkippingIfdef.pop(); + outputLines.push(""); + } else if (line.startsWith("#expand")) { + // Just strip expansion instructions + outputLines.push(line.substring("#expand ".length)); + } else if (line.startsWith("#include")) { + // Uh oh. + if (!shouldSkip) { + let fileToInclude = line.substr("#include ".length).trim(); + let subpath = path.join(path.dirname(innerFile), fileToInclude); + let contents = fs.readFileSync(subpath, { encoding: "utf-8" }); + contents = contents.split(/\n/); + // Recurse: + contents = stripIfdefsFromLines(contents, subpath); + if (innerFile == filename) { + includedRanges.push({ + start: i, + end: i + contents.length, + filename: subpath, + }); + } + // And insert the resulting lines: + input = input.slice(0, i).concat(contents, input.slice(i + 1)); + // Re-process this line now that we've spliced things in. + i--; + } else { + outputLines.push(""); + } + } else { + throw new Error("Unknown preprocessor directive: " + line); + } + + if (term) { + // We always want the first capturing subgroup: + term = term && term[1]; + if (!negate) { + inSkippingIfdef.push(!kIfdefStateForLinting[term]); + } else { + inSkippingIfdef.push(kIfdefStateForLinting[term]); + } + outputLines.push(""); + // Now just continue; we'll include lines depending on the state of `inSkippingIfdef`. + } + } + } + return outputLines; + } + let lines = text.split(/\n/); + return stripIfdefsFromLines(lines, filename).join("\n"); +} + +function addSyntheticLine(line, linePos, addDisableLine) { + lineMap[scriptLines.length] = { line: linePos }; + scriptLines.push(line + (addDisableLine ? "" : " // eslint-disable-line")); +} + +function recursiveExpand(node) { + for (let [attr, value] of Object.entries(node.attributes)) { + if (attr.startsWith("on")) { + if (attr == "oncommand" && value == ";") { + // Ignore these, see bug 371900 for why people might do this. + continue; + } + // Ignore dashes in the tag name + let nodeDesc = node.local.replace(/-/g, ""); + if (node.attributes.id) { + nodeDesc += "_" + node.attributes.id.replace(/[^a-z]/gi, "_"); + } + if (node.attributes.class) { + nodeDesc += "_" + node.attributes.class.replace(/[^a-z]/gi, "_"); + } + addSyntheticLine("function " + nodeDesc + "(event) {", node.textLine); + let processedLines = value.split(/\r?\n/); + let addlLine = 0; + for (let line of processedLines) { + line = line.replace(/^\s*/, ""); + lineMap[scriptLines.length] = { + // Unfortunately, we only get a line number for the <tag> finishing, + // not for individual attributes. + line: node.textLine + addlLine, + }; + scriptLines.push(line); + addlLine++; + } + addSyntheticLine("}", node.textLine + processedLines.length - 1); + } + } + for (let kid of node.children) { + recursiveExpand(kid); + } +} + +module.exports = { + preprocess(text, filename) { + if (filename.includes(".inc")) { + return []; + } + xmlParseError = null; + // The following rules are annoying in XUL. + // Indent because in multiline attributes it's impossible to understand for the XML parser. + // Semicolons because those shouldn't be required for inline event handlers. + // Quotes because we use doublequotes for attributes so using single quotes + // for strings inside them makes sense. + // No-undef because it's a bunch of work to teach this code how to read + // scripts and get globals from them (though ideally we should do that at some point). + scriptLines = [ + "/* eslint-disable indent */", + "/* eslint-disable indent-legacy */", + "/* eslint-disable semi */", + "/* eslint-disable quotes */", + "/* eslint-disable no-undef */", + ]; + lineMap = scriptLines.map(() => ({ line: 0 })); + includedRanges = []; + // Do C-style preprocessing first: + text = dealWithIfdefs(text, filename); + + let xp = new XMLParser(text); + if (xp.lastError) { + xmlParseError = xp.lastError; + } + let doc = xp.document; + if (!doc) { + return []; + } + let node = doc; + for (let kid of node.children) { + recursiveExpand(kid); + } + + let scriptText = scriptLines.join("\n") + "\n"; + return [scriptText]; + }, + + postprocess(messages, filename) { + // If there was an XML parse error then just return that + if (xmlParseError) { + return [xmlParseError]; + } + + // For every message from every script block update the line to point to the + // correct place. + let errors = []; + for (let i = 0; i < messages.length; i++) { + for (let message of messages[i]) { + // ESLint indexes lines starting at 1 but our arrays start at 0 + let mapped = lineMap[message.line - 1]; + // Ensure we don't modify this by making a copy. We might need it for another failure. + let target = mapped.line; + let includedRange = includedRanges.find( + r => target >= r.start && target <= r.end + ); + // If this came from an #included file, indicate this in the message + if (includedRange) { + target = includedRange.start; + message.message += + " (from included file " + + path.basename(includedRange.filename) + + ")"; + } + // Compensate for line numbers shifting as a result of #include: + let includeBallooning = includedRanges + .filter(r => target >= r.end) + .map(r => r.end - r.start) + .reduce((acc, next) => acc + next, 0); + target -= includeBallooning; + // Add back the 1 to go back to 1-indexing. + message.line = target + 1; + + // We never have column information, unfortunately. + message.column = NaN; + + errors.push(message); + } + } + + return errors; + }, +}; 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..4f06390189 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js @@ -0,0 +1,67 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = { + meta: { + docs: { + description: "disallow use of Date for timing measurements", + category: "Best Practices", + }, + schema: [], + }, + + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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, + "use performance.now() instead of Date.now() for timing " + + "measurements" + ); + }, + + NewExpression(node) { + let callee = node.callee; + if ( + callee.type !== "Identifier" || + callee.name !== "Date" || + node.arguments.length > 0 + ) { + return; + } + + context.report( + node, + "use performance.now() instead of new Date() for timing " + + "measurements" + ); + }, + }; + }, +}; 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..99d0ff5027 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js @@ -0,0 +1,64 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var helpers = require("../helpers"); + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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, + "use element.remove() instead of " + + "element.parentNode.removeChild(element)" + ); + } + + 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, + "use element.firstChild.remove() instead of " + + "element.removeChild(element.firstChild)" + ); + } + }, + }; +}; 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..b6fb25139a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js @@ -0,0 +1,147 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + 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; + } + + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + 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( + listener.node, + "No corresponding '{{functionName}}({{type}})' was found.", + { + 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..f714b31cda --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js @@ -0,0 +1,119 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + 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 + ); + } + + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + 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( + observer.node, + "No corresponding 'removeObserver(\"{{observable}}\")' was found.", + { + 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..af51dcea1f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js @@ -0,0 +1,49 @@ +/** + * @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: { + messages: { + consistentIfBracing: "Bracing of if..else bodies should be consistent.", + }, + }, + + 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..bcbdb3ec54 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js @@ -0,0 +1,49 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var path = require("path"); +var helpers = require("../helpers"); +var browserWindowEnv = require("../environments/browser-window"); + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + 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 && + 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..b8545b45f8 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js @@ -0,0 +1,84 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var helpers = require("../helpers"); +var frameScriptEnv = require("../environments/frame-script"); + +// The global environment of SpecialPowers.spawn tasks is +// controlled by the Sandbox environment created by +// SpecialPowersSandbox.jsm. This list should be kept in sync with +// that module. +var sandboxGlobals = [ + "Assert", + "Blob", + "BrowsingContext", + "ChromeUtils", + "ContentTaskUtils", + "EventUtils", + "Services", + "TextDecoder", + "TextEncoder", + "URL", + "assert", + "info", + "is", + "isnot", + "ok", + "todo", + "todo_is", +]; + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + return { + "CallExpression[callee.object.name='ContentTask'][callee.property.name='spawn']": function( + node + ) { + for (let global in frameScriptEnv.globals) { + helpers.addVarToScope( + global, + context.getScope(), + frameScriptEnv.globals[global] + ); + } + }, + "CallExpression[callee.object.name='SpecialPowers'][callee.property.name='spawn']": function( + node + ) { + let globals = [...sandboxGlobals, "SpecialPowers", "content", "docShell"]; + for (let global of globals) { + helpers.addVarToScope(global, context.getScope(), false); + } + }, + "CallExpression[callee.object.name='SpecialPowers'][callee.property.name='spawnChrome']": function( + node + ) { + let globals = [ + ...sandboxGlobals, + "browsingContext", + "windowGlobalParent", + ]; + 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..053a9e702f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js @@ -0,0 +1,15 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = 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..cffd7a4c8e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js @@ -0,0 +1,47 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var fs = require("fs"); +var helpers = require("../helpers"); +var globals = require("../globals"); + +module.exports = function(context) { + function importHead(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()); + } + + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + return { + Program(node) { + let heads = helpers.getTestHeadFiles(context); + for (let head of heads) { + importHead(head, node); + } + }, + }; +}; 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..c535a54a16 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js @@ -0,0 +1,83 @@ +/** + * @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, + message: "Unexpected assignment of non-Array to EXPORTED_SYMBOLS", + }); + return; + } + + for (let element of expression.elements) { + context.markVariableAsUsed(element.value); + } + // Also mark EXPORTED_SYMBOLS as used. + context.markVariableAsUsed("EXPORTED_SYMBOLS"); +} + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // 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() { + return !context.getScope().upper; + } + + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + return { + AssignmentExpression(node, parents) { + if ( + node.operator === "=" && + node.left.type === "MemberExpression" && + node.left.object.type === "ThisExpression" && + node.left.property.name === "EXPORTED_SYMBOLS" && + isGlobalScope() + ) { + markArrayElementsAsUsed(context, node, node.right); + } + }, + + VariableDeclaration(node, parents) { + if (!isGlobalScope()) { + 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, + message: + "EXPORTED_SYMBOLS cannot be declared via `let`. Use `var` or `this.EXPORTED_SYMBOLS =`", + }); + } + + 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..0c0dcfdbe5 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js @@ -0,0 +1,37 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var helpers = require("../helpers"); + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + return { + Program() { + let testType = helpers.getTestType(context); + if (testType == "browser") { + context.markVariableAsUsed("test"); + return; + } + + if (testType == "xpcshell") { + context.markVariableAsUsed("run_test"); + } + }, + }; +}; 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..af7fe06336 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js @@ -0,0 +1,56 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + 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); + } + + 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( + param, + "Parameter '{{name}}' uses Hungarian Notation, " + + "consider using '{{suggestion}}' instead.", + errorObj + ); + } + } + } + + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + return { + FunctionDeclaration: checkFunction, + ArrowFunctionExpression: checkFunction, + FunctionExpression: checkFunction, + }; +}; 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..be0b5c5ffb --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js @@ -0,0 +1,70 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var helpers = require("../helpers"); +var testTypes = new Set(["browser", "xpcshell"]); + +module.exports = { + meta: { + docs: { + description: "disallow setTimeout with non-zero values in tests", + category: "Best Practices", + }, + schema: [], + }, + + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + create(context) { + return { + CallExpression(node) { + // 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; + } + + 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, + "listen for events instead of setTimeout() " + + "with arbitrary delay" + ); + } + }, + }; + }, +}; 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..bb48563fff --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js @@ -0,0 +1,34 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + BinaryExpression(node) { + if ( + ["==", "!="].includes(node.operator) && + (["true", "false"].includes(node.left.raw) || + ["true", "false"].includes(node.right.raw)) + ) { + context.report( + node, + "Don't compare for inexact equality against boolean literals" + ); + } + }, + }; +}; 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..0b441e4712 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js @@ -0,0 +1,49 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +const componentsBlacklist = ["Cc", "Ci", "Cr", "Cu"]; + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + VariableDeclarator(node) { + if ( + node.id.type == "Identifier" && + componentsBlacklist.includes(node.id.name) + ) { + context.report( + node, + `${node.id.name} is now defined in global scope, a separate definition is no longer necessary.` + ); + } + + if (node.id.type == "ObjectPattern") { + for (let property of node.id.properties) { + if ( + property.type == "Property" && + componentsBlacklist.includes(property.value.name) + ) { + context.report( + node, + `${property.value.name} is now defined in global scope, a separate definition is no longer necessary.` + ); + } + } + } + }, + }; +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-task.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-task.js new file mode 100644 index 0000000000..2bab6e8742 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-task.js @@ -0,0 +1,33 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type === "MemberExpression" && + callee.object.type === "Identifier" && + callee.object.name === "Task" + ) { + context.report({ node, message: "Task.jsm is deprecated." }); + } + }, + }; +}; 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..21f27ff130 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js @@ -0,0 +1,100 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +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: { + 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", + }, + }, + + 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..4fd0734465 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js @@ -0,0 +1,147 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = { + meta: { + type: "suggestion", + fixable: "code", + }, + create(context) { + function getRangeAfterArgToEnd(argNumber, args) { + let sourceCode = context.getSourceCode(); + return [ + sourceCode.getTokenAfter(args[argNumber]).range[0], + args[args.length - 1].range[1], + ]; + } + + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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)); + }, + message: `${name}'s third parameter can be omitted when it's false.`, + }); + } + + if (name === "clearUserPref" && args.length > 1) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + message: `${name} takes only 1 parameter.`, + }); + } + + if (name === "removeObserver" && args.length === 3 && isBool(args[2])) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(1, args)); + }, + message: "removeObserver only takes 2 parameters.", + }); + } + + if (name === "appendElement" && args.length === 2 && isFalse(args[1])) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + message: `${name}'s second parameter can be omitted when it's false.`, + }); + } + + if ( + name === "notifyObservers" && + args.length === 3 && + isFalsy(args[2]) + ) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(1, args)); + }, + message: `${name}'s third parameter can be omitted.`, + }); + } + + if ( + name === "getComputedStyle" && + args.length === 2 && + isFalsy(args[1]) + ) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + message: "getComputedStyle's second parameter can be omitted.", + }); + } + + 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) + ); + }, + message: "newURI's last parameters are optional.", + }); + } + }, + }; + }, +}; 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..e58edfcb98 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js @@ -0,0 +1,64 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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( + call, + "use {once: true} instead of removeEventListener as " + + "the first instruction of the listener" + ); + } + }, + }; +}; 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..d16757c347 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js @@ -0,0 +1,72 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = { + meta: { + type: "suggestion", + fixable: "code", + }, + 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 && 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, + ]); + }, + message: + "Useless run_test function - only contains run_next_test; whole function can be removed", + }); + } + }, + }; + }, +}; 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..543d07b765 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js @@ -0,0 +1,130 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- +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: { + type: "suggestion", + fixable: "code", + }, + create(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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 && + node.left.property.name == "length") || + (node.left.type == "Literal" && + node.left.value == 0 && + node.right.property && + 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); + }, + message: "Prefer boolean length check", + }); + } else { + context.report({ + node, + fix: fixer => { + let generateExpression = funcForBooleanLength( + context, + node, + false + ); + return fixer.replaceText(node, generateExpression); + }, + message: "Prefer boolean length check", + }); + } + } + }, + }; + }, +}; 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..45b8c40901 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js @@ -0,0 +1,96 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +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", + category: "Best Practices", + }, + schema: [], + }, + + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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( + callNode, + "prefer to use a single document.l10n.formatValues call instead " + + "of multiple calls to document.l10n.formatValue or document.l10n.formatValues" + ); + } + } + } + + return { + Program: enterBlock, + "Program:exit": exitBlock, + BlockStatement: enterBlock, + "BlockStatement:exit": exitBlock, + + CallExpression(node) { + if (!BlockStack.length) { + context.report(node, "call expression found outside of known block"); + } + + 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-chromeutils-import-null.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-null.js new file mode 100644 index 0000000000..b7fd1593b8 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-null.js @@ -0,0 +1,44 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + CallExpression(node) { + let { callee } = node; + if ( + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "import") && + node.arguments.length >= 2 && + node.arguments[1].type == "Literal" && + node.arguments[1].raw == "null" + ) { + context.report( + node, + "ChromeUtils.import should not be called with (..., null) to " + + "retrieve the JSM global object. Rely on explicit exports instead." + ); + } + }, + }; +}; 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..a2f495ddb7 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js @@ -0,0 +1,62 @@ +/** + * @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 privilegedGlobals = Object.keys( + require("../environments/privileged.js").globals +); + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = { + meta: { + messages: { + unexpectedCall: "Unexpected call to Cu.importGlobalProperties", + unexpectedCallWebIdl: + "Unnecessary call to Cu.importGlobalProperties (webidl names are automatically imported)", + }, + schema: [ + { + // XXX Better name? + 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; see bug 1230369. + 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: "unexpectedCallWebIdl" }); + } + } + } else { + context.report({ node, messageId: "unexpectedCall" }); + } + } + }, + }; + }, +}; 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..eb100b13fa --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js @@ -0,0 +1,34 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var helpers = require("../helpers"); + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + const isRelativePath = function(path) { + return path.startsWith("./") || path.startsWith("../"); + }; + + return { + CallExpression(node) { + const path = helpers.getDevToolsRequirePath(node); + if (path && isRelativePath(path)) { + context.report(node, "relative paths are not allowed with require()"); + } + }, + }; +}; 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..ab31069293 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js @@ -0,0 +1,35 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var helpers = require("../helpers"); + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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, `require(${path}) is not allowed`); + } + }, + }; +}; 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..1bc0bcd58d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js @@ -0,0 +1,42 @@ +/** + * @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"; + +module.exports = { + meta: { + messages: { + rejectRequiresAwait: "Assert.rejects needs to be preceded by await.", + }, + }, + + 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..053f379adb --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js @@ -0,0 +1,44 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +const componentsMap = { + classes: "Cc", + interfaces: "Ci", + results: "Cr", + utils: "Cu", +}; + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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, + `Use ${componentsMap[node.property.name]} rather than Components.${ + node.property.name + }` + ); + } + }, + }; +}; 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..6d72f785de --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js @@ -0,0 +1,103 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +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) + ); +} + +const MSG_NO_JS_QUERY_INTERFACE = + "Please use ChromeUtils.generateQI rather than manually creating " + + "JavaScript QueryInterface functions"; + +const MSG_NO_XPCOMUTILS_GENERATEQI = + "Please use ChromeUtils.generateQI instead of XPCOMUtils.generateQI"; + +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: { + fixable: "code", + }, + + create(context) { + return { + CallExpression(node) { + let { callee } = node; + if (isMemberExpression(callee, "XPCOMUtils", "generateQI")) { + context.report({ + node, + message: MSG_NO_XPCOMUTILS_GENERATEQI, + 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, + message: MSG_NO_JS_QUERY_INTERFACE, + fix(fixer) { + return fixer.replaceText(right, funcToGenerateQI(context, right)); + }, + }); + } + }, + + "Property[key.name='QueryInterface'][value.type='FunctionExpression']": function( + node + ) { + context.report({ + node, + message: MSG_NO_JS_QUERY_INTERFACE, + 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..eefff06e11 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js @@ -0,0 +1,89 @@ +/** + * @fileoverview Reject use of Cu.import and XPCOMUtils.defineLazyModuleGetter + * 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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +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: { + schema: [ + { + type: "object", + properties: { + allowCu: { + type: "boolean", + }, + }, + additionalProperties: false, + }, + ], + fixable: "code", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let { allowCu } = context.options[0] || {}; + let { callee } = node; + + // Is the expression starting with `Cu` or `Components.utils`? + if ( + ((!allowCu && isIdentifier(callee.object, "Cu")) || + isMemberExpression(callee.object, "Components", "utils")) && + isIdentifier(callee.property, "import") + ) { + context.report({ + node, + message: "Please use ChromeUtils.import instead of Cu.import", + fix(fixer) { + return fixer.replaceText(callee, "ChromeUtils.import"); + }, + }); + } + + if ( + isMemberExpression(callee, "XPCOMUtils", "defineLazyModuleGetter") && + node.arguments.length < 4 + ) { + context.report({ + node, + message: + "Please use ChromeUtils.defineModuleGetter instead of " + + "XPCOMUtils.defineLazyModuleGetter", + fix(fixer) { + return fixer.replaceText( + callee, + "ChromeUtils.defineModuleGetter" + ); + }, + }); + } + }, + }; + }, +}; 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..7c2ffaf933 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js @@ -0,0 +1,45 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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; + } + + let msg = "provide a default value instead of using a try/catch block"; + context.report(node, msg); + }, + }; +}; 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..d4b3da5f1e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js @@ -0,0 +1,45 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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, "use .includes instead of .indexOf"); + } + }, + }; +}; 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..34b22a1269 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js @@ -0,0 +1,38 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + 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, + "use .ownerGlobal instead of .ownerDocument.defaultView" + ); + }, + }; +}; 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..eb693d0014 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js @@ -0,0 +1,42 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + ExpressionStatement(node) { + if ( + !node.expression || + node.expression.type != "CallExpression" || + !node.expression.callee || + node.expression.callee.type != "MemberExpression" || + !node.expression.callee.property || + node.expression.callee.property.type != "Identifier" || + (node.expression.callee.property.name != "concat" && + node.expression.callee.property.name != "join" && + node.expression.callee.property.name != "slice") + ) { + return; + } + + context.report( + node, + `{Array/String}.${node.expression.callee.property.name} doesn't modify the instance in-place` + ); + }, + }; +}; 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..1235cd0f99 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js @@ -0,0 +1,45 @@ +/** + * @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; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + return { + CallExpression(node) { + if ( + !node.callee || + !node.callee.property || + 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, + `Use Services.${serviceName} rather than getService().` + ); + }, + }; +}; 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..9365982b0c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js @@ -0,0 +1,34 @@ +/** + * @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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var helpers = require("../helpers"); + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + VariableDeclaration(node) { + if (node.kind === "var") { + if (helpers.getIsGlobalScope(context.getAncestors())) { + return; + } + + context.report(node, "Unexpected var, use let or const instead."); + } + }, + }; +}; 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..dd250ee15e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/services.json @@ -0,0 +1,50 @@ +{ + "mozIJSSubScriptLoader": "scriptloader", + "mozILocaleService": "locale", + "mozIMozIntl": "intl", + "mozIStorageService": "storage", + "nsIAppShellService": "appShell", + "nsIAppStartup": "startup", + "nsICacheStorageService": "cache2", + "nsICategoryManager": "catMan", + "nsIClearDataService": "clearData", + "nsIClipboard": "clipboard", + "nsIConsoleService": "console", + "nsICookieManager": "cookies", + "nsIDOMRequestService": "DOMRequest", + "nsIDOMStorageManager": "domStorageManager", + "nsIDirectoryService": "dirsvc", + "nsIDroppedLinkHandler": "droppedLinkHandler", + "nsIEffectiveTLDService": "eTLD", + "nsIEnterprisePolicies": "policies", + "nsIEventListenerService": "els", + "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", + "nsIScriptSecurityManager": "scriptSecurityManager", + "nsISearchService": "search", + "nsISpeculativeConnect": "io", + "nsIStringBundleService": "strings", + "nsISystemInfo": "sysinfo", + "nsITelemetry": "telemetry", + "nsITextToSubURI": "textToSubURI", + "nsIThreadManager": "tm", + "nsIURLFormatter": "urlFormatter", + "nsIVersionComparator": "vc", + "nsIWindowMediator": "wm", + "nsIWindowWatcher": "ww", + "nsIXULAppInfo": "appinfo", + "nsIXULRuntime": "appinfo" +}
\ No newline at end of file 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..e58dfbb57c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/manifest.tt @@ -0,0 +1,10 @@ +[ + { + "algorithm": "sha512", + "visibility": "public", + "filename": "eslint-plugin-mozilla.tar.gz", + "unpack": true, + "digest": "196e87a20b68bcafb4fb7893c829beb3d44144f6985a4194bc41b135618f373732ae8e0604ed0548ae5e79f1c0eede33a802fbf4f12678c59a3615c481923912", + "size": 5272799 + } +]
\ 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..a385b07225 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/package-lock.json @@ -0,0 +1,2045 @@ +{ + "name": "eslint-plugin-mozilla", + "version": "2.9.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.3.tgz", + "integrity": "sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helpers": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.5.tgz", + "integrity": "sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig==", + "requires": { + "@babel/types": "^7.10.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", + "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" + }, + "@babel/helpers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.5.tgz", + "integrity": "sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ==" + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.5.tgz", + "integrity": "sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.10.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", + "@babel/parser": "^7.10.5", + "@babel/types": "^7.10.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.5.tgz", + "integrity": "sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@eslint/eslintrc": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", + "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + } + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, + "ajv": { + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", + "dev": true, + "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.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array.prototype.map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", + "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.4" + } + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "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==", + "dev": true, + "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 + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + }, + "entities": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "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==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.0.tgz", + "integrity": "sha512-qgtVyLZqKd2ZXWnLQA4NtVbOyH56zivOAdBFWE54RFkSZjokzNrcP4Z0eVWsZ+84ByXv+jL9k/wE1ENYe8xRFw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.1.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.0", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.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==", + "dev": true, + "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==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "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==" + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "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==", + "dev": true + }, + "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==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "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==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" + }, + "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 + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "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": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "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": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "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": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "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==" + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "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-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", + "dev": 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-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", + "dev": true + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==", + "dev": true + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "dev": true, + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "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==", + "dev": true + }, + "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": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "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==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.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==", + "dev": true, + "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==", + "dev": 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==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz", + "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.4.2", + "debug": "4.1.1", + "diff": "4.0.2", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "object.assign": "4.1.0", + "promise.allsettled": "1.0.2", + "serialize-javascript": "4.0.0", + "strip-json-comments": "3.0.1", + "supports-color": "7.1.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.0", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.1" + }, + "dependencies": { + "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==", + "dev": 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==", + "dev": true + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "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==" + }, + "multi-ini": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multi-ini/-/multi-ini-2.1.0.tgz", + "integrity": "sha512-ANOg4WIoTXYfqyT2Y6FGb8YQyItatdJvfAZKAfFMWT1+D3vUstocjkUQ82EduAeK69rCR6a9k0fggxRhsFnAyQ==", + "requires": { + "lodash": "^4.0.0" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "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-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": 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==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.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==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "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==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "promise.allsettled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", + "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", + "dev": true, + "requires": { + "array.prototype.map": "^1.0.1", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "iterate-value": "^1.0.0" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "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" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "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==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "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==" + } + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "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==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + }, + "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" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "workerpool": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", + "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz", + "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "decamelize": "^1.2.0", + "flat": "^4.1.0", + "is-plain-obj": "^1.1.0", + "yargs": "^14.2.3" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + } + } +} 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..8edd8684a5 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/package.json @@ -0,0 +1,55 @@ +{ + "name": "eslint-plugin-mozilla", + "version": "2.9.2", + "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": { + "@babel/core": "7.8.3", + "babel-eslint": "10.1.0", + "eslint-scope": "5.1.0", + "eslint-visitor-keys": "1.3.0", + "estraverse": "4.3.0", + "htmlparser2": "3.10.1", + "multi-ini": "2.1.0", + "sax": "1.2.4" + }, + "devDependencies": { + "eslint": "7.8.0", + "mocha": "^8.1.3" + }, + "peerDependencies": { + "eslint-config-prettier": "^6.0.0", + "eslint-plugin-fetch-options": "^0.0.5", + "eslint-plugin-html": "^6.0.0", + "eslint-plugin-no-unsanitized": "^3.1.4", + "eslint-plugin-prettier": "^3.0.1", + "eslint": "^7.8.0", + "prettier": "^1.17.0" + }, + "engines": { + "node": ">=6.9.1" + }, + "scripts": { + "prepack": "node scripts/createExports.js", + "test": "mocha --reporter 'reporters/mozilla-format.js' tests", + "postpublish": "rm -f lib/modules.json 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..885727e8aa --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/reporters/mozilla-format.js @@ -0,0 +1,50 @@ +/* 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 = 0; + + 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++; + // 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 + }` + ); + }); + + runner.on("end", function() { + console.log("INFO | Result summary:"); + console.log(`INFO | Passed: ${passes}`); + console.log(`INFO | Failed: ${failures}`); + console.log("SUITE-END"); + process.exit(failures); + }); +} 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..60d61b0f72 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/scripts/createExports.js @@ -0,0 +1,89 @@ +/** + * @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 modules.json"); + +const modulesFile = path.join(eslintDir, "modules.json"); +const shipModulesFile = path.join( + eslintDir, + "eslint-plugin-mozilla", + "lib", + "modules.json" +); + +fs.writeFileSync(shipModulesFile, fs.readFileSync(modulesFile)); + +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..597961e8cc --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/avoid-Date-timing.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/avoid-Date-timing"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, type, message) { + return { code, errors: [{ message, 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", + "use performance.now() instead of Date.now() for timing measurements" + ), + invalidCode( + "new Date();", + "NewExpression", + "use performance.now() instead of new Date() for timing measurements" + ), + ], +}); 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..e6a146898d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/avoid-removeChild.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/avoid-removeChild"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, message) { + if (!message) { + message = + "use element.remove() instead of " + + "element.parentNode.removeChild(element)"; + } + return { code, errors: [{ message, 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);", + "use element.firstChild.remove() instead of " + + "element.removeChild(element.firstChild)" + ), + ], +}); 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..ca84e3474f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/balanced-listeners.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/balanced-listeners"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function error(code, message) { + return { + code, + errors: [{ message, type: "Identifier" }], + }; +} + +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);", + "No corresponding 'removeEventListener(click)' was found." + ), + + error( + "elt.addEventListener('click', handler, false);" + + "elt.removeEventListener('click', handler, true);", + "No corresponding 'removeEventListener(click)' was found." + ), + + error( + "elt.addEventListener('click', handler, {capture: false});" + + "elt.removeEventListener('click', handler, true);", + "No corresponding 'removeEventListener(click)' was found." + ), + + error( + "elt.addEventListener('click', handler, {capture: true});" + + "elt.removeEventListener('click', handler);", + "No corresponding 'removeEventListener(click)' was found." + ), + + error( + "elt.addEventListener('click', handler, true);" + + "elt.removeEventListener('click', handler);", + "No corresponding 'removeEventListener(click)' was found." + ), + ], +}); 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..e5f31fa969 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/balanced-observers.js @@ -0,0 +1,74 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function error(code, message) { + return { + code, + errors: [{ message, type: "Identifier" }], + }; +} + +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');", + "No corresponding 'removeObserver(\"observable\")' was found." + ), + + error( + // wrong observable name for Services.obs.removeObserver + "Services.obs.addObserver(observer, 'observable');" + + "Services.obs.removeObserver(observer, 'different-observable');", + "No corresponding 'removeObserver(\"observable\")' was found." + ), + + error( + // missing Services.prefs.removeObserver + "Services.prefs.addObserver('preference.name', otherObserver);", + "No corresponding 'removeObserver(\"preference.name\")' was found." + ), + + error( + // wrong observable name for Services.prefs.removeObserver + "Services.prefs.addObserver('preference.name', otherObserver);" + + "Services.prefs.removeObserver('other.preference', otherObserver);", + "No corresponding 'removeObserver(\"preference.name\")' was found." + ), + + error( + // mismatch Services.prefs vs Services.obs + "Services.obs.addObserver(observer, 'observable');" + + "Services.prefs.removeObserver(observer, 'observable');", + "No corresponding 'removeObserver(\"observable\")' was found." + ), + + error( + "Services.prefs.addObserver('preference.name', otherObserver);" + + // mismatch Services.prefs vs Services.obs + "Services.obs.removeObserver('preference.name', otherObserver);", + "No corresponding 'removeObserver(\"preference.name\")' was found." + ), + ], +}); 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..b6b5b849b9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/consistent-if-bracing.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/consistent-if-bracing"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 8 } }); + +// ------------------------------------------------------------------------------ +// 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 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/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..d004650997 --- /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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, type, message) { + return { code, errors: [{ message, 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", + "EXPORTED_SYMBOLS cannot be declared via `let`. Use `var` or `this.EXPORTED_SYMBOLS =`" + ), + invalidCode( + "var EXPORTED_SYMBOLS = 'foo';", + "VariableDeclaration", + "Unexpected assignment of non-Array to EXPORTED_SYMBOLS" + ), + invalidCode( + "this.EXPORTED_SYMBOLS = 'foo';", + "AssignmentExpression", + "Unexpected assignment of non-Array to EXPORTED_SYMBOLS" + ), + ], +}); 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..907b439b3c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-arbitrary-setTimeout.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/no-arbitrary-setTimeout"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function wrapCode(code, filename = "xpcshell/test_foo.js") { + return { code, filename }; +} + +function invalidCode(code) { + let message = + "listen for events instead of setTimeout() with arbitrary delay"; + let obj = wrapCode(code); + obj.errors = [{ message, 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..722e6b5dda --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-compare-against-boolean-literals.js @@ -0,0 +1,61 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError(message) { + return [{ message, type: "BinaryExpression" }]; +} + +const MESSAGE = "Don't compare for inexact equality against boolean literals"; + +ruleTester.run("no-compare-against-boolean-literals", rule, { + valid: [`if (!foo) {}`, `if (!!foo) {}`], + invalid: [ + { + code: `if (foo == true) {}`, + errors: callError(MESSAGE), + }, + { + code: `if (foo != true) {}`, + errors: callError(MESSAGE), + }, + { + code: `if (foo == false) {}`, + errors: callError(MESSAGE), + }, + { + code: `if (foo != false) {}`, + errors: callError(MESSAGE), + }, + { + code: `if (true == foo) {}`, + errors: callError(MESSAGE), + }, + { + code: `if (true != foo) {}`, + errors: callError(MESSAGE), + }, + { + code: `if (false == foo) {}`, + errors: callError(MESSAGE), + }, + { + code: `if (false != foo) {}`, + errors: callError(MESSAGE), + }, + ], +}); 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..735c7f4654 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-define-cc-etc.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-define-cc-etc"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, varNames) { + if (!Array.isArray(varNames)) { + varNames = [varNames]; + } + return { + code, + errors: varNames.map(name => { + return { + message: `${name} is now defined in global scope, a separate definition is no longer necessary.`, + 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-throw-cr-literal.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-throw-cr-literal.js new file mode 100644 index 0000000000..6750213e72 --- /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: 6 } }); + +// ------------------------------------------------------------------------------ +// 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..0c9b12f6eb --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-parameters.js @@ -0,0 +1,147 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError(message) { + return [{ message, 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("clearUserPref takes only 1 parameter."), + }, + { + code: "Services.prefs.clearUserPref('browser.search.custom',\n false);", + output: "Services.prefs.clearUserPref('browser.search.custom');", + errors: callError("clearUserPref takes only 1 parameter."), + }, + { + code: "Services.removeObserver('notification name', {}, false);", + output: "Services.removeObserver('notification name', {});", + errors: callError("removeObserver only takes 2 parameters."), + }, + { + code: "Services.removeObserver('notification name', {}, true);", + output: "Services.removeObserver('notification name', {});", + errors: callError("removeObserver only takes 2 parameters."), + }, + { + code: "Services.io.newURI('http://example.com', null, null);", + output: "Services.io.newURI('http://example.com');", + errors: callError("newURI's last parameters are optional."), + }, + { + code: "Services.io.newURI('http://example.com', 'utf8', null);", + output: "Services.io.newURI('http://example.com', 'utf8');", + errors: callError("newURI's last parameters are optional."), + }, + { + code: "Services.io.newURI('http://example.com', null);", + output: "Services.io.newURI('http://example.com');", + errors: callError("newURI's last parameters are optional."), + }, + { + code: "Services.io.newURI('http://example.com', '', '');", + output: "Services.io.newURI('http://example.com');", + errors: callError("newURI's last parameters are optional."), + }, + { + code: "Services.io.newURI('http://example.com', '');", + output: "Services.io.newURI('http://example.com');", + errors: callError("newURI's last parameters are optional."), + }, + { + code: "elt.addEventListener('click', handler, false);", + output: "elt.addEventListener('click', handler);", + errors: callError( + "addEventListener's third parameter can be omitted when it's false." + ), + }, + { + code: "elt.removeEventListener('click', handler, false);", + output: "elt.removeEventListener('click', handler);", + errors: callError( + "removeEventListener's third parameter can be omitted when it's" + + " false." + ), + }, + { + code: "Services.obs.addObserver(this, 'topic', false);", + output: "Services.obs.addObserver(this, 'topic');", + errors: callError( + "addObserver's third parameter can be omitted when it's false." + ), + }, + { + code: "Services.prefs.addObserver('branch', this, false);", + output: "Services.prefs.addObserver('branch', this);", + errors: callError( + "addObserver's third parameter can be omitted when it's false." + ), + }, + { + code: "array.appendElement(elt, false);", + output: "array.appendElement(elt);", + errors: callError( + "appendElement's second parameter can be omitted when it's false." + ), + }, + { + code: "Services.obs.notifyObservers(obj, 'topic', null);", + output: "Services.obs.notifyObservers(obj, 'topic');", + errors: callError("notifyObservers's third parameter can be omitted."), + }, + { + code: "Services.obs.notifyObservers(obj, 'topic', '');", + output: "Services.obs.notifyObservers(obj, 'topic');", + errors: callError("notifyObservers's third parameter can be omitted."), + }, + { + code: "window.getComputedStyle(elt, null);", + output: "window.getComputedStyle(elt);", + errors: callError("getComputedStyle's second parameter can be omitted."), + }, + { + code: "window.getComputedStyle(elt, '');", + output: "window.getComputedStyle(elt);", + errors: callError("getComputedStyle's second parameter can be omitted."), + }, + ], +}); 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..9f59c78d05 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-removeEventListener.js @@ -0,0 +1,99 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + let message = + "use {once: true} instead of removeEventListener " + + "as the first instruction of the listener"; + return { code, errors: [{ message, 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..3541467143 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/no-useless-run-test.js @@ -0,0 +1,124 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, output) { + let message = + "Useless run_test function - only contains run_next_test; whole function can be removed"; + return { code, output, errors: [{ message, 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..daf3c6f3d9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/prefer-boolean-length-check.js @@ -0,0 +1,97 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError() { + let message = "Prefer boolean length check"; + return [{ message, 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..0883130f17 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/prefer-formatValues.js @@ -0,0 +1,72 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function error(line, column = undefined) { + return { + message: + "prefer to use a single document.l10n.formatValues call instead " + + "of multiple calls to document.l10n.formatValue or document.l10n.formatValues", + 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-chromeutils-import-null.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-chromeutils-import-null.js new file mode 100644 index 0000000000..6584f728ac --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-chromeutils-import-null.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/reject-chromeutils-import-null"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 8 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError() { + let message = + "ChromeUtils.import should not be called with (..., null) to " + + "retrieve the JSM global object. Rely on explicit exports instead."; + return [{ message, type: "CallExpression" }]; +} + +ruleTester.run("reject-chromeutils-import-null", rule, { + valid: ['ChromeUtils.import("resource://some/path/to/My.jsm")'], + invalid: [ + { + code: 'ChromeUtils.import("resource://some/path/to/My.jsm", null)', + errors: invalidError(), + }, + { + code: ` +ChromeUtils.import( + "resource://some/path/to/My.jsm", + null +);`, + errors: invalidError(), + }, + ], +}); 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..04c0e0408b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-importGlobalProperties.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-importGlobalProperties"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 8 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +ruleTester.run("reject-importGlobalProperties", rule, { + valid: [ + { + code: "Cu.something();", + }, + { + options: ["allownonwebidl"], + code: "Cu.importGlobalProperties(['fetch'])", + }, + ], + invalid: [ + { + code: "Cu.importGlobalProperties(['fetch'])", + options: ["everything"], + errors: [{ messageId: "unexpectedCall" }], + }, + { + code: "Cu.importGlobalProperties(['TextEncoder'])", + options: ["everything"], + errors: [{ messageId: "unexpectedCall" }], + }, + { + code: "Cu.importGlobalProperties(['TextEncoder'])", + options: ["allownonwebidl"], + errors: [{ messageId: "unexpectedCallWebIdl" }], + }, + ], +}); 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..19f30559ec --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-relative-requires.js @@ -0,0 +1,58 @@ +/* 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: 8 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError() { + let message = "relative paths are not allowed with require()"; + return [{ message, 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-some-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-some-requires.js new file mode 100644 index 0000000000..b56dd2cf96 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-some-requires.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/reject-some-requires"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 8 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function requirePathError(path) { + const message = `require(${path}) is not allowed`; + return [{ message, 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/rejects-requires-await.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/rejects-requires-await.js new file mode 100644 index 0000000000..ea6273a8ee --- /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: 8 } }); + +// ------------------------------------------------------------------------------ +// 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..7ad729d22b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-cc-etc.js @@ -0,0 +1,55 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, originalName, newName) { + return { + code, + errors: [ + { + message: `Use ${newName} rather than ${originalName}`, + type: "MemberExpression", + }, + ], + }; +} + +ruleTester.run("use-cc-etc", rule, { + valid: ["Components.Constructor();", "let x = Components.foo;"], + invalid: [ + invalidCode( + "let foo = Components.classes['bar'];", + "Components.classes", + "Cc" + ), + invalidCode( + "let bar = Components.interfaces.bar;", + "Components.interfaces", + "Ci" + ), + invalidCode( + "Components.results.NS_ERROR_ILLEGAL_INPUT;", + "Components.results", + "Cr" + ), + invalidCode( + "Components.utils.reportError('fake');", + "Components.utils", + "Cu" + ), + ], +}); 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..26c6b350bc --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-generateqi.js @@ -0,0 +1,81 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError(message) { + return [{ message, type: "CallExpression" }]; +} +function error(message, type) { + return [{ message, type }]; +} + +const MSG_NO_JS_QUERY_INTERFACE = + "Please use ChromeUtils.generateQI rather than manually creating " + + "JavaScript QueryInterface functions"; + +const MSG_NO_XPCOMUTILS_GENERATEQI = + "Please use ChromeUtils.generateQI instead of XPCOMUtils.generateQI"; + +/* 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: callError(MSG_NO_XPCOMUTILS_GENERATEQI), + }, + { + code: `X.prototype = { QueryInterface: XPCOMUtils.generateQI(["nsIMeh"]) };`, + output: `X.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIMeh"]) };`, + errors: callError(MSG_NO_XPCOMUTILS_GENERATEQI), + }, + { + code: `X.prototype = { QueryInterface: ${QueryInterface} };`, + output: `X.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIMeh", "nsIFlug", "amIFoo"]) };`, + errors: error(MSG_NO_JS_QUERY_INTERFACE, "Property"), + }, + { + code: `X.prototype = { ${String(QueryInterface).replace( + /^function /, + "" + )} };`, + output: `X.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIMeh", "nsIFlug", "amIFoo"]) };`, + errors: error(MSG_NO_JS_QUERY_INTERFACE, "Property"), + }, + { + code: `X.prototype.QueryInterface = ${QueryInterface};`, + output: `X.prototype.QueryInterface = ChromeUtils.generateQI(["nsIMeh", "nsIFlug", "amIFoo"]);`, + errors: error(MSG_NO_JS_QUERY_INTERFACE, "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..635c436233 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-chromeutils-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-chromeutils-import"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function callError(message) { + return [{ message, type: "CallExpression" }]; +} + +const MESSAGE_IMPORT = "Please use ChromeUtils.import instead of Cu.import"; +const MESSAGE_DEFINE = + "Please use ChromeUtils.defineModuleGetter instead of " + + "XPCOMUtils.defineLazyModuleGetter"; + +ruleTester.run("use-chromeutils-import", rule, { + valid: [ + `ChromeUtils.import("resource://gre/modules/Service.jsm");`, + `ChromeUtils.import("resource://gre/modules/Service.jsm", this);`, + `ChromeUtils.defineModuleGetter(this, "Services", + "resource://gre/modules/Service.jsm");`, + `XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Service.jsm", + "Foo");`, + `XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Service.jsm", + undefined, preServicesLambda);`, + { + options: [{ allowCu: true }], + code: `Cu.import("resource://gre/modules/Service.jsm");`, + }, + ], + invalid: [ + { + code: `Cu.import("resource://gre/modules/Services.jsm");`, + output: `ChromeUtils.import("resource://gre/modules/Services.jsm");`, + errors: callError(MESSAGE_IMPORT), + }, + { + code: `Cu.import("resource://gre/modules/Services.jsm", this);`, + output: `ChromeUtils.import("resource://gre/modules/Services.jsm", this);`, + errors: callError(MESSAGE_IMPORT), + }, + { + code: `Components.utils.import("resource://gre/modules/Services.jsm");`, + output: `ChromeUtils.import("resource://gre/modules/Services.jsm");`, + errors: callError(MESSAGE_IMPORT), + }, + { + code: `Components.utils.import("resource://gre/modules/Services.jsm");`, + output: `ChromeUtils.import("resource://gre/modules/Services.jsm");`, + errors: callError(MESSAGE_IMPORT), + }, + { + options: [{ allowCu: true }], + code: `Components.utils.import("resource://gre/modules/Services.jsm", this);`, + output: `ChromeUtils.import("resource://gre/modules/Services.jsm", this);`, + errors: callError(MESSAGE_IMPORT), + }, + { + code: `XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm");`, + output: `ChromeUtils.defineModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm");`, + errors: callError(MESSAGE_DEFINE), + }, + ], +}); 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..f4ad001a08 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-default-preference-values.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-default-preference-values"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + let message = "provide a default value instead of using a try/catch block"; + return { code, errors: [{ message, 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..cb1810b305 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-includes-instead-of-indexOf.js @@ -0,0 +1,38 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + let message = "use .includes instead of .indexOf"; + return { code, errors: [{ message, 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-ownerGlobal.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-ownerGlobal.js new file mode 100644 index 0000000000..b08bdf1632 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-ownerGlobal.js @@ -0,0 +1,35 @@ +/* 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: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code) { + let message = "use .ownerGlobal instead of .ownerDocument.defaultView"; + return { code, errors: [{ message, 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..81952452e4 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-returnValue.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/use-returnValue"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, methodName) { + let message = `{Array/String}.${methodName} doesn't modify the instance in-place`; + return { code, errors: [{ message, 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..b8d5338fc4 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/use-services.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-services"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidCode(code, name) { + let message = `Use Services.${name} rather than getService().`; + return { code, errors: [{ message, type: "CallExpression" }] }; +} + +ruleTester.run("use-services", rule, { + valid: [ + 'Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator)', + 'Components.classes["@mozilla.org/uuid-generator;1"].getService(Components.interfaces.nsIUUIDGenerator)', + "Services.wm.addListener()", + ], + invalid: [ + invalidCode( + 'Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);', + "wm" + ), + invalidCode( + 'Components.classes["@mozilla.org/toolkit/app-startup;1"].getService(Components.interfaces.nsIAppStartup);', + "startup" + ), + ], +}); 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..d8ee4098ec --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/update.sh @@ -0,0 +1,61 @@ +#!/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 + +echo "To complete this script you will need the following tokens from https://mozilla-releng.net/tokens" +echo " - tooltool.upload.public" +echo " - tooltool.download.public" +echo "" +read -p "Are these tokens visible at the above URL (y/n)?" choice +case "$choice" in + y|Y ) + echo "" + echo "1. Go to https://mozilla-releng.net" + echo "2. Log in using your Mozilla LDAP account." + echo "3. Click on \"Tokens.\"" + echo "4. Issue a user token with the permissions tooltool.upload.public and tooltool.download.public." + echo "" + echo "When you click issue you will be presented with a long string. Paste the string into a temporary file called ~/.tooltool-token." + echo "" + read -rsp $'Press any key to continue...\n' -n 1 + ;; + n|N ) + echo "" + echo "You will need to contact somebody that has these permissions... people most likely to have these permissions are members of the releng, ateam, a sheriff, mratcliffe, or jryans" + exit 1 + ;; + * ) + echo "" + echo "Invalid input." + continue + ;; +esac + +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..." +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 --authentication-file=~/.tooltool-token --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/index.js b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/index.js new file mode 100644 index 0000000000..4fb6e340ed --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/index.js @@ -0,0 +1,17 @@ +/** + * @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"), + }, +}; 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..c7dbf697fa --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-spidermonkey-js/lib/processors/self-hosted.js @@ -0,0 +1,49 @@ +/** + * @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"; + +var path = require("path"); + +const selfHostedRegex = /js\/src\/(?:builtin|shell)\/.*?\.js$/; +const macroRegex = /\s*\#(if|ifdef|else|elif|endif|include|define|undef).*/; + +module.exports = { + preprocess(text, filename) { + if (path.win32) { + filename = filename.split(path.sep).join("/"); + } + if (!selfHostedRegex.test(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++) { + lines[i] = "// " + 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) { + return Array.prototype.concat.apply([], messages); + }, +}; 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..61452d9e96 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-spidermonkey-js/package.json @@ -0,0 +1,30 @@ +{ + "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..e984e43d49 --- /dev/null +++ b/tools/lint/eslint/manifest.tt @@ -0,0 +1,10 @@ +[ + { + "algorithm": "sha512", + "visibility": "public", + "filename": "eslint.tar.gz", + "unpack": true, + "digest": "ade0ac783a4efd0cff65fac77e8bbdc18189d6a0ba2f7258882d76e2d1a0d20e14bc56eab928353ab9abf15ae42a18eb3cf286787bd824500058d1194b6ca674", + "size": 8863963 + } +]
\ No newline at end of file diff --git a/tools/lint/eslint/modules.json b/tools/lint/eslint/modules.json new file mode 100644 index 0000000000..849783fc4d --- /dev/null +++ b/tools/lint/eslint/modules.json @@ -0,0 +1,239 @@ +{ + + "Actions.jsm": ["actionTypes", "actionCreators", "actionUtils", "ASRouterActions", "globalImportContext", "UI_CODE", "BACKGROUND_PROCESS", "MAIN_MESSAGE_TYPE", "CONTENT_MESSAGE_TYPE", "PRELOAD_MESSAGE_TYPE"], + "ActivityStreamPrefs.jsm": ["DefaultPrefs", "Prefs"], + "ActivityStreamStorage.jsm": ["ActivityStreamStorage", "getDefaultOptions"], + "addon.js": ["Addon"], + "AddonManager.jsm": ["AddonManager", "AddonManagerPrivate"], + "addons.js": ["AddonsEngine", "AddonValidator"], + "addons.jsm": ["Addon", "STATE_ENABLED", "STATE_DISABLED"], + "addonsreconciler.js": ["AddonsReconciler", "CHANGE_INSTALLED", "CHANGE_UNINSTALLED", "CHANGE_ENABLED", "CHANGE_DISABLED"], + "AddonTestUtils.jsm": ["AddonTestUtils", "MockAsyncShutdown"], + "addonutils.js": ["AddonUtils"], + "ajv-4.1.1.js": ["Ajv"], + "AlertsHelper.jsm": [], + "AppData.jsm": ["makeFakeAppDir"], + "AppInfo.jsm": ["newAppInfo", "getAppInfo", "updateAppInfo"], + "ASRouterTargeting.jsm": ["ASRouterTargeting", "QueryCache", "CachedTargetingGetter"], + "async.js": ["Async"], + "AsyncSpellCheckTestHelper.jsm": ["onSpellCheck"], + "base-loader.js": ["Loader", "resolveURI", "Module", "Require", "unload"], + "bogus_element_type.jsm": [], + "bookmarks.js": ["BookmarksEngine", "PlacesItem", "Bookmark", "BookmarkFolder", "BookmarkQuery", "Livemark", "BookmarkSeparator", "BufferedBookmarksEngine"], + "bookmarks.jsm": ["PlacesItem", "Bookmark", "Separator", "Livemark", "BookmarkFolder", "DumpBookmarks"], + "bookmark_repair.js": ["BookmarkRepairRequestor", "BookmarkRepairResponder"], + "bookmark_validator.js": ["BookmarkValidator", "BookmarkProblemData"], + "browser-loader.js": ["BrowserLoader"], + "browser.js": ["browser", "Context", "WindowState"], + "browserid_identity.js": ["BrowserIDManager", "AuthenticationError"], + "capabilities.js": ["Capabilities", "PageLoadStrategy", "Proxy", "Timeouts", "UnhandledPromptBehavior"], + "cert.js": ["allowAllCerts"], + "clients.js": ["ClientEngine", "ClientsRec"], + "collection_repair.js": ["getRepairRequestor", "getAllRepairRequestors", "CollectionRepairRequestor", "getRepairResponder", "CollectionRepairResponder"], + "collection_validator.js": ["CollectionValidator", "CollectionProblemData"], + "Console.jsm": ["console", "ConsoleAPI"], + "constants.js": ["WEAVE_VERSION", "SYNC_API_VERSION", "STORAGE_VERSION", "PREFS_BRANCH", "DEFAULT_KEYBUNDLE_NAME", "SYNC_KEY_ENCODED_LENGTH", "SYNC_KEY_DECODED_LENGTH", "NO_SYNC_NODE_INTERVAL", "MAX_ERROR_COUNT_BEFORE_BACKOFF", "MINIMUM_BACKOFF_INTERVAL", "MAXIMUM_BACKOFF_INTERVAL", "HMAC_EVENT_INTERVAL", "MASTER_PASSWORD_LOCKED_RETRY_INTERVAL", "DEFAULT_GUID_FETCH_BATCH_SIZE", "DEFAULT_DOWNLOAD_BATCH_SIZE", "SINGLE_USER_THRESHOLD", "MULTI_DEVICE_THRESHOLD", "SCORE_INCREMENT_SMALL", "SCORE_INCREMENT_MEDIUM", "SCORE_INCREMENT_XLARGE", "SCORE_UPDATE_DELAY", "IDLE_OBSERVER_BACK_DELAY", "URI_LENGTH_MAX", "MAX_HISTORY_UPLOAD", "MAX_HISTORY_DOWNLOAD", "STATUS_OK", "SYNC_FAILED", "LOGIN_FAILED", "SYNC_FAILED_PARTIAL", "CLIENT_NOT_CONFIGURED", "STATUS_DISABLED", "MASTER_PASSWORD_LOCKED", "LOGIN_SUCCEEDED", "SYNC_SUCCEEDED", "ENGINE_SUCCEEDED", "LOGIN_FAILED_NO_USERNAME", "LOGIN_FAILED_NO_PASSPHRASE", "LOGIN_FAILED_NETWORK_ERROR", "LOGIN_FAILED_SERVER_ERROR", "LOGIN_FAILED_INVALID_PASSPHRASE", "LOGIN_FAILED_LOGIN_REJECTED", "METARECORD_DOWNLOAD_FAIL", "VERSION_OUT_OF_DATE", "CREDENTIALS_CHANGED", "ABORT_SYNC_COMMAND", "NO_SYNC_NODE_FOUND", "OVER_QUOTA", "SERVER_MAINTENANCE", "RESPONSE_OVER_QUOTA", "ENGINE_UPLOAD_FAIL", "ENGINE_DOWNLOAD_FAIL", "ENGINE_UNKNOWN_FAIL", "ENGINE_APPLY_FAIL", "ENGINE_BATCH_INTERRUPTED", "kSyncMasterPasswordLocked", "kSyncWeaveDisabled", "kSyncNetworkOffline", "kSyncBackoffNotMet", "kFirstSyncChoiceNotMade", "kSyncNotConfigured", "kFirefoxShuttingDown", "DEVICE_TYPE_DESKTOP", "DEVICE_TYPE_MOBILE", "SQLITE_MAX_VARIABLE_NUMBER"], + "Constants.jsm": ["Roles", "Events", "Relations", "Filters", "States", "Prefilters"], + "ContactDB.jsm": ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME", "REVISION_STORE", "DB_VERSION"], + "content-process.jsm": ["init"], + "content.jsm": ["registerContentFrame"], + "ContentCrashHandlers.jsm": ["TabCrashHandler", "SubframeCrashHandler", "UnsubmittedCrashHandler"], + "ContentPrefUtils.jsm": ["ContentPref", "cbHandleResult", "cbHandleError", "cbHandleCompletion", "safeCallback", "_methodsCallableFromChild"], + "cookies.js": ["Cookies"], + "CoverageUtils.jsm": ["CoverageCollector"], + "CrashManagerTest.jsm": ["configureLogging", "getManager", "sleep", "TestingCrashManager"], + "declined.js": ["DeclinedEngines"], + "distribution.js": ["DistributionCustomizer"], + "DNSTypes.jsm": ["DNS_QUERY_RESPONSE_CODES", "DNS_AUTHORITATIVE_ANSWER_CODES", "DNS_CLASS_CODES", "DNS_RECORD_TYPES"], + "doctor.js": ["Doctor"], + "dom.js": ["ContentEventObserverService", "WebElementEventTarget"], + "DOMRequestHelper.jsm": ["DOMRequestIpcHelper"], + "DownloadCore.jsm": ["Download", "DownloadSource", "DownloadTarget", "DownloadError", "DownloadSaver", "DownloadCopySaver", "DownloadLegacySaver"], + "DownloadList.jsm": ["DownloadList", "DownloadCombinedList", "DownloadSummary"], + "driver.js": ["GeckoDriver"], + "element.js": ["ChromeWebElement", "ContentWebElement", "ContentWebFrame", "ContentWebWindow", "element", "WebElement"], + "engines.js": ["EngineManager", "SyncEngine", "Tracker", "Store", "Changeset"], + "enginesync.js": ["EngineSynchronizer"], + "error.js": ["ElementClickInterceptedError", "ElementNotAccessibleError", "ElementNotInteractableError", "InsecureCertificateError", "InvalidArgumentError", "InvalidCookieDomainError", "InvalidElementStateError", "InvalidSelectorError", "InvalidSessionIDError", "JavaScriptError", "MoveTargetOutOfBoundsError", "NoSuchAlertError", "NoSuchElementError", "NoSuchFrameError", "NoSuchWindowError", "ScriptTimeoutError", "SessionNotCreatedError", "StaleElementReferenceError", "TimeoutError", "UnableToSetCookieError", "UnexpectedAlertOpenError", "UnknownCommandError", "UnknownError", "UnsupportedOperationError", "WebDriverError", "error", "stack"], + "evaluate.js": ["evaluate", "sandbox", "Sandboxes"], + "event-emitter.js": ["EventEmitter"], + "extension-storage.js": ["ExtensionStorageEngine", "EncryptionRemoteTransformer", "KeyRingEncryptionRemoteTransformer"], + "Extension.jsm": ["Extension", "ExtensionData"], + "ExtensionAPI.jsm": ["ExtensionAPI", "ExtensionAPIs"], + "ExtensionXPCShellUtils.jsm": ["ExtensionTestUtils"], + "fakeservices.js": ["FakeCryptoService", "FakeFilesystemService", "FakeGUIDService", "fakeSHA256HMAC"], + "file_expandosharing.jsm": ["checkFromJSM"], + "file_stringencoding.jsm": ["checkFromJSM"], + "file_url.jsm": ["checkFromJSM"], + "file_worker_url.jsm": ["checkFromJSM"], + "Finder.jsm": ["Finder", "GetClipboardSearchString", "SetClipboardSearchString"], + "format.js": ["pprint", "truncate"], + "formautofill.jsm": ["Address", "CreditCard", "DumpAddresses", "DumpCreditCards"], + "FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"], + "FormAutofillStorage.jsm": ["formAutofillStorage"], + "FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"], + "FormAutofillUtils.jsm": ["FormAutofillUtils", "AddressDataLoader"], + "forms.js": ["FormEngine", "FormRec", "FormValidator"], + "forms.jsm": ["FormData"], + "FrameScriptManager.jsm": ["getNewLoaderID"], + "fxaccounts.jsm": ["Authentication"], + "FxAccounts.jsm": ["fxAccounts", "FxAccounts"], + "FxAccountsCommands.js": ["SendTab", "FxAccountsCommands"], + "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_COMMAND_RECEIVED_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "ON_NEW_DEVICE_ID", "COMMAND_SENDTAB", "SCOPE_PROFILE", "SCOPE_OLD_SYNC", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "COMMAND_PAIR_HEARTBEAT", "COMMAND_PAIR_SUPP_METADATA", "COMMAND_PAIR_AUTHORIZE", "COMMAND_PAIR_DECLINE", "COMMAND_PAIR_COMPLETE", "COMMAND_PAIR_PREFERENCES", "COMMAND_PROFILE_CHANGE", "COMMAND_CAN_LINK_ACCOUNT", "COMMAND_LOGIN", "COMMAND_LOGOUT", "COMMAND_DELETE", "COMMAND_SYNC_PREFERENCES", "COMMAND_CHANGE_PASSWORD", "COMMAND_FXA_STATUS", "PREF_LAST_FXA_USER", "PREF_REMOTE_PAIRING_URI", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"], + "FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"], + "FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"], + "FxAccountsPush.js": ["FxAccountsPushService"], + "FxAccountsStorage.jsm": ["FxAccountsStorageManagerCanStoreField", "FxAccountsStorageManager"], + "FxAccountsWebChannel.jsm": ["EnsureFxAccountsWebChannel"], + "fxa_utils.js": ["initializeIdentityWithTokenServerResponse"], + "Geometry.jsm": ["Point", "Rect"], + "Gestures.jsm": ["GestureSettings", "GestureTracker"], + "GMPInstallManager.jsm": ["GMPInstallManager", "GMPExtractor", "GMPDownloader", "GMPAddon"], + "GMPProvider.jsm": [], + "GMPUtils.jsm": ["GMP_PLUGIN_IDS", "GMPPrefs", "GMPUtils", "OPEN_H264_ID", "WIDEVINE_ID"], + "hawkclient.js": ["HawkClient"], + "hawkrequest.js": ["HAWKAuthenticatedRESTRequest", "deriveHawkCredentials"], + "helper.js": ["workerHelper"], + "HelperApps.jsm": ["App", "HelperApps"], + "history.js": ["HistoryEngine", "HistoryRec"], + "history.jsm": ["HistoryEntry", "DumpHistory"], + "Http.jsm": ["httpRequest", "percentEncode"], + "httpd.js": ["HTTP_400", "HTTP_401", "HTTP_402", "HTTP_403", "HTTP_404", "HTTP_405", "HTTP_406", "HTTP_407", "HTTP_408", "HTTP_409", "HTTP_410", "HTTP_411", "HTTP_412", "HTTP_413", "HTTP_414", "HTTP_415", "HTTP_417", "HTTP_500", "HTTP_501", "HTTP_502", "HTTP_503", "HTTP_504", "HTTP_505", "HttpError", "HttpServer"], + "import_module.jsm": ["MODULE_IMPORTED", "MODULE_URI", "SUBMODULE_IMPORTED", "same_scope", "SUBMODULE_IMPORTED_TO_SCOPE"], + "import_sub_module.jsm": ["SUBMODULE_IMPORTED", "test_obj"], + "InlineSpellChecker.jsm": ["InlineSpellChecker", "SpellCheckHelper"], + "jsdebugger.jsm": ["addDebuggerToGlobal", "addSandboxedDebuggerToGlobal"], + "jsesc.js": ["jsesc"], + "json2.js": ["JSON"], + "keys.js": ["BulkKeyBundle", "SyncKeyBundle"], + "kinto-http-client.js": ["KintoHttpClient"], + "kinto-offline-client.js": ["Kinto"], + "kinto-storage-adapter.js": ["FirefoxAdapter"], + "kvstore.jsm": ["KeyValueService"], + "L10nRegistry.jsm": ["L10nRegistry", "FileSource", "IndexedFileSource"], + "Launcher.jsm": ["BrowserToolboxLauncher"], + "loader-plugin-raw.jsm": ["requireRawId"], + "loader.js": ["WorkerDebuggerLoader", "worker"], + "Loader.jsm": ["DevToolsLoader", "require", "loader"], + "log.js": ["Log"], + "logger.jsm": ["Logger"], + "logging.js": ["getTestLogger", "initTestLogging"], + "LoginRecipes.jsm": ["LoginRecipesContent", "LoginRecipesParent"], + "logmanager.js": ["LogManager"], + "lz4.js": ["Lz4"], + "lz4_internal.js": ["Primitives"], + "main.js": ["Weave"], + "Manifest.jsm": ["Manifests"], + "MatchURLFilters.jsm": ["MatchURLFilters"], + "mcc_iso3166_table.jsm": ["MCC_ISO3166_TABLE"], + "message.js": ["Command", "Message", "MessageOrigin", "Response"], + "MessagePort.jsm": ["MessagePort", "MessageListener"], + "Messaging.jsm": ["sendMessageToJava", "Messaging", "EventDispatcher"], + "MigrationUtils.jsm": ["MigrationUtils", "MigratorPrototype"], + "MulticastDNSAndroid.jsm": ["MulticastDNS"], + "NativeMessaging.jsm": ["NativeApp"], + "NotificationDB.jsm": [], + "nsFormAutoCompleteResult.jsm": ["FormAutoCompleteResult"], + "observers.js": ["Observers"], + "offlineAppCache.jsm": ["OfflineAppCacheHelper"], + "OrientationChangeHandler.jsm": [], + "osfile.jsm": ["OS", "require"], + "osfile_async_front.jsm": ["OS"], + "osfile_native.jsm": ["read"], + "osfile_shared_allthreads.jsm": ["LOG", "clone", "Config", "Constants", "Type", "HollowStructure", "OSError", "Library", "declareFFI", "declareLazy", "declareLazyFFI", "normalizeBufferArgs", "projectValue", "isArrayBuffer", "isTypedArray", "defineLazyGetter", "OS"], + "osfile_unix_allthreads.jsm": ["declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry", "Type", "POS_START", "POS_CURRENT", "POS_END"], + "osfile_win_allthreads.jsm": ["declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry", "Type", "POS_START", "POS_CURRENT", "POS_END"], + "ospath_unix.jsm": ["basename", "dirname", "join", "normalize", "split", "toFileURI", "fromFileURI"], + "ospath_win.jsm": ["basename", "dirname", "join", "normalize", "split", "winGetDrive", "winIsAbsolute", "toFileURI", "fromFileURI"], + "OutputGenerator.jsm": ["UtteranceGenerator", "BrailleGenerator"], + "packets.js": ["RawPacket", "Packet", "JSONPacket", "BulkPacket"], + "PageMenu.jsm": ["PageMenuParent", "PageMenuChild"], + "PageThumbs.jsm": ["PageThumbs", "PageThumbsStorage"], + "passwords.js": ["PasswordEngine", "LoginRec", "PasswordValidator"], + "passwords.jsm": ["Password", "DumpPasswords"], + "PdfJsNetwork.jsm": ["NetworkManager"], + "PhoneNumberMetaData.jsm": ["PHONE_NUMBER_META_DATA"], + "PluginProvider.jsm": [], + "PointerAdapter.jsm": ["PointerRelay", "PointerAdapter"], + "policies.js": ["ErrorHandler", "SyncScheduler"], + "PreferenceExperiments.jsm": ["PreferenceExperiments", "migrateStorage"], + "prefs.js": ["Branch", "MarionettePrefs"], + "prefs.js": ["PrefsEngine", "PrefRec"], + "prefs.jsm": ["Preference"], + "pretty-fast.js": ["prettyFast"], + "profiler_get_symbols.js": ["wasm_bindgen"], + "PromiseWorker.jsm": ["BasePromiseWorker"], + "PushCrypto.jsm": ["PushCrypto", "concatArray"], + "quit.js": ["goQuitApplication"], + "record.js": ["WBORecord", "RecordManager", "CryptoWrapper", "CollectionKeyManager", "Collection"], + "recursive_importA.jsm": ["foo", "bar"], + "recursive_importB.jsm": ["baz", "qux"], + "Reducers.jsm": ["reducers", "INITIAL_STATE", "insertPinned", "TOP_SITES_DEFAULT_ROWS", "TOP_SITES_MAX_SITES_PER_ROW"], + "Redux.jsm": ["redux"], + "reflect.jsm": ["Reflect"], + "remote-settings.js": ["RemoteSettings", "jexlFilterFunc", "remoteSettingsBroadcastHandler"], + "RemotePageManagerChild.jsm": ["ChildMessagePort"], + "RemotePageManagerParent.jsm": ["RemotePages", "RemotePageManager"], + "resource.js": ["AsyncResource", "Resource"], + "resource://services-common/utils.js": ["CommonUtils"], + "resource://services-crypto/utils.js": ["CryptoUtils"], + "resource://testing-common/services/sync/utils.js": ["AccountState", "MockFxaStorageManager", "SyncTestingInfrastructure", "configureFxAccountIdentity", "configureIdentity", "encryptPayload", "makeFxAccountsInternalMock", "makeIdentityConfig", "promiseNamedTimer", "promiseZeroTimer", "sumHistogram", "syncTestLogging", "waitForZeroTimer"], + "rest.js": ["RESTRequest", "RESTResponse", "TokenAuthenticatedRESTRequest"], + "rotaryengine.js": ["RotaryEngine", "RotaryRecord", "RotaryStore", "RotaryTracker"], + "Schemas.jsm": ["SchemaRoot", "Schemas"], + "SearchShortcuts.jsm": ["checkHasSearchEngine", "getSearchProvider", "SEARCH_SHORTCUTS", "CUSTOM_SEARCH_SHORTCUTS", "SEARCH_SHORTCUTS_EXPERIMENT", "SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF", "SEARCH_SHORTCUTS_HAVE_PINNED_PREF"], + "SectionsManager.jsm": ["SectionsFeed", "SectionsManager"], + "service.js": ["Service"], + "SharedPromptUtils.jsm": ["PromptUtils", "EnableDelayHelper"], + "ShortURL.jsm": ["shortURL", "getETLD"], + "ShutdownLeaksCollector.jsm": ["ContentCollector"], + "SignInToWebsite.jsm": ["SignInToWebsiteController"], + "SiteClassifier.jsm": ["classifySite"], + "Social.jsm": ["Social", "OpenGraphBuilder", "DynamicResizeWatcher", "sizeSocialPanelToContent"], + "SpecialPowersObserver.jsm": ["SpecialPowersObserver", "SpecialPowersObserverFactory"], + "StateMachineHelper.jsm": ["State", "CommandType"], + "status.js": ["Status"], + "storageserver.js": ["ServerBSO", "StorageServerCallback", "StorageServerCollection", "StorageServer", "storageServerForUsers"], + "stream-utils.js": ["StreamUtils"], + "StructuredLog.jsm": ["StructuredLogger", "StructuredFormatter"], + "StyleEditorUtil.jsm": ["getString", "assert", "log", "text", "wire", "showFilePicker"], + "subprocess_common.jsm": ["BaseProcess", "PromiseWorker", "SubprocessConstants"], + "subprocess_shared.js": ["Library", "SubprocessConstants"], + "subprocess_shared_unix.js": ["libc"], + "subprocess_shared_win.js": ["LIBC", "Win", "createPipe", "libc"], + "subprocess_unix.jsm": ["SubprocessImpl"], + "subprocess_win.jsm": ["SubprocessImpl"], + "subprocess_worker_common.js": ["BasePipe", "BaseProcess", "debug"], + "sync.js": ["executeSoon", "DebounceCallback", "IdlePromise", "MessageManagerDestroyedPromise", "PollPromise", "Sleep", "TimedPromise", "waitForEvent", "waitForMessage", "waitForObserverTopic"], + "sync.jsm": ["Authentication"], + "SyncedBookmarksMirror.jsm": ["SyncedBookmarksMirror"], + "tabs.js": ["TabEngine", "TabSetRecord"], + "tabs.jsm": ["BrowserTabs"], + "tcpsocket_test.jsm": ["createSocket", "createServer", "enablePrefsAndPermissions", "socketCompartmentInstanceOfArrayBuffer"], + "telemetry.js": ["SyncTelemetry"], + "test.jsm": ["Foo"], + "test2.jsm": ["Bar"], + "test_bug883784.jsm": ["Test"], + "Timer.jsm": ["setTimeout", "setTimeoutWithTarget", "clearTimeout", "setInterval", "setIntervalWithTarget", "clearInterval", "requestIdleCallback", "cancelIdleCallback"], + "TippyTopProvider.jsm": ["TippyTopProvider", "getDomain"], + "Tokenize.jsm": ["tokenize", "toksToTfIdfVector"], + "tokenserverclient.js": ["TokenServerClient", "TokenServerClientError", "TokenServerClientNetworkError", "TokenServerClientServerError"], + "tps.jsm": ["ACTIONS", "Addons", "Addresses", "Bookmarks", "CreditCards", "Formdata", "History", "Passwords", "Prefs", "Tabs", "TPS", "Windows"], + "Translation.jsm": ["Translation", "TranslationTelemetry"], + "Traversal.jsm": ["TraversalRules", "TraversalHelper"], + "UpdateTelemetry.jsm": ["AUSTLMY"], + "UpdateTopLevelContentWindowIDHelper.jsm": ["trackBrowserWindow"], + "uptake-telemetry.js": ["UptakeTelemetry"], + "UrlbarUtils.jsm": ["UrlbarQueryContext", "UrlbarMuxer", "UrlbarProvider", "UrlbarUtils"], + "util.js": ["getChromeWindow", "Utils", "Svc", "SerializableSet"], + "utils.js": ["encryptPayload", "makeIdentityConfig", "makeFxAccountsInternalMock", "configureFxAccountIdentity", "configureIdentity", "SyncTestingInfrastructure", "waitForZeroTimer", "MockFxaStorageManager", "AccountState", "sumHistogram", "CommonUtils", "CryptoUtils", "TestingUtils", "promiseZeroTimer", "promiseNamedTimer", "getLoginTelemetryScalar", "syncTestLogging"], + "Utils.jsm": ["Utils", "Logger", "PivotContext", "PrefCache"], + "VariablesView.jsm": ["VariablesView", "escapeHTML"], + "version.jsm": ["VERSION"], + "vtt.jsm": ["WebVTT"], + "WebChannel.jsm": ["WebChannel", "WebChannelBroker"], + "windows.jsm": ["BrowserWindows"], + "WindowsJumpLists.jsm": ["WinTaskbarJumpList"], + "WindowsPreviewPerTab.jsm": ["AeroPeek"], + "xul-app.jsm": ["XulApp"] +} diff --git a/tools/lint/eslint/setup_helper.py b/tools/lint/eslint/setup_helper.py new file mode 100644 index 0000000000..24fd8808b9 --- /dev/null +++ b/tools/lint/eslint/setup_helper.py @@ -0,0 +1,408 @@ +# -*- 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 distutils.version import LooseVersion +from filecmp import dircmp + +from mozbuild.nodeutil import ( + find_node_executable, + find_npm_executable, + NPM_MIN_VERSION, + NODE_MIN_VERSION, +) +from mozbuild.util import ensure_subprocess_env +from mozfile.mozfile import remove as mozfileremove + + +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 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: + node_modules_path = os.path.join(project_root, "node_modules") + print("Clobbering %s..." % node_modules_path) + if sys.platform.startswith("win") and have_winrm(): + process = subprocess.Popen(["winrm", "-rf", node_modules_path]) + process.wait() + else: + mozfileremove(node_modules_path) + + 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) + + # 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. + extra_parameters.append("--scripts-prepend-node-path") + + cmd.extend(extra_parameters) + + print('Installing %s for mach using "%s"...' % (package_name, " ".join(cmd))) + result = call_process(package_name, cmd) + + 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(ensure_subprocess_env(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, "r", 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, "r", encoding="utf-8") as f: + expected_modules.update(json.load(f).get("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, "r", 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 LooseVersion("4.0.0") > LooseVersion(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 = LooseVersion(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 = LooseVersion(range_version) + range_max = LooseVersion("%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..3367a599e0 --- /dev/null +++ b/tools/lint/eslint/update.sh @@ -0,0 +1,71 @@ +#!/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 + +echo "To complete this script you will need the following tokens from https://mozilla-releng.net/tokens" +echo " - tooltool.upload.public" +echo " - tooltool.download.public" +echo "" +read -p "Are these tokens visible at the above URL (y/n)?" choice +case "$choice" in + y|Y ) + echo "" + echo "1. Go to https://mozilla-releng.net" + echo "2. Log in using your Mozilla LDAP account." + echo "3. Click on \"Tokens.\"" + echo "4. Issue a user token with the permissions tooltool.upload.public and tooltool.download.public." + echo "" + echo "When you click issue you will be presented with a long string. Paste the string into a temporary file called ~/.tooltool-token." + echo "" + read -rsp $'Press any key to continue...\n' -n 1 + ;; + n|N ) + echo "" + echo "You will need to contact somebody that has these permissions... people most likely to have these permissions are members of the releng, ateam, a sheriff, mratcliffe, or jryans" + exit 1 + ;; + * ) + echo "" + echo "Invalid input." + continue + ;; +esac + +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. +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 --authentication-file=~/.tooltool-token --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..1d0f66e564 --- /dev/null +++ b/tools/lint/file-perm.yml @@ -0,0 +1,45 @@ +--- +file-perm: + description: File permission check + include: + - . + extensions: + - .c + - .cc + - .cpp + - .flac + - .h + - .html + - .jsm + - .jsx + - .m + - .m4s + - .mm + - .mp4 + - .png + - .rs + - .svg + - .ttf + - .wasm + - .xhtml + - .xml + - .xul + - .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..f6cc605b1e --- /dev/null +++ b/tools/lint/file-perm/__init__.py @@ -0,0 +1,43 @@ +# 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 = [] + + if platform.system() == "Windows": + # Windows doesn't have permissions in files + # Exit now + return results + + 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) + continue + + res = { + "path": f, + "message": "Execution permissions on a source file", + "level": "error", + } + results.append(result.from_config(config, **res)) + return results diff --git a/tools/lint/file-whitespace.yml b/tools/lint/file-whitespace.yml new file mode 100644 index 0000000000..278d7a40ca --- /dev/null +++ b/tools/lint/file-whitespace.yml @@ -0,0 +1,201 @@ +--- +file-whitespace: + description: File content sanity check + include: + - . + 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 + - devtools/client/shared/test/doc_inline-debugger-statement.html + # 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_jsterm.html + - devtools/shared/webconsole/test/chrome/test_object_actor_native_getters.html + - docshell/base/crashtests + - docshell/test + - dom/base/crashtests + - dom/base/test + - dom/bindings/Codegen.py + - dom/bindings/Configuration.py + - dom/bindings/parser/WebIDL.py + - dom/bindings/parser/tests/test_attributes_on_types.py + - dom/bindings/parser/tests/test_extended_attributes.py + - dom/bindings/parser/tests/test_interface.py + - dom/bindings/parser/tests/test_record.py + - dom/bindings/parser/tests/test_securecontext_extended_attribute.py + - dom/bindings/parser/tests/test_special_methods.py + - dom/bindings/parser/tests/test_toJSON.py + - dom/bindings/parser/tests/test_typedef.py + - dom/canvas/crashtests + - dom/canvas/test + - dom/events/crashtests + - dom/events/test + - 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/websocket/tests/file_websocket_wsh.py + - 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 + - gfx/vr/vrhost/vrhostapi.cpp + - image/test/crashtests + - image/test/mochitest + - image/test/reftest + - intl/lwbrk/crashtests + - 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/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/mathml/tests + - layout/mathml/crashtests + - 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 + - netwerk/dns/prepare_tlds.py + - parser/htmlparser/tests + - parser/html/java/named-character-references.html + - python/devtools/migrate-l10n/migrate/main.py + - python/l10n/convert_xul_to_fluent/convert.py + - python/l10n/convert_xul_to_fluent/lib/__init__.py + - python/l10n/convert_xul_to_fluent/lib/dtd.py + - python/l10n/convert_xul_to_fluent/lib/fluent.py + - python/l10n/convert_xul_to_fluent/lib/migration.py + - python/l10n/convert_xul_to_fluent/lib/utils.py + - python/l10n/convert_xul_to_fluent/lib/xul.py + - testing/mochitest/bisection.py + - testing/mozharness/configs/raptor/linux64_config_taskcluster.py + - testing/mozharness/configs/talos/linux64_config_taskcluster.py + - testing/mozharness/configs/web_platform_tests/test_config_windows.py + - testing/talos/talos/cmanager_base.py + - testing/talos/talos/pageloader/chrome/pageloader.xhtml + - testing/talos/talos/tests + - testing/talos/talos/unittests/conftest.py + - testing/talos/talos/unittests/test_test.py + - testing/talos/talos/unittests/test_xtalos.py + - 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/css/tools/apiclient/apiclient/__init__.py + - testing/web-platform/tests/css/tools/apiclient/apiclient/apiclient.py + - testing/web-platform/tests/css/tools/apiclient/apiclient/uritemplate.py + - testing/web-platform/tests/css/tools/apiclient/setup.py + - testing/web-platform/tests/css/tools/apiclient/test.py + - testing/web-platform/tests/css/tools/w3ctestlib/Groups.py + - testing/web-platform/tests/css/tools/w3ctestlib/HTMLSerializer.py + - testing/web-platform/tests/css/tools/w3ctestlib/Indexer.py + - testing/web-platform/tests/css/tools/w3ctestlib/OutputFormats.py + - testing/web-platform/tests/css/tools/w3ctestlib/Suite.py + - testing/web-platform/tests/css/tools/w3ctestlib/Utils.py + - testing/web-platform/tests/css/tools/w3ctestlib/__init__.py + - testing/web-platform/tests/css/tools/w3ctestlib/templates/indices.css + - testing/web-platform/tests/css/vendor-imports + - testing/web-platform/tests/html + - testing/web-platform/tests/tools/webdriver/webdriver/transport.py + - testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edgechromium.py + - testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredgechromium.py + - testing/web-platform/tests/tools/wptrunner/wptrunner/manifestupdate.py + - testing/web-platform/tests/tools/wptrunner/wptrunner/metadata.py + - testing/web-platform/tests/tools/wptrunner/wptrunner/tests/test_update.py + - testing/web-platform/tests/tools/lint/tests/dummy/broken.html + - testing/web-platform/tests/tools/lint/tests/dummy/broken_ignored.html + - toolkit/components/telemetry/build_scripts/setup.py + - toolkit/components/telemetry/tests/marionette/mach_commands.py + - toolkit/content/tests/chrome + - toolkit/mozapps/installer/windows/nsis/preprocess-locale.py + - tools/jprof/README.html + - tools/lint/eslint + - view/crashtests + - widget/cocoa/crashtests + - widget/nsFilePickerProxy.cpp + - widget/tests + - widget/windows/tests/TestUrisToValidate.h + - xpcom/reflect/xptcall/porting.html + - xpcom/reflect/xptcall/status.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 + extensions: + - .c + - .cc + - .cpp + - .css + - .dtd + - .ftl + - .h + - .html + - .py + - .properties + - .rs + - .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..f323d29db9 --- /dev/null +++ b/tools/lint/file-whitespace/__init__.py @@ -0,0 +1,120 @@ +# 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"] + + 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: + # 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: + 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") + 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: + # replace \r\n by \n + 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 diff --git a/tools/lint/flake8.yml b/tools/lint/flake8.yml new file mode 100644 index 0000000000..aff6449cec --- /dev/null +++ b/tools/lint/flake8.yml @@ -0,0 +1,13 @@ +--- +flake8: + description: Python linter + # Excludes should be added to topsrcdir/.flake8. + exclude: [] + # The configure option is used by the build system + extensions: ['configure', 'py'] + support-files: + - '**/.flake8' + - 'tools/lint/python/flake8*' + type: external + payload: python.flake8:lint + setup: python.flake8:setup diff --git a/tools/lint/hooks.py b/tools/lint/hooks.py new file mode 100755 index 0000000000..cb5c9dc2e0 --- /dev/null +++ b/tools/lint/hooks.py @@ -0,0 +1,64 @@ +#!/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/. + +from __future__ import absolute_import, print_function + +import os +import signal +import six +import subprocess +import sys +from distutils.spawn import find_executable + +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, six.binary_type): + hooktype = hooktype.decode("UTF-8", "replace") + # --quiet prevents warnings on eslint, it will be ignored by other linters + python = find_executable("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", "--quiet"] + + 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..a239eebe4c --- /dev/null +++ b/tools/lint/hooks_clang_format.py @@ -0,0 +1,95 @@ +#!/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 +from subprocess import check_output, CalledProcessError +import sys + +here = os.path.dirname(os.path.realpath(__file__)) +topsrcdir = os.path.join(here, os.pardir, os.pardir) + +EXTRA_PATHS = ( + "python/mozversioncontrol", + "python/mozbuild", + "testing/mozbase/mozfile", +) +sys.path[:0] = [os.path.join(topsrcdir, p) for p in EXTRA_PATHS] + +from mozversioncontrol import get_repository_object, InvalidRepoPath + + +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.decode().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 = ["sh", "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"] + ).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..7af4ea9243 --- /dev/null +++ b/tools/lint/hooks_js_format.py @@ -0,0 +1,80 @@ +#!/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 +from subprocess import check_output, CalledProcessError +import sys + +here = os.path.dirname(os.path.realpath(__file__)) +topsrcdir = os.path.join(here, os.pardir, os.pardir) + +EXTRA_PATHS = ( + "python/mozversioncontrol", + "python/mozbuild", + "testing/mozbase/mozfile", +) +sys.path[:0] = [os.path.join(topsrcdir, p) for p in EXTRA_PATHS] + +from mozversioncontrol import get_repository_object, InvalidRepoPath + + +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") + path_list = [] + for filename in sorted(changedFiles): + # Ignore files unsupported in eslint and prettier + if filename.decode().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"] + ).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..33c9bbcdf0 --- /dev/null +++ b/tools/lint/l10n.yml @@ -0,0 +1,39 @@ +--- +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 + - mobile/locales/en-US + - netwerk/locales/en-US + - security/manager/locales/en-US + - services/sync/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..66551c6fbb --- /dev/null +++ b/tools/lint/libpref/__init__.py @@ -0,0 +1,112 @@ +# -*- 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/. + +from __future__ import absolute_import, print_function, division +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. +} +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..ca5c6c7834 --- /dev/null +++ b/tools/lint/license.yml @@ -0,0 +1,88 @@ +--- +license: + description: License Check + include: + - . + exclude: + # These paths need to be triaged. + - build/pgo/js-input + # License not super clear + - browser/branding/ + # Trademark + - browser/components/pocket/content/panels/ + - toolkit/components/pdfjs/content/web/images/ + # We probably want a specific license + - browser/extensions/webcompat/injections/ + # 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 + # tests + - js/src/devtools/rootAnalysis/t/ + - mobile/android/components/extensions + - mobile/android/geckoview/src/main/AndroidManifest.xml + - mobile/android/geckoview/src/main/AndroidManifest_overlay.jinja + - mobile/android/geckoview/src/main/res/drawable/ic_generic_file.xml + - mobile/android/geckoview_example/src/main + # 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/browser.developer.entitlements.xml + - security/mac/hardenedruntime/browser.production.entitlements.xml + - security/mac/hardenedruntime/developer.entitlements.xml + - security/mac/hardenedruntime/plugin-container.developer.entitlements.xml + - security/mac/hardenedruntime/plugin-container.production.entitlements.xml + - security/mac/hardenedruntime/production.entitlements.xml + - servo/components/hashglobe/src/alloc.rs + - servo/components/hashglobe/src/shim.rs + - 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 + # By design + - tools/lint/test/ + extensions: + - .c + - .cc + - .cpp + - .css + - .dtd + - .ftl + - .h + - .html + - .js + - .jsm + - .jsx + - .m + - .mm + - .properties + - .py + - .rs + - .svg + - .xhtml + - .xml + - .xul + 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..46f7aa68ea --- /dev/null +++ b/tools/lint/license/__init__.py @@ -0,0 +1,191 @@ +# 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 mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) + +results = [] + +# 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 http://mozilla.org/MPL/2.0/. + """.strip().splitlines(), + "public_domain_license": """ + Any copyright is dedicated to the public domain. + http://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: + # 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", + ".js", + ".jsm", + ".jsx", + ".css", + ]: + 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 + + if ext in [".py", ".ftl", ".properties"] or filename.endswith(".inc.xul"): + for l in license_template: + license.append("# " + l.strip() + "\n") + add_header(log, filename, license) + return + + if ext in [".xml", ".xul", ".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 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 + + +def lint(paths, config, fix=None, **lintargs): + log = lintargs["log"] + files = list(expand_exclusions(paths, config, lintargs["root"])) + + 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): + 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 fix: + fix_me(log, f) + return results diff --git a/tools/lint/license/valid-licenses.txt b/tools/lint/license/valid-licenses.txt new file mode 100644 index 0000000000..d881b095d4 --- /dev/null +++ b/tools/lint/license/valid-licenses.txt @@ -0,0 +1,27 @@ +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 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..9a424de24d --- /dev/null +++ b/tools/lint/mach_commands.py @@ -0,0 +1,136 @@ +# 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 mozbuild.base import ( + BuildEnvironmentNotFoundException, + MachCommandBase, +) + + +from mach.decorators import ( + CommandArgument, + CommandProvider, + Command, +) + + +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", +] + + +def setup_argument_parser(): + from mozlint import cli + + return cli.MozlintParser() + + +def get_global_excludes(topsrcdir): + # exclude misc paths + excludes = GLOBAL_EXCLUDES[:] + + # 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) + ] + ) + + 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 + + +@CommandProvider +class MachCommands(MachCommandBase): + @Command( + "lint", + category="devenv", + description="Run linters.", + parser=setup_argument_parser, + ) + def lint(self, *runargs, **lintargs): + """Run linters.""" + self.activate_virtualenv() + from mozlint import cli, parser + + try: + buildargs = {} + buildargs["substs"] = copy.deepcopy(dict(self.substs)) + buildargs["defines"] = copy.deepcopy(dict(self.defines)) + buildargs["topobjdir"] = self.topobjdir + lintargs.update(buildargs) + except BuildEnvironmentNotFoundException: + pass + + lintargs.setdefault("root", self.topsrcdir) + lintargs["exclude"] = get_global_excludes(lintargs["root"]) + lintargs["config_paths"].insert(0, here) + lintargs["virtualenv_bin_path"] = self.virtualenv_manager.bin_path + lintargs["virtualenv_manager"] = self.virtualenv_manager + for path in EXCLUSION_FILES: + parser.GLOBAL_SUPPORT_FILES.append(os.path.join(self.topsrcdir, path)) + return cli.run(*runargs, **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( + "extra_args", + nargs=argparse.REMAINDER, + help="Extra args that will be forwarded to eslint.", + ) + def eslint(self, paths, extra_args=[], **kwargs): + self._mach_context.commands.dispatch( + "lint", + self._mach_context, + linters=["eslint"], + 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/perfdocs.yml b/tools/lint/perfdocs.yml new file mode 100644 index 0000000000..51a35e65a9 --- /dev/null +++ b/tools/lint/perfdocs.yml @@ -0,0 +1,10 @@ +--- +perfdocs: + description: Performance Documentation linter + # This task handles its own search, so just include cwd + include: ['testing/raptor', 'python/mozperftest', 'testing/talos'] + 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/framework_gatherers.py b/tools/lint/perfdocs/framework_gatherers.py new file mode 100644 index 0000000000..29d34c2d00 --- /dev/null +++ b/tools/lint/perfdocs/framework_gatherers.py @@ -0,0 +1,258 @@ +# 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 __future__ import absolute_import + +import collections +import os +import pathlib +import re + +from manifestparser import TestManifest +from mozperftest.script import ScriptInfo +from perfdocs.utils import read_yaml +from perfdocs.logger import PerfDocLogger + +""" +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): + """ + Generic initialization for a framework gatherer. + """ + self.workspace_dir = workspace_dir + self._yaml_path = yaml_path + self._suite_list = {} + self._test_list = {} + self._urls = {} + self._manifest_path = "" + self._manifest = None + self.script_infos = {} + + 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 = os.path.join(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 = {"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([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_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 + """ + test_manifest = TestManifest([manifest_path], strict=False) + test_list = test_manifest.active_tests(exists=False, disabled=False) + subtest_list = {} + for subtest in test_list: + subtest_list[subtest["name"]] = subtest["manifest"] + self._urls[subtest["name"]] = { + "type": suite_name, + "url": subtest["test_url"], + } + + self._urls = collections.OrderedDict( + sorted(self._urls.items(), key=lambda t: len(t[0])) + ) + + return subtest_list + + 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 i, manifest_path in enumerate(manifest_paths, 1): + subtest_list = self._get_subtests_from_ini(manifest_path, suite_name) + self._test_list[suite_name].update(subtest_list) + + return self._test_list + + def build_test_description(self, title, test_description="", suite_name=""): + matcher = set() + for name, val in self._urls.items(): + if title == name and suite_name == val["type"]: + matcher.add(val["url"]) + break + + if len(matcher) == 0: + for name, val in self._urls.items(): + if title in name and suite_name == val["type"]: + matcher.add(val["url"]) + break + + return [ + "* `" + + title + + " (" + + test_description + + ") " + + "<" + + matcher.pop() + + ">`__" + ] + + 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.ini manifest. + + :return dict: A dictionary with the following structure: { + "suite_name": { + 'perftest_test1', + 'perftest_test2', + }, + } + """ + for path in pathlib.Path(self.workspace_dir).rglob("perftest.ini"): + suite_name = re.sub(self.workspace_dir, "", os.path.dirname(path)) + + # 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.ini + 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, {}).update( + {si["name"]: 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): + """ + Gatherer for the Talos framework. + TODO - Bug 1674220 + """ + + pass diff --git a/tools/lint/perfdocs/gatherer.py b/tools/lint/perfdocs/gatherer.py new file mode 100644 index 0000000000..2f6a0b5e91 --- /dev/null +++ b/tools/lint/perfdocs/gatherer.py @@ -0,0 +1,142 @@ +# 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 __future__ import absolute_import + +import os +import pathlib +import re + +from perfdocs.logger import PerfDocLogger +from perfdocs.utils import read_yaml +from perfdocs.framework_gatherers import ( + RaptorGatherer, + MozperftestGatherer, + TalosGatherer, +) + +logger = PerfDocLogger() + +# TODO: Implement decorator/searcher to find the classes. +frameworks = { + "raptor": RaptorGatherer, + "mozperftest": MozperftestGatherer, + "Talos": TalosGatherer, +} + + +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): + """ + Initialzie the Gatherer. + + :param str workspace_dir: Path to the gecko checkout. + """ + self.workspace_dir = workspace_dir + 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 = ["tools/lint", ".hg", "testing/perfdocs"] + + for path in pathlib.Path(self.workspace_dir).rglob("perfdocs"): + if any(re.search(d, str(path)) 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.endswith(".rst"): + 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 = os.path.join(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": {}, + } + + # Get and then store the frameworks tests + self.framework_gatherers[framework["name"]] = frameworks[framework["name"]]( + framework["yml_path"], self.workspace_dir + ) + + if not yaml_content["static-only"]: + framework["test_list"] = self.framework_gatherers[ + framework["name"] + ].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..6d3c99d00b --- /dev/null +++ b/tools/lint/perfdocs/generator.py @@ -0,0 +1,251 @@ +# 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 __future__ import absolute_import + +import os +import re +import shutil +import tempfile + +from perfdocs.logger import PerfDocLogger +from perfdocs.utils import are_dirs_equal, 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 = os.path.join( + self._workspace, "tools", "lint", "perfdocs", "templates" + ) + self.perfdocs_path = os.path.join( + 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(os.path.join(framework["path"], framework["yml"])) + rst_content = read_file( + os.path.join(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()): + documentation.extend( + self._verifier._gatherer.framework_gatherers[ + yaml_content["name"] + ].build_test_description( + test_name, tests[test_name], suite_name + ) + ) + documentation.append("") + + # Insert documentation into `.rst` file + framework_rst = re.sub( + r"{documentation}", os.linesep.join(documentation), rst_content + ) + frameworks_info[yaml_content["name"]] = { + "dynamic": framework_rst, + "static": [], + } + + # For static `.rst` file + for static_file in framework["static"]: + frameworks_info[yaml_content["name"]]["static"].append( + { + "file": static_file, + "content": read_file( + os.path.join(framework["path"], static_file), stringify=True + ), + } + ) + + 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 = tempfile.mkdtemp() + perfdocs_tmpdir = os.path.join(tmpdir, "generated") + os.mkdir(perfdocs_tmpdir) + except OSError as e: + logger.critical("Error creating temp file: {}".format(e)) + + success = False or os.path.isdir(perfdocs_tmpdir) + if success: + return perfdocs_tmpdir + else: + return success + + 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"], + os.path.join(perfdocs_tmpdir, framework_name), + ) + + for static_name in framework_docs[framework_name]["static"]: + save_file( + static_name["content"], + os.path.join(perfdocs_tmpdir, static_name["file"].split(".")[0]), + ) + + # Get the main page and add the framework links to it + mainpage = read_file( + os.path.join(self.templates_path, "index.rst"), stringify=True + ) + fmt_frameworks = os.linesep.join( + [" * :doc:`%s`" % name for name in frameworks] + ) + fmt_mainpage = re.sub(r"{test_documentation}", fmt_frameworks, mainpage) + save_file(fmt_mainpage, os.path.join(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 os.path.exists(self.perfdocs_path): + shutil.rmtree(self.perfdocs_path) + + try: + saved = shutil.copytree(perfdocs_tmpdir, self.perfdocs_path) + if saved: + logger.log( + "Documentation saved to {}/".format( + re.sub(".*testing", "testing", 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( + [ + os.path.join(entry["path"], entry["yml"]), + os.path.join(entry["path"], entry["rst"]), + ] + ) + return files + + # Throw a warning if there's no need for generating + if not os.path.exists(self.perfdocs_path) 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.", + files=get_possibly_changed_files(), + ) diff --git a/tools/lint/perfdocs/logger.py b/tools/lint/perfdocs/logger.py new file mode 100644 index 0000000000..ff6321a7f1 --- /dev/null +++ b/tools/lint/perfdocs/logger.py @@ -0,0 +1,82 @@ +# 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 __future__ import absolute_import + +import re + + +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): + """ + 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. + """ + if type(files) != list: + files = [files] + + # 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 = re.sub(PerfDocLogger.TOP_DIR, "", file) + + # Filter out any issues that do not relate to the paths + # that are being linted + for path in PerfDocLogger.PATHS: + if path not in file: + continue + + # Output error entry + self.logger.lint_error( + message=msg, + lineno=0, + column=None, + path=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..3df01595c2 --- /dev/null +++ b/tools/lint/perfdocs/perfdocs.py @@ -0,0 +1,79 @@ +# 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 __future__ import absolute_import, print_function + +import os +import re + + +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 + + top_dir = os.environ.get("WORKSPACE", None) + if not top_dir: + floc = os.path.abspath(__file__) + top_dir = floc.split("tools")[0] + top_dir = top_dir.replace("\\", "\\\\") + + PerfDocLogger.LOGGER = logger + PerfDocLogger.TOP_DIR = top_dir + + # Convert all the paths to relative ones + rel_paths = [re.sub(top_dir, "", path) for path in paths] + PerfDocLogger.PATHS = rel_paths + + target_dir = [os.path.join(top_dir, i) for i in rel_paths] + for path in target_dir: + if not os.path.exists(path): + raise Exception("Cannot locate directory at %s" % path) + + # 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) + 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..cd6e258980 --- /dev/null +++ b/tools/lint/perfdocs/templates/index.rst @@ -0,0 +1,14 @@ +################### +Performance Testing +################### + +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, see each projects' documentation: + +{test_documentation} + +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..99b304260b --- /dev/null +++ b/tools/lint/perfdocs/utils.py @@ -0,0 +1,83 @@ +# 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 __future__ import absolute_import + +import filecmp +import os +import yaml +from perfdocs.logger import PerfDocLogger + +logger = PerfDocLogger() + + +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. + """ + with open("{}.{}".format(path, extension), "w") as f: + f.write(file_content) + + +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 open(path, "r") 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 open(yaml_path, "r") as f: + contents = yaml.safe_load(f) + except Exception as e: + logger.warning("Error opening file {}: {}".format(yaml_path, str(e)), 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(dir_1, dir_2) + if dirs_cmp.left_only or dirs_cmp.right_only or dirs_cmp.funny_files: + return False + + _, mismatch, errors = filecmp.cmpfiles( + dir_1, dir_2, dirs_cmp.common_files, shallow=False + ) + + if mismatch or errors: + return False + + for common_dir in dirs_cmp.common_dirs: + subdir_1 = os.path.join(dir_1, common_dir) + subdir_2 = os.path.join(dir_2, common_dir) + if not are_dirs_equal(subdir_1, subdir_2): + return False + + return True diff --git a/tools/lint/perfdocs/verifier.py b/tools/lint/perfdocs/verifier.py new file mode 100644 index 0000000000..a9a34478eb --- /dev/null +++ b/tools/lint/perfdocs/verifier.py @@ -0,0 +1,310 @@ +# 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 __future__ import absolute_import + +import jsonschema +import os +import re + +from perfdocs.logger import PerfDocLogger +from perfdocs.utils import read_file, read_yaml +from perfdocs.gatherer import Gatherer + +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.ini +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 = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "manifest": {"type": "string"}, + "static-only": {"type": "boolean"}, + "suites": { + "type": "object", + "properties": { + "suite_name": { + "type": "object", + "properties": { + "tests": { + "type": "object", + "properties": {"test_name": {"type": "string"}}, + }, + "description": {"type": "string"}, + }, + "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): + """ + Initialize the Verifier. + + :param str workspace_dir: Path to the top-level checkout directory. + """ + self.workspace_dir = workspace_dir + self._gatherer = Gatherer(workspace_dir) + + 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. + + :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 + global_descriptions = {} + 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): + global_descriptions[suite] = [] + if not ytests.get("tests"): + # It's possible a suite entry has no tests + continue + + # Suite found - now check if any tests in YAML + # definitions doesn't exist + ytests = ytests["tests"] + for test_name in ytests: + foundtest = False + for t in framework_info["test_list"][suite]: + tb = os.path.basename(t) + tb = re.sub("\..*", "", tb) + if test_name == tb: + # Found an exact match for the test_name + foundtest = True + break + 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[suite].append(test_name) + 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"], + ) + else: + logger.warning( + "Could not find an existing suite for {} - bad suite name?".format( + suite + ), + framework_info["yml_path"], + ) + + # 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 + + # 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: + continue + + tests_found = 0 + missing_tests = [] + test_to_manifest = {} + for test_name, manifest_path in test_list.items(): + 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 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 = os.path.join(self.workspace_dir, desc) + if os.path.exists(desc_path) and os.path.isfile(desc_path): + with open(desc_path, "r") as f: + desc = f.readlines() + 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( + "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 = os.path.join(matched["path"], matched["yml"]) + matched_rst = os.path.join(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/py2.yml b/tools/lint/py2.yml new file mode 100644 index 0000000000..f435e39d1e --- /dev/null +++ b/tools/lint/py2.yml @@ -0,0 +1,57 @@ +--- +py2: + description: Python 2 compatibility check + include: ['.'] + exclude: + - build + - dom + - editor + - gfx + - ipc + - js/src + - layout + - modules + - mozglue + - netwerk + - nsprpub + - other-licenses + - python/mozbuild/mozbuild/fork_interpose.py + - security + - servo + - taskcluster/docker/funsize-update-generator + - taskcluster/docker/visual-metrics + - testing/condprofile + - testing/gtest + - testing/mochitest + - testing/mozharness + - testing/raptor + - testing/tools + - testing/web-platform + - toolkit + - tools/update-packaging + - xpcom + + # These paths are intentionally excluded (Python 3 only) + - config/create_rc.py + - config/create_res.py + - config/printconfigsetting.py + - python/mozbuild/mozbuild/action/unify_symbols.py + - python/mozbuild/mozbuild/action/unify_tests.py + - python/mozbuild/mozbuild/html_build_viewer.py + - python/mozbuild/mozpack/unify.py + - python/mozbuild/mozpack/test/test_unify.py + - python/mozlint + - python/mozperftest + - python/mozrelease/mozrelease/partner_repack.py + - taskcluster/test + - testing/performance + - tools/crashreporter/system-symbols/win/symsrv-fetch.py + - tools/github-sync + - tools/lint + - tools/tryselect + extensions: ['py'] + support-files: + - 'tools/lint/python/*compat*' + type: external + payload: python.compat:lintpy2 + setup: python.compat:setuppy2 diff --git a/tools/lint/py3.yml b/tools/lint/py3.yml new file mode 100644 index 0000000000..709c4e344b --- /dev/null +++ b/tools/lint/py3.yml @@ -0,0 +1,27 @@ +--- +py3: + description: Python 3 compatibility check + include: ['.'] + exclude: + - browser/app + - build + - dom/canvas/test + - gfx + - ipc/ipdl + - layout/style/ServoCSSPropList.mako.py + - security/manager/ssl + - testing/awsy + - testing/condprofile/condprof/android.py + - testing/condprofile/condprof/desktop.py + - testing/gtest + - testing/mozharness + - testing/tps + - testing/web-platform/tests + - toolkit + - xpcom/idl-parser + extensions: ['py'] + support-files: + - 'tools/lint/python/*compat*' + type: external + payload: python.compat:lintpy3 + setup: python.compat:setuppy3 diff --git a/tools/lint/pylint.yml b/tools/lint/pylint.yml new file mode 100644 index 0000000000..62c8b1a324 --- /dev/null +++ b/tools/lint/pylint.yml @@ -0,0 +1,26 @@ +--- +pylint: + description: A second Python linter + include: + - configure.py + - client.py + - security/ + - accessible/ + - docs/ + - dom/base/ + - dom/websocket/ + - mozglue/ + - toolkit/components/telemetry/ + exclude: + - dom/bindings/Codegen.py + - dom/bindings/Configuration.py + - security/manager/tools/crtshToIdentifyingStruct/crtshToIdentifyingStruct.py + - security/manager/ssl/tests/unit/test_content_signing/pysign.py + - security/ct/tests/gtest/createSTHTestData.py + extensions: ['py'] + support-files: + - '**/.pylint' + - 'tools/lint/python/pylint*' + type: external + payload: python.pylint:lint + setup: python.pylint:setup 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..2fe58a0ea5 --- /dev/null +++ b/tools/lint/python/black.py @@ -0,0 +1,141 @@ +# 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 __future__ import absolute_import, print_function + +import os +import platform +import re +import signal +import subprocess +import sys + +from mozlint import result +from mozlint.pathutils import expand_exclusions +from mozprocess import ProcessHandler + +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 + + return re.match(r"black, version (.*)$", output)[1] + + +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 line in output: + line = line.decode("utf-8") + 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("Unhandled line", line) + return results + + +class BlackProcess(ProcessHandler): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["stream"] = False + ProcessHandler.__init__(self, *args, **kwargs) + + def run(self, *args, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + ProcessHandler.run(self, *args, **kwargs) + signal.signal(signal.SIGINT, orig) + + +def run_process(config, cmd): + proc = BlackProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.output + + +def setup(root, **lintargs): + 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): + 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))) + return parse_issues(config, run_process(config, base_command), paths, log=log) + + +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..52ee1d9aa1 --- /dev/null +++ b/tools/lint/python/black_requirements.in @@ -0,0 +1 @@ +black==20.8b1 diff --git a/tools/lint/python/black_requirements.txt b/tools/lint/python/black_requirements.txt new file mode 100644 index 0000000000..16f6e00fe4 --- /dev/null +++ b/tools/lint/python/black_requirements.txt @@ -0,0 +1,87 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --generate-hashes --output-file=tools/lint/python/black_requirements.txt tools/lint/python/black_requirements.in +# +# MANUAL EDIT - for some reasons, dataclasses isn't added. Do it by hand: +# hashin -r tools/lint/python/black_requirements.txt dataclasses==0.6 +# +appdirs==1.4.4 \ + --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ + --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 \ + # via black +black==20.8b1 \ + --hash=sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea \ + --hash=sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b \ + # via -r tools/lint/python/black_requirements.in +click==7.1.2 \ + --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \ + --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ + # via black +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 \ + # via black +pathspec==0.8.0 \ + --hash=sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0 \ + --hash=sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061 \ + # via black +regex==2020.7.14 \ + --hash=sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204 \ + --hash=sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162 \ + --hash=sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f \ + --hash=sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb \ + --hash=sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6 \ + --hash=sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7 \ + --hash=sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88 \ + --hash=sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99 \ + --hash=sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644 \ + --hash=sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a \ + --hash=sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840 \ + --hash=sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067 \ + --hash=sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd \ + --hash=sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4 \ + --hash=sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e \ + --hash=sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89 \ + --hash=sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e \ + --hash=sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc \ + --hash=sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf \ + --hash=sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341 \ + --hash=sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7 \ + # via black +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 \ + # via black +typed-ast==1.4.1 \ + --hash=sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355 \ + --hash=sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919 \ + --hash=sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa \ + --hash=sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652 \ + --hash=sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75 \ + --hash=sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01 \ + --hash=sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d \ + --hash=sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1 \ + --hash=sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907 \ + --hash=sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c \ + --hash=sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3 \ + --hash=sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b \ + --hash=sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614 \ + --hash=sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb \ + --hash=sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b \ + --hash=sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41 \ + --hash=sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6 \ + --hash=sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34 \ + --hash=sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe \ + --hash=sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4 \ + --hash=sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7 \ + # via black +typing-extensions==3.7.4.3 \ + --hash=sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918 \ + --hash=sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c \ + --hash=sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f \ + # via black +dataclasses==0.6 \ + --hash=sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f \ + --hash=sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84 diff --git a/tools/lint/python/check_compat.py b/tools/lint/python/check_compat.py new file mode 100755 index 0000000000..25a15fcedc --- /dev/null +++ b/tools/lint/python/check_compat.py @@ -0,0 +1,87 @@ +#!/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 ast +import json +import sys + + +def parse_file(f): + with open(f, "rb") as fh: + content = fh.read() + try: + return ast.parse(content) + except SyntaxError as e: + err = { + "path": f, + "message": e.msg, + "lineno": e.lineno, + "column": e.offset, + "source": e.text, + "rule": "is-parseable", + } + print(json.dumps(err)) + + +def check_compat_py2(f): + """Check Python 2 and Python 3 compatibility for a file with Python 2""" + root = parse_file(f) + + # Ignore empty or un-parseable files. + if not root or not root.body: + return + + futures = set() + haveprint = False + future_lineno = 1 + may_have_relative_imports = False + for node in ast.walk(root): + if isinstance(node, ast.ImportFrom): + if node.module == "__future__": + future_lineno = node.lineno + futures |= set(n.name for n in node.names) + else: + may_have_relative_imports = True + elif isinstance(node, ast.Import): + may_have_relative_imports = True + elif isinstance(node, ast.Print): + haveprint = True + + err = { + "path": f, + "lineno": future_lineno, + "column": 1, + } + + if "absolute_import" not in futures and may_have_relative_imports: + err["rule"] = "require absolute_import" + err["message"] = "Missing from __future__ import absolute_import" + print(json.dumps(err)) + + if haveprint and "print_function" not in futures: + err["rule"] = "require print_function" + err["message"] = "Missing from __future__ import print_function" + print(json.dumps(err)) + + +def check_compat_py3(f): + """Check Python 3 compatibility of a file with Python 3.""" + parse_file(f) + + +if __name__ == "__main__": + if sys.version_info[0] == 2: + fn = check_compat_py2 + else: + fn = check_compat_py3 + + manifest = sys.argv[1] + with open(manifest, "r") as fh: + files = fh.read().splitlines() + + for f in files: + fn(f) + + sys.exit(0) diff --git a/tools/lint/python/compat.py b/tools/lint/python/compat.py new file mode 100644 index 0000000000..dd541bd66e --- /dev/null +++ b/tools/lint/python/compat.py @@ -0,0 +1,91 @@ +# 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 distutils.spawn import find_executable + +import mozfile +from mozprocess import ProcessHandlerMixin + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) + +results = [] + + +class PyCompatProcess(ProcessHandlerMixin): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["processOutputLine"] = [self.process_line] + ProcessHandlerMixin.__init__(self, *args, **kwargs) + + def process_line(self, line): + try: + res = json.loads(line) + except ValueError: + print( + "Non JSON output from {} linter: {}".format(self.config["name"], line) + ) + return + + res["level"] = "error" + results.append(result.from_config(self.config, **res)) + + +def setup(python): + """Setup doesn't currently do any bootstrapping. For now, this function + is only used to print the warning message. + """ + binary = find_executable(python) + if not binary: + # TODO Bootstrap python2/python3 if not available + print("warning: {} not detected, skipping py-compat check".format(python)) + + +def run_linter(python, paths, config, **lintargs): + log = lintargs["log"] + binary = find_executable(python) + if not binary: + # If we're in automation, this is fatal. Otherwise, the warning in the + # setup method was already printed. + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + files = expand_exclusions(paths, config, lintargs["root"]) + + with mozfile.NamedTemporaryFile(mode="w") as fh: + fh.write("\n".join(files)) + fh.flush() + + cmd = [binary, os.path.join(here, "check_compat.py"), fh.name] + log.debug("Command: {}".format(" ".join(cmd))) + + proc = PyCompatProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return results + + +def setuppy2(**lintargs): + return setup("python2") + + +def lintpy2(*args, **kwargs): + return run_linter("python2", *args, **kwargs) + + +def setuppy3(**lintargs): + return setup("python3") + + +def lintpy3(*args, **kwargs): + return run_linter("python3", *args, **kwargs) diff --git a/tools/lint/python/flake8.py b/tools/lint/python/flake8.py new file mode 100644 index 0000000000..49ab04b019 --- /dev/null +++ b/tools/lint/python/flake8.py @@ -0,0 +1,191 @@ +# 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 subprocess +import sys + +import mozfile +import mozpack.path as mozpath + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) +FLAKE8_REQUIREMENTS_PATH = os.path.join(here, "flake8_requirements.txt") + +FLAKE8_NOT_FOUND = """ +Could not find flake8! Install flake8 and try again. + + $ pip install -U --require-hashes -r {} +""".strip().format( + FLAKE8_REQUIREMENTS_PATH +) + + +FLAKE8_INSTALL_ERROR = """ +Unable to install correct version of flake8 +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + FLAKE8_REQUIREMENTS_PATH +) + +LINE_OFFSETS = { + # continuation line under-indented for hanging indent + "E121": (-1, 2), + # continuation line missing indentation or outdented + "E122": (-1, 2), + # continuation line over-indented for hanging indent + "E126": (-1, 2), + # continuation line over-indented for visual indent + "E127": (-1, 2), + # continuation line under-indented for visual indent + "E128": (-1, 2), + # continuation line unaligned for hanging indend + "E131": (-1, 2), + # expected 1 blank line, found 0 + "E301": (-1, 2), + # expected 2 blank lines, found 1 + "E302": (-2, 3), +} +"""Maps a flake8 error to a lineoffset tuple. + +The offset is of the form (lineno_offset, num_lines) and is passed +to the lineoffset property of an `Issue`. +""" + + +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") + + +class NothingToLint(Exception): + """Exception used to bail out of flake8's internals if all the specified + files were excluded. + """ + + +def setup(root, **lintargs): + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements( + FLAKE8_REQUIREMENTS_PATH, quiet=True + ) + except subprocess.CalledProcessError: + print(FLAKE8_INSTALL_ERROR) + return 1 + + +def lint(paths, config, **lintargs): + from flake8.main.application import Application + + log = lintargs["log"] + root = lintargs["root"] + virtualenv_bin_path = lintargs.get("virtualenv_bin_path") + config_path = os.path.join(root, ".flake8") + + if lintargs.get("fix"): + fix_cmd = [ + os.path.join(virtualenv_bin_path or default_bindir(), "autopep8"), + "--global-config", + config_path, + "--in-place", + "--recursive", + ] + + if config.get("exclude"): + fix_cmd.extend(["--exclude", ",".join(config["exclude"])]) + + subprocess.call(fix_cmd + paths) + + # Run flake8. + app = Application() + log.debug("flake8 version={}".format(app.version)) + + output_file = mozfile.NamedTemporaryFile(mode="r") + flake8_cmd = [ + "--config", + config_path, + "--output-file", + output_file.name, + "--format", + '{"path":"%(path)s","lineno":%(row)s,' + '"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}', + "--filename", + ",".join(["*.{}".format(e) for e in config["extensions"]]), + ] + log.debug("Command: {}".format(" ".join(flake8_cmd))) + + orig_make_file_checker_manager = app.make_file_checker_manager + + def wrap_make_file_checker_manager(self): + """Flake8 is very inefficient when it comes to applying exclusion + rules, using `expand_exclusions` to turn directories into a list of + relevant python files is an order of magnitude faster. + + Hooking into flake8 here also gives us a convenient place to merge the + `exclude` rules specified in the root .flake8 with the ones added by + tools/lint/mach_commands.py. + """ + # Ignore exclude rules if `--no-filter` was passed in. + config.setdefault("exclude", []) + if lintargs.get("use_filters", True): + config["exclude"].extend(map(mozpath.normpath, self.options.exclude)) + + # Since we use the root .flake8 file to store exclusions, we haven't + # properly filtered the paths through mozlint's `filterpaths` function + # yet. This mimics that though there could be other edge cases that are + # different. Maybe we should call `filterpaths` directly, though for + # now that doesn't appear to be necessary. + filtered = [ + p for p in paths if not any(p.startswith(e) for e in config["exclude"]) + ] + + self.args = self.args + list(expand_exclusions(filtered, config, root)) + + if not self.args: + raise NothingToLint + return orig_make_file_checker_manager() + + app.make_file_checker_manager = wrap_make_file_checker_manager.__get__( + app, Application + ) + + # Make sure to run from repository root so exclusions are joined to the + # repository root and not the current working directory. + oldcwd = os.getcwd() + os.chdir(root) + try: + app.run(flake8_cmd) + except NothingToLint: + pass + finally: + os.chdir(oldcwd) + + results = [] + + def process_line(line): + # Escape slashes otherwise JSON conversion will not work + line = line.replace("\\", "\\\\") + try: + res = json.loads(line) + except ValueError: + print("Non JSON output from linter, will not be processed: {}".format(line)) + return + + if res.get("code") in LINE_OFFSETS: + res["lineoffset"] = LINE_OFFSETS[res["code"]] + + results.append(result.from_config(config, **res)) + + list(map(process_line, output_file.readlines())) + return results diff --git a/tools/lint/python/flake8_requirements.txt b/tools/lint/python/flake8_requirements.txt new file mode 100644 index 0000000000..2a691e6edf --- /dev/null +++ b/tools/lint/python/flake8_requirements.txt @@ -0,0 +1,28 @@ +flake8==3.8.4 \ + --hash=sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839 \ + --hash=sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b +mccabe==0.6.1 \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f +pyflakes==2.2.0 \ + --hash=sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92 \ + --hash=sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8 +pycodestyle==2.6.0 \ + --hash=sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367 \ + --hash=sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e +setuptools==47.3.1 \ + --hash=sha256:4ba6f9789ea243a6b8ba57da81f75a53494456117810436fd9277a74d1c915d1 \ + --hash=sha256:843037738d1e34e8b326b5e061f474aca6ef9d7ece41329afbc8aac6195a3920 +autopep8==1.5.3 \ + --hash=sha256:60fd8c4341bab59963dafd5d2a566e94f547e660b9b396f772afe67d8481dbf0 +entrypoints==0.3 \ + --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \ + --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 +toml==0.10.1 \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +importlib-metadata==1.6.1 \ + --hash=sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545 \ + --hash=sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958 +zipp==3.1.0 \ + --hash=sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b \ + --hash=sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96 diff --git a/tools/lint/python/l10n_lint.py b/tools/lint/python/l10n_lint.py new file mode 100644 index 0000000000..fb79d41a2f --- /dev/null +++ b/tools/lint/python/l10n_lint.py @@ -0,0 +1,158 @@ +# 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 datetime import datetime, timedelta +import os + +from mozboot import util as mb_util +from mozlint import result, pathutils +from mozpack import path as mozpath +import mozversioncontrol.repoupdate + +from compare_locales.lint.linter import L10nLinter +from compare_locales.lint.util import l10n_base_reference_and_tests +from compare_locales import parser +from compare_locales.paths import TOMLParser, ProjectFiles + + +LOCALE = "gecko-strings" + + +PULL_AFTER = timedelta(days=2) + + +def lint(paths, lintconfig, **lintargs): + l10n_base = mb_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) + + # 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(LOCALE, 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 + + +def gecko_strings_setup(**lint_args): + gs = mozpath.join(mb_util.get_state_dir(), LOCALE) + 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 + hg = mozversioncontrol.get_tool_path("hg") + mozversioncontrol.repoupdate.update_mercurial_repo( + hg, "https://hg.mozilla.org/l10n/gecko-strings", gs + ) + with open(marker, "w") as fh: + fh.flush() + + +def load_configs(lintconfig, root, l10n_base): + """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/pylint.py b/tools/lint/python/pylint.py new file mode 100644 index 0000000000..ba95be1f0b --- /dev/null +++ b/tools/lint/python/pylint.py @@ -0,0 +1,137 @@ +# 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 signal + +from mozprocess import ProcessHandler + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) +PYLINT_REQUIREMENTS_PATH = os.path.join(here, "pylint_requirements.txt") + +PYLINT_NOT_FOUND = """ +Could not find pylint! Install pylint and try again. + + $ pip install -U --require-hashes -r {} +""".strip().format( + PYLINT_REQUIREMENTS_PATH +) + + +PYLINT_INSTALL_ERROR = """ +Unable to install correct version of pylint +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + PYLINT_REQUIREMENTS_PATH +) + + +class PylintProcess(ProcessHandler): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["stream"] = False + kwargs["universal_newlines"] = True + ProcessHandler.__init__(self, *args, **kwargs) + + def run(self, *args, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + ProcessHandler.run(self, *args, **kwargs) + signal.signal(signal.SIGINT, orig) + + +def setup(root, **lintargs): + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements( + PYLINT_REQUIREMENTS_PATH, + quiet=True, + # The defined versions of astroid and lazy-object-proxy conflict and fail to + # install with the new 2020 pip resolver (bug 1682959) + legacy_resolver=True, + ) + except subprocess.CalledProcessError: + print(PYLINT_INSTALL_ERROR) + return 1 + + +def get_pylint_binary(): + return "pylint" + + +def run_process(config, cmd): + proc = PylintProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.output + + +def parse_issues(log, config, issues_json, path): + results = [] + + try: + issues = json.loads(issues_json) + except json.decoder.JSONDecodeError: + log.debug("Could not parse the output:") + log.debug("pylint output: {}".format(issues_json)) + return [] + + for issue in issues: + res = { + "path": issue["path"], + "level": issue["type"], + "lineno": issue["line"], + "column": issue["column"], + "message": issue["message"], + "rule": issue["message-id"], + } + results.append(result.from_config(config, **res)) + return results + + +def get_pylint_version(binary): + return subprocess.check_output( + [binary, "--version"], + universal_newlines=True, + stderr=subprocess.STDOUT, + ) + + +def lint(paths, config, **lintargs): + log = lintargs["log"] + + binary = get_pylint_binary() + + log = lintargs["log"] + paths = list(expand_exclusions(paths, config, lintargs["root"])) + + cmd_args = [binary] + results = [] + + # list from https://code.visualstudio.com/docs/python/linting#_pylint + # And ignore a bit more elements + cmd_args += [ + "-fjson", + "--disable=all", + "--enable=F,E,unreachable,duplicate-key,unnecessary-semicolon,global-variable-not-assigned,unused-variable,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode,no-else-return", # NOQA: E501 + "--disable=import-error,no-member", + ] + + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + log.debug("pylint version: {}".format(get_pylint_version(binary))) + output = " ".join(run_process(config, base_command)) + results = parse_issues(log, config, str(output), []) + + return results diff --git a/tools/lint/python/pylint_requirements.txt b/tools/lint/python/pylint_requirements.txt new file mode 100644 index 0000000000..c5e8d15723 --- /dev/null +++ b/tools/lint/python/pylint_requirements.txt @@ -0,0 +1,64 @@ +pylint==2.6.0 \ + --hash=sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210 \ + --hash=sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +mccabe==0.6.1 \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +wrapt==1.12.1 \ + --hash=sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7 +lazy-object-proxy==1.5.0 \ + --hash=sha256:0aef3fa29f7d1194d6f8a99382b1b844e5a14d3bc1ef82c3b1c4fb7e7e2019bc \ + --hash=sha256:159ae2bbb4dc3ba506aeba868d14e56a754c0be402d1f0d7fdb264e0bdf2b095 \ + --hash=sha256:161a68a427022bf13e249458be2cb8da56b055988c584d372a917c665825ae9a \ + --hash=sha256:2d58f0e6395bf41087a383a48b06b42165f3b699f1aa41ba201db84ab77be63d \ + --hash=sha256:311c9d1840042fc8e2dd80fc80272a7ea73e7646745556153c9cda85a4628b18 \ + --hash=sha256:35c3ad7b7f7d5d4a54a80f0ff5a41ab186237d6486843f8dde00c42cfab33905 \ + --hash=sha256:459ef557e669d0046fe2b92eb4822c097c00b5ef9d11df0f9bd7d4267acdfc52 \ + --hash=sha256:4a50513b6be001b9b7be2c435478fe9669249c77c241813907a44cda1fcd03f4 \ + --hash=sha256:51035b175740c44707694c521560b55b66da9d5a7c545cf22582bc02deb61664 \ + --hash=sha256:96f2cdb35bdfda10e075f12892a42cff5179bbda698992b845f36c5e92755d33 \ + --hash=sha256:a0aed261060cd0372abf08d16399b1224dbb5b400312e6b00f2b23eabe1d4e96 \ + --hash=sha256:a6052c4c7d95de2345d9c58fc0fe34fff6c27a8ed8550dafeb18ada84406cc99 \ + --hash=sha256:cbf1354292a4f7abb6a0188f74f5e902e4510ebad105be1dbc4809d1ed92f77e \ + --hash=sha256:da82b2372f5ded8806eaac95b19af89a7174efdb418d4e7beb0c6ab09cee7d95 \ + --hash=sha256:dd89f466c930d7cfe84c94b5cbe862867c88b269f23e5aa61d40945e0d746f54 \ + --hash=sha256:e3183fbeb452ec11670c2d9bfd08a57bc87e46856b24d1c335f995239bedd0e1 \ + --hash=sha256:e9a571e7168076a0d5ecaabd91e9032e86d815cca3a4bf0dafead539ef071aa5 \ + --hash=sha256:ec6aba217d0c4f71cbe48aea962a382dedcd111f47b55e8b58d4aaca519bd360 +astroid==2.4.2 \ + --hash=sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703 \ + --hash=sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386 +isort==4.3.21 \ + --hash=sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1 \ + --hash=sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd +typed-ast==1.4.1 \ + --hash=sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355 \ + --hash=sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919 \ + --hash=sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa \ + --hash=sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652 \ + --hash=sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75 \ + --hash=sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01 \ + --hash=sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d \ + --hash=sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1 \ + --hash=sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907 \ + --hash=sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c \ + --hash=sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3 \ + --hash=sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b \ + --hash=sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614 \ + --hash=sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb \ + --hash=sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b \ + --hash=sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41 \ + --hash=sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6 \ + --hash=sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34 \ + --hash=sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe \ + --hash=sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4 \ + --hash=sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7 +colorama==0.4.3; sys_platform == "win32" \ + --hash=sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff \ + --hash=sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1 diff --git a/tools/lint/rejected-words.yml b/tools/lint/rejected-words.yml new file mode 100644 index 0000000000..39f3392167 --- /dev/null +++ b/tools/lint/rejected-words.yml @@ -0,0 +1,484 @@ +--- +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 + # same as codespell + extensions: + - js + - jsm + - jxs + - xml + - html + - xhtml + - cpp + - c + - h + - configure + - py + - properties + - rst + - md + - ftl + - yml + exclude: + - 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/siteIdentity/browser_no_mcb_for_loopback.js + - browser/base/content/test/siteIdentity/browser_no_mcb_for_onions.js + - browser/base/content/test/static/browser_all_files_referenced.js + - browser/base/content/test/static/browser_misused_characters_in_strings.js + - browser/base/content/test/static/browser_parsable_css.js + - browser/base/content/test/static/browser_parsable_script.js + - browser/base/content/test/tabMediaIndicator/browser_mute_webAudio.js + - browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js + - browser/components/downloads/content/contentAreaDownloadsView.xhtml + - browser/components/enterprisepolicies/helpers/WebsiteFilter.jsm + - browser/components/migration/ChromeMigrationUtils.jsm + - browser/components/migration/ChromeProfileMigrator.jsm + - browser/components/newtab/data/content/activity-stream.bundle.js + - browser/components/ion/content/ion.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/ContentSessionStore.jsm + - browser/components/sessionstore/test/browser_crashedTabs.js + - browser/components/uitour/UITourChild.jsm + - 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/formautofill/FormAutofillSync.jsm + - browser/extensions/screenshots/background/main.js + - browser/modules/SitePermissions.jsm + - browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm + - 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.jsm + - docshell/test/unit/test_URIFixup_info.js + - dom/base/Document.cpp + - dom/base/MaybeCrossOriginObject.cpp + - dom/base/MutationObservers.cpp + - dom/base/nsContentUtils.cpp + - dom/base/nsContentUtils.h + - dom/base/nsDataDocumentContentPolicy.cpp + - dom/base/nsGlobalWindowOuter.cpp + - dom/base/nsGlobalWindowOuter.h + - 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_isHandlingUserInput.xhtml + - dom/bindings/Codegen.py + - dom/bindings/parser/WebIDL.py + - dom/bindings/RemoteObjectProxy.cpp + - dom/canvas/WebGLContext.cpp + - dom/events/EventStateManager.cpp + - dom/events/KeyboardEvent.cpp + - dom/html/MediaError.cpp + - dom/indexedDB/ActorsParent.cpp + - dom/ipc/ContentParent.cpp + - dom/ipc/fuzztest/content_parent_ipc_libfuzz.cpp + - dom/ipc/URLClassifierParent.cpp + - dom/media/autoplay/AutoplayPolicy.cpp + - dom/media/gmp/GMPChild.cpp + - dom/media/ipc/RDDProcessManager.cpp + - dom/media/ipc/RemoteDecoderManagerParent.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/jsapi/PeerConnectionMedia.cpp + - dom/media/webrtc/libwebrtcglue/VideoConduit.cpp + - dom/media/webrtc/transport/stun_socket_filter.cpp + - dom/media/webrtc/transport/test/ice_unittest.cpp + - dom/plugins/base/nsPluginHost.h + - dom/plugins/ipc/PluginQuirks.cpp + - dom/push/PushServiceWebSocket.jsm + - dom/security/nsCSPContext.cpp + - dom/security/nsCSPService.cpp + - dom/security/nsCSPUtils.cpp + - dom/security/nsCSPUtils.h + - dom/security/nsMixedContentBlocker.cpp + - dom/security/ReferrerInfo.cpp + - dom/security/ReferrerInfo.h + - dom/security/test/csp/file_bug802872.js + - dom/security/test/csp/file_ignore_unsafe_inline.html + - dom/security/test/csp/file_nonce_source.html + - dom/security/test/csp/test_blob_data_schemes.html + - dom/security/test/csp/test_bug802872.html + - dom/security/test/csp/test_nonce_snapshot.html + - dom/security/test/csp/test_path_matching_redirect.html + - dom/security/test/csp/test_punycode_host_src.html + - dom/security/test/csp/test_scheme_relative_sources.html + - dom/security/test/csp/test_strict_dynamic_default_src.html + - dom/security/test/csp/test_strict_dynamic.html + - dom/security/test/csp/test_upgrade_insecure.html + - dom/security/test/csp/test_win_open_blocked.html + - dom/security/test/csp/test_worker_src.html + - dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js + - dom/serviceworkers/test/test_error_reporting.html + - dom/serviceworkers/test/test_openWindow.html + - dom/smil/SMILTimeValueSpec.cpp + - dom/smil/SMILTimeValueSpec.h + - 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/ipc/GPUProcessManager.cpp + - gfx/layers/ipc/fuzztest/compositor_manager_parent_ipc_libfuzz.cpp + - gfx/tests/mochitest/test_font_whitelist.html + - gfx/thebes/gfxDWriteFontList.cpp + - gfx/thebes/gfxFcPlatformFontList.cpp + - gfx/thebes/gfxFT2FontList.cpp + - gfx/thebes/gfxPlatform.cpp + - gfx/thebes/gfxPlatformFontList.cpp + - gfx/thebes/gfxPlatformFontList.h + - gfx/thebes/gfxUserFontSet.cpp + - gfx/thebes/gfxWindowsPlatform.cpp + - gfx/thebes/SharedFontList.cpp + - gfx/vr/ipc/VRProcessManager.cpp + - intl/strres/nsStringBundle.cpp + - ipc/glue/GeckoChildProcessHost.cpp + - js/src/debugger/DebugAPI.h + - js/src/devtools/rootAnalysis/analyzeHeapWrites.js + - js/src/gc/Heap.h + - js/src/jit/CodeGenerator.cpp + - js/src/jit-test/tests/auto-regress/bug687399.js + - js/src/jit-test/tests/basic/bug908915.js + - js/src/jit-test/tests/basic/missingArgTest2.js + - js/src/tests/non262/regress/regress-450369.js + - js/src/tests/test262/built-ins/JSON/stringify/replacer-array-proxy.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 + - mfbt/Attributes.h + - mobile/android/app/geckoview-prefs.js + - mobile/android/app/mobile.js + - modules/libpref/docs/index.md + - modules/libpref/init/all.js + - mozglue/baseprofiler/core/platform.cpp + - mozglue/build/AsanOptions.cpp + - mozglue/misc/StackWalk.cpp + - netwerk/base/nsIOService.cpp + - netwerk/base/nsIOService.h + - netwerk/base/nsURLHelper.cpp + - netwerk/cache/nsDiskCacheDeviceSQL.cpp + - netwerk/cookie/CookieCommons.h + - netwerk/dns/nsHostResolver.cpp + - netwerk/dns/nsIDNService.cpp + - netwerk/dns/nsIDNService.h + - netwerk/dns/TRR.cpp + - netwerk/dns/TRR.h + - netwerk/dns/TRRServiceChild.cpp + - netwerk/dns/TRRService.cpp + - netwerk/dns/TRRService.h + - netwerk/dns/TRRServiceParent.cpp + - netwerk/ipc/DocumentLoadListener.cpp + - netwerk/protocol/about/nsAboutProtocolHandler.cpp + - netwerk/protocol/http/Http2Session.cpp + - netwerk/protocol/http/HttpBaseChannel.cpp + - netwerk/protocol/http/HttpConnectionMgrParent.cpp + - netwerk/protocol/http/HttpConnectionMgrShell.h + - netwerk/protocol/http/nsHttpChannel.cpp + - netwerk/protocol/http/nsHttpConnectionMgr.cpp + - netwerk/protocol/http/nsHttpHandler.cpp + - netwerk/protocol/http/nsHttpHandler.h + - netwerk/protocol/http/TRRServiceChannel.cpp + - netwerk/protocol/res/ExtensionProtocolHandler.cpp + - netwerk/protocol/viewsource/nsViewSourceChannel.cpp + - netwerk/protocol/websocket/BaseWebSocketChannel.cpp + - netwerk/socket/nsSOCKSSocketProvider.cpp + - netwerk/test/gtest/TestCookie.cpp + - netwerk/test/unit/head_trr.js + - netwerk/test/unit_ipc/test_dns_by_type_resolve_wrap.js + - netwerk/test/unit_ipc/test_trr_httpssvc_wrap.js + - netwerk/test/unit/test_bug396389.js + - netwerk/test/unit/test_bug427957.js + - netwerk/test/unit/test_bug464591.js + - netwerk/test/unit/test_bug479413.js + - netwerk/test/unit/test_cookie_blacklist.js + - netwerk/test/unit/test_dns_by_type_resolve.js + - netwerk/test/unit/test_idn_blacklist.js + - netwerk/test/unit/test_idn_urls.js + - netwerk/test/unit/test_trr_httpssvc.js + - netwerk/test/unit/test_trr.js + - netwerk/test/unit/test_use_httpssvc.js + - netwerk/url-classifier/AsyncUrlChannelClassifier.cpp + - netwerk/url-classifier/nsChannelClassifier.cpp + - netwerk/url-classifier/nsChannelClassifier.h + - netwerk/url-classifier/UrlClassifierCommon.cpp + - netwerk/url-classifier/UrlClassifierCommon.h + - netwerk/url-classifier/UrlClassifierFeatureBase.cpp + - netwerk/url-classifier/UrlClassifierFeatureBase.h + - netwerk/url-classifier/UrlClassifierFeatureCryptominingAnnotation.cpp + - netwerk/url-classifier/UrlClassifierFeatureCryptominingProtection.cpp + - netwerk/url-classifier/UrlClassifierFeatureCustomTables.cpp + - netwerk/url-classifier/UrlClassifierFeatureCustomTables.h + - netwerk/url-classifier/UrlClassifierFeatureFactory.cpp + - netwerk/url-classifier/UrlClassifierFeatureFactory.h + - netwerk/url-classifier/UrlClassifierFeatureFingerprintingAnnotation.cpp + - netwerk/url-classifier/UrlClassifierFeatureFingerprintingProtection.cpp + - netwerk/url-classifier/UrlClassifierFeatureFlash.cpp + - netwerk/url-classifier/UrlClassifierFeatureLoginReputation.cpp + - netwerk/url-classifier/UrlClassifierFeaturePhishingProtection.cpp + - netwerk/url-classifier/UrlClassifierFeatureSocialTrackingAnnotation.cpp + - netwerk/url-classifier/UrlClassifierFeatureSocialTrackingProtection.cpp + - netwerk/url-classifier/UrlClassifierFeatureTrackingAnnotation.cpp + - netwerk/url-classifier/UrlClassifierFeatureTrackingProtection.cpp + - python/mach/mach/dispatcher.py + - python/mozbuild/mozbuild/backend/recursivemake.py + - python/mozbuild/mozbuild/configure/options.py + - python/mozbuild/mozbuild/vendor/moz_yaml.py + - python/mozbuild/mozbuild/vendor/vendor_rust.py + - remote/Protocol.jsm + - security/certverifier/NSSCertDBTrustDomain.cpp + - security/certverifier/TrustOverrideUtils.h + - security/manager/ssl/DataStorageList.h + - security/manager/ssl/nsNSSIOLayer.cpp + - security/manager/ssl/tests/unit/test_ev_certs.js + - security/manager/ssl/tests/unit/test_intermediate_preloads.js + - security/manager/ssl/tests/unit/test_sanctions_symantec_apple_google.js + - security/manager/ssl/tests/unit/tlsserver/cmd/SanctionsTestServer.cpp + - 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/launch/SandboxLaunch.cpp + - security/sandbox/linux/Sandbox.cpp + - security/sandbox/linux/SandboxFilter.cpp + - security/sandbox/linux/SandboxFilterUtil.h + - security/sandbox/linux/Sandbox.h + - services/automation/ServicesAutomation.jsm + - services/fxaccounts/FxAccountsCommon.js + - services/fxaccounts/FxAccounts.jsm + - services/sync/modules/engines/addons.js + - taskcluster/ci/docker-image/kind.yml + - taskcluster/taskgraph/actions/create_interactive.py + - taskcluster/taskgraph/target_tasks.py + - taskcluster/taskgraph/transforms/tests.py + - taskcluster/taskgraph/try_option_syntax.py + - taskcluster/taskgraph/util/schema.py + - taskcluster/test/test_mach_try_auto.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/raptor/raptor/control_server.py + - testing/raptor/raptor/manifest.py + - testing/specialpowers/content/SpecialPowersChild.jsm + - testing/talos/talos/mainthreadio.py + - testing/talos/talos/tests/devtools/addon/content/pages/custom/debugger/static/js/main.js + - testing/talos/talos/whitelist.py + - testing/talos/talos/xtalos/etlparser.py + - testing/talos/talos/xtalos/xtalos.py + - testing/web-platform/tests/common/security-features/README.md + - testing/web-platform/tests/docs/writing-tests/general-guidelines.md + - testing/web-platform/tests/docs/writing-tests/lint-tool.md + - testing/web-platform/tests/tools/manifest/tests/test_manifest.py + - toolkit/actors/RemotePageChild.jsm + - toolkit/actors/WebChannelChild.jsm + - toolkit/components/aboutperformance/content/aboutPerformance.js + - toolkit/components/antitracking/ContentBlocking.cpp + - toolkit/components/antitracking/PurgeTrackerService.jsm + - toolkit/components/antitracking/StorageAccess.cpp + - toolkit/components/antitracking/test/browser/antitracking_head.js + - toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js + - toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js + - toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js + - toolkit/components/crashes/CrashManager.jsm + - toolkit/components/crashes/tests/xpcshell/test_crash_manager.js + - toolkit/components/extensions/Extension.jsm + - toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js + - toolkit/components/remotepagemanager/RemotePageManagerParent.jsm + - toolkit/components/reputationservice/ApplicationReputation.cpp + - toolkit/components/reputationservice/chromium/chrome/common/safe_browsing/csd.pb.h + - toolkit/components/reputationservice/LoginReputation.cpp + - toolkit/components/reputationservice/LoginReputation.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/reputationservice/test/unit/test_login_rep.js + - toolkit/components/satchel/test/test_form_autocomplete.html + - toolkit/components/search/SearchService.jsm + - toolkit/components/search/tests/xpcshell/test_override_allowlist.js + - toolkit/components/telemetry/docs/data/environment.rst + - toolkit/components/url-classifier/LookupCache.cpp + - toolkit/components/url-classifier/LookupCache.h + - toolkit/components/url-classifier/nsUrlClassifierDBService.cpp + - toolkit/components/url-classifier/nsUrlClassifierUtils.cpp + - toolkit/components/url-classifier/SafeBrowsing.jsm + - 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_classify_track.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_privatebrowsing_trackingprotection.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_bug1312515.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/mochitest/trackingRequest.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_features.js + - toolkit/components/url-classifier/tests/unit/test_platform_specific_threats.js + - toolkit/components/url-classifier/tests/UrlClassifierTestUtils.jsm + - toolkit/content/aboutSupport.js + - toolkit/content/aboutTelemetry.js + - toolkit/content/aboutUrlClassifier.js + - toolkit/content/aboutUrlClassifier.xhtml + - toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js + - toolkit/crashreporter/client/ping.cpp + - toolkit/crashreporter/CrashAnnotations.cpp + - toolkit/crashreporter/generate_crash_reporter_sources.py + - toolkit/modules/BrowserUtils.jsm + - toolkit/modules/E10SUtils.jsm + - toolkit/modules/PermissionsUtils.jsm + - toolkit/modules/RemotePageAccessManager.jsm + - toolkit/modules/tests/browser/browser_AsyncPrefs.js + - toolkit/modules/tests/browser/browser_Troubleshoot.js + - toolkit/modules/tests/browser/browser_web_channel.js + - toolkit/modules/tests/xpcshell/test_PermissionsUtils.js + - toolkit/modules/third_party/jsesc/jsesc.js + - toolkit/modules/Troubleshoot.jsm + - toolkit/mozapps/extensions/internal/XPIInstall.jsm + - toolkit/mozapps/extensions/internal/XPIProvider.jsm + - 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/browser_whitelist.js + - toolkit/mozapps/extensions/test/xpinstall/head.js + - toolkit/xre/nsAppRunner.cpp + - toolkit/xre/nsEmbedFunctions.cpp + - toolkit/xre/nsXREDirProvider.cpp + - tools/crashreporter/system-symbols/win/symsrv-fetch.py + - tools/fuzzing/faulty/Faulty.cpp + - tools/fuzzing/faulty/Faulty.h + - tools/fuzzing/ipc/ProtocolFuzzer.cpp + - tools/fuzzing/ipc/ProtocolFuzzer.h + - 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 + - tools/lint/test/test_codespell.py + - tools/profiler/core/platform.cpp + - tools/tryselect/selectors/chooser/__init__.py + - tools/tryselect/task_config.py + - uriloader/prefetch/nsOfflineCacheUpdate.cpp + - 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/base/CycleCollectedJSRuntime.cpp + - xpcom/base/ErrorList.py + - xpcom/idl-parser/xpidl/xpidl.py + - xpcom/io/FilePreferences.cpp + - xpcom/io/FilePreferences.h + - xpcom/reflect/xptcall/md/unix/xptcinvoke_arm.cpp + - xpcom/reflect/xptcall/md/unix/xptcinvoke_linux_s390.cpp + - xpcom/reflect/xptcall/md/unix/xptcinvoke_linux_s390x.cpp + - xpcom/tests/gtest/TestFilePreferencesUnix.cpp + - xpcom/tests/gtest/TestFilePreferencesWin.cpp + - xpcom/threads/nsThreadUtils.h + +# --- +# Disable for now. Needs some dev to handle this +# avoid-master-and-slave: +# description: "Use words like 'controller', 'worker' instead" 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..1852d0f530 --- /dev/null +++ b/tools/lint/rst/__init__.py @@ -0,0 +1,107 @@ +# 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 mozlint import result +from mozlint.pathutils import expand_exclusions +from mozfile import which + +# 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]*)\) (.*)$") + + +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" + + 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) + 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..72419556fd --- /dev/null +++ b/tools/lint/rst/requirements.in @@ -0,0 +1,2 @@ +rstcheck==3.3.1 +Pygments==2.6.1 diff --git a/tools/lint/rst/requirements.txt b/tools/lint/rst/requirements.txt new file mode 100644 index 0000000000..45fe3ea82f --- /dev/null +++ b/tools/lint/rst/requirements.txt @@ -0,0 +1,18 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --generate-hashes --output-file=tools/lint/rst/requirements.txt tools/lint/rst/requirements.in +# +docutils==0.14 \ + --hash=sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6 \ + --hash=sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274 \ + --hash=sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6 \ + # via rstcheck +pygments==2.6.1 \ + --hash=sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44 \ + --hash=sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324 \ + # via -r tools/lint/rst/requirements.in +rstcheck==3.3.1 \ + --hash=sha256:92c4f79256a54270e0402ba16a2f92d0b3c15c8f4410cb9c57127067c215741f \ + # via -r tools/lint/rst/requirements.in diff --git a/tools/lint/rust/__init__.py b/tools/lint/rust/__init__.py new file mode 100644 index 0000000000..60135adcab --- /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 signal +import six +import re +import subprocess +from collections import namedtuple +from distutils.version import StrictVersion + +from mozfile import which +from mozlint import result +from mozlint.pathutils import expand_exclusions +from mozprocess import ProcessHandler + + +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: + line = six.ensure_text(line) + match = diff_line.match(line) + if match: + if diff: + issues.append(RustfmtDiff(file, line_no, diff.rstrip("\n"))) + diff = "" + file, line_no = match.groups() + else: + diff += line + "\n" + # the algorithm above will always skip adding the last issue + issues.append(RustfmtDiff(file, line_no, diff)) + 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([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 + + +class RustfmtProcess(ProcessHandler): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["stream"] = False + ProcessHandler.__init__(self, *args, **kwargs) + + def run(self, *args, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + ProcessHandler.run(self, *args, **kwargs) + signal.signal(signal.SIGINT, orig) + + +def run_process(config, cmd): + proc = RustfmtProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.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 + + return which("rustfmt") + + +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 StrictVersion(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 = StrictVersion(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] + if not fix: + cmd_args.append("--check") + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + output = run_process(config, base_command) + + if fix: + # Rustfmt is able to fix all issues so don't bother parsing the output. + return [] + return parse_issues(config, output, paths) diff --git a/tools/lint/rustfmt.yml b/tools/lint/rustfmt.yml new file mode 100644 index 0000000000..f5ad4c0b29 --- /dev/null +++ b/tools/lint/rustfmt.yml @@ -0,0 +1,25 @@ +--- +rust: + description: Reformat rust + min_rustfmt_version: 1.4.12 + include: + - '.' + exclude: + - dom/webauthn/libudev-sys/ + - gfx/wr/direct-composition/ + - gfx/wr/peek-poke/ + - gfx/wr/webrender_build/ + - gfx/wgpu/ + - gfx/wr/wr_malloc_size_of/ + - media/audioipc/audioipc/src/ + - media/audioipc/client/src/ + - media/audioipc/server/src/ + - media/mp4parse-rust/ + - servo/ + extensions: + - rs + support-files: + - 'tools/lint/rust/**' + type: external + payload: rust:lint + code_review_warnings: true diff --git a/tools/lint/shell/__init__.py b/tools/lint/shell/__init__.py new file mode 100644 index 0000000000..0c4a2ed96b --- /dev/null +++ b/tools/lint/shell/__init__.py @@ -0,0 +1,149 @@ +# 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 json +from json.decoder import JSONDecodeError + +import mozpack.path as mozpath +from mozfile import which +from mozpack.files import FileFinder + +from mozlint import result +from mozlint.util.implementation import LintProcess + +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..d0b390d4e8 --- /dev/null +++ b/tools/lint/shellcheck.yml @@ -0,0 +1,14 @@ +--- +shellcheck: + description: Shell script linter + include: + - 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..7f692c8d4a --- /dev/null +++ b/tools/lint/spell/__init__.py @@ -0,0 +1,147 @@ +# 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): + def process_line(self, line): + try: + match = CODESPELL_FORMAT_REGEX.match(line) + abspath, line, typo, correct = match.groups() + except AttributeError: + print("Unable to match regex against output: {}".format(line)) + return + + # 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 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"] + + skip_files = "" + if "exclude" in config: + skip_files = "--skip=*.dic,{}".format(",".join(config["exclude"])) + + exclude_list = os.path.join(here, "exclude-list.txt") + 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, + skip_files, + ] + + if fix: + 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) + return results diff --git a/tools/lint/spell/codespell_requirements.txt b/tools/lint/spell/codespell_requirements.txt new file mode 100644 index 0000000000..68bd95713b --- /dev/null +++ b/tools/lint/spell/codespell_requirements.txt @@ -0,0 +1,4 @@ +codespell==2.0.0 \ + --hash=sha256:a10b8bbb9f678e4edff7877af1f654fdc9e27c205f952c3ddee2981ad02ec5f2 \ + --hash=sha256:dd9983e096b9f7ba89dd2d2466d1fc37231d060f19066331b9571341363c77b8 + diff --git a/tools/lint/spell/exclude-list.txt b/tools/lint/spell/exclude-list.txt new file mode 100644 index 0000000000..1b6f595099 --- /dev/null +++ b/tools/lint/spell/exclude-list.txt @@ -0,0 +1,17 @@ +cas +optin +aparent +acount +te +wasn +incrementall +aare +whats +crate +files' +thru +referer +dur +ue +tring + diff --git a/tools/lint/test-disable.yml b/tools/lint/test-disable.yml new file mode 100644 index 0000000000..7d93889704 --- /dev/null +++ b/tools/lint/test-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.ini + - testing/mozbase/manifestparser/tests + - testing/web-platform + - xpcom/tests/unit/data + extensions: ['ini'] + type: regex + payload: ^[ \t]*(#|;)[ \t]*\[ diff --git a/tools/lint/test/conftest.py b/tools/lint/test/conftest.py new file mode 100644 index 0000000000..e9a6b01a2d --- /dev/null +++ b/tools/lint/test/conftest.py @@ -0,0 +1,228 @@ +from __future__ import absolute_import, print_function + +import logging +import os +import sys +from collections import defaultdict + +from mozbuild.base import MozbuildObject +from mozlint.pathutils import findobject +from mozlint.parser import Parser +from mozlint.result import ResultSummary +from mozlog.structuredlog import StructuredLogger +from mozpack import path + +import pytest + +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 + + func = findobject(config["setup"]) + func(build.topsrcdir, virtualenv_manager=build.virtualenv_manager) + + +@pytest.fixture +def lint(config, root): + """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. + """ + 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 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 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 ( + SAMPLE_TEST, + SAMPLE_CONFIG, + temp_dir, + temp_file, + ) + + with temp_dir() as tmpdir: + suitedir = os.path.join(tmpdir, "suite") + perfdocs_dir = os.path.join(tmpdir, "perfdocs") + os.mkdir(perfdocs_dir) + os.mkdir(suitedir) + + with temp_file( + "perftest.ini", tempdir=suitedir, content="[perftest_sample.js]" + ) as tmpmanifest, temp_file( + "perftest_sample.js", tempdir=suitedir, content=SAMPLE_TEST + ) as tmptest, temp_file( + "config.yml", tempdir=perfdocs_dir, content=SAMPLE_CONFIG + ) as tmpconfig, temp_file( + "index.rst", tempdir=perfdocs_dir, content="{documentation}" + ) as tmpindex: + yield { + "top_dir": tmpdir.replace("\\", "\\\\"), + "manifest": tmpmanifest, + "test": tmptest, + "config": tmpconfig, + "index": tmpindex, + } 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..a4236a2de7 --- /dev/null +++ b/tools/lint/test/files/clippy/test1/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/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/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/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 100755 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 100755 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/flake8/.flake8 b/tools/lint/test/files/flake8/.flake8 new file mode 100644 index 0000000000..1933432319 --- /dev/null +++ b/tools/lint/test/files/flake8/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 100 +exclude = + subdir/exclude, diff --git a/tools/lint/test/files/flake8/bad.py b/tools/lint/test/files/flake8/bad.py new file mode 100644 index 0000000000..9d9751c7eb --- /dev/null +++ b/tools/lint/test/files/flake8/bad.py @@ -0,0 +1,5 @@ +# Unused import +import distutils + +print("This is a line that is over 80 characters but under 100. It shouldn't fail.") +print("This is a line that is over not only 80, but 100 characters. It should most certainly cause a failure.") diff --git a/tools/lint/test/files/flake8/custom/.flake8 b/tools/lint/test/files/flake8/custom/.flake8 new file mode 100644 index 0000000000..cfe68833f2 --- /dev/null +++ b/tools/lint/test/files/flake8/custom/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length=110 +ignore= + F401 diff --git a/tools/lint/test/files/flake8/custom/good.py b/tools/lint/test/files/flake8/custom/good.py new file mode 100644 index 0000000000..7f9121a2ba --- /dev/null +++ b/tools/lint/test/files/flake8/custom/good.py @@ -0,0 +1,5 @@ +# Unused import +import distutils + +print("This is a line that is over 80 characters but under 100. It shouldn't fail.") +print("This is a line that is over not only 80, but 100 characters. It should also not cause a failure.") diff --git a/tools/lint/test/files/flake8/ext/bad.configure b/tools/lint/test/files/flake8/ext/bad.configure new file mode 100644 index 0000000000..8214ebb3c0 --- /dev/null +++ b/tools/lint/test/files/flake8/ext/bad.configure @@ -0,0 +1,2 @@ +# unused import +import os diff --git a/tools/lint/test/files/flake8/subdir/exclude/bad.py b/tools/lint/test/files/flake8/subdir/exclude/bad.py new file mode 100644 index 0000000000..9d9751c7eb --- /dev/null +++ b/tools/lint/test/files/flake8/subdir/exclude/bad.py @@ -0,0 +1,5 @@ +# Unused import +import distutils + +print("This is a line that is over 80 characters but under 100. It shouldn't fail.") +print("This is a line that is over not only 80, but 100 characters. It should most certainly cause a failure.") diff --git a/tools/lint/test/files/flake8/subdir/exclude/exclude_subdir/bad.py b/tools/lint/test/files/flake8/subdir/exclude/exclude_subdir/bad.py new file mode 100644 index 0000000000..9d9751c7eb --- /dev/null +++ b/tools/lint/test/files/flake8/subdir/exclude/exclude_subdir/bad.py @@ -0,0 +1,5 @@ +# Unused import +import distutils + +print("This is a line that is over 80 characters but under 100. It shouldn't fail.") +print("This is a line that is over not only 80, but 100 characters. It should most certainly cause a failure.") 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/pylint/bad.py b/tools/lint/test/files/pylint/bad.py new file mode 100644 index 0000000000..61b69a49cf --- /dev/null +++ b/tools/lint/test/files/pylint/bad.py @@ -0,0 +1,5 @@ +def foo(): + useless_var = 1 + useless_var = true + return "true" + print("unreachable")
\ No newline at end of file diff --git a/tools/lint/test/files/pylint/good.py b/tools/lint/test/files/pylint/good.py new file mode 100644 index 0000000000..c867dc66ec --- /dev/null +++ b/tools/lint/test/files/pylint/good.py @@ -0,0 +1,3 @@ +def foo(): + a = 1 + 1 + return a 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/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/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/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.ini b/tools/lint/test/python.ini new file mode 100644 index 0000000000..5acd0a1e81 --- /dev/null +++ b/tools/lint/test/python.ini @@ -0,0 +1,34 @@ +[DEFAULT] +subsuite = mozlint +skip-if = python == 2 + +[test_black.py] +requirements = tools/lint/python/black_requirements.txt +skip-if = os == "mac" # pip unable to find black +[test_eslint.py] +[test_flake8.py] +requirements = tools/lint/python/flake8_requirements.txt +skip-if = os == "mac" # pip unable to find 'flake8==3.5.0' +[test_file_perm.py] +skip-if = os == "win" +[test_file_whitespace.py] +[test_file_license.py] +[test_lintpref.py] +[test_shellcheck.py] +[test_rst.py] +requirements = tools/lint/rst/requirements.txt +skip-if = os == "mac" # pip unable to install +[test_codespell.py] +skip-if = os == "win" || os == "mac" # codespell installed on Linux +[test_yaml.py] +[test_clippy.py] +skip-if = os == "win" || os == "mac" # only installed on Linux +[test_rustfmt.py] +skip-if = os == "win" || os == "mac" # only installed on Linux +[test_clang_format.py] +skip-if = os == "win" || os == "mac" # only installed on Linux +[test_perfdocs.py] +[test_pylint.py] +skip-if = os == "win" || os == "mac" # only installed on linux +requirements = tools/lint/python/pylint_requirements.txt + diff --git a/tools/lint/test/test_black.py b/tools/lint/test/test_black.py new file mode 100644 index 0000000000..a5482c6e11 --- /dev/null +++ b/tools/lint/test/test_black.py @@ -0,0 +1,26 @@ +# -*- 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" + + +def test_lint_black(lint, paths): + results = lint(paths()) + assert len(results) == 2 + + print(results) + 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..a04fde6267 --- /dev/null +++ b/tools/lint/test/test_clang_format.py @@ -0,0 +1,48 @@ +import mozunit + +from conftest import build + +LINTER = "clang-format" + + +def test_good(lint, config, paths): + results = lint(paths("good/"), root=build.topsrcdir, use_filters=False) + print(results) + assert len(results) == 0 + + +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 == 4 + assert "bad.cpp" in results[0].path + assert "int main ( ) {" in results[0].diff + + +def test_dir(lint, config, paths): + results = lint(paths("bad/"), root=build.topsrcdir, use_filters=False) + print(results) + assert len(results) >= 4 + + assert "Reformat C/C++" in results[0].message + assert results[0].level == "warning" + assert results[0].lineno == 1 + assert results[0].column == 4 + assert "bad.cpp" in results[0].path + assert "int main ( ) {" in results[0].diff + + assert "Reformat C/C++" in results[5].message + assert results[5].level == "warning" + assert results[5].lineno == 1 + assert results[5].column == 18 + assert "bad2.c" in results[5].path + assert "#include" in results[5].diff + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_clippy.py b/tools/lint/test/test_clippy.py new file mode 100644 index 0000000000..553aba8876 --- /dev/null +++ b/tools/lint/test/test_clippy.py @@ -0,0 +1,103 @@ +import mozunit +import os + +LINTER = "clippy" + + +def test_basic(lint, config, paths): + results = lint(paths("test1/")) + print(results) + assert len(results) > 7 + + assert ( + "is never read" in results[0].message or "but never used" in results[0].message + ) + assert results[0].level == "warning" + assert results[0].lineno == 7 + assert results[0].column == 9 + assert results[0].rule == "unused_assignments" + assert results[0].relpath == "test1/bad.rs" + assert "tools/lint/test/files/clippy/test1/bad.rs" in results[0].path + + assert "this looks like you are trying to swap `a` and `b`" in results[1].message + assert results[1].level == "error" + assert results[1].relpath == "test1/bad.rs" + assert results[1].rule == "clippy::almost_swapped" + assert "or maybe you should use `std::mem::replace`" in results[1].hint + + assert "value assigned to `b` is never read" in results[2].message + assert results[2].level == "warning" + assert results[2].relpath == "test1/bad.rs" + + assert "variable does not need to be mutable" in results[5].message + assert results[5].relpath == "test1/bad2.rs" + assert results[5].rule == "unused_mut" + + assert "this range is empty so" in results[6].message + assert results[6].level == "error" + assert results[6].relpath == "test1/bad2.rs" + assert results[6].rule == "clippy::reversed_empty_ranges" + + +def test_error(lint, config, paths): + results = lint(paths("test1/Cargo.toml")) + # Should fail. We don't accept Cargo.toml as input + assert results == 1 + + +def test_file_and_path_provided(lint, config, paths): + results = lint(paths("./test2/src/bad_1.rs", "test1/")) + # even if clippy analyzed it + # we should not have anything from bad_2.rs + # as mozlint is filtering out the file + print(results) + assert len(results) > 16 + assert "value assigned to `a` is never read" in results[0].message + assert results[0].level == "warning" + assert results[0].lineno == 7 + assert results[0].column == 9 + assert results[0].rule == "unused_assignments" + assert results[0].relpath == "test1/bad.rs" + assert "tools/lint/test/files/clippy/test1/bad.rs" in results[0].path + assert "value assigned to `a` is never read" in results[0].message + assert results[8].level == "warning" + assert results[8].lineno == 1 + assert results[8].column == 4 + assert results[8].rule == "dead_code" + assert results[8].relpath == "test2/src/bad_1.rs" + assert "tools/lint/test/files/clippy/test2/src/bad_1.rs" in results[8].path + for r in results: + assert "bad_2.rs" not in r.relpath + + +def test_file_provided(lint, config, paths): + results = lint(paths("./test2/src/bad_1.rs")) + # even if clippy analyzed it + # we should not have anything from bad_2.rs + # as mozlint is filtering out the file + print(results) + assert len(results) > 8 + assert results[0].level == "warning" + assert results[0].lineno == 1 + assert results[0].column == 4 + assert results[0].rule == "dead_code" + assert results[0].relpath == "test2/src/bad_1.rs" + assert "tools/lint/test/files/clippy/test2/src/bad_1.rs" in results[0].path + for r in results: + assert "bad_2.rs" not in r.relpath + + +def test_cleanup(lint, paths, root): + # If Cargo.lock does not exist before clippy run, delete it + lint(paths("test1/")) + assert not os.path.exists(os.path.join(root, "test1/target/")) + assert not os.path.exists(os.path.join(root, "test1/Cargo.lock")) + + # If Cargo.lock exists before clippy run, keep it after cleanup + lint(paths("test2/")) + assert not os.path.exists(os.path.join(root, "test2/target/")) + assert os.path.exists(os.path.join(root, "test2/Cargo.lock")) + + +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..41912e01a7 --- /dev/null +++ b/tools/lint/test/test_codespell.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import, print_function + +import mozunit + +LINTER = "codespell" + + +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_eslint.py b/tools/lint/test/test_eslint.py new file mode 100644 index 0000000000..01456a4063 --- /dev/null +++ b/tools/lint/test/test_eslint.py @@ -0,0 +1,30 @@ +import mozunit + +from conftest import build + +LINTER = "eslint" + + +def test_lint_with_global_exclude(lint, config, paths): + config["exclude"] = ["subdir", "import"] + results = lint(paths(), config=config, root=build.topsrcdir) + assert len(results) == 0 + + +def test_no_files_to_lint(lint, config, paths): + # A directory with no files to lint. + results = lint(paths("nolint"), root=build.topsrcdir) + assert results == [] + + # Errors still show up even when a directory with no files is passed in. + results = lint(paths("nolint", "subdir/bad.js"), root=build.topsrcdir) + assert len(results) == 1 + + +def test_bad_import(lint, config, paths): + results = lint(paths("import"), config=config, root=build.topsrcdir) + assert results == 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..b7f0efac60 --- /dev/null +++ b/tools/lint/test/test_file_license.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import, print_function + +import mozunit + +LINTER = "license" + + +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 + + +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..df5de9adb8 --- /dev/null +++ b/tools/lint/test/test_file_perm.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, print_function + +import pytest + +import mozunit + +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) + print(results) + + 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")) + print(results) + 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..b6fa79799b --- /dev/null +++ b/tools/lint/test/test_file_whitespace.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, print_function + +import mozunit + +LINTER = "file-whitespace" + + +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 + + +if __name__ == "__main__": + mozunit.main() diff --git a/tools/lint/test/test_flake8.py b/tools/lint/test/test_flake8.py new file mode 100644 index 0000000000..66befab4e7 --- /dev/null +++ b/tools/lint/test/test_flake8.py @@ -0,0 +1,109 @@ +import os + +import mozunit + +LINTER = "flake8" + + +def test_lint_single_file(lint, paths): + results = lint(paths("bad.py")) + assert len(results) == 2 + assert results[0].rule == "F401" + assert results[1].rule == "E501" + assert results[1].lineno == 5 + + # run lint again to make sure the previous results aren't counted twice + results = lint(paths("bad.py")) + assert len(results) == 2 + + +def test_lint_custom_config_ignored(lint, paths): + results = lint(paths("custom")) + assert len(results) == 2 + + results = lint(paths("custom/good.py")) + assert len(results) == 2 + + +def test_lint_fix(lint, create_temp_file): + contents = """ +import distutils + +def foobar(): + pass +""".lstrip() + + path = create_temp_file(contents, name="bad.py") + results = lint([path]) + assert len(results) == 2 + + # Make sure the missing blank line is fixed, but the unused import isn't. + results = lint([path], fix=True) + assert len(results) == 1 + + # Also test with a directory + path = os.path.dirname(create_temp_file(contents, name="bad2.py")) + results = lint([path], fix=True) + # There should now be two files with 2 combined errors + assert len(results) == 2 + assert all(r.rule != "E501" for r in results) + + +def test_lint_fix_uses_config(lint, create_temp_file): + contents = """ +foo = ['A list of strings', 'that go over 80 characters', 'to test if autopep8 fixes it'] +""".lstrip() + + path = create_temp_file(contents, name="line_length.py") + lint([path], fix=True) + + # Make sure autopep8 reads the global config under lintargs['root']. If it + # didn't, then the line-length over 80 would get fixed. + with open(path, "r") as fh: + assert fh.read() == contents + + +def test_lint_excluded_file(lint, paths, config): + # First file is globally excluded, second one is from .flake8 config. + files = paths("bad.py", "subdir/exclude/bad.py", "subdir/exclude/exclude_subdir") + config["exclude"] = paths("bad.py") + results = lint(files, config) + print(results) + assert len(results) == 0 + + # Make sure excludes also apply when running from a different cwd. + cwd = paths("subdir")[0] + os.chdir(cwd) + + results = lint(paths("subdir/exclude")) + print(results) + assert len(results) == 0 + + +def test_lint_excluded_file_with_glob(lint, paths, config): + config["exclude"] = paths("ext/*.configure") + + files = paths("ext") + results = lint(files, config) + print(results) + assert len(results) == 0 + + files = paths("ext/bad.configure") + results = lint(files, config) + print(results) + assert len(results) == 0 + + +def test_lint_excluded_file_with_no_filter(lint, paths, config): + results = lint(paths("subdir/exclude"), use_filters=False) + print(results) + assert len(results) == 4 + + +def test_lint_uses_custom_extensions(lint, paths): + assert len(lint(paths("ext"))) == 1 + assert len(lint(paths("ext/bad.configure"))) == 1 + + +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..4fb3b8d23c --- /dev/null +++ b/tools/lint/test/test_lintpref.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import, print_function + +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_perfdocs.py b/tools/lint/test/test_perfdocs.py new file mode 100644 index 0000000000..f977330fda --- /dev/null +++ b/tools/lint/test/test_perfdocs.py @@ -0,0 +1,423 @@ +import contextlib +import mock +import os +import pytest +import shutil +import tempfile + +import mozunit + +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: "" +""" + + +@contextlib.contextmanager +def temp_file(name="temp", tempdir=None, content=None): + if tempdir is None: + tempdir = tempfile.mkdtemp() + path = os.path.join(tempdir, name) + if content is not None: + with open(path, "w") as f: + f.write(content) + try: + yield path + finally: + try: + shutil.rmtree(tempdir) + except FileNotFoundError: + pass + + +@contextlib.contextmanager +def temp_dir(): + tempdir = tempfile.mkdtemp() + try: + yield tempdir + finally: + try: + shutil.rmtree(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.verifier as vf + import perfdocs.gatherer as gt + import perfdocs.generator as gn + + gt.logger = logger + vf.logger = logger + gn.logger = logger + + +@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, 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=[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("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=[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_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(yaml_path) + + assert valid + + +@mock.patch("perfdocs.verifier.jsonschema") +@mock.patch("perfdocs.logger.PerfDocLogger") +def test_perfdocs_verifier_invalid_yaml( + logger, jsonschema, 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 + + jsonschema.validate = mock.Mock(side_effect=Exception("Schema/ValidationError")) + verifier = Verifier("top_dir") + valid = verifier.validate_yaml(yaml_path) + + expected = ("YAML ValidationError: Schema/ValidationError", yaml_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_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(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") 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"] + 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") 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_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 + + Verifier.validate_yaml = mock.Mock(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 + + +@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") 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"] + fg._get_subtests_from_ini = mock.Mock() + fg._get_subtests_from_ini.return_value = { + "Example": perfdocs_sample["manifest"] + } + + for suite, suitetests in fg.get_test_list().items(): + assert suite == "suite" + for test, manifest in suitetests.items(): + assert test == "Example" + assert manifest == perfdocs_sample["manifest"] + + +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_pylint.py b/tools/lint/test/test_pylint.py new file mode 100644 index 0000000000..6ee2217089 --- /dev/null +++ b/tools/lint/test/test_pylint.py @@ -0,0 +1,24 @@ +import mozunit + +LINTER = "pylint" + + +def test_lint_single_file(lint, paths): + results = lint(paths("bad.py")) + assert len(results) == 3 + assert results[1].rule == "E0602" + assert results[2].rule == "W0101" + assert results[2].lineno == 5 + + # run lint again to make sure the previous results aren't counted twice + results = lint(paths("bad.py")) + assert len(results) == 3 + + +def test_lint_single_file_good(lint, paths): + results = lint(paths("good.py")) + assert len(results) == 0 + + +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..532ac332d6 --- /dev/null +++ b/tools/lint/test/test_rst.py @@ -0,0 +1,25 @@ +import pytest +import mozunit +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_rustfmt.py b/tools/lint/test/test_rustfmt.py new file mode 100644 index 0000000000..4eaf333825 --- /dev/null +++ b/tools/lint/test/test_rustfmt.py @@ -0,0 +1,44 @@ +import mozunit + + +LINTER = "rustfmt" + + +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 + + +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..aa6fdc4494 --- /dev/null +++ b/tools/lint/test/test_shellcheck.py @@ -0,0 +1,26 @@ +import pytest +import mozunit +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_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..fb629566ab --- /dev/null +++ b/tools/lint/tox/tox_requirements.txt @@ -0,0 +1,4 @@ +pluggy==0.4.0 --hash=sha256:d2766caddfbbc8ef641d47da556d2ae3056860ce4d553aa04009e42b76a09951 +py==1.4.33 --hash=sha256:81b5e37db3cc1052de438375605fb5d3b3e97f950f415f9143f04697c684d7eb +tox==2.7.0 --hash=sha256:0f37ea637ead4a5bbae91531b0bf8fd327c7152e20255e5960ee180598228d21 +virtualenv==15.1.0 --hash=sha256:39d88b533b422825d644087a21e78c45cf5af0ef7a99a1fc9fbb7b481e5c85b0 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..2dcbb39132 --- /dev/null +++ b/tools/lint/wpt/wpt.py @@ -0,0 +1,60 @@ +# -*- 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 sys + +from mozprocess import ProcessHandler + +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: + 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 = ["python2", os.path.join(tests_dir, "wpt"), "lint", "--json"] + files + log.debug("Command: {}".format(" ".join(cmd))) + + proc = ProcessHandler( + cmd, env=os.environ, processOutputLine=process_line, universal_newlines=True + ) + proc.run() + try: + 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..840ad6eec3 --- /dev/null +++ b/tools/lint/yaml.yml @@ -0,0 +1,18 @@ +--- +yamllint: + description: YAML linter + include: + - .cron.yml + - .taskcluster.yml + - browser/config/ + - python/mozlint/ + - security/nss/.taskcluster.yml + - taskcluster + - testing/mozharness + - tools + 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..24ba4d7c4e --- /dev/null +++ b/tools/lint/yamllint_/__init__.py @@ -0,0 +1,102 @@ +# 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 os +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 |