Write lint rules fast.
Search for "checkr" in the VS Code extensions tab (Ctrl+Shift+X to open).
- Follow the husky installation instructions.
- Copy
checkr-hook.js
into the new.husky
folder. - Add the line
node "$(dirname "$0")/checkr-hook.js"
to the bottom of the new.husky/pre-commit
file.
That's it!
Linting is a powerful tool for enforcing project consistency and finding common issues. Many tools such as ESLint, JSHint, and others exist for this purpose. However, they do not have project specific rules, and writing a custom eslint-rule requires more setup and prior knowledge than a checkr.js
file.
A checkr rule results in less code review nits and catches bugs.
On file save and open, checkr looks for a checkr.js
file in the current directory and iterates up to the root, applying any other checkr.js
files along the way.
This means you can put global checks in the project root checkr.js
file, but also have directory specific checks. For example tests/checkr.js
only applies to files in the tests
folder and its subdirectories.
A checkr.js
file should contain a single array of functions to run on file save and open.
Each function is passed the file
being saved or opened, a function to underline
code, a function to find code
, and a set of utils
.
interface params {
fileName: string; // Eg "fooUtil".
fileExtension: string; // Eg "js", "css", the empty string, etc.
fileContents: string; // Eg "console.log('In fooUtil.js file!')".
filePath: string; // Eg "C:\code\cool_project".
underline: (
regexOrText: RegExp | string, // Eg /foo*bar/g or an exact string to match, such as "foobar".
hoverMessage: string, // Eg "Prefer bar".
alert?: "error" | "warn" | "info"
) => void;
code: (string: string, ...expressions: string[]) => RegExp;
fs: NodeFileModule; // Eg fs.readFileSync('C:/foobar.txt');
path: NodePathModule; // Eg path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
child_process: NodeProcessModule; // Eg child_process.execSync('git status', { encoding: 'utf8' });
}
example checkr.js
file layout
[
function check1({ fileContents, underline, code }) { ... },
function check2({ fileContents, fileExtension, underline, fs }) { ... },
function check3({ fileContents, fileName, underline, code }) { ... },
]
code
translates a simple "code finding syntax" into a RegExp
which can be used to underline
things.
Play around with code
syntax here
Token | Matches | Query | Example Match |
---|---|---|---|
$a | variable | if ($a == $b) return { $a; } | if (foo == bar) { return foo; } |
$1 | literal | $1 + $2 + $1 + $2 | 5 + "four" + 5 + "four" |
$@ | operator | 5 |
5 * 10 + 15 * 33 |
$# | keyword | $# ($a == true) | do (baz == true) |
$$ | non-greedy any | if ( |
if (foo, bar, foo) { getFoo(); getBar(); return 33; } |
$$$ | greedy any | case |
case "Apples": return 1; case "Bananas": throw; case "Mangos": throw; |
REGEX(...) | regex escape hatch | REGEX(3+9+2*) 5 + 5 | 33922225+5 |
Variable matchers: `$a`, `$Abcd`, `$foobar55` ✔️ `$`, `$44a`, `$^` ❌
Literal matchers: `$1`, `$2`, `$333` ✔️ `$`, `$az1`, `$\` ❌
Operator matchers: `$@`, `$@a`, `$@Operator22` ✔️ `$@@`, `$@2`, `$@%` ❌
Keyword matchers: `$#`, `$#keyword`, `$#Key1` ✔️ `$#@`, `$#5`, `$##` ❌
While code
returns a RegExp
, it also adds two methods to the returned object.
.matchAll(str)
returns all the captured results in an easy format, or an empty array for no matches.
code`$#w ($a + $1) { return $$; }`.matchAll(`
do(foo + "baz") {
return getBar();
}`);
// returns
[
{
blocks: ['getBar()'],
keywords: ['do'],
literals: ['"baz"'],
operators: [],
others: [],
variables: ['foo'],
},
];
.matchFirst(str)
returns the first captured result.
Note if there is nothing to capture or no matches, it will return an object with empty arrays.
// Match, but no capture groups specified, eg $a, $1, $#k, etc.
code`foo = bar;`.matchFirst(`foo = bar;`);
// No match.
code`foo = bar;`.matchFirst(`nomatch`);
// both return
{
blocks: [],
keywords: [],
literals: [],
operators: [],
others: [],
variables: [],
}
[
function requirePropDestructing({ fileContents, underline, code }) {
const underlineComponents = match => {
if (!match.blocks[2].includes('= props;'))
underline(code`${match.variables[0]}($$ props $$)`, "❌ `props` must be destructed.", "error");
}
code`function $a($$ props $$) { $$$ }`
.matchAll(fileContents)
.forEach(underlineComponents);
},
function requireButtonTypeAttribute({ fileContents, underline, code }) {
const underlineInvalidButtons = match => match
.blocks
.filter(x => !x.includes('type='))
.forEach(x => underline(x, "⚠️ `type` should be on buttons.", "warn"));
code`<button $$>`
.matchAll(fileContents)
.forEach(underlineInvalidButtons);
},
function enforceBooleanPropNaming({ fileContents, underline, code }) {
const underlineInvalidBooleanNames = match => {
const is = match.variables[0].startsWith("is");
const has = match.variables[0].startsWith("has");
const should = match.variables[0].startsWith("should");
const isRecommended = is || has || should;
const hoverMessage = "💬 Consider prefix with 'is', 'has', or 'should'.";
if (!isRecommended)
underline(code`${match.variables[0]}: PropTypes.bool$$`, hoverMessage, "info");
};
code`$a: PropTypes.bool$$`
.matchAll(fileContents)
.forEach(underlineInvalidBooleanNames)
}
];
- Verify an ESLint rule for the problem doesn't already exist.
- Prefer writing a custom ESLint rule for complicated checks.
- Avoid solving problems with linters that may be better handled with other methods.
- Prefer checks that are actionable and accurate over 90% of the time.
More best practices here.
Test code
patterns here.
Test regex patterns here.
All improvements are welcome. When opening a PR or updating the wiki feel free to add your name to the contributors.md and an emoji if you'd like, so your name can be immortalized until the end of time!
Feel free to take any ideas or invent your own:
import
support incheckr.js
files.- Adding useful checks to the
examples/
folder. - More options to run at intervals or on other events.
- Optimizations such as file caching.
To debug the extension:
git clone
the repo.- Install
yarn
and run in it the project root. Seepackage.json
for a full list of commands. - Run
yarn watch
to compile the TypeScript into JavaScript and "watch" for any file changes. - Open the project in VS code, and press
F5
. Ctrl+R
in the debug window reloads the extension.