Introduce code-analysis bot

This commit is contained in:
Christopher Chedeau 2015-12-09 15:29:00 -08:00
parent c0c8e7cfdf
commit 402fd06725
3 changed files with 245 additions and 1 deletions

View File

@ -28,6 +28,7 @@ script:
elif [ "$TEST_TYPE" = js ]
then
cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; flow --json) | GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" node bots/code-analysis-bot.js
flow check && npm test -- '\/Libraries\/'
elif [ "$TEST_TYPE" = packager ]

242
bots/code-analysis-bot.js Normal file
View File

@ -0,0 +1,242 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
if (!process.env.TRAVIS_REPO_SLUG) {
console.error('Missing TRAVIS_REPO_SLUG. Example: facebook/react-native');
process.exit(1);
}
if (!process.env.GITHUB_TOKEN) {
console.error('Missing GITHUB_TOKEN. Example: 5fd88b964fa214c4be2b144dc5af5d486a2f8c1e');
process.exit(1);
}
if (!process.env.TRAVIS_PULL_REQUEST) {
console.error('Missing TRAVIS_PULL_REQUEST. Example: 4687');
process.exit(1);
}
var GitHubApi = require('github');
var path = require('path');
var github = new GitHubApi({
version: '3.0.0',
});
github.authenticate({
type: 'oauth',
token: process.env.GITHUB_TOKEN,
});
function push(arr, key, value) {
if (!arr[key]) {
arr[key] = [];
}
arr[key].push(value);
}
/**
* There is unfortunately no standard format to report an error, so we have
* to write a specific converter for each tool we want to support.
*
* Those functions take a json object as input and fill the output with the
* following format:
*
* { [ path: string ]: Array< { message: string, line: number }> }
*
* This is an object where the keys are the path of the files and values
* is an array of objects of the shape message and line.
*/
var converters = {
raw: function(output, input) {
for (var key in input) {
input[key].forEach(function(message) {
push(output, key, message);
});
}
},
flow: function(output, input) {
if (!input || !input.errors) {
return;
}
input.errors.forEach(function(error) {
push(output, error.message[0].path, {
message: error.message.map(message => message.descr).join(' '),
line: error.message[0].line,
});
});
},
eslint: function(output, input) {
if (!input) {
return;
}
input.forEach(function(file) {
file.messages.forEach(function(message) {
push(output, file.filePath, {
message: message.ruleId + ': ' + message.message,
line: message.line,
});
});
});
}
};
function getShaFromPullRequest(user, repo, number, callback) {
github.pullRequests.get({user, repo, number}, (error, res) => {
if (error) {
console.log(error);
return;
}
callback(res.head.sha);
});
}
function getFilesFromCommit(user, repo, sha, callback) {
github.repos.getCommit({user, repo, sha}, (error, res) => {
if (error) {
console.log(error);
return;
}
callback(res.files);
});
}
/**
* Sadly we can't just give the line number to github, we have to give the
* line number relative to the patch file which is super annoying. This
* little function builds a map of line number in the file to line number
* in the patch file
*/
function getLineMapFromPatch(patchString) {
var diffLineIndex = 0;
var fileLineIndex = 0;
var lineMap = {};
patchString.split('\n').forEach((line) => {
if (line.match(/^@@/)) {
fileLineIndex = line.match(/\+([0-9]+)/)[1] - 1;
return;
}
diffLineIndex++;
if (line[0] !== '-') {
fileLineIndex++;
if (line[0] === '+') {
lineMap[fileLineIndex] = diffLineIndex;
}
}
});
return lineMap;
}
function sendComment(user, repo, number, sha, filename, lineMap, message) {
if (!lineMap[message.line]) {
// Do not send messages on lines that did not change
return;
}
var opts = {
user,
repo,
number,
sha,
path: filename,
commit_id: sha,
body: message.message,
position: lineMap[message.line],
};
github.pullRequests.createComment(opts, function(error, res) {
if (error) {
console.log(error);
return;
}
});
console.log('Sending comment', opts);
}
function main(messages, user, repo, number) {
// No message, we don't need to do anything :)
if (Object.keys(messages).length === 0) {
return;
}
getShaFromPullRequest(user, repo, number, (sha) => {
getFilesFromCommit(user, repo, sha, (files) => {
files
.filter((file) => messages[file.filename])
.forEach((file) => {
var lineMap = getLineMapFromPatch(file.patch);
messages[file.filename].forEach((message) => {
sendComment(user, repo, number, sha, file.filename, lineMap, message);
});
});
});
});
}
var content = '';
process.stdin.resume();
process.stdin.on('data', function(buf) { content += buf.toString(); });
process.stdin.on('end', function() {
var messages = {};
// Since we send a few http requests to setup the process, we don't want
// to run this file one time per code analysis tool. Instead, we write all
// the results in the same stdin stream.
// The format of this stream is
//
// name-of-the-converter
// {"json":"payload"}
// name-of-the-other-converter
// {"other": ["json", "payload"]}
//
// In order to generate such stream, here is a sample bash command:
//
// cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; flow --json) | node code-analysis-bot.js
var lines = content.trim().split('\n');
for (var i = 0; i < Math.ceil(lines.length / 2); ++i) {
var converter = converters[lines[i * 2]];
if (!converter) {
throw new Error('Unknown converter ' + lines[i * 2]);
}
var json;
try {
json = JSON.parse(lines[i * 2 + 1]);
} catch (e) {}
converter(messages, json);
}
// The paths are returned in absolute from code analysis tools but github works
// on paths relative from the root of the project. Doing the normalization here.
var pwd = path.resolve('.');
for (var absolutePath in messages) {
var relativePath = path.relative(pwd, absolutePath);
if (relativePath === absolutePath) {
continue;
}
messages[relativePath] = messages[absolutePath];
delete messages[absolutePath];
}
// TRAVIS_REPO_SLUG // 'facebook/react-native'
var user_repo = process.env.TRAVIS_REPO_SLUG.split('/');
var user = user_repo[0];
var repo = user_repo[1];
var number = process.env.TRAVIS_PULL_REQUEST;
// intentional lint warning to make sure that the bot is working :)
main(messages, user, repo, number)
});

View File

@ -46,7 +46,7 @@
],
"scripts": {
"test": "NODE_ENV=test jest",
"lint": "eslint Examples/ Libraries/",
"lint": "eslint Examples/ Libraries/ bots/code-analysis-bot.js",
"start": "/usr/bin/env bash -c './packager/packager.sh \"$@\" || true' --"
},
"bin": {
@ -122,6 +122,7 @@
"yeoman-generator": "^0.20.3"
},
"devDependencies": {
"github": "^0.2.4",
"jest-cli": "0.7.1",
"babel-eslint": "4.1.4",
"eslint": "1.3.1",