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.' ,
2020-12-07 14:10:46 +03:00
'google-java-format' :
'`google-java-format` found some issues. See https://github.com/google/google-java-format' ,
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-12-07 14:10:46 +03:00
'google-java-format' : function ( output , input ) {
if ( ! input ) {
return ;
}
input . forEach ( function ( change ) {
push ( output , change . file , {
message : ` \` google-java-format \` suggested changes:
\ ` \` \` diff
$ { change . description }
\ ` \` \`
` ,
line : change . line ,
converter : 'google-java-format' ,
} ) ;
} ) ;
} ,
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
} ;
/ * *
* 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-12-07 14:10:46 +03:00
async function sendReview (
octokit ,
owner ,
repo ,
pull _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 ,
2020-12-07 14:10:46 +03:00
pull _number ,
2018-10-05 21:26:14 +03:00
commit _id ,
body ,
event ,
comments ,
} ;
2020-12-07 14:10:46 +03:00
await octokit . pulls . createReview ( opts ) ;
2018-10-05 21:26:14 +03:00
} 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
}
2020-12-07 14:10:46 +03:00
async function main ( messages , owner , repo , pull _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-12-07 14:10:46 +03:00
const opts = {
owner ,
repo ,
pull _number ,
} ;
2018-10-05 21:26:14 +03:00
2020-12-07 14:10:46 +03:00
const { data : pull } = await octokit . pulls . get ( opts ) ;
const { data : files } = await octokit . pulls . listFiles ( opts ) ;
const comments = [ ] ;
const convertersUsed = [ ] ;
files
. filter ( file => messages [ file . filename ] )
. forEach ( file => {
// github api sometimes does not return a patch on large commits
if ( ! file . patch ) {
return ;
}
const lineMap = getLineMapFromPatch ( file . patch ) ;
messages [ file . filename ] . forEach ( message => {
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
let body = '**Code analysis results:**\n\n' ;
const uniqueconvertersUsed = [ ... new Set ( convertersUsed ) ] ;
uniqueconvertersUsed . forEach ( converter => {
body += '* ' + converterSummary [ converter ] + '\n' ;
} ) ;
await sendReview (
octokit ,
owner ,
repo ,
pull _number ,
pull . head . sha ,
body ,
comments ,
) ;
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
2020-12-07 14:10:46 +03:00
( async ( ) => {
await main ( messages , owner , repo , number ) ;
} ) ( ) ;
2015-12-10 02:29:00 +03:00
} ) ;