diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 20fb3821438a5..09b755bb2bdd0 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -96,7 +96,7 @@ Optional arguments Show program's version number and exit. -.. option:: -O FORMAT, --output FORMAT {json} +.. option:: -O {json,github}, --output {json,github} Set a custom output format. diff --git a/mypy/error_formatter.py b/mypy/error_formatter.py index ffc6b67475962..bca9912cbc227 100644 --- a/mypy/error_formatter.py +++ b/mypy/error_formatter.py @@ -34,4 +34,28 @@ def report_error(self, error: "MypyError") -> str: ) -OUTPUT_CHOICES = {"json": JSONFormatter()} +class GitHubFormatter(ErrorFormatter): + """Formatter for GitHub Actions output format.""" + + def report_error(self, error: "MypyError") -> str: + """Prints out the errors as GitHub Actions annotations.""" + command = "error" if error.severity == "error" else "notice" + title = f"Mypy ({error.errorcode.code})" if error.errorcode is not None else "Mypy" + + message = f"{error.message}." + + if error.hints: + message += "%0A%0A" + message += "%0A".join(error.hints) + + return ( + f"::{command} " + f"file={error.file_path}," + f"line={error.line}," + f"col={error.column}," + f"title={title}" + f"::{message}" + ) + + +OUTPUT_CHOICES = {"json": JSONFormatter(), "github": GitHubFormatter()} diff --git a/mypy/main.py b/mypy/main.py index f177bb1c2062c..b112483104b9d 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -530,11 +530,7 @@ def add_invertible_flag( ) general_group.add_argument( - "-O", - "--output", - metavar="FORMAT", - help="Set a custom output format", - choices=OUTPUT_CHOICES, + "-O", "--output", help="Set a custom output format", choices=OUTPUT_CHOICES ) config_group = parser.add_argument_group( diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py index 41f6881658c8c..204bd61b2ab48 100644 --- a/mypy/test/testoutput.py +++ b/mypy/test/testoutput.py @@ -56,3 +56,42 @@ def test_output_json(testcase: DataDrivenTestCase) -> None: normalized_output = [line.replace(test_temp_dir + json_os_separator, "") for line in output] assert normalized_output == testcase.output + + +class OutputGitHubsuite(DataSuite): + files = ["outputgithub.test"] + + def run_case(self, testcase: DataDrivenTestCase) -> None: + test_output_github(testcase) + + +def test_output_github(testcase: DataDrivenTestCase) -> None: + """Run Mypy in a subprocess, and ensure that `--output=github` works as intended.""" + mypy_cmdline = ["--output=github"] + mypy_cmdline.append(f"--python-version={'.'.join(map(str, PYTHON3_VERSION))}") + + # Write the program to a file. + program_path = os.path.join(test_temp_dir, "main") + mypy_cmdline.append(program_path) + with open(program_path, "w", encoding="utf8") as file: + for s in testcase.input: + file.write(f"{s}\n") + + output = [] + # Type check the program. + out, err, returncode = api.run(mypy_cmdline) + # split lines, remove newlines, and remove directory of test case + for line in (out + err).rstrip("\n").splitlines(): + if line.startswith(test_temp_dir + os.sep): + output.append(line[len(test_temp_dir + os.sep) :].rstrip("\r\n")) + else: + output.append(line.rstrip("\r\n")) + + if returncode > 1: + output.append("!!! Mypy crashed !!!") + + # Remove temp file. + os.remove(program_path) + + normalized_output = [line.replace(test_temp_dir + os.sep, "") for line in output] + assert normalized_output == testcase.output diff --git a/test-data/unit/outputgithub.test b/test-data/unit/outputgithub.test new file mode 100644 index 0000000000000..471cefcc68bba --- /dev/null +++ b/test-data/unit/outputgithub.test @@ -0,0 +1,44 @@ +-- Test cases for `--output=json`. +-- These cannot be run by the usual unit test runner because of the backslashes +-- in the output, which get normalized to forward slashes by the test suite on +-- Windows. + +[case testOutputGitHubNoIssues] +# flags: --output=github +def foo() -> None: + pass + +foo() +[out] + +[case testOutputGitHubSimple] +# flags: --output=github +def foo() -> None: + pass + +foo(1) +[out] +::error file=main,line=5,col=0,title=Mypy (call-arg)::Too many arguments for "foo". + +[case testOutputGitHubWithHint] +# flags: --output=github +from typing import Optional, overload + +@overload +def foo() -> None: ... +@overload +def foo(x: int) -> None: ... + +def foo(x: Optional[int] = None) -> None: + ... + +reveal_type(foo) + +foo('42') + +def bar() -> None: ... +bar('42') +[out] +::notice file=main,line=12,col=12,title=Mypy (misc)::Revealed type is "Overload(def (), def (x: builtins.int))". +::error file=main,line=14,col=0,title=Mypy (call-overload)::No overload variant of "foo" matches argument type "str".%0A%0APossible overload variants:%0A def foo() -> None%0A def foo(x: int) -> None +::error file=main,line=17,col=0,title=Mypy (call-arg)::Too many arguments for "bar".