Skip to content

Instantly share code, notes, and snippets.

@rgoldfinger
Created December 20, 2017 17:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rgoldfinger/305f1a6c59118be1c5e9ab6e6edb0569 to your computer and use it in GitHub Desktop.
Save rgoldfinger/305f1a6c59118be1c5e9ab6e6edb0569 to your computer and use it in GitHub Desktop.
#!/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