Skip to content

Commit

Permalink
Add mypy python linter
Browse files Browse the repository at this point in the history
  • Loading branch information
ascandella committed Feb 9, 2020
1 parent 3c8c7c2 commit f894f77
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ jobs:
pip install -r ./requirements.txt
cd ../flake8/
pip install -r ./requirements.txt
cd ../mypy/
pip install -r ./requirements.txt
# Ruby

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- **Python:**
- [Black](https://black.readthedocs.io)
- [Flake8](http://flake8.pycqa.org)
- [Mypy](https://mypy.readthedocs.io/)
- **Ruby:**
- [RuboCop](https://rubocop.readthedocs.io)
- **Swift:**
Expand Down Expand Up @@ -137,7 +138,7 @@ jobs:
### Linter-specific options
`[linter]` can be one of `black`, `eslint`, `flake8`, `gofmt`, `golint`, `prettier`, `rubocop`, `stylelint`, `swiftformat` and `swiftlint`:
`[linter]` can be one of `black`, `eslint`, `flake8`, `gofmt`, `golint`, `mypy`, `prettier`, `rubocop`, `stylelint`, `swiftformat` and `swiftlint`:

- **`[linter]`:** Enables the linter in your repository. Default: `false`
- **`[linter]_args`**: Additional arguments to pass to the linter. Example: `eslint_args: "--max-warnings 0"` if ESLint checks should fail even if there are no errors and only warnings. Default: `""`
Expand Down
16 changes: 16 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ inputs:
required: false
default: "py"

mypy:
description: Enable or disable mypy checks
required: false
default: false
mypy_args:
description: Additional arguments to pass to the linter
required: false
default: ""
mypy_dir:
description: Directory where the mypy command should be run
required: false
mypy_extensions:
description: Extensions of files to check with mypy
required: false
default: "py"

# 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 @@ -3,6 +3,7 @@ const ESLint = require("./eslint");
const Flake8 = require("./flake8");
const Gofmt = require("./gofmt");
const Golint = require("./golint");
const Mypy = require("./mypy");
const Prettier = require("./prettier");
const RuboCop = require("./rubocop");
const Stylelint = require("./stylelint");
Expand All @@ -14,6 +15,7 @@ const linters = {
eslint: ESLint,
flake8: Flake8,
golint: Golint,
mypy: Mypy,
rubocop: RuboCop,
stylelint: Stylelint,
swiftlint: SwiftLint,
Expand Down
102 changes: 102 additions & 0 deletions src/linters/mypy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const fs = require("fs");
const { sep } = require("path");

const commandExists = require("../../vendor/command-exists");
const { log, run } = require("../utils/action");
const { initLintResult } = require("../utils/lint-result");

const PARSE_REGEX = /^(.*):([0-9]+): (\w*): (.*)$/gm;

/**
* https://mypy.readthedocs.io/en/stable/
*/
class Mypy {
static get name() {
return "Mypy";
}

/**
* Verifies that all required programs are installed. Throws an error if programs are missing
* @param {string} dir - Directory to run the linting program in
*/
static async verifySetup(dir) {
// Verify that Python is installed (required to execute Flake8)
if (!(await commandExists("python"))) {
throw new Error("Python is not installed");
}

// Verify that mypy is installed
if (!(await commandExists("mypy"))) {
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
* @returns {{status: number, stdout: string, stderr: string}} - Output of the lint command
*/
static lint(dir, extensions, args = "", fix = false) {
if (fix) {
log(`${this.name} does not support auto-fixing`, "warning");
}

let specifiedPath = false;
// Check if they passed a directory as an arg
for (const arg of args.split(" ")) {
if (fs.existsSync(arg)) {
specifiedPath = true;
break;
}
}
let extraArgs = "";
if (!specifiedPath) {
extraArgs = ` ${dir}`;
}
return run(`mypy ${args}${extraArgs}`, {
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 {{isSuccess: boolean, warning: [], error: []}} - 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, level, 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);
const result = {
path,
firstLine: lineNr,
lastLine: lineNr,
message: text,
};
if (level === "error") {
lintResult.error.push(result);
} else if (level === "warning") {
lintResult.warning.push(result);
}
}

return lintResult;
}
}

module.exports = Mypy;
2 changes: 2 additions & 0 deletions test/linters/linters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const eslintTypescriptParams = require("./params/eslint-typescript");
const flake8Params = require("./params/flake8");
const gofmtParams = require("./params/gofmt");
const golintParams = require("./params/golint");
const mypyParams = require("./params/mypy");
const prettierParams = require("./params/prettier");
const ruboCopParams = require("./params/rubocop");
const stylelintParams = require("./params/stylelint");
Expand All @@ -22,6 +23,7 @@ const linterParams = [
flake8Params,
gofmtParams,
golintParams,
mypyParams,
prettierParams,
ruboCopParams,
stylelintParams,
Expand Down
45 changes: 45 additions & 0 deletions test/linters/params/mypy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const { EOL } = require("os");

const Mypy = require("../../../src/linters/mypy");

const testName = "mypy";
const linter = Mypy;
const extensions = ["py"];

// Linting without auto-fixing
function getLintParams(dir) {
const stdoutPart1 = `file1.py:7: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str"`;
const stdoutPart2 = `file1.py:11: error: Argument 1 to "main" has incompatible type "List[str]"; expected "str"`;
return {
// Expected output of the linting function
cmdOutput: {
status: 1,
stdoutParts: [stdoutPart1, stdoutPart2],
stdout: `${stdoutPart1}${EOL}${stdoutPart2}`,
},
// Expected output of the parsing function
lintResult: {
isSuccess: false,
warning: [],
error: [
{
path: "file1.py",
firstLine: 7,
lastLine: 7,
message: `Dict entry 0 has incompatible type "str": "int"; expected "str": "str"`,
},
{
path: "file1.py",
firstLine: 11,
lastLine: 11,
message: `Argument 1 to "main" has incompatible type "List[str]"; expected "str"`,
},
],
},
};
}

// Linting with auto-fixing
const getFixParams = getLintParams; // Does not support auto-fixing -> option has no effect

module.exports = [testName, linter, extensions, getLintParams, getFixParams];
11 changes: 11 additions & 0 deletions test/linters/projects/mypy/file1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from file2 import helper


def main(input_str: str):
print(input_str)
print(helper({
input_str: 42,
}))


main(["hello"])
5 changes: 5 additions & 0 deletions test/linters/projects/mypy/file2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Mapping


def helper(var: Mapping[str, str]):
pass
1 change: 1 addition & 0 deletions test/linters/projects/mypy/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mypy>=0.761

0 comments on commit f894f77

Please sign in to comment.