Introduce code-analysis bot
This commit is contained in:
parent
c0c8e7cfdf
commit
402fd06725
|
@ -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 ]
|
||||
|
|
|
@ -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)
|
||||
});
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue