Skip to content

Commit

Permalink
Add ruff
Browse files Browse the repository at this point in the history
  • Loading branch information
pickfire committed May 31, 2023
1 parent 2b692c4 commit 96daf5e
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://beta.ruff.rs)
- **Ruby:**
- [ERB Lint](https://github.com/Shopify/erb-lint)
- [RuboCop](https://rubocop.readthedocs.io)
Expand Down Expand Up @@ -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 | ✅ | ✅ |
Expand Down
24 changes: 24 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions src/linters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -45,6 +46,7 @@ const linters = {
dotnet_format: DotnetFormat,
gofmt: Gofmt,
oitnb: Oitnb,
ruff: Ruff,
rustfmt: RustFmt,
prettier: Prettier,
swift_format_lockwood: SwiftFormatLockwood,
Expand Down
91 changes: 91 additions & 0 deletions src/linters/ruff.js
Original file line number Diff line number Diff line change
@@ -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://beta.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 --exit-non-zero-on-fix" : "";
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;
2 changes: 2 additions & 0 deletions test/linters/linters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -44,6 +45,7 @@ const linterParams = [
prettierParams,
pylintParams,
ruboCopParams,
ruffParams,
rustfmtParams,
stylelintParams,
tscParams,
Expand Down
67 changes: 67 additions & 0 deletions test/linters/params/ruff.js
Original file line number Diff line number Diff line change
@@ -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];
8 changes: 8 additions & 0 deletions test/linters/projects/ruff/file1.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions test/linters/projects/ruff/file2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
if a: a = False
1 change: 1 addition & 0 deletions test/linters/projects/ruff/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruff>=0.0.257

0 comments on commit 96daf5e

Please sign in to comment.