diff --git a/bears/general/QuotesBear.py b/bears/general/QuotesBear.py new file mode 100644 index 0000000000..f15806eb7f --- /dev/null +++ b/bears/general/QuotesBear.py @@ -0,0 +1,82 @@ +from coalib.bears.LocalBear import LocalBear +from bears.general.AnnotationBear import AnnotationBear +from coalib.results.Diff import Diff +from coalib.results.HiddenResult import HiddenResult +from coalib.results.Result import Result + + +class QuotesBear(LocalBear): + + BEAR_DEPS = {AnnotationBear} + AUTHORS = {'The coala developers'} + AUTHORS_EMAILS = {'coala-devel@googlegroups.com'} + LICENSE = 'AGPL-3.0' + CAN_DETECT = {'Formatting'} + + def correct_single_line_str(self, filename, file, sourcerange, + preferred_quotation): + """ + Corrects a given single line string assuming it does not use the + preferred quotation. If the preferred quotation mark is used inside the + string, no correction will be made. + + This function will yield one or no Result objects. + + :param filename: + The filename of the file to correct the line in. + :param file: + The file contents as list of lines. + :param sourcerange: + The sourcerange indicating where to find the string. + :param preferred_quotation: + ``'`` or ``"`` respectively. + """ + str_contents = file[sourcerange.start.line - 1][ + sourcerange.start.column:sourcerange.end.column-1] + + if preferred_quotation in str_contents: + return + + before = file[sourcerange.start.line - 1][:sourcerange.start.column-1] + after = file[sourcerange.end.line - 1][sourcerange.end.column:] + + replacement = (before + preferred_quotation + str_contents + + preferred_quotation + after) + + diff = Diff(file) + diff.change_line(sourcerange.start.line, + file[sourcerange.start.line - 1], + replacement) + yield Result(self, 'You do not use the preferred quotation marks.', + diff.affected_code(filename), diffs={filename: diff}) + + def run(self, filename, file, dependency_results, + preferred_quotation: str='"'): + """ + Checks and corrects your quotation style. + + For all single line strings, this bear will correct the quotation to + your preferred quotation style if that kind of quote is not included + within the string. Multi line strings are not supported. + + :param preferred_quotation: Your preferred quotation character, e.g. + ``"`` or ``'``. + """ + if not isinstance(dependency_results[AnnotationBear.name][0], + HiddenResult): + return + if isinstance(dependency_results[AnnotationBear.name][0].contents, + str): + self.err(dependency_results[AnnotationBear.name][0].contents) + return + + ranges = dependency_results[AnnotationBear.name][0].contents['strings'] + + for string_range in ranges: + if (file[string_range.start.line-1][string_range.start.column-1] == + preferred_quotation): + continue + + if string_range.start.line == string_range.end.line: + yield from self.correct_single_line_str( + filename, file, string_range, preferred_quotation) diff --git a/tests/general/QuotesBearTest.py b/tests/general/QuotesBearTest.py new file mode 100644 index 0000000000..196d5c6ad2 --- /dev/null +++ b/tests/general/QuotesBearTest.py @@ -0,0 +1,99 @@ +import unittest +from queue import Queue +from textwrap import dedent + +from coalib.results.HiddenResult import HiddenResult, Result +from bears.general.QuotesBear import QuotesBear +from coalib.results.SourceRange import SourceRange +from coalib.settings.Section import Section +from coalib.testing.LocalBearTestHelper import verify_local_bear, execute_bear + + +class QuotesBearDiffTest(unittest.TestCase): + + def setUp(self): + self.section = Section('') + self.uut = QuotesBear(self.section, Queue()) + + self.double_quote_file = dedent(""" + ''' + Multiline string + ''' + "a string with double quotes!" + 'A single quoted string with " in it' + """).splitlines(True) + + self.single_quote_file = dedent(""" + ''' + Multiline string + ''' + 'a string with single quotes!' + "A double quoted string with ' in it" + """).splitlines(True) + + self.filename = 'f' + + self.dep_results = { + 'AnnotationBear': + [HiddenResult( + 'AnnotationBear', + {'comments': (), 'strings': ( + SourceRange.from_values(self.filename, 2, 1, 4, 3), + SourceRange.from_values(self.filename, 5, 1, 5, 30), + SourceRange.from_values(self.filename, 6, 1, 6, 37)) + } + )] + } + + def test_error_handling(self): + dep_results = {'AnnotationBear': [Result("test", "test")]} + with execute_bear(self.uut, self.filename, self.double_quote_file, + dependency_results=dep_results) as results: + self.assertEqual(len(results), 0) + + dep_results = {'AnnotationBear': [HiddenResult('a', 'error!')]} + with execute_bear(self.uut, self.filename, self.double_quote_file, + dependency_results=dep_results) as results: + self.assertEqual(len(results), 0) + + def test_valid_quotes(self): + with execute_bear(self.uut, self.filename, self.double_quote_file, + dependency_results=self.dep_results) as results: + self.assertEqual(len(results), 0) + + self.section['preferred_quotation'] = "'" + with execute_bear(self.uut, self.filename, self.single_quote_file, + dependency_results=self.dep_results) as results: + self.assertEqual(len(results), 0) + + def test_invalid_quotes(self): + with execute_bear(self.uut, self.filename, self.single_quote_file, + dependency_results=self.dep_results) as results: + res_list = list(results) + self.assertEqual(len(res_list), 1) + self.assertEqual(res_list[0].diffs[self.filename].unified_diff, + '--- \n' + '+++ \n' + '@@ -2,5 +2,5 @@\n' + " '''\n" + " Multiline string\n" + " '''\n" + "-'a string with single quotes!'\n" + '+"a string with single quotes!"\n' + ' "A double quoted string with \' in it"\n') + + self.section['preferred_quotation'] = "'" + with execute_bear(self.uut, self.filename, self.double_quote_file, + dependency_results=self.dep_results) as results: + res_list = list(results) + self.assertEqual(len(res_list), 1) + self.assertEqual(res_list[0].diffs[self.filename].unified_diff, + '--- \n' + '+++ \n' + '@@ -2,5 +2,5 @@\n' + " '''\n" + " Multiline string\n" + " '''\n" + '-"a string with double quotes!"\n' + "+'a string with double quotes!'\n" + " 'A single quoted string with \" in it'\n")