2018-09-18 00:09:02 +03:00
#!/usr/bin/env node
2020-03-20 23:28:31 +03:00
const { GitProcess } = require ( 'dugite' ) ;
const childProcess = require ( 'child_process' ) ;
const fs = require ( 'fs' ) ;
const klaw = require ( 'klaw' ) ;
const minimist = require ( 'minimist' ) ;
const path = require ( 'path' ) ;
2018-09-18 00:09:02 +03:00
2020-03-20 23:28:31 +03:00
const SOURCE _ROOT = path . normalize ( path . dirname ( _ _dirname ) ) ;
const DEPOT _TOOLS = path . resolve ( SOURCE _ROOT , '..' , 'third_party' , 'depot_tools' ) ;
2018-09-18 00:09:02 +03:00
2020-06-09 21:29:29 +03:00
const IGNORELIST = new Set ( [
2019-06-19 23:56:58 +03: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-14 02:24:28 +03: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 23:28:31 +03:00
] . map ( tokens => path . join ( SOURCE _ROOT , ... tokens ) ) ) ;
2018-09-18 00:09:02 +03:00
2020-10-19 22:08:13 +03:00
const IS _WINDOWS = process . platform === 'win32' ;
2018-09-18 00:09:02 +03:00
function spawnAndCheckExitCode ( cmd , args , opts ) {
2020-03-20 23:28:31 +03:00
opts = Object . assign ( { stdio : 'inherit' } , opts ) ;
const status = childProcess . spawnSync ( cmd , args , opts ) . status ;
if ( status ) process . exit ( status ) ;
2018-09-18 00:09:02 +03:00
}
2019-05-02 15:05:37 +03:00
function cpplint ( args ) {
2020-10-19 22:08:13 +03:00
const result = childProcess . spawnSync ( IS _WINDOWS ? 'cpplint.bat' : 'cpplint.py' , args , { encoding : 'utf8' , shell : true } ) ;
2019-05-02 15:05:37 +03: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 23:28:31 +03:00
console . warn ( line ) ;
2019-05-02 15:05:37 +03:00
}
}
}
2020-10-19 22:08:13 +03:00
if ( result . status !== 0 ) {
if ( result . error ) console . error ( result . error ) ;
process . exit ( result . status || 1 ) ;
2019-05-02 15:05:37 +03:00
}
}
2020-02-04 23:19:40 +03:00
function isObjCHeader ( filename ) {
2020-03-20 23:28:31 +03:00
return /\/(mac|cocoa)\// . test ( filename ) ;
2020-02-04 23:19:40 +03:00
}
2020-03-20 18:12:18 +03:00
const LINTERS = [ {
2018-09-18 00:09:02 +03:00
key : 'c++' ,
2019-12-05 12:46:34 +03:00
roots : [ 'shell' ] ,
2020-02-04 23:19:40 +03:00
test : filename => filename . endsWith ( '.cc' ) || ( filename . endsWith ( '.h' ) && ! isObjCHeader ( filename ) ) ,
2018-09-18 00:09:02 +03:00
run : ( opts , filenames ) => {
2018-10-16 08:59:45 +03:00
if ( opts . fix ) {
2020-03-20 23:28:31 +03:00
spawnAndCheckExitCode ( 'python' , [ 'script/run-clang-format.py' , '--fix' , ... filenames ] ) ;
2018-10-16 08:59:45 +03:00
} else {
2020-03-20 23:28:31 +03:00
spawnAndCheckExitCode ( 'python' , [ 'script/run-clang-format.py' , ... filenames ] ) ;
2018-10-16 08:59:45 +03:00
}
2020-03-20 23:28:31 +03:00
cpplint ( filenames ) ;
2019-05-02 15:05:37 +03:00
}
} , {
key : 'objc' ,
2019-06-19 23:56:58 +03:00
roots : [ 'shell' ] ,
2019-05-02 15:05:37 +03:00
test : filename => filename . endsWith ( '.mm' ) ,
run : ( opts , filenames ) => {
if ( opts . fix ) {
2020-03-20 23:28:31 +03:00
spawnAndCheckExitCode ( 'python' , [ 'script/run-clang-format.py' , '--fix' , ... filenames ] ) ;
2019-05-02 15:05:37 +03:00
} else {
2020-03-20 23:28:31 +03:00
spawnAndCheckExitCode ( 'python' , [ 'script/run-clang-format.py' , ... filenames ] ) ;
2018-09-19 16:42:03 +03:00
}
2019-05-02 15:05:37 +03:00
const filter = [
'-readability/casting' ,
'-whitespace/braces' ,
'-whitespace/indent' ,
'-whitespace/parens'
2020-03-20 23:28:31 +03:00
] ;
cpplint ( [ '--extensions=mm' , ` --filter= ${ filter . join ( ',' ) } ` , ... filenames ] ) ;
2018-09-18 00:09:02 +03:00
}
} , {
key : 'python' ,
roots : [ 'script' ] ,
test : filename => filename . endsWith ( '.py' ) ,
run : ( opts , filenames ) => {
2020-03-20 23:28:31 +03: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 ) ;
spawnAndCheckExitCode ( 'pylint.py' , args , { env } ) ;
2018-09-18 00:09:02 +03:00
}
} , {
key : 'javascript' ,
2020-10-22 01:43:52 +03:00
roots : [ 'build' , 'default_app' , 'lib' , 'npm' , 'script' , 'spec' , 'spec-main' ] ,
2019-11-01 23:37:02 +03:00
ignoreRoots : [ 'spec/node_modules' , 'spec-main/node_modules' ] ,
2019-02-06 21:27:20 +03:00
test : filename => filename . endsWith ( '.js' ) || filename . endsWith ( '.ts' ) ,
2018-09-18 00:09:02 +03:00
run : ( opts , filenames ) => {
2020-03-20 23:28:31 +03:00
const cmd = path . join ( SOURCE _ROOT , 'node_modules' , '.bin' , 'eslint' ) ;
2020-10-22 01:44:38 +03:00
const args = [ '--cache' , '--ext' , '.js,.ts' ] ;
2020-03-20 23:28:31 +03:00
if ( opts . fix ) args . unshift ( '--fix' ) ;
2020-10-22 01:44:38 +03:00
// Windows has a max command line length of 2047 characters, so we can't provide
// all of the filenames without going over that. To work around it, run eslint
// multiple times and chunk the filenames so that each run is under that limit.
// Use a much higher limit on other platforms which will effectively be a no-op.
const MAX _FILENAME _ARGS _LENGTH = IS _WINDOWS ? 1900 : 100 * 1024 ;
const cmdOpts = { stdio : 'inherit' , shell : IS _WINDOWS , cwd : SOURCE _ROOT } ;
if ( IS _WINDOWS ) {
// When running with shell spaces in filenames are problematic
filenames = filenames . map ( filename => ` " ${ filename } " ` ) ;
}
const chunkedFilenames = filenames . reduce ( ( chunkedFilenames , filename ) => {
const currentChunk = chunkedFilenames [ chunkedFilenames . length - 1 ] ;
const currentChunkLength = currentChunk . reduce ( ( totalLength , _filename ) => totalLength + _filename . length , 0 ) ;
if ( currentChunkLength + filename . length > MAX _FILENAME _ARGS _LENGTH ) {
chunkedFilenames . push ( [ filename ] ) ;
} else {
currentChunk . push ( filename ) ;
}
return chunkedFilenames ;
} , [ [ ] ] ) ;
const allOk = chunkedFilenames . map ( filenames => {
const result = childProcess . spawnSync ( cmd , [ ... args , ... filenames ] , cmdOpts ) ;
if ( result . error ) {
console . error ( result . error ) ;
process . exit ( result . status || 1 ) ;
}
return result . status === 0 ;
} ) . every ( x => x ) ;
if ( ! allOk ) {
process . exit ( 1 ) ;
}
2018-09-18 00:09:02 +03:00
}
2018-10-04 02:03:26 +03:00
} , {
key : 'gn' ,
roots : [ '.' ] ,
test : filename => filename . endsWith ( '.gn' ) || filename . endsWith ( '.gni' ) ,
run : ( opts , filenames ) => {
const allOk = filenames . map ( filename => {
2018-10-24 21:25:13 +03:00
const env = Object . assign ( {
CHROMIUM _BUILDTOOLS _PATH : path . resolve ( SOURCE _ROOT , '..' , 'buildtools' ) ,
DEPOT _TOOLS _WIN _TOOLCHAIN : '0'
2020-03-20 23:28:31 +03:00
} , process . env ) ;
2018-10-24 21:25:13 +03:00
// Users may not have depot_tools in PATH.
2020-03-20 23:28:31 +03: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-04 02:03:26 +03:00
if ( result . status === 0 ) {
2020-03-20 23:28:31 +03:00
return true ;
2018-10-04 02:03:26 +03:00
} else if ( result . status === 2 ) {
2020-03-20 23:28:31 +03:00
console . log ( ` GN format errors in " ${ filename } ". Run 'gn format " ${ filename } "' or rerun with --fix to fix them. ` ) ;
return false ;
2018-10-04 02:03:26 +03:00
} else {
2020-03-20 23:28:31 +03:00
console . log ( ` Error running 'gn format --dry-run " ${ filename } "': exit code ${ result . status } ` ) ;
return false ;
2018-10-04 02:03:26 +03:00
}
2020-03-20 23:28:31 +03:00
} ) . every ( x => x ) ;
2018-10-04 02:03:26 +03:00
if ( ! allOk ) {
2020-03-20 23:28:31 +03:00
process . exit ( 1 ) ;
2018-10-04 02:03:26 +03:00
}
}
2019-06-19 20:48:15 +03:00
} , {
key : 'patches' ,
roots : [ 'patches' ] ,
test : ( ) => true ,
2019-11-04 22:04:18 +03:00
run : ( opts , filenames ) => {
2020-03-20 23:28:31 +03:00
const patchesDir = path . resolve ( _ _dirname , '../patches' ) ;
2019-06-19 20:48:15 +03:00
for ( const patchTarget of fs . readdirSync ( patchesDir ) ) {
2020-03-20 23:28:31 +03:00
const targetDir = path . resolve ( patchesDir , patchTarget ) ;
2019-06-19 20:48:15 +03:00
// If the config does not exist that is OK, we just skip this dir
2020-03-20 23:28:31 +03:00
const targetConfig = path . resolve ( targetDir , 'config.json' ) ;
if ( ! fs . existsSync ( targetConfig ) ) continue ;
2019-06-19 20:48:15 +03:00
2020-03-20 23:28:31 +03:00
const config = JSON . parse ( fs . readFileSync ( targetConfig , 'utf8' ) ) ;
2019-06-19 20:48:15 +03:00
for ( const key of Object . keys ( config ) ) {
// The directory the config points to should exist
2020-03-20 23:28:31 +03:00
const targetPatchesDir = path . resolve ( _ _dirname , '../../..' , key ) ;
if ( ! fs . existsSync ( targetPatchesDir ) ) throw new Error ( ` target patch directory: " ${ targetPatchesDir } " does not exist ` ) ;
2019-06-19 20:48:15 +03:00
// We need a .patches file
2020-03-20 23:28:31 +03:00
const dotPatchesPath = path . resolve ( targetPatchesDir , '.patches' ) ;
if ( ! fs . existsSync ( dotPatchesPath ) ) throw new Error ( ` .patches file: " ${ dotPatchesPath } " does not exist ` ) ;
2019-06-19 20:48:15 +03:00
// Read the patch list
2020-03-20 23:28:31 +03:00
const patchFileList = fs . readFileSync ( dotPatchesPath , 'utf8' ) . trim ( ) . split ( '\n' ) ;
const patchFileSet = new Set ( patchFileList ) ;
2019-06-19 20:48:15 +03:00
patchFileList . reduce ( ( seen , file ) => {
if ( seen . has ( file ) ) {
2020-03-20 23:28:31 +03:00
throw new Error ( ` ' ${ file } ' is listed in ${ dotPatchesPath } more than once ` ) ;
2019-06-19 20:48:15 +03:00
}
2020-03-20 23:28:31 +03: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' ) ;
2019-06-19 20:48:15 +03:00
for ( const file of fs . readdirSync ( targetPatchesDir ) ) {
// Ignore the .patches file and READMEs
2020-03-20 23:28:31 +03:00
if ( file === '.patches' || file === 'README.md' ) continue ;
2019-06-19 20:48:15 +03:00
if ( ! patchFileSet . has ( file ) ) {
2020-03-20 23:28:31 +03:00
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 20:48:15 +03:00
}
2020-03-20 23:28:31 +03:00
patchFileSet . delete ( file ) ;
2019-06-19 20:48:15 +03:00
}
// If anything is left in this set, it means it did not exist on disk
if ( patchFileSet . size > 0 ) {
2020-03-20 23:28:31 +03:00
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 20:48:15 +03:00
}
}
}
2019-11-04 22:04:18 +03:00
2020-03-20 23:28:31 +03:00
let ok = true ;
2019-11-04 22:04:18 +03:00
filenames . filter ( f => f . endsWith ( '.patch' ) ) . forEach ( f => {
2020-03-20 23:28:31 +03:00
const patchText = fs . readFileSync ( f , 'utf8' ) ;
2019-12-13 20:18:45 +03:00
if ( /^Subject: .*$\s+^diff/m . test ( patchText ) ) {
2020-03-20 23:28:31 +03: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. ` ) ;
ok = false ;
2019-11-04 22:04:18 +03:00
}
2020-10-20 04:40:58 +03:00
const trailingWhitespace = patchText . split ( '\n' ) . filter ( line => line . startsWith ( '+' ) ) . some ( line => / \ s + $ / . test ( line ) ) ;
if ( trailingWhitespace ) {
console . warn ( ` Patch file ' ${ f } ' has trailing whitespace on some lines. ` ) ;
ok = false ;
}
2020-03-20 23:28:31 +03:00
} ) ;
2019-11-04 22:04:18 +03:00
if ( ! ok ) {
2020-03-20 23:28:31 +03:00
process . exit ( 1 ) ;
2019-11-04 22:04:18 +03:00
}
2019-06-19 20:48:15 +03:00
}
2020-03-20 23:28:31 +03:00
} ] ;
2018-09-18 00:09:02 +03:00
function parseCommandLine ( ) {
2020-03-20 23:28:31 +03:00
let help ;
2018-09-18 00:09:02 +03:00
const opts = minimist ( process . argv . slice ( 2 ) , {
2020-03-20 18:12:18 +03:00
boolean : [ 'c++' , 'objc' , 'javascript' , 'python' , 'gn' , 'patches' , 'help' , 'changed' , 'fix' , 'verbose' , 'only' ] ,
2018-09-18 00:09:02 +03:00
alias : { 'c++' : [ 'cc' , 'cpp' , 'cxx' ] , javascript : [ 'js' , 'es' ] , python : 'py' , changed : 'c' , help : 'h' , verbose : 'v' } ,
2020-03-20 23:28:31 +03:00
unknown : arg => { help = true ; }
} ) ;
2018-09-18 00:09:02 +03:00
if ( help || opts . help ) {
2020-03-20 23:28:31 +03: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-18 00:09:02 +03:00
}
2020-03-20 23:28:31 +03:00
return opts ;
2018-09-18 00:09:02 +03:00
}
async function findChangedFiles ( top ) {
2020-03-20 23:28:31 +03:00
const result = await GitProcess . exec ( [ 'diff' , '--name-only' , '--cached' ] , top ) ;
2018-09-18 00:09:02 +03:00
if ( result . exitCode !== 0 ) {
2020-03-20 23:28:31 +03:00
console . log ( 'Failed to find changed files' , GitProcess . parseError ( result . stderr ) ) ;
process . exit ( 1 ) ;
2018-09-18 00:09:02 +03:00
}
2020-03-20 23:28:31 +03: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-18 00:09:02 +03:00
}
async function findMatchingFiles ( top , test ) {
return new Promise ( ( resolve , reject ) => {
2020-03-20 23:28:31 +03:00
const matches = [ ] ;
2019-04-30 23:59:47 +03:00
klaw ( top , {
filter : f => path . basename ( f ) !== '.bin'
} )
2018-09-18 00:09:02 +03:00
. on ( 'end' , ( ) => resolve ( matches ) )
. on ( 'data' , item => {
if ( test ( item . path ) ) {
2020-03-20 23:28:31 +03:00
matches . push ( item . path ) ;
2018-09-18 00:09:02 +03:00
}
2020-03-20 23:28:31 +03:00
} ) ;
} ) ;
2018-09-18 00:09:02 +03:00
}
async function findFiles ( args , linter ) {
2020-03-20 23:28:31 +03:00
let filenames = [ ] ;
2020-06-09 21:29:29 +03:00
let includelist = null ;
2018-09-18 00:09:02 +03:00
2020-06-09 21:29:29 +03:00
// build the includelist
2018-09-18 00:09:02 +03:00
if ( args . changed ) {
2020-06-09 21:29:29 +03:00
includelist = await findChangedFiles ( SOURCE _ROOT ) ;
if ( ! includelist . size ) {
2020-03-20 23:28:31 +03:00
return [ ] ;
2018-09-18 00:09:02 +03:00
}
2019-01-22 01:46:32 +03:00
} else if ( args . only ) {
2020-06-09 21:29:29 +03:00
includelist = new Set ( args . _ . map ( p => path . resolve ( p ) ) ) ;
2018-09-18 00:09:02 +03:00
}
// accumulate the raw list of files
for ( const root of linter . roots ) {
2020-03-20 23:28:31 +03:00
const files = await findMatchingFiles ( path . join ( SOURCE _ROOT , root ) , linter . test ) ;
filenames . push ( ... files ) ;
2018-09-18 00:09:02 +03:00
}
2018-09-20 08:41:01 +03:00
for ( const ignoreRoot of ( linter . ignoreRoots ) || [ ] ) {
2020-03-20 23:28:31 +03:00
const ignorePath = path . join ( SOURCE _ROOT , ignoreRoot ) ;
if ( ! fs . existsSync ( ignorePath ) ) continue ;
2018-09-20 08:41:01 +03:00
2020-03-20 23:28:31 +03:00
const ignoreFiles = new Set ( await findMatchingFiles ( ignorePath , linter . test ) ) ;
filenames = filenames . filter ( fileName => ! ignoreFiles . has ( fileName ) ) ;
2018-09-20 08:41:01 +03:00
}
2020-06-09 21:29:29 +03:00
// remove ignored files
filenames = filenames . filter ( x => ! IGNORELIST . has ( x ) ) ;
2018-09-18 00:09:02 +03:00
2020-06-09 21:29:29 +03:00
// if a includelist exists, remove anything not in it
if ( includelist ) {
filenames = filenames . filter ( x => includelist . has ( x ) ) ;
2018-09-18 00:09:02 +03:00
}
2018-10-16 08:59:45 +03: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 23:28:31 +03:00
return filenames . map ( x => path . relative ( SOURCE _ROOT , x ) ) ;
2018-09-18 00:09:02 +03:00
}
async function main ( ) {
2020-03-20 23:28:31 +03:00
const opts = parseCommandLine ( ) ;
2018-09-18 00:09:02 +03:00
// no mode specified? run 'em all
2020-01-29 20:03:53 +03:00
if ( ! opts [ 'c++' ] && ! opts . javascript && ! opts . objc && ! opts . python && ! opts . gn && ! opts . patches ) {
2020-03-20 23:28:31 +03:00
opts [ 'c++' ] = opts . javascript = opts . objc = opts . python = opts . gn = opts . patches = true ;
2018-09-18 00:09:02 +03:00
}
2020-03-20 23:28:31 +03:00
const linters = LINTERS . filter ( x => opts [ x . key ] ) ;
2018-09-18 00:09:02 +03:00
for ( const linter of linters ) {
2020-03-20 23:28:31 +03:00
const filenames = await findFiles ( opts , linter ) ;
2018-09-18 00:09:02 +03:00
if ( filenames . length ) {
2020-03-20 23:28:31 +03:00
if ( opts . verbose ) { console . log ( ` linting ${ filenames . length } ${ linter . key } ${ filenames . length === 1 ? 'file' : 'files' } ` ) ; }
linter . run ( opts , filenames ) ;
2018-09-18 00:09:02 +03:00
}
}
}
if ( process . mainModule === module ) {
2018-09-20 08:41:01 +03:00
main ( ) . catch ( ( error ) => {
2020-03-20 23:28:31 +03:00
console . error ( error ) ;
process . exit ( 1 ) ;
} ) ;
2018-09-18 00:09:02 +03:00
}