Ejemplo n.º 1
0
class LintProcessor(object):
    LINTERS = {
        'eslint': ESLinter,
        'flake8': Flake8Linter,
        'pep8': PyCodeStyleLinter,
        'pycodestyle': PyCodeStyleLinter,
        'csslint': CSSLinter,
        'shellcheck': ShellCheckLinter,
        'jsonlint': JSONLinter,
        'yamllint': YAMLLinter,
        'bandit': BanditLinter,
        'rstlint': RestructuredTextLinter,
        'pylint': PylintLinter,
        'sasslint': SassLinter,
        'stylelint': StyleLinter,
        'mypy': MypyLinter,
    }

    def __init__(self, context, spec, working_dir=None):
        self.context = context
        self.spec = spec
        self.working_dir = working_dir or context.clone_path
        self.problems = Problems()
        self.pr = PullRequest(bitbucket, context.repository)
        commit_hash = context.source['commit']['hash']
        self.build_status = BuildStatus(
            bitbucket, context.source['repository']['full_name'], commit_hash,
            'badwolf/lint', 'https://bitbucket.org/{}/pull-requests/{}'.format(
                context.repository, context.pr_id))

    def load_changes(self):
        try:
            changes = self.pr.diff(self.context.pr_id)
        except (BitbucketAPIError, UnidiffParseError):
            logger.exception('Error getting pull request diff from API')
            sentry.captureException()
            return

        self.problems.set_changes(changes)
        return changes

    def process(self):
        if not self.spec.linters:
            logger.info('No linters configured, ignore lint.')
            return

        logger.info('Running code linting')
        patch = self.load_changes()
        if not patch:
            logger.info('Load changes failed, ignore lint.')
            return

        lint_files = patch.added_files + patch.modified_files
        if not lint_files:
            logger.info('No changed files found, ignore lint')
            return

        self.update_build_status('INPROGRESS', 'Lint in progress')
        files = [f.path for f in lint_files]
        self._execute_linters(files)

        total_problems = len(self.problems)
        self.problems.limit_to_changes()
        in_diff_problems = len(self.problems)

        # Report error and cleanup outdated lint comments
        submitted_problems, fixed_problems = self._report()
        if total_problems > 0:
            if in_diff_problems == total_problems:
                description = 'Found {} new issues'.format(total_problems)
            else:
                description = 'Found {} issues'.format(total_problems)
                description += ', {} issues in diff'.format(in_diff_problems)
                if submitted_problems > 0:
                    description += ', {} new issues'.format(submitted_problems)
            if fixed_problems > 0:
                description += ' {} issues fixed'.format(fixed_problems)
        else:
            description = 'No code issues found'

        has_error = any(p for p in self.problems if p.is_error)
        if has_error:
            logger.info('Lint failed: %s', description)
            self.update_build_status('FAILED', description)
        else:
            logger.info('Lint successful: %s', description)
            self.update_build_status('SUCCESSFUL', description)

    def _execute_linters(self, files):
        for linter_option in self.spec.linters:
            name = linter_option.name
            linter_cls = self.LINTERS.get(name)
            if not linter_cls:
                logger.info('Linter %s not found, ignore.', name)
                continue

            linter = linter_cls(self.working_dir, self.problems, linter_option)
            if not linter.is_usable():
                logger.info('Linter %s is not usable, ignore.', name)
                continue

            logger.info('Running %s code linter', name)
            linter.execute(files)

    def _report(self):
        try:
            comments = self.pr.all_comments(self.context.pr_id)
        except BitbucketAPIError:
            logger.exception('Error fetching all comments for pull request')
            sentry.captureException()
            comments = []

        existing_comments_ids = {}
        for comment in comments:
            inline = comment.get('inline')
            if not inline:
                continue

            raw = comment['content']['raw']
            if not raw.startswith(':broken_heart: **'):
                continue
            filename = inline['path']
            line = inline['to'] or inline['from']
            if line is None:
                continue
            existing_comments_ids[(filename, line, raw)] = comment['id']

        if len(self.problems) == 0:
            return 0, 0

        revision_before = self.context.target['commit']['hash']
        revision_after = self.context.source['commit']['hash']
        lint_comments = set()
        problem_count = 0
        for problem in self.problems:
            content = ':broken_heart: **{}**: {}'.format(
                problem.linter, problem.message)
            comment_tuple = (problem.filename, problem.line, content)
            lint_comments.add(comment_tuple)
            if comment_tuple in existing_comments_ids:
                continue

            comment_kwargs = {
                'filename': problem.filename,
                'anchor': revision_after,
                'dest_rev': revision_before,
            }
            if problem.has_line_change:
                comment_kwargs['line_to'] = problem.line
            else:
                comment_kwargs['line_from'] = problem.line
            try:
                self.pr.comment(self.context.pr_id, content, **comment_kwargs)
            except BitbucketAPIError:
                logger.exception(
                    'Error creating inline comment for pull request')
                sentry.captureException()
            else:
                problem_count += 1

        logger.info('Code lint result: %d problems found, %d submitted',
                    len(self.problems), problem_count)

        outdated_cleaned = 0
        outdated_comments = set(existing_comments_ids.keys()) - lint_comments
        logger.info('%d outdated lint comments found', len(outdated_comments))
        for comment in outdated_comments:
            # Delete comment
            try:
                self.pr.delete_comment(self.context.pr_id,
                                       existing_comments_ids[comment])
                outdated_cleaned += 1
            except BitbucketAPIError:
                logger.exception('Error deleting pull request comment')
                sentry.captureException()
        return problem_count, outdated_cleaned

    def update_build_status(self, state, description=None):
        try:
            self.build_status.update(state, description=description)
        except BitbucketAPIError:
            logger.exception('Error calling Bitbucket API')
            sentry.captureException()
