2015-12-10 02:29:00 +03:00
/ * *
2018-09-12 01:27:47 +03:00
* Copyright ( c ) Facebook , Inc . and its affiliates .
2015-12-10 02:29:00 +03:00
*
2018-02-17 05:24:55 +03:00
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree .
2018-05-11 23:32:37 +03:00
*
* @ format
2015-12-10 02:29:00 +03:00
* /
2018-05-11 23:32:37 +03:00
2015-12-10 02:29:00 +03:00
'use strict' ;
2018-10-05 21:26:14 +03:00
if ( ! process . env . GITHUB _OWNER ) {
console . error ( 'Missing GITHUB_OWNER. Example: facebook' ) ;
2016-03-18 18:09:20 +03:00
process . exit ( 1 ) ;
}
2018-10-05 21:26:14 +03:00
if ( ! process . env . GITHUB _REPO ) {
console . error ( 'Missing GITHUB_REPO. Example: react-native' ) ;
2015-12-10 02:29:00 +03:00
process . exit ( 1 ) ;
}
2018-09-29 03:00:18 +03:00
const path = require ( 'path' ) ;
2015-12-10 02:29:00 +03:00
function push ( arr , key , value ) {
if ( ! arr [ key ] ) {
arr [ key ] = [ ] ;
}
arr [ key ] . push ( value ) ;
}
2019-02-13 19:38:37 +03:00
const converterSummary = {
eslint :
'`eslint` found some issues. Run `yarn lint --fix` to automatically fix problems.' ,
2019-05-22 05:35:40 +03:00
flow :
'`flow` found some issues. Run `yarn flow check` to analyze your code and address any errors.' ,
shellcheck :
'`shellcheck` found some issues. Run `yarn shellcheck` to analyze shell scripts.' ,
2019-02-13 19:38:37 +03:00
} ;
2015-12-10 02:29:00 +03:00
/ * *
* 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 .
* /
2018-09-29 03:00:18 +03:00
const converters = {
2020-03-25 07:35:58 +03:00
raw : function ( output , input ) {
2018-09-29 03:00:18 +03:00
for ( let key in input ) {
2020-03-25 07:35:58 +03:00
input [ key ] . forEach ( function ( message ) {
2015-12-10 02:29:00 +03:00
push ( output , key , message ) ;
} ) ;
}
} ,
2020-03-25 07:35:58 +03:00
flow : function ( output , input ) {
2015-12-10 02:29:00 +03:00
if ( ! input || ! input . errors ) {
return ;
}
2020-03-25 07:35:58 +03:00
input . errors . forEach ( function ( error ) {
2015-12-10 02:29:00 +03:00
push ( output , error . message [ 0 ] . path , {
2020-03-25 07:35:58 +03:00
message : error . message . map ( message => message . descr ) . join ( ' ' ) ,
2015-12-10 02:29:00 +03:00
line : error . message [ 0 ] . line ,
2018-09-29 03:00:18 +03:00
converter : 'flow' ,
2015-12-10 02:29:00 +03:00
} ) ;
} ) ;
} ,
2020-03-25 07:35:58 +03:00
eslint : function ( output , input ) {
2015-12-10 02:29:00 +03:00
if ( ! input ) {
return ;
}
2020-03-25 07:35:58 +03:00
input . forEach ( function ( file ) {
file . messages . forEach ( function ( message ) {
2015-12-10 02:29:00 +03:00
push ( output , file . filePath , {
message : message . ruleId + ': ' + message . message ,
line : message . line ,
2018-09-29 03:00:18 +03:00
converter : 'eslint' ,
2015-12-10 02:29:00 +03:00
} ) ;
} ) ;
} ) ;
2018-05-11 23:32:37 +03:00
} ,
2018-09-29 03:00:18 +03:00
2020-03-25 07:35:58 +03:00
shellcheck : function ( output , input ) {
2018-09-29 03:00:18 +03:00
if ( ! input ) {
return ;
}
2020-03-25 07:35:58 +03:00
input . forEach ( function ( report ) {
2018-09-29 03:00:18 +03:00
push ( output , report . file , {
message :
'**[SC' +
report . code +
'](https://github.com/koalaman/shellcheck/wiki/SC' +
report . code +
'):** (' +
report . level +
') ' +
report . message ,
line : report . line ,
endLine : report . endLine ,
column : report . column ,
endColumn : report . endColumn ,
converter : 'shellcheck' ,
} ) ;
} ) ;
} ,
2015-12-10 02:29:00 +03:00
} ;
2020-02-21 02:19:24 +03:00
function getShaFromPullRequest ( octokit , owner , repo , number , callback ) {
2018-08-24 06:23:32 +03:00
octokit . pullRequests . get ( { owner , repo , number } , ( error , res ) => {
2015-12-10 02:29:00 +03:00
if ( error ) {
2018-08-24 06:23:32 +03:00
console . error ( error ) ;
2015-12-10 02:29:00 +03:00
return ;
}
2018-08-24 06:23:32 +03:00
callback ( res . data . head . sha ) ;
2015-12-10 02:29:00 +03:00
} ) ;
}
2020-02-21 02:19:24 +03:00
function getFilesFromPullRequest ( octokit , owner , repo , number , callback ) {
2018-12-06 00:12:50 +03:00
octokit . pullRequests . listFiles (
{ owner , repo , number , per _page : 100 } ,
( error , res ) => {
if ( error ) {
console . error ( error ) ;
return ;
}
callback ( res . data ) ;
} ,
) ;
2015-12-10 02:29:00 +03:00
}
/ * *
* 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 ) {
2018-09-29 03:00:18 +03:00
let diffLineIndex = 0 ;
let fileLineIndex = 0 ;
let lineMap = { } ;
2015-12-10 02:29:00 +03:00
2020-03-25 07:35:58 +03:00
patchString . split ( '\n' ) . forEach ( line => {
2015-12-10 02:29:00 +03:00
if ( line . match ( /^@@/ ) ) {
fileLineIndex = line . match ( /\+([0-9]+)/ ) [ 1 ] - 1 ;
return ;
}
diffLineIndex ++ ;
if ( line [ 0 ] !== '-' ) {
fileLineIndex ++ ;
if ( line [ 0 ] === '+' ) {
lineMap [ fileLineIndex ] = diffLineIndex ;
}
}
} ) ;
return lineMap ;
}
2020-02-21 02:19:24 +03:00
function sendReview ( octokit , owner , repo , number , commit _id , body , comments ) {
2018-10-05 21:26:14 +03:00
if ( process . env . GITHUB _TOKEN ) {
if ( comments . length === 0 ) {
// Do not leave an empty review.
return ;
2019-05-22 05:35:40 +03:00
} else if ( comments . length > 5 ) {
// Avoid noisy reviews and rely solely on the body of the review.
comments = [ ] ;
2018-10-05 21:26:14 +03:00
}
2018-08-31 02:31:54 +03:00
2018-10-05 21:26:14 +03:00
const event = 'REQUEST_CHANGES' ;
const opts = {
owner ,
repo ,
number ,
commit _id ,
body ,
event ,
comments ,
} ;
2020-03-25 07:35:58 +03:00
octokit . pullRequests . createReview ( opts , function ( error , res ) {
2018-10-05 21:26:14 +03:00
if ( error ) {
console . error ( error ) ;
return ;
}
} ) ;
} else {
if ( comments . length === 0 ) {
console . log ( 'No issues found.' ) ;
2018-08-31 02:31:54 +03:00
return ;
}
2018-10-05 21:26:14 +03:00
if ( process . env . CIRCLE _CI ) {
console . error (
'Code analysis found issues, but the review cannot be posted to GitHub without an access token.' ,
) ;
process . exit ( 1 ) ;
}
let results = body + '\n' ;
2020-03-25 07:35:58 +03:00
comments . forEach ( comment => {
2018-10-05 21:26:14 +03:00
results +=
comment . path + ':' + comment . position + ': ' + comment . body + '\n' ;
} ) ;
console . log ( results ) ;
}
2018-08-31 02:31:54 +03:00
}
2018-08-24 06:23:32 +03:00
function main ( messages , owner , repo , number ) {
2015-12-10 02:29:00 +03:00
// No message, we don't need to do anything :)
if ( Object . keys ( messages ) . length === 0 ) {
return ;
}
2020-02-21 02:19:24 +03:00
if ( ! process . env . GITHUB _TOKEN ) {
2018-10-05 21:26:14 +03:00
console . log (
2019-05-22 05:35:40 +03:00
'Missing GITHUB_TOKEN. Example: 5fd88b964fa214c4be2b144dc5af5d486a2f8c1e. Review feedback with code analysis results will not be provided on GitHub without a valid token.' ,
2018-09-29 03:00:18 +03:00
) ;
}
2020-02-21 02:19:24 +03:00
// https://octokit.github.io/rest.js/
const { Octokit } = require ( '@octokit/rest' ) ;
const octokit = new Octokit ( {
auth : process . env . GITHUB _TOKEN ,
} ) ;
2020-03-25 07:35:58 +03:00
getShaFromPullRequest ( octokit , owner , repo , number , sha => {
getFilesFromPullRequest ( octokit , owner , repo , number , files => {
2018-09-29 03:00:18 +03:00
let comments = [ ] ;
let convertersUsed = [ ] ;
2019-03-14 16:55:56 +03:00
files
2020-03-25 07:35:58 +03:00
. filter ( file => messages [ file . filename ] )
. forEach ( file => {
2019-03-14 16:55:56 +03:00
// github api sometimes does not return a patch on large commits
if ( ! file . patch ) {
return ;
2018-08-31 02:31:54 +03:00
}
2019-03-14 16:55:56 +03:00
const lineMap = getLineMapFromPatch ( file . patch ) ;
2020-03-25 07:35:58 +03:00
messages [ file . filename ] . forEach ( message => {
2019-03-14 16:55:56 +03:00
if ( lineMap [ message . line ] ) {
const comment = {
path : file . filename ,
position : lineMap [ message . line ] ,
body : message . message ,
} ;
convertersUsed . push ( message . converter ) ;
comments . push ( comment ) ;
}
} ) ; // forEach
} ) ; // filter
2018-08-31 02:31:54 +03:00
2018-10-05 21:26:14 +03:00
let body = '**Code analysis results:**\n\n' ;
const uniqueconvertersUsed = [ ... new Set ( convertersUsed ) ] ;
2020-03-25 07:35:58 +03:00
uniqueconvertersUsed . forEach ( converter => {
2019-02-13 19:38:37 +03:00
body += '* ' + converterSummary [ converter ] + '\n' ;
2018-10-05 21:26:14 +03:00
} ) ;
2020-02-21 02:19:24 +03:00
sendReview ( octokit , owner , repo , number , sha , body , comments ) ;
2018-12-06 00:12:50 +03:00
} ) ; // getFilesFromPullRequest
2018-08-31 02:31:54 +03:00
} ) ; // getShaFromPullRequest
2015-12-10 02:29:00 +03:00
}
2018-09-29 03:00:18 +03:00
let content = '' ;
2015-12-10 02:29:00 +03:00
process . stdin . resume ( ) ;
2020-03-25 07:35:58 +03:00
process . stdin . on ( 'data' , function ( buf ) {
2018-05-11 23:32:37 +03:00
content += buf . toString ( ) ;
} ) ;
2020-03-25 07:35:58 +03:00
process . stdin . on ( 'end' , function ( ) {
2018-09-29 03:00:18 +03:00
let messages = { } ;
2015-12-10 02:29:00 +03:00
// 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
2018-09-29 03:00:18 +03:00
const lines = content . trim ( ) . split ( '\n' ) ;
for ( let i = 0 ; i < Math . ceil ( lines . length / 2 ) ; ++ i ) {
const converter = converters [ lines [ i * 2 ] ] ;
2015-12-10 02:29:00 +03:00
if ( ! converter ) {
throw new Error ( 'Unknown converter ' + lines [ i * 2 ] ) ;
}
2018-09-29 03:00:18 +03:00
let json ;
2015-12-10 02:29:00 +03:00
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.
2018-09-29 03:00:18 +03:00
const pwd = path . resolve ( '.' ) ;
for ( let absolutePath in messages ) {
const relativePath = path . relative ( pwd , absolutePath ) ;
2015-12-10 02:29:00 +03:00
if ( relativePath === absolutePath ) {
continue ;
}
messages [ relativePath ] = messages [ absolutePath ] ;
delete messages [ absolutePath ] ;
}
2018-10-05 21:26:14 +03:00
const owner = process . env . GITHUB _OWNER ;
const repo = process . env . GITHUB _REPO ;
2018-09-29 03:00:18 +03:00
2018-10-05 21:26:14 +03:00
if ( ! process . env . GITHUB _PR _NUMBER ) {
2019-05-22 05:35:40 +03:00
console . error (
'Missing GITHUB_PR_NUMBER. Example: 4687. Review feedback with code analysis results cannot be provided on GitHub without a valid pull request number.' ,
) ;
2018-09-29 03:00:18 +03:00
// for master branch, don't throw an error
process . exit ( 0 ) ;
}
2018-10-05 21:26:14 +03:00
const number = process . env . GITHUB _PR _NUMBER ;
2015-12-10 02:29:00 +03:00
// intentional lint warning to make sure that the bot is working :)
2018-08-24 06:23:32 +03:00
main ( messages , owner , repo , number ) ;
2015-12-10 02:29:00 +03:00
} ) ;