From 91754ff9ae399f58adef68a084ac522efa48618d Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Tue, 21 Mar 2023 23:24:43 +0800 Subject: [PATCH] Add ruff --- .github/workflows/test.yml | 2 +- README.md | 2 + action.yml | 24 ++++++ src/linters/index.js | 2 + src/linters/ruff.js | 91 +++++++++++++++++++++ test/linters/linters.test.js | 2 + test/linters/params/ruff.js | 67 +++++++++++++++ test/linters/projects/ruff/file1.py | 8 ++ test/linters/projects/ruff/file2.py | 1 + test/linters/projects/ruff/requirements.txt | 1 + 10 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 src/linters/ruff.js create mode 100644 test/linters/params/ruff.js create mode 100644 test/linters/projects/ruff/file1.py create mode 100644 test/linters/projects/ruff/file2.py create mode 100644 test/linters/projects/ruff/requirements.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0849f0b9..48084c13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,7 +103,7 @@ jobs: - name: Install Python dependencies run: | cd ./test/linters/projects/ - pip install -r ./autopep8/requirements.txt -r ./black/requirements.txt -r ./flake8/requirements.txt -r ./mypy/requirements.txt -r ./oitnb/requirements.txt -r ./pylint/requirements.txt + pip install -r ./autopep8/requirements.txt -r ./black/requirements.txt -r ./flake8/requirements.txt -r ./mypy/requirements.txt -r ./oitnb/requirements.txt -r ./pylint/requirements.txt -r ./ruff/requirements.txt # Ruby diff --git a/README.md b/README.md index a3b839e1..d2496441 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ _**Note:** The behavior of actions like this one is currently limited in the con - [Mypy](https://mypy.readthedocs.io/) - [oitnb](https://pypi.org/project/oitnb/) - [Pylint](https://pylint.pycqa.org) + - [Ruff](https://ruff.rs) - **Ruby:** - [ERB Lint](https://github.com/Shopify/erb-lint) - [RuboCop](https://rubocop.readthedocs.io) @@ -443,6 +444,7 @@ Some options are not available for specific linters: | prettier | ✅ | ✅ | | pylint | ❌ | ❌ (py) | | rubocop | ✅ | ❌ (rb) | +| ruff | ✅ | ❌ (py) | | rustfmt | ✅ | ❌ (rs) | | stylelint | ✅ | ✅ | | swift_format_official | ✅ | ✅ | diff --git a/action.yml b/action.yml index 0bc82a37..0bfe6f7f 100644 --- a/action.yml +++ b/action.yml @@ -416,6 +416,30 @@ inputs: required: false default: "false" + ruff: + description: Enable or disable ruff checks + required: false + default: "false" + ruff_args: + description: Additional arguments to pass to the linter + required: false + default: "" + ruff_dir: + description: Directory where the ruff command should be run + required: false + ruff_extensions: + description: Extensions of files to check with ruff + required: false + default: "py" + ruff_command_prefix: + description: Shell command to prepend to the linter command + required: false + default: "" + ruff_auto_fix: + description: Whether this linter should try to fix code style issues automatically. If set to `true`, it will only work if "auto_fix" is set to `true` as well + required: false + default: "true" + # Ruby rubocop: diff --git a/src/linters/index.js b/src/linters/index.js index 800ce798..94db6259 100644 --- a/src/linters/index.js +++ b/src/linters/index.js @@ -14,6 +14,7 @@ const PHPCodeSniffer = require("./php-codesniffer"); const Prettier = require("./prettier"); const Pylint = require("./pylint"); const RuboCop = require("./rubocop"); +const Ruff = require("./ruff"); const RustFmt = require("./rustfmt"); const Stylelint = require("./stylelint"); const SwiftFormatLockwood = require("./swift-format-lockwood"); @@ -45,6 +46,7 @@ const linters = { dotnet_format: DotnetFormat, gofmt: Gofmt, oitnb: Oitnb, + ruff: Ruff, rustfmt: RustFmt, prettier: Prettier, swift_format_lockwood: SwiftFormatLockwood, diff --git a/src/linters/ruff.js b/src/linters/ruff.js new file mode 100644 index 00000000..dfc110c8 --- /dev/null +++ b/src/linters/ruff.js @@ -0,0 +1,91 @@ +const { sep } = require("path"); + +const { run } = require("../utils/action"); +const commandExists = require("../utils/command-exists"); +const { initLintResult } = require("../utils/lint-result"); +const { capitalizeFirstLetter } = require("../utils/string"); + +const PARSE_REGEX = /^(.*):([0-9]+):[0-9]+: (\w*) (.*)$/gm; + +/** @typedef {import('../utils/lint-result').LintResult} LintResult */ + +/** + * https://ruff.rs + */ +class Ruff { + static get name() { + return "Ruff"; + } + + /** + * Verifies that all required programs are installed. Throws an error if programs are missing + * @param {string} dir - Directory to run the linting program in + * @param {string} prefix - Prefix to the lint command + */ + static async verifySetup(dir, prefix = "") { + // Verify that Python is installed (required to execute Ruff) + if (!(await commandExists("python"))) { + throw new Error("Python is not installed"); + } + + // Verify that Ruff is installed + try { + run(`${prefix} ruff --version`, { dir }); + } catch (err) { + throw new Error(`${this.name} is not installed`); + } + } + + /** + * Runs the linting program and returns the command output + * @param {string} dir - Directory to run the linter in + * @param {string[]} extensions - File extensions which should be linted + * @param {string} args - Additional arguments to pass to the linter + * @param {boolean} fix - Whether the linter should attempt to fix code style issues automatically + * @param {string} prefix - Prefix to the lint command + * @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command + */ + static lint(dir, extensions, args = "", fix = false, prefix = "") { + if (extensions.length !== 1 || extensions[0] !== "py") { + throw new Error(`${this.name} error: File extensions are not configurable`); + } + const fixArg = fix ? "--fix-only" : ""; + return run(`${prefix} ruff check --quiet ${fixArg} ${args} .`, { + dir, + ignoreErrors: true, + }); + } + + /** + * Parses the output of the lint command. Determines the success of the lint process and the + * severity of the identified code style violations + * @param {string} dir - Directory in which the linter has been run + * @param {{status: number, stdout: string, stderr: string}} output - Output of the lint command + * @returns {LintResult} - Parsed lint result + */ + static parseOutput(dir, output) { + const lintResult = initLintResult(); + lintResult.isSuccess = output.status === 0; + + const matches = output.stdout.matchAll(PARSE_REGEX); + for (const match of matches) { + const [_, pathFull, line, rule, text] = match; + const leadingSep = `.${sep}`; + let path = pathFull; + if (path.startsWith(leadingSep)) { + path = path.substring(2); // Remove "./" or ".\" from start of path + } + const lineNr = parseInt(line, 10); + lintResult.error.push({ + path, + firstLine: lineNr, + lastLine: lineNr, + message: `${capitalizeFirstLetter(text)} (${rule})`, + }); + } + + return lintResult; + } +} + +module.exports = Ruff; diff --git a/test/linters/linters.test.js b/test/linters/linters.test.js index 6c7366b9..f3d9966c 100644 --- a/test/linters/linters.test.js +++ b/test/linters/linters.test.js @@ -19,6 +19,7 @@ const phpCodeSnifferParams = require("./params/php-codesniffer"); const prettierParams = require("./params/prettier"); const pylintParams = require("./params/pylint"); const ruboCopParams = require("./params/rubocop"); +const ruffParams = require("./params/ruff"); const rustfmtParams = require("./params/rustfmt"); const stylelintParams = require("./params/stylelint"); const swiftFormatLockwood = require("./params/swift-format-lockwood"); @@ -44,6 +45,7 @@ const linterParams = [ prettierParams, pylintParams, ruboCopParams, + ruffParams, rustfmtParams, stylelintParams, tscParams, diff --git a/test/linters/params/ruff.js b/test/linters/params/ruff.js new file mode 100644 index 00000000..d5cd4503 --- /dev/null +++ b/test/linters/params/ruff.js @@ -0,0 +1,67 @@ +const { EOL } = require("os"); + +const Ruff = require("../../../src/linters/ruff"); + +const testName = "ruff"; +const linter = Ruff; +const args = ""; +const commandPrefix = ""; +const extensions = ["py"]; + +// Linting without auto-fixing +function getLintParams(dir) { + const stdoutFile1 = `file1.py:3:8: F401 [*] \`os\` imported but unused`; + const stdoutFile2 = `file2.py:1:4: F821 Undefined name \`a\`${EOL}file2.py:1:5: E701 Multiple statements on one line (colon)`; + return { + // Expected output of the linting function + cmdOutput: { + status: 1, + stdoutParts: [stdoutFile1, stdoutFile2], + stdout: `${stdoutFile1}${EOL}${stdoutFile2}`, + }, + // Expected output of the parsing function + lintResult: { + isSuccess: false, + warning: [], + error: [ + { + path: "file1.py", + firstLine: 3, + lastLine: 3, + message: "[*] `os` imported but unused (F401)", + }, + { + path: "file2.py", + firstLine: 1, + lastLine: 1, + message: "Undefined name `a` (F821)", + }, + { + path: "file2.py", + firstLine: 1, + lastLine: 1, + message: "Multiple statements on one line (colon) (E701)", + }, + ], + }, + }; +} + +// Linting with auto-fixing +function getFixParams(dir) { + return { + // Expected output of the linting function + cmdOutput: { + status: 1, + stdout: "", + }, + // Expected output of the parsing function + lintResult: { + isSuccess: false, + warning: [], + error: [], + }, + }; +} + +module.exports = [testName, linter, commandPrefix, extensions, args, getLintParams, getFixParams]; diff --git a/test/linters/projects/ruff/file1.py b/test/linters/projects/ruff/file1.py new file mode 100644 index 00000000..26c0b4a8 --- /dev/null +++ b/test/linters/projects/ruff/file1.py @@ -0,0 +1,8 @@ +from typing import List + +import os + + +def sum_even_numbers(numbers: List[int]) -> int: + """Given a list of integers, return the sum of all even numbers in the list.""" + return sum(num for num in numbers if num % 2 == 0) diff --git a/test/linters/projects/ruff/file2.py b/test/linters/projects/ruff/file2.py new file mode 100644 index 00000000..77273e77 --- /dev/null +++ b/test/linters/projects/ruff/file2.py @@ -0,0 +1 @@ +if a: a = False diff --git a/test/linters/projects/ruff/requirements.txt b/test/linters/projects/ruff/requirements.txt new file mode 100644 index 00000000..96545fb1 --- /dev/null +++ b/test/linters/projects/ruff/requirements.txt @@ -0,0 +1 @@ +ruff>=0.0.257