Skip to content
This repository has been archived by the owner on Nov 11, 2019. It is now read-only.

Commit

Permalink
shipit_static_analysis: Parse clang-tidy output to extract detected c…
Browse files Browse the repository at this point in the history
…ode problems #357 & #363

* shipit_static_analysis: Parse clang-tidy output to extract detected code problems.
* shipit_static_analysis: Send email through TC.
* shipit_static_analysis: Support full pulse message for commits, refs #363
* shipit_pulse_listener: Support full pulse message for commits, refs #363

* shipit_static_analysis: Add MozReview URL to email reports.
  • Loading branch information
jankeromnes authored and Bastien Abadie committed Jun 2, 2017
1 parent ce5f078 commit d971488
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 17 deletions.
7 changes: 5 additions & 2 deletions src/shipit_pulse_listener/shipit_pulse_listener/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ def parse_payload(self, payload):
return

# Extract commits
commits = [c['rev'] for c in payload.get('commits', [])]
commits = [
'{rev}:{review_request_id}:{diffset_revision}'.format(**c)
for c in payload.get('commits', [])
]
logger.info('Received new commits', commits=commits)
return {
'REVISIONS': ' '.join(commits),
'COMMITS': ' '.join(commits),
}


Expand Down
6 changes: 6 additions & 0 deletions src/shipit_static_analysis/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ let
# Used by taskclusterProxy
("secrets:get:" + secretsKey)

# Send emails to relman
"notify:email:[email protected]"
"notify:email:[email protected]"
"notify:email:[email protected]"
"notify:email:[email protected]"

# Used by cache
("docker-worker:cache:" + cacheKey)
];
Expand Down
27 changes: 22 additions & 5 deletions src/shipit_static_analysis/shipit_static_analysis/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,23 @@
from cli_common.log import init_logger
from cli_common.taskcluster import get_secrets
import click
import logging
import re

logger = logging.getLogger(__name__)

REGEX_COMMIT = re.compile(r'^(\w+):(\d+):(\d+)$')


