Skip to content

Instantly share code, notes, and snippets.

@milichev
Last active January 24, 2022 10:59
Show Gist options
  • Save milichev/db3ea9fcb2fb086db57c3f2c4f770db2 to your computer and use it in GitHub Desktop.
Save milichev/db3ea9fcb2fb086db57c3f2c4f770db2 to your computer and use it in GitHub Desktop.
suppress react-hooks eslint warnings
/**
* Accepts eslint `stdout` and processes all error messages related to React Hooks, if any.
*
* Essentially, the "fix" is only about adding a respective `eslint-disable-line` comments to mute the error unless it is addressed appropriately.
*
* Requires the following modules available:
* - lodash
*
* Usage:
*
* Run in console:
* eslint client/components/ | node mute-eslint-hooks.js
*
* If something went wrong and the script should be fixed to proceed:
* 1. Fix the script to edit the source file correctly,
* 2. Run in console specifying the file to rollback and process again:
* export FN=client/components/items/bulk-actions.js && git checkout -- $FN && eslint $FN | node mute-eslint-hooks.js
*/
/* eslint-disable @typescript-eslint/no-var-requires,no-console */
const readline = require("readline");
const fs = require("fs");
const $path = require("path");
const _ = require("lodash");
const { EOL } = require("os");
const cwd = process.cwd();
collectErrors()
.then((errorsByFile) => Promise.all(errorsByFile.map(processFile)))
.then((results) =>
results
.filter(Boolean)
.map(({ path, count, fixedCount }) => `\n${$path.relative(cwd, path)}\n fixed: ${fixedCount} / ${count}`)
.join(EOL)
)
.then((msg) => msg && console.log(msg))
.catch((err) => console.error(err));
function collectErrors() {
return new Promise((resolve, reject) => {
const result = [];
const pathRe = /^(?:\/\w[-.\w]+)+\.(?:js|jsx|ts|tsx)$/;
const errorRe = /^\s+(\d+):(\d+)\s+(warning|error)\s+(.+)\s+([-@\/\w]+)$/;
let current;
readline
.createInterface({
input: process.stdin,
output: process.stdout,
})
.on("line", function (line) {
const pathMatch = line.match(pathRe);
if (pathMatch) {
current = {
path: pathMatch[0],
errors: {},
};
result.push(current);
} else {
const errorMatch = line.match(errorRe);
if (errorMatch) {
const ln = Number(errorMatch[1]);
const ch = Number(errorMatch[2]);
const type = errorMatch[3];
const name = errorMatch[5];
const message = errorMatch[4].trim();
const byLine = current.errors[ln] || (current.errors[ln] = []);
byLine.push({
ln,
ch,
type,
message,
name,
});
}
}
})
.on("close", function () {
this.close();
resolve(result);
});
});
}
async function processFile({ path, errors }) {
const lines = await readFileLines(path);
let fixedCount = 0;
let count = 0;
Object.keys(errors).forEach((key) => {
const ln = Number(key);
count += errors[ln].length;
const toSuppress = _(errors[ln]).filter(({ name }) => name === "react-hooks/exhaustive-deps" || name === "react-hooks/rules-of-hooks");
if (!toSuppress.size()) {
return;
}
const line = lines[ln - 1];
const nameList = toSuppress.map("name").uniq().value().join(",");
lines[ln - 1] =
fixBareArrayOpening({ line, nameList }) ||
fixArrayAfterBrace({ line, nameList }) ||
fixRuleStatement({ line, nameList }) ||
fixCallbackDef({ line, nameList }) ||
`${lines[ln - 1]} // eslint-disable-line ${nameList}`;
fixedCount += toSuppress.size();
});
await fs.promises.writeFile(path, lines.join(EOL), { encoding: "utf8" });
return { path, fixedCount, count };
}
function fixBareArrayOpening({ line, nameList }) {
const match = line.match(/^(\s+)\[/);
return match && `${match[1]}// eslint-disable-next-line ${nameList}${EOL}${line}`;
}
function fixArrayAfterBrace({ line, nameList }) {
const match = line.match(/^(\s*)},\s*\[/);
return match && `${match[1]}${match[1]}// eslint-disable-next-line ${nameList}${EOL}${line}`;
}
function fixRuleStatement({ line, nameList }) {
const match = line.match(/^(\s*).*use[A-Z][\w]+(?:<[^>]+>)?\(/);
return match && `${match[1]}// eslint-disable-next-line ${nameList}${EOL}${line}`;
}
function fixCallbackDef({ line, nameList }) {
const match = line.match(/^(\s+)(?:(?:const|let|var)\s+)?\w+\s*=\s*(?:(?:function\s*\([^)]*\)\s*)|(?:\([^)]*\)\s*=>))/);
return match && `${match[1]}// eslint-disable-next-line ${nameList}${EOL}${line}`;
}
async function readFileLines(path) {
return new Promise((resolve) => {
const lines = [];
const stream = fs.createReadStream(path, { encoding: "utf8" });
readline
.createInterface({
input: stream,
})
.on("line", function (line) {
lines.push(line);
})
.on("close", function () {
stream.close();
this.close();
lines.push("");
resolve(lines);
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment