diff --git a/bear-languages.yaml b/bear-languages.yaml index b8095b55a9..9144835d06 100644 --- a/bear-languages.yaml +++ b/bear-languages.yaml @@ -329,6 +329,7 @@ RadonBear: - Python - Python 2 - Python 3 +RegexLintBear: RuboCopBear: - Ruby RubyFastererBear: diff --git a/bear-requirements.txt b/bear-requirements.txt index 8d6a60a46f..30aafdbb7d 100644 --- a/bear-requirements.txt +++ b/bear-requirements.txt @@ -31,6 +31,7 @@ pylint~=1.7.2 pyroma~=2.2.0 pyyaml~=3.12 radon==1.4.0 +regexlint~=1.6 restructuredtext-lint~=1.0 rstcheck~=3.1 safety~=1.8.2 diff --git a/bear-requirements.yaml b/bear-requirements.yaml index d168d4535c..deda87c2e7 100644 --- a/bear-requirements.yaml +++ b/bear-requirements.yaml @@ -68,6 +68,8 @@ pip_requirements: version: ~=3.12 radon: version: ==1.4.0 + regexlint: + version: ~=1.6 restructuredtext-lint: version: ~=1.0 rstcheck: diff --git a/bears/general/RegexLintBear.py b/bears/general/RegexLintBear.py new file mode 100644 index 0000000000..806a1b583d --- /dev/null +++ b/bears/general/RegexLintBear.py @@ -0,0 +1,55 @@ +import re + +from queue import Queue +from sarge import run, Capture +from contextlib import suppress + +from bears.general.AnnotationBear import AnnotationBear + +from coalib.bears.LocalBear import LocalBear +from coalib.results.Result import Result +from coalib.settings.Section import Section +from coalib.settings.Setting import Setting +from coalib.testing.LocalBearTestHelper import execute_bear + +from dependency_management.requirements.PipRequirement import PipRequirement + + +class RegexLintBear(LocalBear): + LANGUAGES = {'All'} + REQUIREMENTS = {PipRequirement('regexlint', '1.6')} + AUTHORS = {'The coala developers'} + AUTHORS_EMAILS = {'coala-devel@googlegroups.com'} + LICENSE = 'AGPL-3.0' + CAN_DETECT = {'Formatting'} + + def run(self, filename, file, language: str): + """ + Bear for linting regex through regexlint. + + :param language: + The programming language of the file(s). + """ + section = Section('') + section.append(Setting('language', language)) + bear = AnnotationBear(section, Queue()) + + with execute_bear(bear, filename, file) as result: + for src_range in result[0].contents['strings']: + src_line = src_range.affected_source({filename: file})[0] + regex = src_line[src_range.start.column:src_range.end.column-1] + with suppress(re.error): + re.compile(regex) + out = run('regexlint --regex "{}"'.format(regex), + stdout=Capture()).stdout.text + if out[-3:-1] == 'OK': + continue + yield Result.from_values( + origin=self, + message=out, + file=filename, + line=src_range.start.line, + column=src_range.start.column, + end_line=src_range.end.line, + end_column=src_range.end.column, + ) diff --git a/tests/general/RegexLintBearTest.py b/tests/general/RegexLintBearTest.py new file mode 100644 index 0000000000..3c527de47e --- /dev/null +++ b/tests/general/RegexLintBearTest.py @@ -0,0 +1,81 @@ +from os.path import abspath +from queue import Queue + +from bears.general.RegexLintBear import RegexLintBear +from coalib.results.Result import Result +from coalib.settings.Section import Section +from coalib.settings.Setting import Setting +from coalib.testing.LocalBearTestHelper import ( + LocalBearTestHelper, execute_bear) + + +test_good_py_file = """ +some_regex = r'[a-zA-Z]]' +""" + +test_bad_py_file = """ +some_regex = r'(else|elseif)' +""" + +test_good_cpp_file = """ +char some_regex[] = "[a-zA-Z]]"; +""" + +test_bad_cpp_file = """ +char some_regex[13] = "(else|elseif)"; +""" + +test_re_error_file = """ +some_regex = r'*ab' # This should be skipped +some_other_regex = r'[a-z]' +""" + +BAD_MESSAGE = """ +E105:argv:root:0: Potential out of order alternation between 'else' and 'elseif' + '(else|elseif)' + ^ here +""".lstrip() + + +class RegexLintBearTest(LocalBearTestHelper): + + def setUp(self): + self.section = Section('') + self.queue = Queue() + self.uut = RegexLintBear(self.section, self.queue) + + def test_good_python_file(self): + self.section.append(Setting('language', 'python 3')) + self.check_validity(self.uut, test_good_py_file.splitlines()) + + def test_bad_python_file(self): + self.section.append(Setting('language', 'python 3')) + with execute_bear(self.uut, abspath('bad_python_file'), + test_bad_py_file.splitlines()) as results: + self.assertEqual(len(results), 1) + self.assertEqual(results[0], + Result.from_values(origin=self.uut, + message=BAD_MESSAGE, + file='bad_python_file', + line=2, column=15, + end_line=2, end_column=29)) + + def test_good_cpp_file(self): + self.section.append(Setting('language', 'cpp')) + self.check_validity(self.uut, test_good_cpp_file.splitlines()) + + def test_bad_cpp_file(self): + self.section.append(Setting('language', 'cpp')) + with execute_bear(self.uut, abspath('bad_cpp_file'), + test_bad_cpp_file.splitlines()) as results: + self.assertEqual(len(results), 1) + self.assertEqual(results[0], + Result.from_values(origin=self.uut, + message=BAD_MESSAGE, + file='bad_cpp_file', + line=2, column=23, + end_line=2, end_column=37)) + + def test_re_error_file(self): + self.section.append(Setting('language', 'cpp')) + self.check_validity(self.uut, test_re_error_file.splitlines())