test: add tests for recommended rule set

This commit is contained in:
Christopher Willis-Ford 2025-04-30 14:29:46 -07:00
parent d01085335c
commit 6b37df906c
14 changed files with 1629 additions and 24 deletions

View file

@ -12,28 +12,31 @@ Install the config along with its peer dependencies, `eslint` and `prettier`:
npm install -D eslint-config-scratch eslint@^9 prettier@^3
```
Add `eslint.config.mjs` to your project root and, optionally, configure parser options for rules that require type
information:
Add `eslint.config.mjs` to your project root.
For a TypeScript project, you can add `languageOptions` to enable type checking:
```js
// myProjectRoot/eslint.config.mjs
import { eslintConfigScratch } from 'eslint-config-scratch'
// for plain JavaScript:
export default eslintConfigScratch.recommended
// for a TypeScript project:
export default eslintConfigScratch.config(
eslintConfigScratch.recommended,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
}
export default eslintConfigScratch.config(eslintConfigScratch.recommended, {
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
)
})
```
For a JavaScript project, it might look like this:
```js
// myProjectRoot/eslint.config.mjs
import { eslintConfigScratch } from 'eslint-config-scratch'
export default eslintConfigScratch.recommended
```
The function `eslintConfigScratch.config` is a re-export of the `config` function from `typescript-eslint`, and helps

View file

@ -1,8 +1,13 @@
import { globalIgnores } from 'eslint/config'
import globals from 'globals'
import { eslintConfigScratch } from './lib/index.mjs'
export default eslintConfigScratch.config(eslintConfigScratch.recommended, {
languageOptions: {
globals: globals.node,
export default eslintConfigScratch.config(
eslintConfigScratch.recommended,
{
languageOptions: {
globals: globals.node,
},
},
})
globalIgnores(['test/**/*.bad.*']),
)

1277
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,8 +6,9 @@
"scripts": {
"prepare": "husky install",
"format": "prettier --write . && eslint --fix",
"lint": "eslint && prettier --check .",
"test": "npm run lint"
"test:lint": "eslint && prettier --check .",
"test:vitest": "vitest run",
"test": "npm run test:lint && npm run test:vitest"
},
"repository": {
"type": "git",
@ -54,7 +55,8 @@
"eslint": "9.25.1",
"husky": "8.0.3",
"scratch-semantic-release-config": "3.0.0",
"semantic-release": "24.2.3"
"semantic-release": "24.2.3",
"vitest": "^3.1.1"
},
"config": {
"commitizen": {

View file

@ -0,0 +1,112 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`'recommended' > Plain TS (bad) 1`] = `
[
{
"column": 7,
"endColumn": 25,
"endLine": 5,
"line": 5,
"messageId": "noInferrableType",
"nodeType": "VariableDeclarator",
"ruleId": "@typescript-eslint/no-inferrable-types",
},
{
"column": 7,
"endColumn": 12,
"endLine": 5,
"line": 5,
"messageId": "unusedVar",
"nodeType": null,
"ruleId": "@typescript-eslint/no-unused-vars",
},
{
"column": 10,
"endColumn": 13,
"endLine": 8,
"line": 8,
"messageId": "unusedVar",
"nodeType": null,
"ruleId": "@typescript-eslint/no-unused-vars",
},
{
"column": 14,
"endColumn": 26,
"endLine": 17,
"line": 17,
"messageId": "anyAssignment",
"nodeType": "VariableDeclarator",
"ruleId": "@typescript-eslint/no-unsafe-assignment",
},
{
"column": 20,
"endColumn": 24,
"endLine": 17,
"line": 17,
"messageId": "unsafeCall",
"nodeType": "Identifier",
"ruleId": "@typescript-eslint/no-unsafe-call",
},
]
`;
exports[`'recommended' > Plain TS (good) 1`] = `[]`;
exports[`'recommended' > React JSX (bad) 1`] = `
[
{
"column": 10,
"endColumn": 13,
"endLine": 4,
"line": 4,
"messageId": "unusedVar",
"nodeType": "Identifier",
"ruleId": "no-unused-vars",
},
{
"column": 20,
"endColumn": 24,
"endLine": 14,
"line": 14,
"messageId": "undef",
"nodeType": "Identifier",
"ruleId": "no-undef",
},
]
`;
exports[`'recommended' > React JSX (good) 1`] = `[]`;
exports[`'recommended' > plain JS (bad) 1`] = `
[
{
"column": 10,
"endColumn": 13,
"endLine": 4,
"line": 4,
"messageId": "unusedVar",
"nodeType": "Identifier",
"ruleId": "no-unused-vars",
},
{
"column": 26,
"endColumn": 46,
"endLine": 12,
"line": 12,
"messageId": "noJSXWithExtension",
"nodeType": "JSXElement",
"ruleId": "react/jsx-filename-extension",
},
{
"column": 20,
"endColumn": 24,
"endLine": 15,
"line": 15,
"messageId": "undef",
"nodeType": "Identifier",
"ruleId": "no-undef",
},
]
`;
exports[`'recommended' > plain JS (good) 1`] = `[]`;

110
test/eslint.test.mjs Normal file
View file

@ -0,0 +1,110 @@
import { ESLint } from 'eslint'
import path from 'path'
import util from 'util'
import { beforeAll, describe, expect, test } from 'vitest'
/**
* @typedef {object} EslintTestInfo
* @property {string} name - the title/message for this test
* @property {string} filePath - the path to the file to lint
* @property {number} warningCount - the number of warnings to expect
* @property {number} errorCount - the number of errors to expect
*/
// TSX is omitted because TypeScript insists on fully knowing the React types,
// and I would rather not add React as a dependency of eslint-config-scratch.
/** @type {Record<string, EslintTestInfo[]>} */
const testInfo = {
recommended: [
{
name: 'plain JS (good)',
filePath: 'plain.good.mjs',
warningCount: 0,
errorCount: 0,
},
{
name: 'React JSX (good)',
filePath: 'react.good.jsx',
warningCount: 0,
errorCount: 0,
},
{
name: 'plain JS (bad)',
filePath: 'plain.bad.mjs',
warningCount: 0,
errorCount: 3,
},
{
name: 'React JSX (bad)',
filePath: 'react.bad.jsx',
warningCount: 0,
errorCount: 2,
},
{
name: 'Plain TS (good)',
filePath: 'plain.good.ts',
warningCount: 0,
errorCount: 0,
},
{
name: 'Plain TS (bad)',
filePath: 'plain.bad.ts',
warningCount: 0,
errorCount: 5,
},
],
}
/**
* Create a snapshot of a lint message.
* Excludes properties that may change without affecting correctness, such as human-readable text.
* @param {ESLint.LintMessage} result - the lint message to filter
* @returns {object} a filtered snapshot of the lint message
*/
const messageSnapshot = result =>
Object.fromEntries(
Object.entries(result).filter(([k]) =>
['line', 'column', 'endLine', 'endColumn', 'messageId', 'nodeType', 'ruleId'].includes(k),
),
)
test('make sure eslint works at all', async () => {
const source = 'foo(42)'
const eslint = new ESLint({
overrideConfigFile: true,
})
const results = await eslint.lintText(source)
expect(results).toBeDefined()
expect(results.length).toBe(1)
expect(results[0].warningCount).toEqual(0)
expect(results[0].errorCount).toEqual(0)
})
describe.concurrent.for(Object.entries(testInfo))('$0', ([subdir, testList]) => {
/**
* @type {ESLint.LintResult[]}
*/
let results
// Linting one file at a time takes much longer
beforeAll(async () => {
const eslint = new ESLint({
overrideConfigFile: path.resolve(import.meta.dirname, subdir, 'eslint.config.mjs'),
})
results = await eslint.lintFiles(testList.map(info => path.resolve(import.meta.dirname, subdir, info.filePath)))
})
test('results container', () => {
expect(results).toBeDefined()
expect(results.length).toBe(testList.length)
})
testList.forEach(({ name, filePath, warningCount, errorCount }, i) => {
test(name, () => {
expect(path.resolve(results[i].filePath)).toBe(path.resolve(import.meta.dirname, subdir, filePath))
expect(results[i].warningCount, util.inspect(results[i])).toBe(warningCount)
expect(results[i].errorCount, util.inspect(results[i])).toBe(errorCount)
expect(results[i].messages.map(messageSnapshot), util.inspect(results[i].messages)).toMatchSnapshot()
})
})
})

View file

@ -0,0 +1,14 @@
import { eslintConfigScratch } from '../../lib/index.mjs'
const config = eslintConfigScratch.config(eslintConfigScratch.recommended, {
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
project: ['./tsconfig.json'],
},
},
})
export default config

View file

@ -0,0 +1,15 @@
import ESLint from 'eslint'
// foo isn't used
function foo() {
const eslint = new ESLint({
overrideConfigFile: true,
})
return eslint
}
// React isn't allowed in plain JS
export const myElement = <div>{'hello'}</div>
// foo2 isn't defined
export const bar = foo2()

View file

@ -0,0 +1,17 @@
import { ESLint } from 'eslint'
// @typescript-eslint/no-inferrable-types
// @typescript-eslint/no-unused-vars
const forty: number = 40
// @typescript-eslint/no-unused-vars
function foo(): ESLint {
const eslint = new ESLint({
overrideConfigFile: true,
})
return eslint
}
// @typescript-eslint/no-unsafe-call (`foo2` is an error-typed value)
// @typescript-eslint/no-unsafe-assignment (`foo2()` is an error-typed value)
export const bar = foo2()

View file

@ -0,0 +1,10 @@
import ESLint from 'eslint'
function foo() {
const eslint = new ESLint({
overrideConfigFile: true,
})
return eslint
}
export const bar = foo()

View file

@ -0,0 +1,10 @@
import { ESLint } from 'eslint'
function foo(): ESLint {
const eslint = new ESLint({
overrideConfigFile: true,
})
return eslint
}
export const bar = foo()

View file

@ -0,0 +1,14 @@
import ESLint from 'eslint'
// foo isn't used
function foo() {
const eslint = new ESLint({
overrideConfigFile: true,
})
return eslint
}
export const myElement = <div>{'hello'}</div>
// foo2 isn't defined
export const bar = foo2()

View file

@ -0,0 +1,12 @@
import ESLint from 'eslint'
function foo() {
const eslint = new ESLint({
overrideConfigFile: true,
})
return eslint
}
export const myElement = <div>{'hello'}</div>
export const bar = foo()

View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true
},
"include": ["**/*.ts", "**/*.tsx"]
}