243 lines
6.3 KiB
JavaScript
243 lines
6.3 KiB
JavaScript
/**
|
|
* 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)
|
|
});
|