2018-09-17 21:09:02 +00:00
#!/usr/bin/env node
2020-12-10 18:57:06 +00:00
const crypto = require ( 'crypto' ) ;
2020-03-20 20:28:31 +00:00
const { GitProcess } = require ( 'dugite' ) ;
const childProcess = require ( 'child_process' ) ;
2020-12-01 01:47:29 +00:00
const { ESLint } = require ( 'eslint' ) ;
2020-03-20 20:28:31 +00:00
const fs = require ( 'fs' ) ;
const klaw = require ( 'klaw' ) ;
const minimist = require ( 'minimist' ) ;
const path = require ( 'path' ) ;
2018-09-17 21:09:02 +00:00
2020-03-20 20:28:31 +00:00
const SOURCE _ROOT = path . normalize ( path . dirname ( _ _dirname ) ) ;
const DEPOT _TOOLS = path . resolve ( SOURCE _ROOT , '..' , 'third_party' , 'depot_tools' ) ;
2018-09-17 21:09:02 +00:00
2020-06-09 18:29:29 +00:00
const IGNORELIST = new Set ( [
2019-06-19 20:56:58 +00:00
[ 'shell' , 'browser' , 'resources' , 'win' , 'resource.h' ] ,
[ 'shell' , 'browser' , 'notifications' , 'mac' , 'notification_center_delegate.h' ] ,
[ 'shell' , 'browser' , 'ui' , 'cocoa' , 'event_dispatching_window.h' ] ,
[ 'shell' , 'browser' , 'ui' , 'cocoa' , 'NSColor+Hex.h' ] ,
[ 'shell' , 'browser' , 'ui' , 'cocoa' , 'NSString+ANSI.h' ] ,
[ 'shell' , 'common' , 'node_includes.h' ] ,
2019-02-13 23:24:28 +00:00
[ 'spec' , 'static' , 'jquery-2.0.3.min.js' ] ,
[ 'spec' , 'ts-smoke' , 'electron' , 'main.ts' ] ,
[ 'spec' , 'ts-smoke' , 'electron' , 'renderer.ts' ] ,
[ 'spec' , 'ts-smoke' , 'runner.js' ]
2020-03-20 20:28:31 +00:00
] . map ( tokens => path . join ( SOURCE _ROOT , ... tokens ) ) ) ;
2018-09-17 21:09:02 +00:00
2020-10-19 19:08:13 +00:00
const IS _WINDOWS = process . platform === 'win32' ;
2018-09-17 21:09:02 +00:00
function spawnAndCheckExitCode ( cmd , args , opts ) {
2020-03-20 20:28:31 +00:00
opts = Object . assign ( { stdio : 'inherit' } , opts ) ;
2021-09-29 17:10:13 +00:00
const { error , status , signal } = childProcess . spawnSync ( cmd , args , opts ) ;
if ( error ) {
// the subsprocess failed or timed out
console . error ( error ) ;
process . exit ( 1 ) ;
}
if ( status === null ) {
// the subprocess terminated due to a signal
console . error ( signal ) ;
process . exit ( 1 ) ;
}
if ( status !== 0 ) {
// `status` is an exit code
process . exit ( status ) ;
}
2018-09-17 21:09:02 +00:00
}
2019-05-02 12:05:37 +00:00
function cpplint ( args ) {
2021-09-29 21:44:41 +00:00
args . unshift ( ` --project_root= ${ SOURCE _ROOT } ` ) ;
2020-10-19 19:08:13 +00:00
const result = childProcess . spawnSync ( IS _WINDOWS ? 'cpplint.bat' : 'cpplint.py' , args , { encoding : 'utf8' , shell : true } ) ;
2019-05-02 12:05:37 +00:00
// cpplint.py writes EVERYTHING to stderr, including status messages
if ( result . stderr ) {
for ( const line of result . stderr . split ( /[\r\n]+/ ) ) {
if ( line . length && ! line . startsWith ( 'Done processing ' ) && line !== 'Total errors found: 0' ) {
2020-03-20 20:28:31 +00:00
console . warn ( line ) ;
2019-05-02 12:05:37 +00:00
}
}
}
2020-10-19 19:08:13 +00:00
if ( result . status !== 0 ) {
if ( result . error ) console . error ( result . error ) ;
process . exit ( result . status || 1 ) ;
2019-05-02 12:05:37 +00:00
}
}
2020-02-04 20:19:40 +00:00
function isObjCHeader ( filename ) {
2020-03-20 20:28:31 +00:00
return /\/(mac|cocoa)\// . test ( filename ) ;
2020-02-04 20:19:40 +00:00
}
2020-03-20 15:12:18 +00:00
const LINTERS = [ {
2018-09-17 21:09:02 +00:00
key : 'c++' ,
2019-12-05 09:46:34 +00:00
roots : [ 'shell' ] ,
2020-02-04 20:19:40 +00:00
test : filename => filename . endsWith ( '.cc' ) || ( filename . endsWith ( '.h' ) && ! isObjCHeader ( filename ) ) ,
2018-09-17 21:09:02 +00:00
run : ( opts , filenames ) => {
2018-10-16 05:59:45 +00:00
if ( opts . fix ) {
2020-03-20 20:28:31 +00:00
spawnAndCheckExitCode ( 'python' , [ 'script/run-clang-format.py' , '--fix' , ... filenames ] ) ;
2018-10-16 05:59:45 +00:00
} else {
2020-03-20 20:28:31 +00:00
spawnAndCheckExitCode ( 'python' , [ 'script/run-clang-format.py' , ... filenames ] ) ;
2018-10-16 05:59:45 +00:00
}
2020-03-20 20:28:31 +00:00
cpplint ( filenames ) ;
2019-05-02 12:05:37 +00:00
}
} , {
key : 'objc' ,
2019-06-19 20:56:58 +00:00
roots : [ 'shell' ] ,
2019-05-02 12:05:37 +00:00
test : filename => filename . endsWith ( '.mm' ) ,
run : ( opts , filenames ) => {
if ( opts . fix ) {
2020-03-20 20:28:31 +00:00
spawnAndCheckExitCode ( 'python' , [ 'script/run-clang-format.py' , '--fix' , ... filenames ] ) ;
2019-05-02 12:05:37 +00:00
} else {
2020-03-20 20:28:31 +00:00
spawnAndCheckExitCode ( 'python' , [ 'script/run-clang-format.py' , ... filenames ] ) ;
2018-09-19 13:42:03 +00:00
}
2019-05-02 12:05:37 +00:00
const filter = [
'-readability/casting' ,
'-whitespace/braces' ,
'-whitespace/indent' ,
'-whitespace/parens'
2020-03-20 20:28:31 +00:00
] ;
cpplint ( [ '--extensions=mm' , ` --filter= ${ filter . join ( ',' ) } ` , ... filenames ] ) ;
2018-09-17 21:09:02 +00:00
}
} , {
key : 'python' ,
roots : [ 'script' ] ,
test : filename => filename . endsWith ( '.py' ) ,
run : ( opts , filenames ) => {
2020-03-20 20:28:31 +00:00
const rcfile = path . join ( DEPOT _TOOLS , 'pylintrc' ) ;
const args = [ '--rcfile=' + rcfile , ... filenames ] ;
const env = Object . assign ( { PYTHONPATH : path . join ( SOURCE _ROOT , 'script' ) } , process . env ) ;
2021-09-29 17:10:13 +00:00
spawnAndCheckExitCode ( 'pylint' , args , { env } ) ;
2018-09-17 21:09:02 +00:00
}
} , {
key : 'javascript' ,
2020-10-21 22:43:52 +00:00
roots : [ 'build' , 'default_app' , 'lib' , 'npm' , 'script' , 'spec' , 'spec-main' ] ,
2019-11-01 20:37:02 +00:00
ignoreRoots : [ 'spec/node_modules' , 'spec-main/node_modules' ] ,
2019-02-06 18:27:20 +00:00
test : filename => filename . endsWith ( '.js' ) || filename . endsWith ( '.ts' ) ,
2020-12-01 01:47:29 +00:00
run : async ( opts , filenames ) => {
const eslint = new ESLint ( {
2020-12-10 18:57:06 +00:00
// Do not use the lint cache on CI builds
cache : ! process . env . CI ,
cacheLocation : ` node_modules/.eslintcache. ${ crypto . createHash ( 'md5' ) . update ( fs . readFileSync ( _ _filename ) ) . digest ( 'hex' ) } ` ,
2020-12-01 01:47:29 +00:00
extensions : [ '.js' , '.ts' ] ,
fix : opts . fix
} ) ;
const formatter = await eslint . loadFormatter ( ) ;
let successCount = 0 ;
2020-12-10 18:57:06 +00:00
const results = await eslint . lintFiles ( filenames ) ;
for ( const result of results ) {
2020-12-01 01:47:29 +00:00
successCount += result . errorCount === 0 ? 1 : 0 ;
2020-12-10 18:57:06 +00:00
if ( opts . verbose && result . errorCount === 0 && result . warningCount === 0 ) {
2020-12-01 01:47:29 +00:00
console . log ( ` ${ result . filePath } : no errors or warnings ` ) ;
2020-10-21 22:44:38 +00:00
}
2020-12-01 01:47:29 +00:00
}
2020-12-10 18:57:06 +00:00
console . log ( formatter . format ( results ) ) ;
if ( opts . fix ) {
await ESLint . outputFixes ( results ) ;
}
2020-12-01 01:47:29 +00:00
if ( successCount !== filenames . length ) {
console . error ( 'Linting had errors' ) ;
2020-10-21 22:44:38 +00:00
process . exit ( 1 ) ;
}
2018-09-17 21:09:02 +00:00
}
2018-10-03 23:03:26 +00:00
} , {
key : 'gn' ,
roots : [ '.' ] ,
test : filename => filename . endsWith ( '.gn' ) || filename . endsWith ( '.gni' ) ,
run : ( opts , filenames ) => {
const allOk = filenames . map ( filename => {
2018-10-24 18:25:13 +00:00
const env = Object . assign ( {
CHROMIUM _BUILDTOOLS _PATH : path . resolve ( SOURCE _ROOT , '..' , 'buildtools' ) ,
DEPOT _TOOLS _WIN _TOOLCHAIN : '0'
2020-03-20 20:28:31 +00:00
} , process . env ) ;
2018-10-24 18:25:13 +00:00
// Users may not have depot_tools in PATH.
2020-03-20 20:28:31 +00:00
env . PATH = ` ${ env . PATH } ${ path . delimiter } ${ DEPOT _TOOLS } ` ;
const args = [ 'format' , filename ] ;
if ( ! opts . fix ) args . push ( '--dry-run' ) ;
const result = childProcess . spawnSync ( 'gn' , args , { env , stdio : 'inherit' , shell : true } ) ;
2018-10-03 23:03:26 +00:00
if ( result . status === 0 ) {
2020-03-20 20:28:31 +00:00
return true ;
2018-10-03 23:03:26 +00:00
} else if ( result . status === 2 ) {
2020-03-20 20:28:31 +00:00
console . log ( ` GN format errors in " ${ filename } ". Run 'gn format " ${ filename } "' or rerun with --fix to fix them. ` ) ;
return false ;
2018-10-03 23:03:26 +00:00
} else {
2020-03-20 20:28:31 +00:00
console . log ( ` Error running 'gn format --dry-run " ${ filename } "': exit code ${ result . status } ` ) ;
return false ;
2018-10-03 23:03:26 +00:00
}
2020-03-20 20:28:31 +00:00
} ) . every ( x => x ) ;
2018-10-03 23:03:26 +00:00
if ( ! allOk ) {
2020-03-20 20:28:31 +00:00
process . exit ( 1 ) ;
2018-10-03 23:03:26 +00:00
}
}
2019-06-19 17:48:15 +00:00
} , {
key : 'patches' ,
roots : [ 'patches' ] ,
2020-11-30 07:49:01 +00:00
test : filename => filename . endsWith ( '.patch' ) ,
2019-11-04 19:04:18 +00:00
run : ( opts , filenames ) => {
2020-03-20 20:28:31 +00:00
const patchesDir = path . resolve ( _ _dirname , '../patches' ) ;
2020-11-30 07:49:01 +00:00
const patchesConfig = path . resolve ( patchesDir , 'config.json' ) ;
// If the config does not exist, that's a proiblem
if ( ! fs . existsSync ( patchesConfig ) ) {
process . exit ( 1 ) ;
}
2019-06-19 17:48:15 +00:00
2020-11-30 07:49:01 +00:00
const config = JSON . parse ( fs . readFileSync ( patchesConfig , 'utf8' ) ) ;
for ( const key of Object . keys ( config ) ) {
// The directory the config points to should exist
const targetPatchesDir = path . resolve ( _ _dirname , '../../..' , key ) ;
if ( ! fs . existsSync ( targetPatchesDir ) ) throw new Error ( ` target patch directory: " ${ targetPatchesDir } " does not exist ` ) ;
// We need a .patches file
const dotPatchesPath = path . resolve ( targetPatchesDir , '.patches' ) ;
if ( ! fs . existsSync ( dotPatchesPath ) ) throw new Error ( ` .patches file: " ${ dotPatchesPath } " does not exist ` ) ;
2019-06-19 17:48:15 +00:00
2020-11-30 07:49:01 +00:00
// Read the patch list
const patchFileList = fs . readFileSync ( dotPatchesPath , 'utf8' ) . trim ( ) . split ( '\n' ) ;
const patchFileSet = new Set ( patchFileList ) ;
patchFileList . reduce ( ( seen , file ) => {
if ( seen . has ( file ) ) {
throw new Error ( ` ' ${ file } ' is listed in ${ dotPatchesPath } more than once ` ) ;
2019-06-19 17:48:15 +00:00
}
2020-11-30 07:49:01 +00:00
return seen . add ( file ) ;
} , new Set ( ) ) ;
if ( patchFileList . length !== patchFileSet . size ) throw new Error ( 'each patch file should only be in the .patches file once' ) ;
for ( const file of fs . readdirSync ( targetPatchesDir ) ) {
// Ignore the .patches file and READMEs
if ( file === '.patches' || file === 'README.md' ) continue ;
2019-06-19 17:48:15 +00:00
2020-11-30 07:49:01 +00:00
if ( ! patchFileSet . has ( file ) ) {
throw new Error ( ` Expected the .patches file at " ${ dotPatchesPath } " to contain a patch file (" ${ file } ") present in the directory but it did not ` ) ;
2019-06-19 17:48:15 +00:00
}
2020-11-30 07:49:01 +00:00
patchFileSet . delete ( file ) ;
}
// If anything is left in this set, it means it did not exist on disk
if ( patchFileSet . size > 0 ) {
throw new Error ( ` Expected all the patch files listed in the .patches file at " ${ dotPatchesPath } " to exist but some did not: \n ${ JSON . stringify ( [ ... patchFileSet . values ( ) ] , null , 2 ) } ` ) ;
2019-06-19 17:48:15 +00:00
}
}
2019-11-04 19:04:18 +00:00
2020-11-30 07:49:01 +00:00
const allOk = filenames . length > 0 && filenames . map ( f => {
2020-03-20 20:28:31 +00:00
const patchText = fs . readFileSync ( f , 'utf8' ) ;
2020-11-30 07:49:01 +00:00
const subjectAndDescription = / S u b j e c t : ( . * ? ) \ n \ n ( [ \ s \ S ] * ? ) \ s * ( ? = d i f f ) / m s . e x e c ( p a t c h T e x t ) ;
if ( ! subjectAndDescription [ 2 ] ) {
2020-03-20 20:28:31 +00:00
console . warn ( ` Patch file ' ${ f } ' has no description. Every patch must contain a justification for why the patch exists and the plan for its removal. ` ) ;
2020-11-30 07:49:01 +00:00
return false ;
2019-11-04 19:04:18 +00:00
}
2021-04-15 17:43:35 +00:00
const trailingWhitespaceLines = patchText . split ( /\r?\n/ ) . map ( ( line , index ) => [ line , index ] ) . filter ( ( [ line ] ) => line . startsWith ( '+' ) && /\s+$/ . test ( line ) ) . map ( ( [ , lineNumber ] ) => lineNumber + 1 ) ;
if ( trailingWhitespaceLines . length > 0 ) {
console . warn ( ` Patch file ' ${ f } ' has trailing whitespace on some lines ( ${ trailingWhitespaceLines . join ( ',' ) } ). ` ) ;
2020-11-30 07:49:01 +00:00
return false ;
2020-10-20 01:40:58 +00:00
}
2020-11-30 07:49:01 +00:00
return true ;
} ) . every ( x => x ) ;
if ( ! allOk ) {
2020-03-20 20:28:31 +00:00
process . exit ( 1 ) ;
2019-11-04 19:04:18 +00:00
}
2019-06-19 17:48:15 +00:00
}
2020-03-20 20:28:31 +00:00
} ] ;
2018-09-17 21:09:02 +00:00
function parseCommandLine ( ) {
2020-03-20 20:28:31 +00:00
let help ;
2018-09-17 21:09:02 +00:00
const opts = minimist ( process . argv . slice ( 2 ) , {
2020-03-20 15:12:18 +00:00
boolean : [ 'c++' , 'objc' , 'javascript' , 'python' , 'gn' , 'patches' , 'help' , 'changed' , 'fix' , 'verbose' , 'only' ] ,
2018-09-17 21:09:02 +00:00
alias : { 'c++' : [ 'cc' , 'cpp' , 'cxx' ] , javascript : [ 'js' , 'es' ] , python : 'py' , changed : 'c' , help : 'h' , verbose : 'v' } ,
2020-03-20 20:28:31 +00:00
unknown : arg => { help = true ; }
} ) ;
2018-09-17 21:09:02 +00:00
if ( help || opts . help ) {
2020-03-20 20:28:31 +00:00
console . log ( 'Usage: script/lint.js [--cc] [--js] [--py] [-c|--changed] [-h|--help] [-v|--verbose] [--fix] [--only -- file1 file2]' ) ;
process . exit ( 0 ) ;
2018-09-17 21:09:02 +00:00
}
2020-03-20 20:28:31 +00:00
return opts ;
2018-09-17 21:09:02 +00:00
}
async function findChangedFiles ( top ) {
2020-03-20 20:28:31 +00:00
const result = await GitProcess . exec ( [ 'diff' , '--name-only' , '--cached' ] , top ) ;
2018-09-17 21:09:02 +00:00
if ( result . exitCode !== 0 ) {
2020-03-20 20:28:31 +00:00
console . log ( 'Failed to find changed files' , GitProcess . parseError ( result . stderr ) ) ;
process . exit ( 1 ) ;
2018-09-17 21:09:02 +00:00
}
2020-03-20 20:28:31 +00:00
const relativePaths = result . stdout . split ( /\r\n|\r|\n/g ) ;
const absolutePaths = relativePaths . map ( x => path . join ( top , x ) ) ;
return new Set ( absolutePaths ) ;
2018-09-17 21:09:02 +00:00
}
async function findMatchingFiles ( top , test ) {
return new Promise ( ( resolve , reject ) => {
2020-03-20 20:28:31 +00:00
const matches = [ ] ;
2019-04-30 20:59:47 +00:00
klaw ( top , {
filter : f => path . basename ( f ) !== '.bin'
} )
2018-09-17 21:09:02 +00:00
. on ( 'end' , ( ) => resolve ( matches ) )
. on ( 'data' , item => {
if ( test ( item . path ) ) {
2020-03-20 20:28:31 +00:00
matches . push ( item . path ) ;
2018-09-17 21:09:02 +00:00
}
2020-03-20 20:28:31 +00:00
} ) ;
} ) ;
2018-09-17 21:09:02 +00:00
}
async function findFiles ( args , linter ) {
2020-03-20 20:28:31 +00:00
let filenames = [ ] ;
2020-06-09 18:29:29 +00:00
let includelist = null ;
2018-09-17 21:09:02 +00:00
2020-06-09 18:29:29 +00:00
// build the includelist
2018-09-17 21:09:02 +00:00
if ( args . changed ) {
2020-06-09 18:29:29 +00:00
includelist = await findChangedFiles ( SOURCE _ROOT ) ;
if ( ! includelist . size ) {
2020-03-20 20:28:31 +00:00
return [ ] ;
2018-09-17 21:09:02 +00:00
}
2019-01-21 22:46:32 +00:00
} else if ( args . only ) {
2020-06-09 18:29:29 +00:00
includelist = new Set ( args . _ . map ( p => path . resolve ( p ) ) ) ;
2018-09-17 21:09:02 +00:00
}
// accumulate the raw list of files
for ( const root of linter . roots ) {
2020-03-20 20:28:31 +00:00
const files = await findMatchingFiles ( path . join ( SOURCE _ROOT , root ) , linter . test ) ;
filenames . push ( ... files ) ;
2018-09-17 21:09:02 +00:00
}
2018-09-20 05:41:01 +00:00
for ( const ignoreRoot of ( linter . ignoreRoots ) || [ ] ) {
2020-03-20 20:28:31 +00:00
const ignorePath = path . join ( SOURCE _ROOT , ignoreRoot ) ;
if ( ! fs . existsSync ( ignorePath ) ) continue ;
2018-09-20 05:41:01 +00:00
2020-03-20 20:28:31 +00:00
const ignoreFiles = new Set ( await findMatchingFiles ( ignorePath , linter . test ) ) ;
filenames = filenames . filter ( fileName => ! ignoreFiles . has ( fileName ) ) ;
2018-09-20 05:41:01 +00:00
}
2020-06-09 18:29:29 +00:00
// remove ignored files
filenames = filenames . filter ( x => ! IGNORELIST . has ( x ) ) ;
2018-09-17 21:09:02 +00:00
2020-06-09 18:29:29 +00:00
// if a includelist exists, remove anything not in it
if ( includelist ) {
filenames = filenames . filter ( x => includelist . has ( x ) ) ;
2018-09-17 21:09:02 +00:00
}
2018-10-16 05:59:45 +00:00
// it's important that filenames be relative otherwise clang-format will
// produce patches with absolute paths in them, which `git apply` will refuse
// to apply.
2020-03-20 20:28:31 +00:00
return filenames . map ( x => path . relative ( SOURCE _ROOT , x ) ) ;
2018-09-17 21:09:02 +00:00
}
async function main ( ) {
2020-03-20 20:28:31 +00:00
const opts = parseCommandLine ( ) ;
2018-09-17 21:09:02 +00:00
// no mode specified? run 'em all
2020-01-29 17:03:53 +00:00
if ( ! opts [ 'c++' ] && ! opts . javascript && ! opts . objc && ! opts . python && ! opts . gn && ! opts . patches ) {
2020-03-20 20:28:31 +00:00
opts [ 'c++' ] = opts . javascript = opts . objc = opts . python = opts . gn = opts . patches = true ;
2018-09-17 21:09:02 +00:00
}
2020-03-20 20:28:31 +00:00
const linters = LINTERS . filter ( x => opts [ x . key ] ) ;
2018-09-17 21:09:02 +00:00
for ( const linter of linters ) {
2020-03-20 20:28:31 +00:00
const filenames = await findFiles ( opts , linter ) ;
2018-09-17 21:09:02 +00:00
if ( filenames . length ) {
2020-03-20 20:28:31 +00:00
if ( opts . verbose ) { console . log ( ` linting ${ filenames . length } ${ linter . key } ${ filenames . length === 1 ? 'file' : 'files' } ` ) ; }
2020-12-01 01:47:29 +00:00
await linter . run ( opts , filenames ) ;
2018-09-17 21:09:02 +00:00
}
}
}
if ( process . mainModule === module ) {
2018-09-20 05:41:01 +00:00
main ( ) . catch ( ( error ) => {
2020-03-20 20:28:31 +00:00
console . error ( error ) ;
process . exit ( 1 ) ;
} ) ;
2018-09-17 21:09:02 +00:00
}