From 9c7d939f2204b279a37e4d50f94de28c55c36760 Mon Sep 17 00:00:00 2001 From: Alexander-N Date: Sun, 6 Aug 2017 05:01:01 +0200 Subject: [PATCH] JSStandardBear: Add fixing capabilities The corrected code is retrieved by piping to `standard`. Since this is not supported in standardJS 7 change the npm requirement to version 8. If there are several issues in a single line, the messages are displayed together to make it easy to understand the proposed fix. Closes https://github.com/coala/coala-bears/issues/1952 --- bears/js/JSStandardBear.py | 74 ++++++++++++++++++++++++++++++-- package.json | 2 +- tests/js/JSStandardBearTest.py | 77 +++++++++++++++++++++++++++++++++- 3 files changed, 146 insertions(+), 7 deletions(-) diff --git a/bears/js/JSStandardBear.py b/bears/js/JSStandardBear.py index a68f999259..1a49a4853e 100644 --- a/bears/js/JSStandardBear.py +++ b/bears/js/JSStandardBear.py @@ -1,11 +1,17 @@ +from collections import defaultdict +import re +from subprocess import Popen, PIPE +from typing import List, Iterable, Tuple + from coalib.bearlib.abstractions.Linter import linter from dependency_management.requirements.NpmRequirement import NpmRequirement +from coalib.results.Result import Result +from coalib.results.Diff import Diff @linter(executable='standard', - output_format='regex', - output_regex=r'\s*[^:]+:(?P\d+):(?P\d+):' - r'\s*(?P.+)') + use_stdin=True, + use_stderr=True) class JSStandardBear: """ One JavaScript Style to Rule Them All. @@ -16,7 +22,7 @@ class JSStandardBear: """ LANGUAGES = {'JavaScript', 'JSX'} - REQUIREMENTS = {NpmRequirement('standard', '7')} + REQUIREMENTS = {NpmRequirement('standard', '8')} AUTHORS = {'The coala developers'} AUTHORS_EMAILS = {'coala-devel@googlegroups.com'} LICENSE = 'AGPL-3.0' @@ -24,5 +30,65 @@ class JSStandardBear: CAN_FIX = {'Formatting'} SEE_MORE = 'https://standardjs.com/rules.html' + issue_regex = re.compile( + r'\s*[^:]+:(?P\d+):(?P\d+):' + r'\s*(?P.+)') + def create_arguments(self, filename, file, config_file): return (filename, '--verbose') + + @staticmethod + def _get_corrected_code(old_code: List[str]) -> List[str]: + """ + Pipes the code to JSStandard and returns the corrected code. + """ + p = Popen( + ('standard', '--stdin', '--fix'), + stdin=PIPE, stdout=PIPE, stderr=PIPE) + p.stdin.write(bytes(''.join(old_code), 'UTF-8')) + out, err = p.communicate() + return out.decode('UTF-8').splitlines(True) + + def _get_issues(self, stdout: str) -> Iterable[Tuple[int, str]]: + """ + Gets the issues from the output of JSStandard. + + The issues get parsed with `self.issue_regex` and merged if they + concern the same line. + + :param stdout: Output from which the issues get parsed. + :return: List of tuples containing the line number and the message. + """ + match_objects = ( + self.issue_regex.match(line) for line in stdout.splitlines()) + issues = ( + match_object.groupdict() + for match_object in match_objects if match_object is not None) + line_number_to_messages = defaultdict(list) + for issue in issues: + line_number = int(issue['line']) + line_number_to_messages[line_number].append(issue['message']) + return ( + (line_number, '\n'.join(messages)) + for line_number, messages + in sorted(line_number_to_messages.items())) + + def process_output(self, output, filename, file): + stdout, stderr = output + corrected_code = None + if '--fix' in stderr: + corrected_code = self._get_corrected_code(file) + if len(file) != len(corrected_code): + corrected_code = None + + for line_number, message in self._get_issues(stdout): + diff = None + if corrected_code: + diff = Diff(file) + diff.modify_line(line_number, corrected_code[line_number-1]) + yield Result.from_values( + origin=self, + message=message, + file=filename, + line=line_number, + diffs={filename: diff}) diff --git a/package.json b/package.json index 260e464a60..b2b14a39e1 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "remark-cli": "~2", "remark-lint": "~5", "remark-validate-links": "~5", - "standard": "~7", + "standard": "~8", "stylelint": "~7", "stylint": "~1.5.9", "textlint": "~7.3.0", diff --git a/tests/js/JSStandardBearTest.py b/tests/js/JSStandardBearTest.py index 30b638de0b..c39a6b246e 100644 --- a/tests/js/JSStandardBearTest.py +++ b/tests/js/JSStandardBearTest.py @@ -1,5 +1,9 @@ -from coalib.testing.LocalBearTestHelper import verify_local_bear +from queue import Queue +from coalib.settings.Section import Section +from coalib.testing.BearTestHelper import generate_skip_decorator +from coalib.testing.LocalBearTestHelper import verify_local_bear +from coalib.testing.LocalBearTestHelper import LocalBearTestHelper from bears.js.JSStandardBear import JSStandardBear @@ -17,7 +21,7 @@ bar = bar === 1 ? bar : 1 - if ((x = 33)) { + if ((x === 33)) { console.log(bar + "hello 'world'") } } else { @@ -77,3 +81,72 @@ bad_file_undef, bad_file_ifelse, bad_file_func_name,)) + + +@generate_skip_decorator(JSStandardBear) +class JSStandardBearTestClass(LocalBearTestHelper): + bad_code = """ +(function () { + console.log('wrong indentation') + console.log('wrong indentation and semicolon'); +}()) +""".splitlines(True) + + def setUp(self): + section = Section('name') + self.js_standard_bear = JSStandardBear(section, Queue()) + + def test_messages(self): + results = self.check_invalidity( + self.js_standard_bear, + self.bad_code) + self.assertEqual(2, len(results)) + self.assertIn('(indent)', results[0].message) + self.assertEqual(1, len(results[0].affected_code)) + self.assertEqual(3, results[0].affected_code[0].start.line) + self.assertEqual(3, results[0].affected_code[0].end.line) + + # Check that messages for lines with multiple issues get merged. + self.assertIn('(indent)', results[1].message) + self.assertIn('(semi)', results[1].message) + self.assertEqual(1, len(results[1].affected_code)) + self.assertEqual(4, results[1].affected_code[0].start.line) + self.assertEqual(4, results[1].affected_code[0].end.line) + + def test_corrected_code(self): + """ + Check that the corrected code is valid. + """ + corrected_code = self.js_standard_bear._get_corrected_code( + self.bad_code) + self.check_validity( + self.js_standard_bear, + corrected_code) + + def test_get_issues_from_output(self): + output = """ +coala-bears/test.js:2:5: Expected indentation of 2 spaces but found 4. (indent) +coala-bears/test.js:3:5: Expected indentation of 2 spaces but found 4. (indent) +coala-bears/test.js:3:51: Extra semicolon. (semi) +""" + issues = list(self.js_standard_bear._get_issues(output)) + line_numbers = [line_number for line_number, _ in issues] + self.assertEqual(line_numbers, [2, 3]) + messages = [message for _, message in issues] + expected_messages = [ + 'Expected indentation of 2 spaces but found 4. (indent)', + ('Expected indentation of 2 spaces but found 4. (indent)\n' + 'Extra semicolon. (semi)')] + self.assertEqual(messages, expected_messages) + + def test_different_number_of_lines(self): + """ + Check a case where the number of lines of the invalid and the + corrected code differs. + """ + bad_code_with_empty_lines = ['\n', '\n'] + self.bad_code + results = self.check_invalidity( + self.js_standard_bear, + bad_code_with_empty_lines) + self.assertEqual(3, len(results)) + self.assertIn('(no-multiple-empty-lines)', results[0].message)