From 177aad00450e13264ea91f1a8794571b52ff647c Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Wed, 27 Nov 2024 23:19:25 +0900 Subject: [PATCH] [valid-expect] Support typecheck (#576) * fix: support typecheck * test: add support typecheck test * refactor: add type reason --- src/rules/valid-expect.ts | 5 +++ src/utils/parse-vitest-fn-call.ts | 12 ++--- src/utils/types.ts | 11 ++++- tests/valid-expect.test.ts | 74 ++++++++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/src/rules/valid-expect.ts b/src/rules/valid-expect.ts index 6c8841e..5e8469b 100644 --- a/src/rules/valid-expect.ts +++ b/src/rules/valid-expect.ts @@ -2,6 +2,7 @@ import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils' import { createEslintRule, FunctionExpression, getAccessorValue, isFunction, isSupportedAccessor } from '../utils' import { parseVitestFnCallWithReason } from '../utils/parse-vitest-fn-call' import { ModifierName } from '../utils/types' +import { parsePluginSettings } from 'src/utils/parse-plugin-settings' export const RULE_NAME = 'valid-expect' export type MESSAGE_IDS = @@ -205,6 +206,7 @@ export default createEslintRule<[ return { CallExpression(node) { const vitestFnCall = parseVitestFnCallWithReason(node, context) + const settings = parsePluginSettings(context.settings) if (typeof vitestFnCall === 'string') { const reportingNode @@ -241,6 +243,9 @@ export default createEslintRule<[ } return } + else if (vitestFnCall?.type ==="expectTypeOf" && settings.typecheck){ + return + } else if (vitestFnCall?.type !== 'expect') { return } diff --git a/src/utils/parse-vitest-fn-call.ts b/src/utils/parse-vitest-fn-call.ts index 4d797fe..8717637 100644 --- a/src/utils/parse-vitest-fn-call.ts +++ b/src/utils/parse-vitest-fn-call.ts @@ -59,6 +59,8 @@ interface ParsedGeneralVitestFnCall extends BaseParsedVitestFnCall { type: Exclude & Exclude } +type Reason = 'matcher-not-called' | 'modifier-unknown' | 'matcher-not-found' + export interface ParsedExpectVitestFnCall extends BaseParsedVitestFnCall, ModifiersAndMatcher { type: 'expect' | 'expectTypeOf' } @@ -88,13 +90,13 @@ export const parseVitestFnCall = ( const parseVitestFnCallCache = new WeakMap< TSESTree.CallExpression, - ParsedVitestFnCall | string | null + ParsedVitestFnCall | Reason | null >() export const parseVitestFnCallWithReason = ( node: TSESTree.CallExpression, context: TSESLint.RuleContext -): ParsedVitestFnCall | string | null => { +): ParsedVitestFnCall | Reason | null => { let parsedVitestFnCall = parseVitestFnCallCache.get(node) if (parsedVitestFnCall) @@ -131,7 +133,7 @@ const determineVitestFnType = (name: string): VitestFnType => { const findModifiersAndMatcher = ( members: KnownMemberExpressionProperty[] -): ModifiersAndMatcher | string => { +): ModifiersAndMatcher | Reason => { const modifiers: KnownMemberExpressionProperty[] = [] for (const member of members) { @@ -182,7 +184,7 @@ const findModifiersAndMatcher = ( return 'matcher-not-found' } -const parseVitestExpectCall = (typelessParsedVitestFnCall: Omit, type: 'expect' | 'expectTypeOf'): ParsedExpectVitestFnCall | string => { +const parseVitestExpectCall = (typelessParsedVitestFnCall: Omit, type: 'expect' | 'expectTypeOf'): ParsedExpectVitestFnCall | Reason => { const modifiersMatcher = findModifiersAndMatcher(typelessParsedVitestFnCall.members) if (typeof modifiersMatcher === 'string') @@ -222,7 +224,7 @@ export const findTopMostCallExpression = ( const parseVitestFnCallWithReasonInner = ( node: TSESTree.CallExpression, context: TSESLint.RuleContext -): ParsedVitestFnCall | string | null => { +): ParsedVitestFnCall | Reason | null => { const chain = getNodeChain(node) if (!chain?.length) diff --git a/src/utils/types.ts b/src/utils/types.ts index e1dd24c..6994168 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -25,7 +25,16 @@ export enum HookName { export enum ModifierName { not = 'not', rejects = 'rejects', - resolves = 'resolves' + resolves = 'resolves', + returns = 'returns', + branded = 'branded', + asserts = 'asserts', + constructorParameters = 'constructorParameters', + parameters = 'parameters', + thisParameter = 'thisParameter', + guards = 'guards', + instance = 'instance', + items = 'items' } /** diff --git a/tests/valid-expect.test.ts b/tests/valid-expect.test.ts index ff339cf..2d066fa 100644 --- a/tests/valid-expect.test.ts +++ b/tests/valid-expect.test.ts @@ -109,7 +109,79 @@ ruleTester.run(RULE_NAME, rule, { { code: 'test("valid-expect", async () => { expect(Promise.resolve(2)).toResolve(); });', options: [{ asyncMatchers: ['toResolveWith'] }] - } + }, + { + code: `expectTypeOf().returns.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, + { + code: `expectTypeOf().branded.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, + { + code: `expectTypeOf().asserts.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, + { + code: `expectTypeOf().constructorParameters.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, + { + code: `expectTypeOf().parameters.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, + { + code: `expectTypeOf().thisParameter.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, + { + code: `expectTypeOf().guards.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, + { + code: `expectTypeOf().instance.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, + { + code: `expectTypeOf().items.toMatchTypeOf();`, + settings: { + vitest: { + typecheck: true + } + }, + }, ], invalid: [ {