tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 8e2f1e924bfe4773f742c1b572b35bf2141028f9
parent 857218c417175fc24622793ce599bd2b6a11bf19
Author: Alex Hochheiden <ahochheiden@mozilla.com>
Date:   Wed,  7 Jan 2026 17:17:28 +0000

Bug 2006716 - Add `ruff-format` linter r=ahal,linter-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D276897

Diffstat:
Mpyproject.toml | 9+++++++--
Mtaskcluster/kinds/source-test/mozlint.yml | 17+++++++++++++++++
Mtools/lint/mach_commands.py | 2+-
Mtools/lint/python/ruff.py | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Atools/lint/ruff-format.yml | 20++++++++++++++++++++
Atools/lint/test/files/ruff-format/bad.py | 6++++++
Atools/lint/test/files/ruff-format/good.py | 6++++++
Atools/lint/test/files/ruff-format/invalid.py | 4++++
Mtools/lint/test/python.toml | 2++
Atools/lint/test/test_ruff_format.py | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 211 insertions(+), 7 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml @@ -1,5 +1,5 @@ [tool.ruff] -line-length = 99 +line-length = 88 target-version = "py39" # See https://beta.ruff.rs/docs/rules/ for a full list of rules. lint.select = [ @@ -21,7 +21,7 @@ lint.ignore = [ "PLC0415", # `import` should be at the top-level of a file # These are handled by ruff format. - "E1", "E4", "E5", "W2", "W5", + "E1", "E4", "E5", "W1", "W2", "W5", ] builtins = ["gdb"] extend-include = ["*.configure"] @@ -133,3 +133,8 @@ exclude = [ "python/mozbuild/mozbuild/util.py" = ["F821"] "testing/mozharness/mozharness/mozilla/testing/android.py" = ["F821"] "testing/mochitest/runtests.py" = ["F821"] + +[tool.ruff.format] +# Enable preview mode for --output-format=json to get improved line/column reporting. +# TODO: Remove preview once these features are in the stable release. +preview = true diff --git a/taskcluster/kinds/source-test/mozlint.yml b/taskcluster/kinds/source-test/mozlint.yml @@ -398,6 +398,23 @@ mscom-init: - '**/*.rs' - 'tools/lint/mscom-init.yml' +py-ruff-format: + description: ruff format run over the gecko codebase + treeherder: + symbol: py(ruff-format) + run: + mach: lint -v -l ruff-format -f treeherder -f json:/builds/worker/mozlint.json . + when: + files-changed: + # The list of extensions should match tools/lint/ruff-format.yml + - '**/*.py' + - '**/moz.build' + - '**/*.configure' + - '**/*.mozbuild' + - 'pyproject.toml' + - 'tools/lint/ruff-format.yml' + - 'tools/lint/python/ruff.py' + py-ruff: description: Run ruff over the gecko codebase treeherder: diff --git a/tools/lint/mach_commands.py b/tools/lint/mach_commands.py @@ -25,7 +25,7 @@ if os.path.exists(thunderbird_excludes): GLOBAL_EXCLUDES = ["**/node_modules", "tools/lint/test/files", ".hg", ".git"] -VALID_FORMATTERS = {"clang-format", "eslint", "rustfmt", "stylelint"} +VALID_FORMATTERS = {"ruff-format", "clang-format", "eslint", "rustfmt", "stylelint"} VALID_ANDROID_FORMATTERS = {"android-format"} # Code-review bot must index issues from the whole codebase when pushing diff --git a/tools/lint/python/ruff.py b/tools/lint/python/ruff.py @@ -9,7 +9,9 @@ import re import signal import subprocess import sys +from pathlib import Path +import toml from mozlint import result here = os.path.abspath(os.path.dirname(__file__)) @@ -24,6 +26,16 @@ def default_bindir(): return os.path.join(sys.prefix, "bin") +def get_pyproject_excludes(pyproject_toml: Path): + if not pyproject_toml.exists(): + return [] + + with pyproject_toml.open() as f: + data = toml.load(f) + + return data.get("tool", {}).get("ruff", {}).get("exclude", []) + + def get_ruff_version(binary): """ Returns found binary's version @@ -57,8 +69,9 @@ def run_process(cmd, log): log.debug(line) except KeyboardInterrupt: proc.kill() + return "", -1 - return stdout + return stdout, proc.returncode def lint(paths, config, log, **lintargs): @@ -84,16 +97,26 @@ def lint(paths, config, log, **lintargs): fix_args.append(f"--extend-ignore={','.join(warning_rules)}") log.debug(f"Running --fix: {fix_args}") - output = run_process(fix_args, log) + output, returncode = run_process(fix_args, log) + if returncode == 2: + log.error( + f"ruff terminated abnormally (invalid config, CLI options, or internal error): {output}" + ) + return {"results": [], "fixed": 0} matches = re.match(r"Fixed (\d+) errors?.", output) if matches: fixed = int(matches[1]) args += ["--output-format=json"] log.debug(f"Running with args: {args}") - output = run_process(args, log) + output, returncode = run_process(args, log) + if returncode == 2: + log.error( + f"ruff terminated abnormally (invalid config, CLI options, or internal error): {output}" + ) + return {"results": [], "fixed": fixed} if not output: - return [] + return {"results": [], "fixed": fixed} try: issues = json.loads(output) @@ -122,3 +145,72 @@ def lint(paths, config, log, **lintargs): results.append(result.from_config(config, **res)) return {"results": results, "fixed": fixed} + + +def format(paths, config, log, **lintargs): + """Run ruff format to check/fix Python formatting.""" + fixed = 0 + results = [] + + if not paths: + return {"results": results, "fixed": fixed} + + args = ["ruff", "format", "--force-exclude"] + paths + + log.debug(f"Ruff version {get_ruff_version('ruff')}") + + topsrcdir = Path(lintargs["root"]) + pyproject_toml = topsrcdir / "pyproject.toml" + exclude_patterns = get_pyproject_excludes(pyproject_toml) + + # Merge pyproject.toml excludes with config excludes. + # This is needed because ruff format's --exclude replaces rather than extends + # the pyproject.toml excludes (unlike ruff check which has --extend-exclude). + if config.get("exclude"): + exclude_patterns.extend(config["exclude"]) + + for exclude in exclude_patterns: + args.extend(["--exclude", exclude]) + + if lintargs.get("fix"): + # Do a first pass to fix, as JSON output doesn't include fix counts + log.debug(f"Running --fix: {args}") + output, returncode = run_process(args, log) + if returncode == 2: + log.error( + f"ruff terminated abnormally (invalid config, CLI options, or internal error): {output}" + ) + return {"results": [], "fixed": 0} + match = re.search(r"(\d+) files? reformatted", output) + if match: + fixed = int(match.group(1)) + + args += ["--check", "--output-format=json"] + log.debug(f"Running with args: {args}") + + output, returncode = run_process(args, log) + if returncode == 2 and not output: + log.error( + "ruff format terminated abnormally (invalid config, CLI options, or internal error)" + ) + return {"results": [], "fixed": fixed} + if not output: + return {"results": [], "fixed": fixed} + + try: + issues = json.loads(output) + except json.JSONDecodeError: + log.error(f"could not parse output: {output}") + return {"results": [], "fixed": fixed} + + for issue in issues: + res = { + "path": issue["filename"], + "lineno": issue["location"]["row"], + "column": issue["location"]["column"], + "message": issue["message"], + "level": "error", + } + results.append(result.from_config(config, **res)) + + return {"results": results, "fixed": fixed} diff --git a/tools/lint/ruff-format.yml b/tools/lint/ruff-format.yml @@ -0,0 +1,20 @@ +--- +ruff-format: + description: Reformat python with ruff + # Excludes should be added to topsrcdir/pyproject.toml + exclude: + - gfx/harfbuzz/src/meson.build + - '**/*.mako.py' + - python/mozbuild/mozbuild/test/frontend/data/reader-error-syntax/moz.build + - testing/mozharness/configs/test/test_malformed.py + - testing/web-platform/tests + extensions: + - build + - configure + - mozbuild + - py + support-files: + - 'tools/lint/python/**' + - '**/pyproject.toml' + type: external + payload: python.ruff:format diff --git a/tools/lint/test/files/ruff-format/bad.py b/tools/lint/test/files/ruff-format/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/ruff-format/good.py b/tools/lint/test/files/ruff-format/good.py @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +def hello(): + print("world") diff --git a/tools/lint/test/files/ruff-format/invalid.py b/tools/lint/test/files/ruff-format/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/python.toml b/tools/lint/test/python.toml @@ -60,6 +60,8 @@ skip-if = ["os == 'win'"] ["test_ruff.py"] +["test_ruff_format.py"] + ["test_rustfmt.py"] ["test_shellcheck.py"] diff --git a/tools/lint/test/test_ruff_format.py b/tools/lint/test/test_ruff_format.py @@ -0,0 +1,52 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import mozunit + +LINTER = "ruff-format" +fixed = 0 + + +def test_lint_fix(lint, create_temp_file): + contents = """def is_unique( + s + ): + s = list(s + ) + s.sort() + + + for i in range(len(s) - 1): + if s[i] == s[i + 1]: + return 0 + else: + return 1 + + +if __name__ == "__main__": + print( + is_unique(input()) + ) """ + + path = create_temp_file(contents, "bad.py") + lint([path], fix=True) + assert fixed == 1 + + +def test_lint_ruff_format(lint, paths): + results = lint(paths()) + assert len(results) == 2 + + assert results[0].level == "error" + assert results[0].relpath.endswith("bad.py") + assert results[0].lineno == 4 + assert results[0].column == 6 + + assert "EOF" in results[1].message + assert results[1].level == "error" + assert results[1].relpath.endswith("invalid.py") + + +if __name__ == "__main__": + mozunit.main()