Created
December 20, 2017 17:58
-
-
Save rgoldfinger/305f1a6c59118be1c5e9ab6e6edb0569 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
const fs = require('mz/fs'); | |
const markdownTable = require('markdown-table'); | |
const { spawnSync } = require('child_process'); | |
const { memoize } = require('lodash'); | |
const BASE = '.'; | |
const audits = new Map(); | |
const blacklistDirs = [ | |
'.git', | |
'build', | |
'docs', | |
'flow-typed', | |
'node_modules', | |
'public', | |
'reports', | |
'selenium_screenshots', | |
]; | |
const eslintRules = ['react/no-array-index-key', 'prefer-promise-reject-errors']; | |
const getOwnerForDirectory = memoize(path => { | |
const ownersPath = `${path}/OWNERS.txt`; | |
return fs.existsSync(ownersPath) && fs.readFileSync(ownersPath, 'utf8').replace(/\s/g, ''); | |
}); | |
const findOwnedDirs = memoize(path => { | |
const files = fs.readdirSync(path) || []; | |
const dirs = files.filter(f => fs.statSync(`${path}/${f}`).isDirectory()); | |
return dirs.reduce((acc, dir) => { | |
// either the subdir has an owners.txt or its children do | |
const fullDirPath = `${path}/${dir}`; | |
if (getOwnerForDirectory(fullDirPath)) { | |
acc.push(fullDirPath); | |
return acc; | |
} else { | |
acc.concat(findOwnedDirs(fullDirPath)); | |
return acc; | |
} | |
}, []); | |
}); | |
function writeAudit() { | |
const headers = [ | |
'path', | |
'team', | |
'file count', | |
'mixin count', | |
'createClass', | |
'@noflow', | |
'FlowFixMeProps', | |
'sync set state', | |
...eslintRules, | |
]; | |
const rows = []; | |
const ownerCounts = {}; | |
audits.forEach((audit, path) => { | |
const owner = getOwnerForDirectory(path); | |
const rulesAudit = eslintRules.map(rule => audit[rule]); | |
const pathCounts = [ | |
audit.fileCount, | |
audit.mixinCount, | |
audit.createClass, | |
audit.noflow, | |
audit.fixMeProps, | |
audit.syncSetState, | |
...rulesAudit, | |
]; | |
if (ownerCounts[owner]) { | |
for (let i = 0; i < pathCounts.length; i += 1) { | |
ownerCounts[owner][i] += pathCounts[i]; | |
} | |
} else { | |
ownerCounts[owner] = pathCounts; | |
} | |
rows.push([path, owner, ...pathCounts]); | |
}); | |
for (const owner in ownerCounts) { | |
const counts = ownerCounts[owner]; | |
rows.push(['*', owner, ...counts]); | |
} | |
const table = markdownTable([headers, ...rows]); | |
const content = ['This table was autogenerated by bin/audit.\n', table].join('\n'); | |
fs.writeFileSync('MAINTENANCE_AUDIT.md', content); | |
} | |
function walkSync(dir, fileHandler) { | |
const files = fs.readdirSync(dir) || []; | |
files.forEach(file => { | |
const subpath = `${dir}/${file}`; | |
if (fs.statSync(subpath).isDirectory()) { | |
if (getOwnerForDirectory(subpath)) { | |
auditOwnedDir(subpath); | |
} else { | |
walkSync(`${subpath}/`, fileHandler); | |
} | |
} else { | |
const contents = fs.readFileSync(subpath, 'utf8'); | |
fileHandler(contents, subpath); | |
} | |
}); | |
} | |
function auditPath(path) { | |
let fileCount = 0; | |
let mixinCount = 0; | |
let syncSetState = 0; | |
let createClass = 0; | |
let noflow = 0; | |
let fixMeProps = 0; | |
const lintAudits = eslintRules.reduce((acc, value) => { | |
acc[value] = 0; | |
return acc; | |
}, {}); | |
function auditFile(contents, filePath) { | |
if (filePath.endsWith('.js') || filePath.endsWith('.jsx') || filePath.endsWith('.sh')) { | |
fileCount += 1; | |
} | |
if (contents.match(/mixins: \[/)) { | |
mixinCount += 1; | |
} | |
if (contents.match(/setState\(.*state/)) { | |
syncSetState += 1; | |
} | |
if (contents.match(/createReactClass/)) { | |
createClass += 1; | |
} | |
if (contents.match(/@noflow/)) { | |
noflow += 1; | |
} | |
if (contents.match(/FlowFixMeProps/)) { | |
fixMeProps += 1; | |
} | |
} | |
function lintDir(subpath) { | |
// minor perf optimiation because eslint is slow to start | |
const hasJSFiles = !!fs | |
.readdirSync(subpath) | |
.filter(p => /.jsx?$/.test(p)) | |
.map(p => `${subpath}/${p}`).length; | |
if (hasJSFiles) { | |
const ownedDirs = findOwnedDirs(subpath); | |
const ignores = ownedDirs.map(d => `--ignore-pattern '${d}'`).join(' '); | |
const args = `--ext=.js,.jsx -f node_modules/eslint-json ${subpath}${ignores} -- --eff-by-issue`.split( | |
' ' | |
); | |
const ps = spawnSync('./node_modules/.bin/eslint', args); | |
const reports = JSON.parse(ps.stdout.toString()); | |
eslintRules.forEach(rule => { | |
lintAudits[rule] = reports.reduce( | |
(total, report) => | |
total + report.messages.filter(message => message.ruleId === rule).length, | |
0 | |
); | |
}); | |
} | |
} | |
lintDir(path); | |
walkSync(path, auditFile); | |
const audit = audits.get(path) || {}; | |
audits.set( | |
path, | |
Object.assign(audit, lintAudits, { | |
fileCount, | |
mixinCount, | |
syncSetState, | |
createClass, | |
noflow, | |
fixMeProps, | |
}) | |
); | |
} | |
function auditOwnedDir(subpath) { | |
const path = subpath; | |
if (!fs.lstatSync(path).isDirectory()) { | |
return; | |
} | |
if (!getOwnerForDirectory(subpath)) { | |
return; | |
} | |
if (blacklistDirs.includes(subpath)) { | |
return; | |
} | |
console.log(`Auditing ${path}`); | |
auditPath(path); | |
} | |
fs | |
.readdir(BASE) | |
.then(listing => listing.forEach(p => auditOwnedDir(p))) | |
.then(() => writeAudit()) | |
.catch(err => console.error('audit failed', err)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment