Пример #1
0
class RadonBear(LocalBear):
    LANGUAGES = {"Python", "Python 2", "Python 3"}
    REQUIREMENTS = {PipRequirement('radon', '1.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Complexity'}

    def run(self,
            filename,
            file,
            radon_ranks_info: typed_list(str) = (),
            radon_ranks_normal: typed_list(str) = ('C', 'D'),
            radon_ranks_major: typed_list(str) = ('E', 'F')):
        """
        Uses radon to compute complexity of a given file.

        :param radon_ranks_info:   The ranks (given by radon) to
                                   treat as severity INFO.
        :param radon_ranks_normal: The ranks (given by radon) to
                                   treat as severity NORMAL.
        :param radon_ranks_major:  The ranks (given by radon) to
                                   treat as severity MAJOR.
        """
        severity_map = {
            RESULT_SEVERITY.INFO: radon_ranks_info,
            RESULT_SEVERITY.NORMAL: radon_ranks_normal,
            RESULT_SEVERITY.MAJOR: radon_ranks_major
        }
        for visitor in radon.complexity.cc_visit("".join(file)):
            rank = radon.complexity.cc_rank(visitor.complexity)
            severity = None
            for result_severity, rank_list in severity_map.items():
                if rank in rank_list:
                    severity = result_severity
            if severity is None:
                continue

            col = visitor.col_offset if visitor.col_offset else None
            visitor_range = SourceRange.from_values(filename, visitor.lineno,
                                                    col, visitor.endline)
            message = "{} has a cyclomatic complexity of {}".format(
                visitor.name, rank)

            yield Result(self,
                         message,
                         severity=severity,
                         affected_code=(visitor_range, ))
Пример #2
0
class SpellCheckBear:
    """
    Lints files to check for incorrect spellings using ``scspell``.

    See <https://pypi.python.org/pypi/scspell> for more information.
    """
    LANGUAGES = {"Natural Language"}
    REQUIREMENTS = {PipRequirement('scspell3k', '2.0')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Spelling'}

    @staticmethod
    def create_arguments(filename, file, config_file):
        return '--report-only', filename
Пример #3
0
class YAMLLintBear:
    """
    Check yaml code for errors and possible problems.

    You can read more about capabilities at
    <http://yamllint.readthedocs.org/en/latest/rules.html>.
    """

    LANGUAGES = {"YAML"}
    REQUIREMENTS = {PipRequirement('yamllint', '1.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Syntax', 'Formatting'}

    @staticmethod
    def generate_config(filename, file,
                        document_start: bool=False):
        """
        :param document_start:
            Use this rule to require or forbid the use of document start
            marker (---).
        """
        yamllint_configs = {
            'extends': 'default',
            'rules': {
                'document-start': {
                    'present': False
                 }
            }
        }
        if document_start:
            yamllint_configs['rules']['document-start']['present'] = True

        return yaml.dump(yamllint_configs)

    @staticmethod
    def create_arguments(filename, file, config_file, yamllint_config: str=''):
        """
        :param yamllint_config: Path to a custom configuration file.
        """
        args = ('-f', 'parsable', filename)
        if yamllint_config:
            args += ('--config=' + yamllint_config,)
        else:
            args += ('--config-file=' + config_file,)
        return args
Пример #4
0
class PEP8Bear(LocalBear):
    LANGUAGES = {"Python", "Python 2", "Python 3"}
    REQUIREMENTS = {PipRequirement('autopep8', '1.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_FIX = {'Formatting'}

    @deprecate_settings(indent_size='tab_width')
    def run(self,
            filename,
            file,
            max_line_length: int = 79,
            indent_size: int = SpacingHelper.DEFAULT_TAB_WIDTH,
            pep_ignore: typed_list(str) = (),
            pep_select: typed_list(str) = (),
            local_pep8_config: bool = False):
        """
        Detects and fixes PEP8 incompliant code. This bear will not change
        functionality of the code in any way.

        :param max_line_length:   Maximum number of characters for a line.
        :param indent_size:       Number of spaces per indentation level.
        :param pep_ignore:        A list of errors/warnings to ignore.
        :param pep_select:        A list of errors/warnings to exclusively
                                  apply.
        :param local_pep8_config: Set to true if autopep8 should use a config
                                  file as if run normally from this directory.
        """
        options = {
            "ignore": pep_ignore,
            "select": pep_select,
            "max_line_length": max_line_length,
            "indent_size": indent_size
        }

        corrected = autopep8.fix_code(''.join(file),
                                      apply_config=local_pep8_config,
                                      options=options).splitlines(True)

        diffs = Diff.from_string_arrays(file, corrected).split_diff()

        for diff in diffs:
            yield Result(self,
                         "The code does not comply to PEP8.",
                         affected_code=(diff.range(filename), ),
                         diffs={filename: diff})
Пример #5
0
class PyFlakesBear:
    """
    Checks Python files for errors using ``pyflakes``.

    See https://github.com/PyCQA/pyflakes for more info.
    """
    LANGUAGES = {'Python', 'Python 3'}
    REQUIREMENTS = {PipRequirement('pyflakes', '1.3.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    ASCIINEMA_URL = 'https://asciinema.org/a/92503'
    CAN_DETECT = {'Syntax', 'Unused Code', 'Undefined Element'}

    @staticmethod
    def create_arguments(filename, file, config_file):
        return filename,
Пример #6
0
class VintBear:
    """
    Check vimscript code for possible style problems.

    See <https://github.com/Kuniwak/vint> for more information.
    """

    LANGUAGES = {"VimScript"}
    REQUIREMENTS = {PipRequirement('vim-vint', '0.3.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Formatting'}

    @staticmethod
    def create_arguments(filename, file, config_file):
        return filename,
Пример #7
0
class VultureBear:
    """
    Checks Python code for unused variables and functions using ``vulture``.

    See <https://bitbucket.org/jendrikseipp/vulture> for more information.
    """
    LANGUAGES = {"Python", "Python 3"}
    REQUIREMENTS = {PipRequirement('happiness', '0.10.0')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    ASCIINEMA_URL = 'https://asciinema.org/a/82256'
    CAN_DETECT = {'Unused Code'}

    @staticmethod
    def create_arguments(filename, file, config_file):
        return filename,
Пример #8
0
class PyLintBear:
    """
    Checks the code with pylint. This will run pylint over each file
    separately.
    """
    LANGUAGES = {"Python", "Python 2", "Python 3"}
    REQUIREMENTS = {PipRequirement('pylint', '1.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {
        'Unused Code', 'Formatting', 'Duplication', 'Security', 'Syntax'
    }

    @staticmethod
    def create_arguments(filename,
                         file,
                         config_file,
                         pylint_disable: typed_list(str) = None,
                         pylint_enable: typed_list(str) = None,
                         pylint_cli_options: str = "",
                         pylint_rcfile: str = ""):
        """
        :param pylint_disable:     Disable the message, report, category or
                                   checker with the given id(s).
        :param pylint_enable:      Enable the message, report, category or
                                   checker with the given id(s).
        :param pylint_cli_options: Any command line options you wish to be
                                   passed to pylint.
        :param pylint_rcfile:      The rcfile for PyLint.
        """
        args = ('--reports=n', '--persistent=n',
                '--msg-template="L{line}C{column}: {msg_id} - {msg}"')
        if pylint_disable:
            args += ('--disable=' + ','.join(pylint_disable), )
        if pylint_enable:
            args += ('--enable=' + ','.join(pylint_enable), )
        if pylint_cli_options:
            args += tuple(shlex.split(pylint_cli_options))
        if pylint_rcfile:
            args += ('--rcfile=' + pylint_rcfile, )
        else:
            args += ('--rcfile=' + os.devnull, )

        return args + (filename, )
Пример #9
0
class CPPCleanBear:
    """
    Find problems in C++ source code that slow down development in large code
    bases. This includes finding unused code, among other features.

    Read more about available routines at
    <https://github.com/myint/cppclean#features>.
    """

    LANGUAGES = {'C++'}
    REQUIREMENTS = {PipRequirement('cppclean', '0.12')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Smell', 'Unused Code', 'Security'}

    @staticmethod
    def create_arguments(filename, file, config_file):
        return filename,
Пример #10
0
class PyCommentedCodeBear(LocalBear):
    LANGUAGES = {'Python', 'Python 2', 'Python 3'}
    REQUIREMENTS = {PipRequirement('eradicate', '0.1')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Commented Code'}

    def run(self, filename, file):
        """
        Detects commented out source code in Python.
        """
        corrected = tuple(eradicate.filter_commented_out_code(''.join(file)))

        for diff in Diff.from_string_arrays(file, corrected).split_diff():
            yield Result(self,
                         'This file contains commented out source code.',
                         affected_code=(diff.range(filename), ),
                         diffs={filename: diff})
Пример #11
0
class PycodestyleBear:
    """
    A wrapper for the tool ``pycodestyle`` formerly known as ``pep8``.
    """
    LANGUAGES = {"Python", "Python 2", "Python 3"}
    REQUIREMENTS = {PipRequirement('pycodestyle')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Formatting'}

    @staticmethod
    def create_arguments(filename,
                         file,
                         config_file,
                         pycodestyle_ignore: str = "",
                         pycodestyle_select: str = "",
                         max_line_length: int = 79):
        """
        :param pycodestyle_ignore:
            Comma separated list of errors to ignore.
            See ``pydocstyle`` documentation for a complete list of errors.
        :param pycodestyle_select:
            Comma separated list of errors to detect. If given only
            these errors are going to be detected.
            See ``pydocstyle`` documentation for a complete list of errors.
        :param max_line_length:
            Limit lines to this length.
        """
        arguments = [r"--format='%(row)d %(col)d %(code)s %(text)s'"]

        if pycodestyle_ignore:
            arguments.append("--ignore=" + pycodestyle_ignore)

        if pycodestyle_select:
            arguments.append("--select=" + pycodestyle_select)

        arguments.append("--max-line-length=" + str(max_line_length))

        arguments.append(filename)

        return arguments
Пример #12
0
class PyDocStyleBear(LocalBear, Lint):
    executable = 'pydocstyle'
    output_regex = r'(.*\.py):(?P<line>\d+) (.+):\n\s+(?P<message>.*)'
    use_stderr = True
    LANGUAGES = {"Python", "Python 2", "Python 3"}
    REQUIREMENTS = {PipRequirement('pydocstyle', '1.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Formatting', 'Documentation'}

    def run(self,
            filename,
            file,
            pydocstyle_select: typed_list(str) = (),
            pydocstyle_ignore: typed_list(str) = ()):
        '''
        Checks python docstrings.

        :param pydocstyle_select:      List of checked errors by specifying
                                       which errors to check for.
        :param pydocstyle_ignore:      List of checked errors by specifying
                                       which errors to ignore.

        Note: pydocstyle_select and pydocstyle_ignore are mutually exclusive.
              They cannot be used together.

        '''
        self.arguments = '{filename}'
        if pydocstyle_ignore and pydocstyle_select:
            self.err("The arguments pydocstyle_select and pydocstyle_ignore "
                     "are both given but mutually exclusive.")
            return
        elif pydocstyle_ignore:
            ignore = ','.join(part.strip() for part in pydocstyle_ignore)
            self.arguments += " --ignore={}".format(ignore)
        elif pydocstyle_select:
            select = ','.join(part.strip() for part in pydocstyle_select)
            self.arguments += " --select={} ".format(select)
        return self.lint(filename, file)
Пример #13
0
class PyUnusedCodeBear(LocalBear):
    LANGUAGES = {"Python", "Python 2", "Python 3"}
    REQUIREMENTS = {PipRequirement('autoflake', '0.6.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Unused Code'}

    def run(self, filename, file):
        """
        Detects unused code. This functionality is limited to:

        - Unneeded pass statements.
        - Unneeded builtin imports. (Others might have side effects.)
        """
        corrected = autoflake.fix_code(''.join(file)).splitlines(True)

        for diff in Diff.from_string_arrays(file, corrected).split_diff():
            yield Result(self,
                         "This file contains unused source code.",
                         affected_code=(diff.range(filename),),
                         diffs={filename: diff})
Пример #14
0
class PyDocStyleBear:
    """
    Checks python docstrings.
    """
    LANGUAGES = {'Python', 'Python 2', 'Python 3'}
    REQUIREMENTS = {PipRequirement('pydocstyle', '1.1')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Formatting', 'Documentation'}

    def create_arguments(self,
                         filename,
                         file,
                         config_file,
                         pydocstyle_select: typed_list(str) = (),
                         pydocstyle_ignore: typed_list(str) = ()):
        """
        :param pydocstyle_select:
            List of checked errors by specifying which errors to check for.
            Can't be used together with ``pydocstyle_ignore``.
        :param pydocstyle_ignore:
            List of checked errors by specifying which errors to ignore. Can't
            be used together with ``pydocstyle_select``.
        """
        args = (filename, )
        if pydocstyle_ignore and pydocstyle_select:
            self.err('The arguments pydocstyle_select and pydocstyle_ignore '
                     'are both given but mutually exclusive.')
            return
        elif pydocstyle_ignore:
            ignore = ','.join(part.strip() for part in pydocstyle_ignore)
            args += ('--ignore=' + ignore, )
        elif pydocstyle_select:
            select = ','.join(part.strip() for part in pydocstyle_select)
            args += ('--select=' + select, )

        return args
Пример #15
0
class CMakeLintBear:
    """
    Check CMake code for syntactical or formatting issues.

    For more information consult <https://github.com/richq/cmake-lint>.
    """
    LANGUAGES = {'CMake'}
    REQUIREMENTS = {PipRequirement('cmakelint', '1.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Syntax', 'Formatting'}

    @staticmethod
    def create_arguments(filename, file, config_file,
                         cmakelint_config: path=""):
        """
        :param cmakelint_config: The location of the cmakelintrc config file.
        """
        args = ()
        if cmakelint_config:
            args += ('--config=' + cmakelint_config,)
        return args + (filename,)
Пример #16
0
class VultureBear(GlobalBear):
    LANGUAGES = {'Python', 'Python 3'}
    REQUIREMENTS = {PipRequirement('vulture', '0.10.0')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    ASCIINEMA_URL = 'https://asciinema.org/a/82256'
    CAN_DETECT = {'Unused Code'}

    EXECUTABLE = 'vulture'
    OUTPUT_REGEX = re.compile(
        r'(?P<filename>.*):(?P<line>.*):\s*(?P<message>.*)')

    @classmethod
    def check_prerequisites(cls):
        return ('Vulture is missing. Make sure to install it using '
                '`pip3 install vulture`.'
                if which('vulture') is None else True)

    def run(self):
        """
        Check Python code for unused variables and functions using `vulture`.

        See <https://bitbucket.org/jendrikseipp/vulture> for more information.
        """
        stdout_output, _ = run_shell_command(
            (self.EXECUTABLE,) +
            tuple(filename for filename in self.file_dict.keys()),
            cwd=self.get_config_dir())

        for match in re.finditer(self.OUTPUT_REGEX, stdout_output):
            groups = match.groupdict()
            yield Result.from_values(origin=self,
                                     message=groups['message'],
                                     file=groups['filename'],
                                     line=int(groups['line']))
Пример #17
0
class HTMLLintBear:
    """
    Check HTML source code for invalid or misformatted code.

    See also <https://pypi.python.org/pypi/html-linter>.
    """

    _html_lint = which('html_lint.py')

    LANGUAGES = {'HTML'}
    REQUIREMENTS = {PipRequirement('html-linter', '0.3')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Syntax', 'Formatting'}

    @staticmethod
    def create_arguments(filename, file, config_file,
                         htmllint_ignore: typed_list(str)=()):
        """
        :param htmllint_ignore: List of checkers to ignore.
        """
        ignore = ','.join(part.strip() for part in htmllint_ignore)
        return HTMLLintBear._html_lint, '--disable=' + ignore, filename
Пример #18
0
class PEP8NotebookBear(LocalBear):
    LANGUAGES = {"Python", "Python 2", "Python 3"}
    REQUIREMENTS = {
        PipRequirement('autopep8', '1.*'),
        PipRequirement('nbformat', '4.*')
    }
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    ASCIINEMA_URL = 'https://asciinema.org/a/83333'
    CAN_FIX = {'Formatting'}

    def run(self,
            filename,
            file,
            max_line_length: int = 79,
            indent_size: int = SpacingHelper.DEFAULT_TAB_WIDTH,
            pep_ignore: typed_list(str) = (),
            pep_select: typed_list(str) = (),
            local_pep8_config: bool = False):
        """
        Detects and fixes PEP8 incompliant code in Jupyter Notebooks. This bear
        will not change functionality of the code in any way.

        :param max_line_length:   Maximum number of characters for a line.
        :param indent_size:       Number of spaces per indent level.
        :param pep_ignore:        A list of errors/warnings to ignore.
        :param pep_select:        A list of errors/warnings to exclusively
                                  apply.
        :param local_pep8_config: Set to true if autopep8 should use a config
                                  file as if run normally from this directory.
        """
        options = {
            "ignore": pep_ignore,
            "select": pep_select,
            "max_line_length": max_line_length,
            "indent_size": indent_size
        }
        notebook_node = notebook_node_from_string_list(file)
        cells = notebook_node['cells']

        for cell in cells:
            if cell['cell_type'] != 'code':
                continue
            cell['source'] = autopep8_fix_code_cell(cell['source'],
                                                    local_pep8_config, options)

        corrected = notebook_node_to_string_list(notebook_node)

        # If newline at eof in `file` but not in `corrected`, add
        # final newline character to `corrected` to make sure this difference
        # does not pop up in `diffs`.
        if file[-1].endswith('\n') and not corrected[-1].endswith('\n'):
            corrected[-1] += '\n'

        diffs = Diff.from_string_arrays(file, corrected).split_diff()

        for diff in diffs:
            yield Result(self,
                         "The code does not comply to PEP8.",
                         affected_code=(diff.range(filename), ),
                         diffs={filename: diff})
Пример #19
0
class LanguageToolBear(LocalBear):
    LANGUAGES = {"Natural Language"}
    REQUIREMENTS = {
        PipRequirement('guess-language-spirit', '0.5.*'),
        PipRequirement('language-check', '0.8.*')
    }
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Spelling', 'Grammar'}

    @classmethod
    def check_prerequisites(cls):
        if shutil.which("java") is None:
            return "java is not installed."
        else:
            try:
                from language_check import LanguageTool, correct
                return True
            except ImportError:  # pragma: no cover
                return "Please install the `language-check` pip package."

    @deprecate_settings(language='locale')
    def run(self,
            filename,
            file,
            language: str = 'auto',
            languagetool_disable_rules: typed_list(str) = ()):
        '''
        Checks the code with LanguageTool.

        :param language:                   A locale representing the language
                                           you want to have checked. If set to
                                           'auto' the language is guessed.
                                           If the language cannot be guessed,
                                           'en-US' is used.
        :param languagetool_disable_rules: List of rules to disable checks for.
        '''
        # Defer import so the check_prerequisites can be run without
        # language_check being there.
        from language_check import LanguageTool, correct

        joined_text = "".join(file)
        language = (guess_language(joined_text)
                    if language == 'auto' else language)
        language = 'en-US' if not language else language

        tool = LanguageTool(language, motherTongue="en_US")
        tool.disabled.update(languagetool_disable_rules)

        matches = tool.check(joined_text)
        for match in matches:
            if not match.replacements:
                diffs = None
            else:
                replaced = correct(joined_text, [match]).splitlines(True)
                diffs = {filename: Diff.from_string_arrays(file, replaced)}

            rule_id = match.ruleId
            if match.subId is not None:
                rule_id += '[{}]'.format(match.subId)

            message = match.msg + ' (' + rule_id + ')'
            source_range = SourceRange.from_values(filename, match.fromy + 1,
                                                   match.fromx + 1,
                                                   match.toy + 1,
                                                   match.tox + 1)
            yield Result(self,
                         message,
                         diffs=diffs,
                         affected_code=(source_range, ))
Пример #20
0
class PySafetyBear(LocalBear):
    """
    Checks if any of your Python dependencies have known security issues.

    Data is taken from pyup.io's vulnerability database hosted at
    https://github.com/pyupio/safety.
    """

    LANGUAGES = {
        'Python Requirements',
        'Python 2 Requirements',
        'Python 3 Requirements',
    }
    AUTHORS = {'Bence Nagy'}
    REQUIREMENTS = {PipRequirement('safety', '0.5')}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL'
    CAN_DETECT = {'Security'}

    def run(self, filename, file):
        """
        Checks for vulnerable package versions in requirements files.
        """
        packages = list(
            Package(key=req.key, version=req.specs[0][1])
            for req in self.try_parse_requirements(file)
            if len(req.specs) == 1 and req.specs[0][0] == '=='
        )

        if not packages:
            return

        for vulnerability in safety.check(packages=packages):
            if vulnerability.is_cve:
                message_template = (
                    '{vuln.name}{vuln.spec} is vulnerable to {vuln.cve_id} '
                    'and your project is using {vuln.version}.'
                )
            else:
                message_template = (
                    '{vuln.name}{vuln.spec} is vulnerable and your project is '
                    'using {vuln.version}.'
                )

            # StopIteration should not ever happen so skipping its branch
            line_number, line = next(  # pragma: no branch
                (index, line) for index, line in enumerate(file, start=1)
                if vulnerability.name in line
            )
            version_spec_match = re.search(r'[=<>]+(\S+?)(?:$|\s|#)', line)
            source_range = SourceRange.from_values(
                filename,
                line_number,
                version_spec_match.start(1) + 1,
                line_number,
                version_spec_match.end(1) + 1,
            )

            yield Result(
                self,
                message_template.format(vuln=vulnerability),
                additional_info=vulnerability.description,
                affected_code=(source_range, ),
            )

    @staticmethod
    def try_parse_requirements(lines: typed_list(str)):
        """
        Yields all package requirements parseable from the given lines.

        :param lines: An iterable of lines from a requirements file.
        """
        for line in lines:
            try:
                yield from pkg_resources.parse_requirements(line)
            except pkg_resources.RequirementParseError:
                # unsupported requirement specification
                pass
Пример #21
0
class RuboCopBear:
    """
    Check Ruby code for syntactic, formatting as well as semantic problems.

    See <https://github.com/bbatsov/rubocop#cops> for more information.
    """

    LANGUAGES = {'Ruby'}
    REQUIREMENTS = {
        GemRequirement('rubocop'),
        PipRequirement('pyyaml', '3.12')
    }
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    ASCIINEMA_URL = 'https://asciinema.org/a/39241'
    CAN_DETECT = {'Simplification'}
    CAN_FIX = {'Syntax', 'Formatting'}

    severity_map = {
        'error': RESULT_SEVERITY.MAJOR,
        'warning': RESULT_SEVERITY.NORMAL,
        'convention': RESULT_SEVERITY.INFO
    }

    @staticmethod
    def create_arguments(filename,
                         file,
                         config_file,
                         rubocop_config: str = ''):
        # Need both stdin and filename. Explained in this comment:
        # https://github.com/bbatsov/rubocop/pull/2146#issuecomment-131403694
        args = (filename, '--stdin', '--format=json')
        if rubocop_config:
            args += ('--config', rubocop_config)
        else:
            args += ('--config', config_file)
        return args

    @staticmethod
    @deprecate_settings(indent_size='tab_width',
                        method_length_count_comments='method_count_comments',
                        method_naming_convention='method_name_case',
                        variable_naming_convention='variable_name_case')
    def generate_config(filename,
                        file,
                        access_modifier_indentation: str = 'indent',
                        preferred_alias: str = 'prefer_alias',
                        align_hash_rocket_by: str = 'key',
                        align_colon_by: str = 'key',
                        inspect_last_argument_hash: str = 'always_inspect',
                        align_parameters: str = 'with_first_parameter',
                        class_check: str = 'is_a?',
                        comment_keywords: tuple = ('TODO', 'FIXME', 'OPTIMIZE',
                                                   'HACK', 'REVIEW'),
                        min_if_unless_guard: int = 1,
                        indent_size: int = 2,
                        method_naming_convention: str = 'snake',
                        string_literals: str = 'single_quotes',
                        variable_naming_convention: str = 'snake',
                        max_class_length: int = 100,
                        class_length_count_comments: bool = False,
                        max_module_length: int = 100,
                        module_length_count_comments: bool = False,
                        cyclomatic_complexity: int = 6,
                        max_line_length: int = 79,
                        line_length_allow_here_doc: bool = True,
                        line_length_allow_uri: bool = True,
                        max_method_length: int = 10,
                        method_length_count_comments: bool = False,
                        max_parameters: int = 5,
                        count_keyword_args: bool = True,
                        ignore_unused_block_args_if_empty: bool = True,
                        allow_unused_block_keyword_arguments: bool = False,
                        ignore_unused_method_args_if_empty: bool = True,
                        allow_unused_method_keyword_args: bool = False):
        """
        Not all settings added.
        Notable settings missing: Rails settings.

        :param access_modifier_indentation:
            Indent private/protected/public as deep as method definitions
            options:
                ``indent`` :  Indent modifiers like class members.
                ``outdent`` : Indent modifiers one level less than
                              class members.
        :param preferred_alias:
            Which method to use for aliasing in ruby.
            options : ``alias`` , ``alias_method``.
        :param align_hash_rocket_by:
            Alignment of entries using hash rocket as separator.
        :param align_colon_by:
            Alignment of entries using colon as separator.
        :param inspect_last_argument_hash:
            Select whether hashes that are the last argument in a method call
            should be inspected.
            options: ``always_inspect``, ``always_ignore``,
                     ``ignore_implicit``, ``ignore_explicit``.
        :param align_parameters:
            Alignment of parameters in multi-line method calls.

            options:
                ``with_first_parameter``: Aligns the following lines
                                          along the same column as the
                                          first parameter.

                ``with_fixed_indentation``: Aligns the following lines with one
                                            level of indentation relative to
                                            the start of the line with the
                                            method call.
        :param class_check:
            How to check type of class.
            options: ``is_a?``, ``kind_of?``.
        :param comment_keywords:
            Checks formatting of special comments based on keywords like
            TODO, FIXME etc.
        :param min_if_unless_guard:
            The number of lines that are tolerable within an if/unless block,
            more than these lines call for the usage of a guard clause.
        :param indent_size:
            Number of spaces per indentation level.
        :param method_naming_convention:
            Case of a method's name.
            options: ``snake``, ``camel``.
        :param string_literals:
            Use ' or " as string literals.
            options: ``single_quotes``, ``double_quotes``.
        :param variable_naming_convention:
            Case of a variable's name.
            options: ``snake``, ``camel``.
        :param max_class_length:
            Max lines in a class.
        :param class_length_count_comments:
            Whether or not to count comments while calculating the class
            length.
        :param max_module_length:
            Max lines in a module.
        :param module_length_count_comments:
            Whether or not to count comments while calculating
            the module length.
        :param cyclomatic_complexity:
            Cyclomatic Complexity of the file.
        :param max_line_length:
            Max length of a line.
        :param line_length_allow_here_doc:
            Allow here-doc lines to be more than the max line length.
        :param line_length_allow_uri:
            To make it possible to copy or click on URIs in the code,
            we allow ignore long lines containing a URI to be longer than max
            line length.
        :param max_method_length:
            Max number of lines in a method.
        :param method_length_count_comments:
            Whether or not to count full line comments while calculating
            method length.
        :param max_parameters:
            Max number of parameters in parameter list.
        :param count_keyword_args:
            Count keyword args while counting all arguments?
        :param ignore_unused_block_args_if_empty:
            Ignore unused block arguments if block is empty.
        :param allow_unused_block_keyword_arguments:
            Allow unused block keyword arguments.
        :param ignore_unused_method_args_if_empty:
            Allows unused method argument if method is empty.
        :param allow_unused_method_keyword_args:
            Allows unused keyword arguments in a method.
        """
        naming_convention = {'camel': 'camelCase', 'snake': 'snake_case'}
        options = {
            'Style/AccessModifierIndentation': {
                'EnforcedStyle': access_modifier_indentation
            },
            'Style/Alias': {
                'EnforcedStyle': preferred_alias
            },
            'Style/AlignHash': {
                'EnforcedHashRocketStyle': align_hash_rocket_by,
                'EnforcedColonStyle': align_colon_by,
                'EnforcedLastArgumentHashStyle': inspect_last_argument_hash
            },
            'Style/AlignParameters': {
                'EnforcedStyle': align_parameters
            },
            'Style/ClassCheck': {
                'EnforcedStyle': class_check
            },
            'Style/CommentAnnotation': {
                'Keywords': comment_keywords
            },
            'Style/GuardClause': {
                'MinBodyLength': min_if_unless_guard
            },
            'Style/IndentationWidth': {
                'Width': indent_size
            },
            'Style/MethodName': {
                'EnforcedStyle':
                naming_convention.get(method_naming_convention,
                                      method_naming_convention)
            },
            'Style/StringLiterals': {
                'EnforcedStyle': string_literals
            },
            'Style/VariableName': {
                'EnforcedStyle':
                naming_convention.get(variable_naming_convention,
                                      variable_naming_convention)
            },
            'Metrics/ClassLength': {
                'Max': max_class_length,
                'CountComments': class_length_count_comments
            },
            'Metrics/ModuleLength': {
                'CountComments': module_length_count_comments,
                'Max': max_module_length
            },
            'Metrics/CyclomaticComplexity': {
                'Max': cyclomatic_complexity
            },
            'Metrics/LineLength': {
                'Max': max_line_length,
                'AllowHeredoc': line_length_allow_here_doc,
                'AllowURI': line_length_allow_uri
            },
            'Metrics/MethodLength': {
                'CountComments': method_length_count_comments,
                'Max': max_method_length
            },
            'Metrics/ParameterLists': {
                'Max': max_parameters,
                'CountKeywordArgs': count_keyword_args
            },
            'Lint/UnusedBlockArgument': {
                'IgnoreEmptyBlocks': ignore_unused_block_args_if_empty,
                'AllowUnusedKeywordArguments':
                allow_unused_block_keyword_arguments
            },
            'Lint/UnusedMethodArgument': {
                'AllowUnusedKeywordArguments':
                allow_unused_method_keyword_args,
                'IgnoreEmptyMethods': ignore_unused_method_args_if_empty
            },
        }
        return yaml.dump(options, default_flow_style=False)

    def process_output(self, output, filename, file):
        output = json.loads(output)
        assert len(output['files']) == 1
        for result in output['files'][0]['offenses']:
            # TODO: Add condition for auto-correct, when rubocop is updated.
            # Relevant Issue: https://github.com/bbatsov/rubocop/issues/2932
            yield Result.from_values(
                origin='{class_name} ({rule})'.format(
                    class_name=self.__class__.__name__,
                    rule=result['cop_name']),
                message=result['message'],
                file=filename,
                diffs=None,
                severity=self.severity_map[result['severity']],
                line=result['location']['line'],
                column=result['location']['column'],
                # Tested with linebreaks, it's secure.
                end_column=result['location']['column'] +
                result['location']['length'])
Пример #22
0
 def test_installed_requirement(self):
     self.assertTrue(PipRequirement('pip').is_installed())
Пример #23
0
 def test_install_command_with_version(self):
     self.assertEqual(
         [sys.executable, '-m', 'pip', 'install', 'setuptools==19.2'],
         PipRequirement('setuptools', '19.2').install_command())
Пример #24
0
 def test_not_installed_requirement(self):
     self.assertFalse(PipRequirement('some_bad_package').is_installed())
Пример #25
0
class YapfBear(LocalBear):
    """
    Check and correct formatting of Python code using ``yapf`` utility.

    See <https://github.com/google/yapf> for more information.
    """
    LANGUAGES = {'Python', 'Python 2', 'Python 3'}
    AUTHORS = {'The coala developers'}
    REQUIREMENTS = {PipRequirement('yapf', '0.14')}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_FIX = {'Formatting'}
    ASCIINEMA_URL = 'https://asciinema.org/a/89021'

    @deprecate_settings(indent_size='tab_width')
    def run(self, filename, file,
            max_line_length: int=79,
            indent_size: int=SpacingHelper.DEFAULT_TAB_WIDTH,
            allow_multiline_lambdas: bool=False,
            blank_line_before_nested_class_or_def: bool=False,
            continuation_tab_width: int=SpacingHelper.DEFAULT_TAB_WIDTH,
            dedent_closing_brackets: bool=False,
            indent_dictionary_value: bool=False,
            coalesce_brackets: bool=False,
            join_multiple_lines: bool=True,
            spaces_around_power_operator: bool=True,
            spaces_before_comment: int=2,
            space_between_ending_comma_and_closing_bracket: bool=True,
            split_arguments_when_comma_terminated: bool=False,
            split_before_bitwise_operator: bool=False,
            split_before_first_argument: bool=False,
            split_before_logical_operator: bool=False,
            split_before_named_assigns: bool=True,
            use_spaces: bool=True,
            based_on_style: str='pep8',
            prefer_line_break_after_opening_bracket: bool=True):
        """
        :param max_line_length:
            Maximum number of characters for a line.
        :param indent_size:
            Number of spaces per indentation level.
        :param allow_multiline_lambdas:
            Allows lambdas to be formatted on more than one line.
        :param blank_line_before_nested_class_or_def:
            Inserts a blank line before a ``def`` or ``class`` immediately
            nested within another ``def`` or ``class``.
        :param continuation_tab_width:
            Indent width used for line continuations.
        :param dedent_closing_brackets:
            Puts closing brackets on a separate line, dedented, if the
            bracketed expression can't fit in a single line. Applies to all
            kinds of brackets, including function definitions and calls.
        :param indent_dictionary_value:
            Indents the dictionary value if it cannot fit on the same line as
            the dictionary key.
        :param coalesce_brackets:
            Prevents splitting consecutive brackets. Only relevant when
            ``dedent_closing_brackets`` is set.
            Example:
            If ``True``,

            ```
            call_func_that_takes_a_dict(
                {
                    'key1': 'value1',
                    'key2': 'value2',
                }
            )
            ```
            would reformat to:
            ```
            call_func_that_takes_a_dict({
                'key1': 'value1',
                'key2': 'value2',
            })
            ```
        :param join_multiple_lines:
            Joins short lines into one line.
        :param spaces_around_power_operator:
            Set to ``True`` to prefer using spaces around ``**``.
        :param spaces_before_comment:
            The number of spaces required before a trailing comment.
        :param space_between_ending_comma_and_closing_bracket:
            Inserts a space between the ending comma and closing bracket of a
            list, etc.
        :param split_arguments_when_comma_terminated:
            Splits before arguments if the argument list is terminated by a
            comma.
        :param split_before_bitwise_operator:
            Set to ``True`` to prefer splitting before ``&``, ``|`` or ``^``
            rather than after.
        :param split_before_first_argument:
            If an argument / parameter list is going to be split, then split
            before the first argument.
        :param split_before_logical_operator:
            Set to ``True`` to prefer splitting before ``and`` or ``or`` rather
            than after.
        :param split_before_named_assigns:
            Splits named assignments into individual lines.
        :param use_spaces:
            Uses spaces for indentation.
        :param based_on_style:
            The formatting style to be used as reference.
        :param prefer_line_break_after_opening_bracket:
            If True, splitting right after a open bracket will not be
            preferred.
        """
        if not file:
            # Yapf cannot handle zero-byte files well, and adds a redundent
            # newline into the file. To avoid this, we don't parse zero-byte
            # files as they cannot have anything to format either.
            return

        options = """
[style]
indent_width = {indent_size}
column_limit = {max_line_length}
allow_multiline_lambdas = {allow_multiline_lambdas}
continuation_indent_width = {continuation_tab_width}
dedent_closing_brackets = {dedent_closing_brackets}
indent_dictionary_value = {indent_dictionary_value}
join_multiple_lines = {join_multiple_lines}
spaces_around_power_operator = {spaces_around_power_operator}
spaces_before_comment = {spaces_before_comment}
coalesce_brackets = {coalesce_brackets}
split_before_bitwise_operator = {split_before_bitwise_operator}
split_before_first_argument = {split_before_first_argument}
split_before_logical_operator = {split_before_logical_operator}
split_before_named_assigns = {split_before_named_assigns}
based_on_style = {based_on_style}
blank_line_before_nested_class_or_def = {blank_line_before_nested_class_or_def}
split_arguments_when_comma_terminated = {split_arguments_when_comma_terminated}
space_between_ending_comma_and_closing_bracket= \
{space_between_ending_comma_and_closing_bracket}
"""
        options += 'use_tabs = ' + str(not use_spaces) + '\n'
        options += ('split_penalty_after_opening_bracket = ' +
                    ('30' if prefer_line_break_after_opening_bracket
                     else '0') + '\n')
        options = options.format(**locals())

        try:
            with prepare_file(options.splitlines(keepends=True),
                              None) as (file_, fname):
                corrected = FormatCode(
                    ''.join(file), style_config=fname)[0].splitlines(True)
        except SyntaxError as err:
            if isinstance(err, IndentationError):
                error_type = 'indentation errors (' + err.args[0] + ')'
            else:
                error_type = 'syntax errors'
            yield Result.from_values(
                self,
                'The code cannot be parsed due to {0}.'.format(error_type),
                filename, line=err.lineno, column=err.offset)
            return
        diffs = Diff.from_string_arrays(file, corrected).split_diff()
        for diff in diffs:
            yield Result(self,
                         'The code does not comply with the settings '
                         'provided.',
                         affected_code=(diff.range(filename),),
                         diffs={filename: diff})
Пример #26
0
class YapfBear(LocalBear):
    """
    Check and correct formatting of Python code using ``yapf`` utility.

    See <https://github.com/google/yapf> for more information.
    """
    LANGUAGES = {"Python", "Python 2", "Python 3"}
    AUTHORS = {'The coala developers'}
    REQUIREMENTS = {PipRequirement('yapf', '0.10')}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_FIX = {'Formatting'}

    @deprecate_settings(indent_size='tab_width')
    def run(self,
            filename,
            file,
            max_line_length: int = 79,
            indent_size: int = SpacingHelper.DEFAULT_TAB_WIDTH,
            allow_multiline_lambdas: bool = False,
            blank_line_before_nested_class_or_def: bool = False,
            continuation_tab_width: int = SpacingHelper.DEFAULT_TAB_WIDTH,
            dedent_closing_brackets: bool = False,
            indent_dictionary_value: bool = False,
            coalesce_brackets: bool = False,
            join_multiple_lines: bool = True,
            spaces_around_power_operator: bool = True,
            spaces_before_comment: int = 2,
            space_between_ending_comma_and_closing_bracket: bool = False,
            split_arguments_when_comma_terminated: bool = False,
            split_before_bitwise_operator: bool = False,
            split_before_first_argument: bool = False,
            split_before_logical_operator: bool = False,
            split_before_named_assigns: bool = True,
            use_spaces: bool = True,
            based_on_style: str = 'pep8'):
        """
        :param max_line_length:
            Maximum number of characters for a line.
        :param indent_size:
            Number of spaces per indentation level.
        :param allow_multiline_lambdas:
            Allows lambdas to be formatted on more than one line.
        :param blank_line_before_nested_class_or_def:
            Inserts a blank line before a ``def`` or ``class`` immediately
            nested within another ``def`` or ``class``.
        :param continuation_tab_width:
            Indent width used for line continuations.
        :param dedent_closing_brackets:
            Puts closing brackets on a separate line, dedented, if the
            bracketed expression can't fit in a single line. Applies to all
            kinds of brackets, including function definitions and calls.
        :param indent_dictionary_value:
            Indents the dictionary value if it cannot fit on the same line as
            the dictionary key.
        :param coalesce_brackets:
            Prevents splitting consecutive brackets. Only relevant when
            ``dedent_closing_brackets`` is set.
            Example:
            If ``True``,

            ```
            call_func_that_takes_a_dict(
                {
                    'key1': 'value1',
                    'key2': 'value2',
                }
            )
            ```
            would reformat to:
            ```
            call_func_that_takes_a_dict({
                'key1': 'value1',
                'key2': 'value2',
            })
            ```
        :param join_multiple_lines:
            Joins short lines into one line.
        :param spaces_around_power_operator:
            Set to ``True`` to prefer using spaces around ``**``.
        :param spaces_before_comment:
            The number of spaces required before a trailing comment.
        :param space_between_ending_comma_and_closing_bracket:
            Inserts a space between the ending comma and closing bracket of a
            list, etc.
        :param split_arguments_when_comma_terminated:
            Splits before arguments if the argument list is terminated by a
            comma.
        :param split_before_bitwise_operator:
            Set to ``True`` to prefer splitting before ``&``, ``|`` or ``^``
            rather than after.
        :param split_before_first_argument:
            If an argument / parameter list is going to be split, then split
            before the first argument.
        :param split_before_logical_operator:
            Set to ``True`` to prefer splitting before ``and`` or ``or`` rather
            than after.
        :param split_before_named_assigns:
            Splits named assignments into individual lines.
        :param use_spaces:
            Uses spaces for indentation.
        :param based_on_style:
            The formatting style to be used as reference.
        """

        options = """
[style]
indent_width = {indent_size}
column_limit = {max_line_length}
allow_multiline_lambdas = {allow_multiline_lambdas}
continuation_indent_width = {continuation_tab_width}
dedent_closing_brackets = {dedent_closing_brackets}
indent_dictionary_value = {indent_dictionary_value}
join_multiple_lines = {join_multiple_lines}
spaces_around_power_operator = {spaces_around_power_operator}
spaces_before_comment = {spaces_before_comment}
coalesce_brackets = {coalesce_brackets}
split_before_bitwise_operator = {split_before_bitwise_operator}
split_before_first_argument = {split_before_first_argument}
split_before_logical_operator = {split_before_logical_operator}
split_before_named_assigns = {split_before_named_assigns}
based_on_style = {based_on_style}
blank_line_before_nested_class_or_def = {blank_line_before_nested_class_or_def}
split_arguments_when_comma_terminated = {split_arguments_when_comma_terminated}
space_between_ending_comma_and_closing_bracket= \
{space_between_ending_comma_and_closing_bracket}
"""
        options += 'use_tabs = ' + str(not use_spaces)
        options = options.format(**locals())

        with prepare_file(options.splitlines(keepends=True),
                          None) as (file_, fname):
            corrected = FormatFile(filename,
                                   style_config=fname)[0].splitlines(True)
        diffs = Diff.from_string_arrays(file, corrected).split_diff()
        for diff in diffs:
            yield Result(self, "The code does not comply with the settings "
                         "provided.",
                         affected_code=(diff.range(filename), ),
                         diffs={filename: diff})

    @classmethod
    def check_prerequisites(cls):  # pragma: no cover
        if not sys.version_info >= (3, 4):
            return 'Yapf only supports Python 2.7 and Python 3.4+'
        else:
            return True
Пример #27
0
class InvalidLinkBear(LocalBear):
    DEFAULT_TIMEOUT = 2
    LANGUAGES = {'All'}
    REQUIREMENTS = {PipRequirement('requests', '2.12')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Documentation'}

    # IP Address of www.google.com
    check_connection_url = 'http://216.58.218.174'

    @classmethod
    def check_prerequisites(cls):
        code = cls.get_status_code(cls.check_connection_url,
                                   cls.DEFAULT_TIMEOUT)
        return ('You are not connected to the internet.'
                if code is None else True)

    @staticmethod
    def get_status_code(url, timeout):
        try:
            code = requests.head(url, allow_redirects=False,
                                 timeout=timeout).status_code
            return code
        except requests.exceptions.RequestException:
            pass

    @staticmethod
    def parse_pip_vcs_url(link):
        splitted_at = link.split('@')[0]
        splitted_schema = splitted_at[splitted_at.index('+') + 1:]
        return splitted_schema

    @staticmethod
    def find_links_in_file(file, timeout, link_ignore_regex):
        link_ignore_regex = re.compile(link_ignore_regex)
        regex = re.compile(
            r"""
            ((git\+|bzr\+|svn\+|hg\+|)  # For VCS URLs
            https?://                   # http:// or https:// as only these
                                        # are supported by the ``requests``
                                        # library
            [^.:%\s_/?#[\]@\\]+         # Initial part of domain
            \.                          # A required dot `.`
            (
                (?:[^\s()%\'"`<>|\\]+)  # Path name
                                        # This part does not allow
                                        # any parenthesis: balanced or
                                        # unbalanced.
            |                           # OR
                \([^\s()%\'"`<>|\\]*\)  # Path name contained within ()
                                        # This part allows path names that
                                        # are explicitly enclosed within one
                                        # set of parenthesis.
                                        # An example can be:
                                        # http://wik.org/Hello_(Adele_song)/200
            )
            *)
                                        # Thus, the whole part above
                                        # prevents matching of
                                        # Unbalanced parenthesis
            (?<!\.)(?<!,)               # Exclude trailing `.` or `,` from URL
            """, re.VERBOSE)
        for line_number, line in enumerate(file):
            match = regex.search(line)
            if match:
                link = match.group()
                if not link_ignore_regex.search(link):
                    if link.startswith(('hg+', 'bzr+', 'git+', 'svn+')):
                        link = InvalidLinkBear.parse_pip_vcs_url(link)
                    code = InvalidLinkBear.get_status_code(link, timeout)
                    yield line_number + 1, link, code

    @deprecate_settings(link_ignore_regex='ignore_regex')
    def run(self,
            filename,
            file,
            timeout: int = DEFAULT_TIMEOUT,
            link_ignore_regex: str = '([.\/]example\.com|\{|\$)',
            follow_redirects: bool = False):
        """
        Find links in any text file and check if they are valid.

        A link is considered valid if the server responds with a 2xx code.

        This bear can automatically fix redirects, but ignores redirect
        URLs that have a huge difference with the original URL.

        Warning: This bear will make HEAD requests to all URLs mentioned in
        your codebase, which can potentially be destructive. As an example,
        this bear would naively just visit the URL from a line that goes like
        `do_not_ever_open = 'https://api.acme.inc/delete-all-data'` wiping out
        all your data.

        :param timeout:          Request timeout period.
        :param link_ignore_regex:     A regex for urls to ignore.
        :param follow_redirects: Set to true to autocorrect redirects.
        """
        for line_number, link, code in InvalidLinkBear.find_links_in_file(
                file, timeout, link_ignore_regex):
            if code is None:
                yield Result.from_values(
                    origin=self,
                    message=('Broken link - unable to connect to '
                             '{url}').format(url=link),
                    file=filename,
                    line=line_number,
                    severity=RESULT_SEVERITY.MAJOR)
            elif not 200 <= code < 300:
                # HTTP status 404, 410 or 50x
                if code in (404, 410) or 500 <= code < 600:
                    yield Result.from_values(
                        origin=self,
                        message=('Broken link - unable to connect to {url} '
                                 '(HTTP Error: {code})').format(url=link,
                                                                code=code),
                        file=filename,
                        line=line_number,
                        severity=RESULT_SEVERITY.NORMAL)
                if follow_redirects and 300 <= code < 400:  # HTTP status 30x
                    redirect_url = requests.head(link,
                                                 allow_redirects=True).url
                    matcher = SequenceMatcher(None, redirect_url, link)
                    if (matcher.real_quick_ratio() > 0.7
                            and matcher.ratio()) > 0.7:
                        diff = Diff(file)
                        current_line = file[line_number - 1]
                        start = current_line.find(link)
                        end = start + len(link)
                        replacement = current_line[:start] + \
                            redirect_url + current_line[end:]
                        diff.change_line(line_number, current_line,
                                         replacement)

                        yield Result.from_values(
                            self,
                            'This link redirects to ' + redirect_url,
                            diffs={filename: diff},
                            file=filename,
                            line=line_number,
                            severity=RESULT_SEVERITY.NORMAL)
Пример #28
0
class GitCommitBear(GlobalBear):
    LANGUAGES = {"Git"}
    REQUIREMENTS = {PipRequirement('nltk', '3.1.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    ASCIINEMA_URL = 'https://asciinema.org/a/e146c9739ojhr8396wedsvf0d'
    CAN_DETECT = {'Formatting'}

    @classmethod
    def check_prerequisites(cls):
        if shutil.which("git") is None:
            return "git is not installed."
        else:
            return True

    @classmethod
    def get_shortlog_checks_metadata(cls):
        return FunctionMetadata.from_function(cls.check_shortlog,
                                              omit={"self", "shortlog"})

    @classmethod
    def get_body_checks_metadata(cls):
        return FunctionMetadata.from_function(cls.check_body,
                                              omit={"self", "body"})

    @classmethod
    def get_metadata(cls):
        return FunctionMetadata.merge(
            FunctionMetadata.from_function(cls.run,
                                           omit={"self",
                                                 "dependency_results"}),
            cls.get_shortlog_checks_metadata(), cls.get_body_checks_metadata())

    def run(self, allow_empty_commit_message: bool = False, **kwargs):
        """
        Check the current git commit message at HEAD.

        This bear ensures automatically that the shortlog and body do not
        exceed a given line-length and that a newline lies between them.

        :param allow_empty_commit_message: Whether empty commit messages are
                                           allowed or not.
        """
        with change_directory(self.get_config_dir() or os.getcwd()):
            stdout, stderr = run_shell_command("git log -1 --pretty=%B")

        if stderr:
            self.err("git:", repr(stderr))
            return

        stdout = stdout.rstrip("\n").splitlines()

        if len(stdout) == 0:
            if not allow_empty_commit_message:
                yield Result(self, "HEAD commit has no message.")
            return

        yield from self.check_shortlog(
            stdout[0],
            **self.get_shortlog_checks_metadata().filter_parameters(kwargs))
        yield from self.check_body(
            stdout[1:],
            **self.get_body_checks_metadata().filter_parameters(kwargs))

    def check_shortlog(self,
                       shortlog,
                       shortlog_length: int = 50,
                       shortlog_regex: str = "",
                       shortlog_trailing_period: bool = None,
                       shortlog_imperative_check: bool = True,
                       shortlog_wip_check: bool = True):
        """
        Checks the given shortlog.

        :param shortlog:                 The shortlog message string.
        :param shortlog_length:          The maximum length of the shortlog.
                                         The newline character at end does not
                                         count to the length.
        :param regex:                    A regex to check the shortlog with.
        :param shortlog_trailing_period: Whether a dot shall be enforced at end
                                         end or not (or ``None`` for "don't
                                         care").
        :param shortlog_wip_check:       Whether a "WIP" in the shortlog text
                                         should yield a result or not.
        """
        diff = len(shortlog) - shortlog_length
        if diff > 0:
            yield Result(
                self, "Shortlog of HEAD commit is {} character(s) longer "
                "than the limit ({} > {}).".format(diff, len(shortlog),
                                                   shortlog_length))

        if (shortlog[-1] != ".") == shortlog_trailing_period:
            yield Result(
                self, "Shortlog of HEAD commit contains no period at end."
                if shortlog_trailing_period else
                "Shortlog of HEAD commit contains a period at end.")

        if shortlog_regex:
            match = re.fullmatch(shortlog_regex, shortlog)
            if not match:
                yield Result(
                    self,
                    "Shortlog of HEAD commit does not match given regex.")

        if shortlog_imperative_check:
            colon_pos = shortlog.find(':')
            shortlog = (shortlog[colon_pos +
                                 1:] if colon_pos != -1 else shortlog)
            has_flaws = self.check_imperative(shortlog)
            if has_flaws:
                bad_word = has_flaws[0]
                yield Result(
                    self, "Shortlog of HEAD commit isn't imperative mood, "
                    "bad words are '{}'".format(bad_word))
        if shortlog_wip_check:
            if "wip" in shortlog.lower()[:4]:
                yield Result(
                    self,
                    "This commit seems to be marked as work in progress and "
                    "should not be used in production. Treat carefully.")

    def check_imperative(self, paragraph):
        """
        Check the given sentence/s for Imperatives.

        :param paragraph:
            The input paragraph to be tested.
        :returns:
            A list of tuples having 2 elements (invalid word, parts of speech)
            or an empty list if no invalid words are found.
        """
        try:
            words = nltk.word_tokenize(nltk.sent_tokenize(paragraph)[0])
            # VBZ : Verb, 3rd person singular present, like 'adds', 'writes'
            #       etc
            # VBD : Verb, Past tense , like 'added', 'wrote' etc
            # VBG : Verb, Present participle, like 'adding', 'writing'
            word, tag = nltk.pos_tag(['I'] + words)[1:2][0]
            if (tag.startswith('VBZ') or tag.startswith('VBD')
                    or tag.startswith('VBG')
                    or word.endswith('ing')):  # Handle special case for VBG
                return (word, tag)
            else:
                return None
        except LookupError as error:  # pragma: no cover
            self.err("NLTK data missing, install by running following "
                     "commands `python3 -m nltk.downloader punkt"
                     " maxent_treebank_pos_tagger averaged_perceptron_tagger`")
            return

    def check_body(self,
                   body,
                   body_line_length: int = 72,
                   force_body: bool = False):
        """
        Checks the given commit body.

        :param body:             The commit body splitted by lines.
        :param body_line_length: The maximum line-length of the body. The
                                 newline character at each line end does not
                                 count to the length.
        :param force_body:       Whether a body shall exist or not.
        """
        if len(body) == 0:
            if force_body:
                yield Result(self, "No commit message body at HEAD.")
            return

        if body[0] != "":
            yield Result(self, "No newline between shortlog and body at HEAD.")
            return

        if any(len(line) > body_line_length for line in body[1:]):
            yield Result(self, "Body of HEAD commit contains too long lines.")
Пример #29
0
class InvalidLinkBear(LocalBear):
    DEFAULT_TIMEOUT = 2
    LANGUAGES = {"All"}
    REQUIREMENTS = {PipRequirement('requests', '2.*')}
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Documentation'}

    # IP Address of www.google.com
    check_connection_url = "http://216.58.218.174"

    @classmethod
    def check_prerequisites(cls):
        code = cls.get_status_code(cls.check_connection_url,
                                   cls.DEFAULT_TIMEOUT)
        return ("You are not connected to the internet."
                if code is None else True)

    @staticmethod
    def get_status_code(url, timeout):
        try:
            code = requests.head(url, allow_redirects=False,
                                 timeout=timeout).status_code
            return code
        except requests.exceptions.RequestException:
            pass

    @staticmethod
    def find_links_in_file(file, timeout, link_ignore_regex):
        link_ignore_regex = re.compile(link_ignore_regex)
        regex = re.compile(
            r'(https?://[^.:%\s_/?#[\]@\\]+\.(?:[^\s()%\'"`<>|\\]+|'
            r'\([^\s()%\'"`<>|\\]*\))*)(?<!\.)(?<!,)')
        for line_number, line in enumerate(file):
            match = regex.search(line)
            if match:
                link = match.group()
                if not link_ignore_regex.search(link):
                    code = InvalidLinkBear.get_status_code(link, timeout)
                    yield line_number + 1, link, code

    @deprecate_settings(link_ignore_regex='ignore_regex')
    def run(self,
            filename,
            file,
            timeout: int = DEFAULT_TIMEOUT,
            link_ignore_regex: str = "([.\/]example\.com|\{|\$)",
            follow_redirects: bool = False):
        """
        Find links in any text file and check if they are valid.

        A link is considered valid if the server responds with a 2xx code.

        This bear can automatically fix redirects, but ignores redirect
        URLs that have a huge difference with the original URL.

        Warning: This bear will make HEAD requests to all URLs mentioned in
        your codebase, which can potentially be destructive. As an example,
        this bear would naively just visit the URL from a line that goes like
        `do_not_ever_open = 'https://api.acme.inc/delete-all-data'` wiping out
        all your data.

        :param timeout:          Request timeout period.
        :param link_ignore_regex:     A regex for urls to ignore.
        :param follow_redirects: Set to true to autocorrect redirects.
        """
        for line_number, link, code in InvalidLinkBear.find_links_in_file(
                file, timeout, link_ignore_regex):
            if code is None:
                yield Result.from_values(
                    origin=self,
                    message=('Broken link - unable to connect to '
                             '{url}').format(url=link),
                    file=filename,
                    line=line_number,
                    severity=RESULT_SEVERITY.MAJOR)
            elif not 200 <= code < 300:
                # HTTP status 404, 410 or 50x
                if code in (404, 410) or 500 <= code < 600:
                    yield Result.from_values(
                        origin=self,
                        message=('Broken link - unable to connect to {url} '
                                 '(HTTP Error: {code})').format(url=link,
                                                                code=code),
                        file=filename,
                        line=line_number,
                        severity=RESULT_SEVERITY.NORMAL)
                if follow_redirects and 300 <= code < 400:  # HTTP status 30x
                    redirect_url = requests.head(link,
                                                 allow_redirects=True).url
                    matcher = SequenceMatcher(None, redirect_url, link)
                    if (matcher.real_quick_ratio() > 0.7
                            and matcher.ratio()) > 0.7:
                        diff = Diff(file)
                        current_line = file[line_number - 1]
                        start = current_line.find(link)
                        end = start + len(link)
                        replacement = current_line[:start] + \
                            redirect_url + current_line[end:]
                        diff.change_line(line_number, current_line,
                                         replacement)

                        yield Result.from_values(
                            self,
                            'This link redirects to ' + redirect_url,
                            diffs={filename: diff},
                            file=filename,
                            line=line_number,
                            severity=RESULT_SEVERITY.NORMAL)
Пример #30
0
class SCSSLintBear:
    """
    Check SCSS code to keep it clean and readable.

    More information is available at <https://github.com/brigade/scss-lint>.
    """

    LANGUAGES = {'SCSS'}
    REQUIREMENTS = {
        GemRequirement('scss-lint', '', 'false'),
        PipRequirement('pyyaml', '3.12')
    }
    AUTHORS = {'The coala developers'}
    AUTHORS_EMAILS = {'*****@*****.**'}
    LICENSE = 'AGPL-3.0'
    CAN_DETECT = {'Syntax', 'Formatting'}

    @staticmethod
    def create_arguments(filename, file, config_file):
        return filename, '--config', config_file

    @staticmethod
    def generate_config(filename,
                        file,
                        space_around_bang: list = [True, False],
                        allow_chained_classes: bool = False,
                        prefer_color_keywords: bool = False,
                        use_color_variables: bool = True,
                        allow_debug_statement: bool = False,
                        check_declaration_order: bool = True,
                        allow_duplicate_properties: bool = False,
                        allow_consecutives_duplicate_property: bool = False,
                        else_on_same_line: bool = True,
                        force_empty_line_between_blocks: bool = True,
                        allow_empty_rules: bool = False,
                        use_short_hexadecimal_length_style: bool = True,
                        use_lowercase_hexadecimal: bool = True,
                        validate_hexadecimal: bool = True,
                        allow_id_selector: bool = False,
                        allow_important_rule_in_properties: bool = False,
                        use_spaces: bool = True,
                        indent_size: int = 2,
                        exclude_leading_zero: bool = True,
                        allow_mergeable_selectors: bool = False,
                        allow_leading_underscore: bool = True,
                        function_naming_convention: str = 'hyphen',
                        mixin_naming_convention: str = 'hyphen',
                        variable_naming_convention: str = 'hyphen',
                        placeholder_naming_convention: str = 'hyphen',
                        max_nesting_depth: int = 3,
                        use_placeholder_selector_in_extend: bool = True,
                        max_properties: int = 10,
                        allow_unit_on_zero_values: bool = False,
                        check_ulrs_format: bool = True,
                        urls_in_quotes: bool = True,
                        allow_unnecesseary_parent_reference: bool = False,
                        allow_unnecessary_mantissa: bool = False,
                        allow_trailing_whitespaces: bool = False,
                        allow_trailing_semicolon: bool = True,
                        check_imports_path: bool = True,
                        allow_filename_leading_underscore: bool = False,
                        allow_filename_extension: bool = False,
                        use_length_variables: bool = True,
                        check_properties_spelling: bool = True,
                        extra_properties: list = (),
                        disabled_properties: list = (),
                        check_pseudo_elements: bool = True,
                        spaces_between_parentheses: int = 0,
                        spaces_around_operators: str = 1):
        """
        :param space_around_bang:
            Enforces a space before and/or after ``!`` (the "bang").
        :param allow_chained_classes:
            Allows defining a rule set using a selector with chained classes.
        :param prefer_color_keywords:
            Prefers color keywords over hexadecimal color codes.
        :param use_color_variables:
            Prefers color literals (keywords or hexadecimal codes) to be used
            only in variable declarations.
        :param allow_debug_statement:
            Allows ``@debug`` statements.
        :param check_declaration_order:
            Rule sets should be ordered as follows: ``@extend`` declarations,
            ``@include`` declarations without inner ``@content``, properties,
            ``@include`` declarations with inner ``@content``, then nested rule
            sets.
        :param allow_duplicate_properties:
            Allows defining the same property twice in a single rule set.
        :param allow_consecutives_duplicate_property:
            Allows defining the same property consecutively in a single rule
            set.
        :param else_on_same_line:
            Places ``@else`` statements on the same line as the preceding curly
            brace.
        :param force_empty_line_between_blocks:
            Separate rule, function, and mixin declarations with empty lines.
        :param allow_empty_rules:
            Allows empty rule set.
        :param use_short_hexadecimal_length_style:
            Prefer shorthand or long-form hexadecimal colors by setting the
            style option to short or long, respectively.
        :param use_lowercase_hexadecimal:
            Checks if hexadecimal colors are written in lowercase or uppercase.
        :param validate_hexadecimal:
            Ensure hexadecimal colors are valid (either three or six digits).
        :param allow_id_selector:
            Allows using ID selectors.
        :param allow_important_rule_in_property:
            Allows using ``!important`` in properties.
        :param use_spaces:
            Use spaces for indentation (tabs otherwise).
        :param indent_size:
            Number of spaces per indentation level.
        :param exclude_leading_zero:
            Determines whether leading zeros should be written or not in
            numeric values with a decimal point.
        :param allow_mergeable_selectors:
            Allows defining the same selector twice in a single sheet.
        :param allow_leading_underscore:
            Allows names to start with a single underscore.
        :param function_naming_convention:
            Name of convention (``hyphen``(use lowercase letters and hyphens)
            (default), ``camel``, ``snake``), or a ``regex`` the name must
            match (eg: ``^[a-zA-Z]+$``) to use for functions.
        :param mixin_naming_convention:
            Name of convention (``hyphen`` (default), ``camel``, ``snake``), or
            a regex the name must match (eg: ``^[a-zA-Z]+$``) to use for
            mixins.
        :param variable_naming_convention:
            Name of convention (``hyphen`` (default), ``camel``, ``snake``), or
            a regex the name must match (eg: ``^[a-zA-Z]+$``) to use for
            variables.
        :param placeholder_naming_convention:
            Name of convention (``hyphen`` (default), ``camel``, ``snake``), or
            a regex the name must match (eg: ``^[a-zA-Z]+$``) to use for
            placeholders.
        :param max_nesting_depth:
            Maximum nesting depth.
        :param use_placeholder_selector_in_extend:
            Enforces using placeholder selectors in ``@extend``.
        :param max_properties:
            Enforces a limit on the number of properties in a rule set.
        :param allow_unit_on_zero_values:
            Allow omitting length units on zero values.
        :param check_urls_format:
            URLs should be valid and not contain protocols or domain names.
        :param urls_in_quotes:
            URLs should always be enclosed within quotes.
        :param allow_unnecessary_parent_reference:
            Allows use of the parent selector references ``&`` even when they
            are not unnecessary.
        :param allow_unnecessary_mantissa:
            Numeric values can contain unnecessary fractional portions.
        :param allow_traling_whitespaces:
            Unables trailing whitespace.
        :param allow_trailing_semicolon:
            Property values; ``@extend``, ``@include``, and ``@import``
            directives; and variable declarations should always end with a
            semicolon.
        :param check_imports_path:
            The basenames of ``@import``ed SCSS partials should not begin with
            an underscore and should not include the filename extension.
            These requirements can be modified by changing
            ``allow_filename_leading_underscore``, and ``allow_extensions``.
        :param allow_filename_leading_underscore:
            Requires basenames of ``@import``ed SCSS partials to begin with an
            underscore.  This setting require ``check_import_paths`` to be
            enabled.
        :param allow_filename_extension:
            Requires basenames of ``@import``ed SCSS partials to include
            filename extension, this setting require ``check_import_paths`` to
            be enabled.
        :param use_length_variables:
            Prefer length literals (numbers with units) to be used only in
            variable declarations.

            ::
                 div {
                   width: 100px;
                 }

            Is not valid, whereas

            ::
                 $column-width: 100px;

                 div {
                   width: $column-width;
                 }
            is valid.
        :param check_properties_spelling:
            Reports when an unknown or disabled CSS property is used
            (ignoring vendor-prefixed properties).
        :param extra_properties:
            List of extra properties to allow.
        :param disabled_properties:
            List of existing properties to deny.
        :param check_pseudo_elements:
            Pseudo-elements, like ``::before``, and ``::first-letter``,
            should be declared with two colons. Pseudo-classes, like ``:hover``
            and ``:first-child``, should be declared with one colon.

            ::
                p::before {
                  content: '>'
                }

                p:hover {
                  color: red;
                }

        :param spaces_between_parentheses:
            Spaces to require between parentheses.
        :param spaces_around_operators:
            Operators should be formatted with a single space on both sides of
            an infix operator. The different value for this setting are ``1``,
            ``0`` or a number greater that ``1``.
        """
        naming_convention_map = {
            'camel': 'camel_case',
            'snake': 'snake_case',
            'hyphen': 'hyphenated_lowercase'
        }
        space_setting_map = {'one_space': 1, 'no_space': 0}
        options = {
            'BangFormat': {
                'enabled': True,
                'space_before_bang': space_around_bang[0],
                'space_after_bang': space_around_bang[1]
            },
            'ChainedClasses': {
                'enabled': not allow_chained_classes
            },
            'ColorKeyword': {
                'enabled': not prefer_color_keywords
            },
            'ColorVariable': {
                'enabled': use_color_variables
            },
            'DebugStatement': {
                'enabled': not allow_debug_statement
            },
            'DeclarationOrder': {
                'enabled': check_declaration_order
            },
            'DuplicateProperty': {
                'enabled': not allow_duplicate_properties,
                'ignore_consecutive': allow_consecutives_duplicate_property
            },
            'ElsePlacement': {
                'enabled': True,
                'style': ('same_line' if else_on_same_line else '')
            },
            'EmptyLineBetweenBlocks': {
                'enabled': force_empty_line_between_blocks,
                'ignore_single_line_blocks': True
            },
            'EmptyRule': {
                'enabled': not allow_empty_rules
            },
            'HexLength': {
                'enabled':
                True,
                'style':
                ('short' if use_short_hexadecimal_length_style else 'long')
            },
            'HexNotation': {
                'enabled':
                True,
                'style':
                ('lowercase' if use_lowercase_hexadecimal else 'uppercase')
            },
            'HexValidation': {
                'enabled': validate_hexadecimal
            },
            'IdSelector': {
                'enabled': not allow_id_selector
            },
            'ImportantRule': {
                'enabled': not allow_important_rule_in_properties
            },
            'Indentation': {
                'enabled': True,
                'allow_non_nested_indentation': False,
                'character': ('space' if use_spaces else 'tab'),
                'width': indent_size
            },
            'LeadingZero': {
                'enabled':
                True,
                'style':
                ('exclude_zero' if exclude_leading_zero else 'include_zero')
            },
            'MergeableSelector': {
                'enabled': allow_mergeable_selectors,
                'force_nesting': True
            },
            'NestingDepth': {
                'enabled': True,
                'max_depth': max_nesting_depth,
                'ignore_parent_selectors': False
            },
            'NameFormat': {
                'enabled':
                True,
                'allow_leading_underscore':
                allow_leading_underscore,
                'function_convention':
                naming_convention_map.get(function_naming_convention,
                                          function_naming_convention),
                'mixin_convention':
                naming_convention_map.get(mixin_naming_convention,
                                          mixin_naming_convention),
                'variable_convention':
                naming_convention_map.get(variable_naming_convention,
                                          variable_naming_convention),
                'placeholder_convention':
                naming_convention_map.get(placeholder_naming_convention,
                                          placeholder_naming_convention)
            },
            'PlaceholderInExtend': {
                'enabled': use_placeholder_selector_in_extend
            },
            'PropertyCount': {
                'enabled': False,
                'include_nested': False,
                'max_properties': max_properties
            },
            'ZeroUnit': {
                'enabled': not allow_unit_on_zero_values
            },
            'UrlFormat': {
                'enabled': check_ulrs_format
            },
            'UrlQuotes': {
                'enabled': urls_in_quotes
            },
            'UnnecessaryMantissa': {
                'enabled': not allow_unnecessary_mantissa
            },
            'UnnecessaryParentReference': {
                'enabled': not allow_unnecesseary_parent_reference
            },
            'TrailingSemicolon': {
                'enabled': allow_trailing_semicolon
            },
            'TrailingWhitespace': {
                'enabled': not allow_trailing_whitespaces
            },
            'ImportPath': {
                'enabled': check_imports_path,
                'leading_underscore': allow_filename_leading_underscore,
                'filename_extension': allow_filename_extension
            },
            'LengthVariable': {
                'enabled': use_length_variables
            },
            'PropertySpelling': {
                'enabled': check_properties_spelling,
                'extra_properties': extra_properties,
                'disabled_properties': disabled_properties
            },
            'SpaceBetweenParens': {
                'enabled': True,
                'spaces': spaces_between_parentheses
            },
            'SpaceAroundOperator': {
                'enabled':
                True,
                'style':
                space_setting_map.get(spaces_around_operators,
                                      'at_least_one_space')
            }
        }
        configs = {'linters': options}
        return yaml.dump(configs, default_flow_style=False)