eslint rule

This commit is contained in:
Alessio Gravili
2026-09-02 17:30:45 -07:00
parent a912baa400
commit 24238cb467
4 changed files with 179 additions and 0 deletions
@@ -0,0 +1,100 @@
const SCOPED_METHODS = new Set([
'afterAll',
'afterEach',
'aroundAll',
'aroundEach',
'beforeAll',
'beforeEach',
'describe',
'suite',
])
/**
* Returns the identifier at the start of a member or call chain.
*
* Examples:
* - `test.describe` -> `test`
* - `test.describe.each` -> `test`
* - `test.options({ db: 'mongo' }).describe` -> `test`
*/
function getRootIdentifier(node) {
if (!node) {
return null
}
if (node.type === 'Identifier') {
return node
}
if (node.type === 'CallExpression') {
return getRootIdentifier(node.callee)
}
if (node.type === 'ChainExpression') {
return getRootIdentifier(node.expression)
}
if (node.type === 'MemberExpression') {
return getRootIdentifier(node.object)
}
return null
}
function getStaticPropertyName(node) {
if (!node.computed && node.property.type === 'Identifier') {
return node.property.name
}
if (node.computed && node.property.type === 'Literal') {
return typeof node.property.value === 'string' ? node.property.value : null
}
return null
}
/** @type {import('eslint').Rule.RuleModule} */
export const rule = {
meta: {
type: 'problem',
docs: {
description:
'Require standalone Vitest suite and hook functions so editor test discovery uses the correct test name',
category: 'Possible Errors',
recommended: false,
},
messages: {
useStandalone:
'Use the standalone `{{method}}(...)` API instead of `{{testIdentifier}}.{{method}}(...)`. Scoped suite and hook methods can make the VS Code Vitest extension discover the wrong test name.',
},
schema: [],
},
create(context) {
return {
MemberExpression(node) {
const method = getStaticPropertyName(node)
if (!method || !SCOPED_METHODS.has(method)) {
return
}
const rootIdentifier = getRootIdentifier(node.object)
if (!rootIdentifier || !['it', 'test'].includes(rootIdentifier.name)) {
return
}
context.report({
node,
messageId: 'useStandalone',
data: {
method,
testIdentifier: rootIdentifier.name,
},
})
},
}
},
}
export default rule
@@ -0,0 +1,76 @@
import { RuleTester } from 'eslint'
import { afterAll, describe, it } from 'vitest'
import rule from './no-vitest-scoped-methods.js'
// Wire ESLint's RuleTester into Vitest so each case becomes a real test. These
// static hooks exist at runtime but are not declared in `@types/eslint`.
const ruleTesterHooks = RuleTester as unknown as {
afterAll: typeof afterAll
describe: typeof describe
it: typeof it
itOnly: typeof it.only
}
ruleTesterHooks.afterAll = afterAll
ruleTesterHooks.describe = describe
ruleTesterHooks.it = it
ruleTesterHooks.itOnly = it.only
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
})
ruleTester.run('no-vitest-scoped-methods', rule, {
valid: [
"describe('posts', () => {})",
'beforeEach(() => {})',
"test('creates a post', () => {})",
"test.skip('creates a post', () => {})",
"test.each([])('creates a post', () => {})",
"test.runIf(true)('creates a post', () => {})",
"playwrightTest.describe('posts', () => {})",
],
invalid: [
{
code: "test.describe('posts', () => {})",
errors: [
{ data: { method: 'describe', testIdentifier: 'test' }, messageId: 'useStandalone' },
],
},
{
code: 'test.beforeEach(() => {})',
errors: [
{ data: { method: 'beforeEach', testIdentifier: 'test' }, messageId: 'useStandalone' },
],
},
{
code: 'it.afterAll(() => {})',
errors: [{ data: { method: 'afterAll', testIdentifier: 'it' }, messageId: 'useStandalone' }],
},
{
code: "test.suite({ config: './config.ts' })('posts', () => {})",
errors: [{ data: { method: 'suite', testIdentifier: 'test' }, messageId: 'useStandalone' }],
},
{
code: "test.describe.each([])('posts', () => {})",
errors: [
{ data: { method: 'describe', testIdentifier: 'test' }, messageId: 'useStandalone' },
],
},
{
code: "test.options({ db: 'mongo' }).describe('posts', () => {})",
errors: [
{ data: { method: 'describe', testIdentifier: 'test' }, messageId: 'useStandalone' },
],
},
{
code: "test['beforeEach'](() => {})",
errors: [
{ data: { method: 'beforeEach', testIdentifier: 'test' }, messageId: 'useStandalone' },
],
},
],
})
+2
View File
@@ -1,6 +1,7 @@
import noJsxImportStatements from './customRules/no-jsx-import-statements.js'
import noNonRetryableAssertions from './customRules/no-non-retryable-assertions.js'
import noRelativeMonorepoImports from './customRules/no-relative-monorepo-imports.js'
import noVitestScopedMethods from './customRules/no-vitest-scoped-methods.js'
import noImportsFromExportsDir from './customRules/no-imports-from-exports-dir.js'
import noFlakyAssertions from './customRules/no-flaky-assertions.js'
import noImportsFromSelf from './customRules/no-imports-from-self.js'
@@ -15,6 +16,7 @@ const index = {
rules: {
'no-jsx-import-statements': noJsxImportStatements,
'no-relative-monorepo-imports': noRelativeMonorepoImports,
'no-vitest-scoped-methods': noVitestScopedMethods,
'no-imports-from-exports-dir': noImportsFromExportsDir,
'no-imports-from-self': noImportsFromSelf,
'no-conflicting-lexical-markdown-imports': noConflictingLexicalMarkdownImports,
+1
View File
@@ -47,6 +47,7 @@ export const testEslintConfig = [
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'payload/no-vitest-scoped-methods': 'error',
'vitest/no-standalone-expect': [
'error',
{