@click.command()
@taskcluster_options
@click.argument('revisions', nargs=-1, envvar='REVISIONS')
@click.argument('commits', nargs=-1, envvar='COMMITS')
@click.option(
'--cache-root',
required=True,
help='Cache root, used to pull changesets'
)
def main(revisions,
def main(commits,
cache_root,
taskcluster_secret,
taskcluster_client_id,
Expand All @@ -29,6 +35,7 @@ def main(revisions,

secrets = get_secrets(taskcluster_secret,
config.PROJECT_NAME,
required=('STATIC_ANALYSIS_NOTIFICATIONS', ),
taskcluster_client_id=taskcluster_client_id,
taskcluster_access_token=taskcluster_access_token,
)
Expand All @@ -40,9 +47,19 @@ def main(revisions,
MOZDEF=secrets.get('MOZDEF'),
)

w = Workflow(cache_root)
for rev in revisions:
w.run(rev)
w = Workflow(cache_root,
secrets['STATIC_ANALYSIS_NOTIFICATIONS'],
taskcluster_client_id,
taskcluster_access_token,
)

for commit in commits:
commit_args = REGEX_COMMIT.search(commit)
if commit_args is None:
logger.warn('Skipping commit {}'.format(commit))
continue

w.run(*commit_args.groups())


if __name__ == '__main__':
Expand Down
131 changes: 121 additions & 10 deletions src/shipit_static_analysis/shipit_static_analysis/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

import hglib
import os
import re

from cli_common.taskcluster import get_service
from cli_common.log import get_logger
from cli_common.command import run_check

Expand All @@ -15,20 +17,67 @@
REPO_CENTRAL = b'https://hg.mozilla.org/mozilla-central'
REPO_REVIEW = b'https://reviewboard-hg.mozilla.org/gecko'

REGEX_HEADER = re.compile(r'^(.+):(\d+):(\d+): (warning|error|note): (.*)\n', re.MULTILINE)


class Issue(object):
"""
An issue reported by clang-tidy
"""
def __init__(self, header_data, work_dir):
assert isinstance(header_data, tuple)
assert len(header_data) == 5
self.path, self.line, self.char, self.type, self.message = header_data
if self.path.startswith(work_dir):
self.path = self.path[len(work_dir):]
self.line = int(self.line)
self.char = int(self.char)
self.body = None
self.notes = []

def __str__(self):
return '[{}] {} {}:{}'.format(self.type, self.path, self.line, self.char)

def is_problem(self):
return self.type in ('warning', 'error')

def as_markdown(self):
out = [
'# {} : {}'.format(self.type, self.path),
'**Position**: {}:{}'.format(self.line, self.char),
'**Snippet**: {}'.format(self.message),
'',
]
out += [
'* note on {} at {}:{} : {}'.format(
n.path, n.line, n.char, n.message
)
for n in self.notes
]
return '\n'.join(out)


class Workflow(object):
"""
Static analysis workflow
"""
taskcluster = None

def __init__(self, cache_root): # noqa
def __init__(self, cache_root, emails, client_id=None, access_token=None):
self.emails = emails
self.cache_root = cache_root
assert os.path.isdir(self.cache_root), \
"Cache root {} is not a dir.".format(self.cache_root)

# Load TC services & secrets
self.notify = get_service(
'notify',
client_id=client_id,
access_token=access_token,
)

# Clone mozilla-central
self.repo_dir = os.path.join(self.cache_root, 'static-analysis')
self.repo_dir = os.path.join(self.cache_root, 'static-analysis/')
shared_dir = os.path.join(self.cache_root, 'static-analysis-shared')
logger.info('Clone mozilla central', dir=self.repo_dir)
cmd = hglib.util.cmdbuilder('robustcheckout',
Expand All @@ -47,7 +96,7 @@ def __init__(self, cache_root): # noqa
# Open new hg client
self.hg = hglib.open(self.repo_dir)

def run(self, revision):
def run(self, revision, review_request_id, diffset_revision):
"""
Run the static analysis workflow:
* Pull revision from review
Expand Down Expand Up @@ -75,19 +124,21 @@ def run(self, revision):
logger.info('Modified files', files=modified_files)

# mach configure
logger.info('Mach configure ...')
logger.info('Mach configure...')
run_check(['gecko-env', './mach', 'configure'], cwd=self.repo_dir)

# Build CompileDB backend
logger.info('Mach build backend ...')
run_check(['gecko-env', './mach', 'build-backend', '--backend=CompileDB'], cwd=self.repo_dir)
logger.info('Mach build backend...')
cmd = ['gecko-env', './mach', 'build-backend', '--backend=CompileDB']
run_check(cmd, cwd=self.repo_dir)

# Build exports
logger.info('Mach build exports ...')
logger.info('Mach build exports...')
run_check(['gecko-env', './mach', 'build', 'pre-export'], cwd=self.repo_dir)
run_check(['gecko-env', './mach', 'build', 'export'], cwd=self.repo_dir)

# Run static analysis through run-clang-tidy.py
logger.info('Run clang-tidy...')
checks = [
'-*',
'modernize-loop-convert',
Expand All @@ -104,7 +155,67 @@ def run(self, revision):
'-p', 'obj-x86_64-pc-linux-gnu/',
'-checks={}'.format(','.join(checks)),
] + modified_files
clang_output = run_check(cmd, cwd=self.repo_dir)
clang_output = run_check(cmd, cwd=self.repo_dir).decode('utf-8')

# Parse clang-tidy's output to indentify potential code problems
logger.info('Process static analysis results...')
issues = self.parse_issues(clang_output)

# TODO Analyse clang output
logger.info('Clang output', output=clang_output)
logger.info('Detected {} code issue(s)'.format(len(issues)))

# Notify by email
if issues:
logger.info('Send email to admins')
self.notify_admins(review_request_id, issues)

def parse_issues(self, clang_output):
"""
Parse clang-tidy output into structured issues
"""

# Limit clang output parsing to "Enabled checks:"
end = re.search(r'^Enabled checks:\n', clang_output, re.MULTILINE)
if end is not None:
clang_output = clang_output[:end.start()-1]

# Sort headers by positions
headers = sorted(
REGEX_HEADER.finditer(clang_output),
key=lambda h: h.start()
)

issues = []
for i, header in enumerate(headers):
issue = Issue(header.groups(), self.repo_dir)

# Get next header
if i+1 < len(headers):
next_header = headers[i+1]
issue.body = clang_output[header.end():next_header.start() - 1]
else:
issue.body = clang_output[header.end():]

if issue.is_problem():
# Save problem to append notes
issues.append(issue)
logger.info('Found code issue {}'.format(issue))

elif issues:
# Link notes to last problem
issues[-1].notes.append(issue)

return issues

def notify_admins(self, review_request_id, issues):
"""
Send an email to administrators
"""
review_url = 'https://reviewboard.mozilla.org/r/' + review_request_id + '/'
content = review_url + '\n\n' + '\n'.join([i.as_markdown() for i in issues])
for email in self.emails:
self.notify.email({
'address': email,
'subject': 'New Static Analysis Review',
'content': content,
'template': 'fullscreen',
})

0 comments on commit d971488

Please sign in to comment.