Ejemplo n.º 2
0
class LintProcessor(object):
    LINTERS = {
        'eslint': ESLinter,
        'flake8': Flake8Linter,
        'jscs': JSCSLinter,
        'pep8': PEP8Linter,
        'csslint': CSSLinter,
        'shellcheck': ShellCheckLinter,
        'jsonlint': JSONLinter,
        'yamllint': YAMLLinter,
        'bandit': BanditLinter,
        'rstlint': RestructuredTextLinter,
        'pylint': PylintLinter,
        'sasslint': SassLinter,
        'stylelint': StyleLinter,
    }

    def __init__(self, context, spec, working_dir):
        self.context = context
        self.spec = spec
        self.working_dir = working_dir
        self.problems = Problems()
        self.pr = PullRequest(bitbucket, context.repository)
        commit_hash = context.source['commit']['hash']
        self.build_status = BuildStatus(
            bitbucket, context.source['repository']['full_name'], commit_hash,
            'badwolf/lint',
            url_for('log.lint_log', sha=commit_hash, _external=True))

    def load_changes(self):
        try:
            changes = self.pr.diff(self.context.pr_id)
        except (BitbucketAPIError, UnidiffParseError):
            logger.exception('Error getting pull request diff from API')
            return

        self.problems.set_changes(changes)
        return changes

    def process(self):
        if not self.spec.linters:
            logger.info('No linters configured, ignore lint.')
            return

        logger.info('Running code linting')
        patch = self.load_changes()
        if not patch:
            logger.info('Load changes failed, ignore lint.')
            return

        lint_files = patch.added_files + patch.modified_files
        if not lint_files:
            logger.info('No changed files found, ignore lint')
            return

        self.update_build_status('INPROGRESS', 'Lint in progress')
        files = [f.path for f in lint_files]
        self._execute_linters(files)
        logger.info('%d problems found before limit to changes',
                    len(self.problems))

        self.problems.limit_to_changes()

        has_error = any(p for p in self.problems if p.is_error)
        if len(self.problems):
            description = 'Found {} code issues'.format(len(self.problems))
        else:
            description = 'No code issues found'
            logger.info('No problems found when linting codes')

        # Report error or cleanup lint
        self._report()

        if has_error:
            self.update_build_status('FAILED', description)
        else:
            self.update_build_status('SUCCESSFUL', description)

    def _execute_linters(self, files):
        for linter_option in self.spec.linters:
            name = linter_option.name
            linter_cls = self.LINTERS.get(name)
            if not linter_cls:
                logger.info('Linter %s not found, ignore.', name)
                continue

            linter = linter_cls(self.working_dir, self.problems, linter_option)
            if not linter.is_usable():
                logger.info('Linter %s is not usable, ignore.', name)
                continue

            logger.info('Running %s code linter', name)
            linter.execute(files)

    def _report(self):
        try:
            comments = self.pr.all_comments(self.context.pr_id)
        except BitbucketAPIError:
            logger.exception('Error fetching all comments for pull request')
            comments = []

        hash_set = set()
        for comment in comments:
            inline = comment.get('inline')
            if not inline:
                continue

            raw = comment['content']['raw']
            if self.context.cleanup_lint and raw.startswith(':broken_heart:'):
                # Delete comment
                try:
                    self.pr.delete_comment(self.context.pr_id, comment['id'])
                except BitbucketAPIError:
                    logger.exception('Error deleting pull request comment')
            else:
                filename = inline['path']
                line_to = inline['to']
                hash_set.add(hash('{}{}{}'.format(filename, line_to, raw)))

        if len(self.problems) == 0:
            return

        revision_before = self.context.target['commit']['hash']
        revision_after = self.context.source['commit']['hash']
        problem_count = 0
        for problem in self.problems:
            content = ':broken_heart: **{}**: {}'.format(
                problem.linter, problem.message)
            comment_hash = hash('{}{}{}'.format(
                problem.filename,
                problem.line,
                content,
            ))
            if comment_hash in hash_set:
                continue

            try:
                self.pr.comment(
                    self.context.pr_id,
                    content,
                    line_to=problem.line,
                    filename=problem.filename,
                    anchor=revision_after,
                    dest_rev=revision_before,
                )
            except BitbucketAPIError:
                logger.exception(
                    'Error creating inline comment for pull request')
            else:
                problem_count += 1

        logger.info('Code lint result: %d problems found, %d submited',
                    len(self.problems), problem_count)
        return problem_count

    def update_build_status(self, state, description=None):
        try:
            self.build_status.update(state, description=description)
        except BitbucketAPIError:
            logger.exception('Error calling Bitbucket API')