diff --git a/.changeset/bold-lines-sleep.md b/.changeset/bold-lines-sleep.md new file mode 100644 index 0000000..7b769f4 --- /dev/null +++ b/.changeset/bold-lines-sleep.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-zod-x': patch +--- + +fix: `detectZodSchemaRootNode` doesn't detect named `z` import diff --git a/src/rules/array-style.spec.ts b/src/rules/array-style.spec.ts index 3810118..db9b619 100644 --- a/src/rules/array-style.spec.ts +++ b/src/rules/array-style.spec.ts @@ -8,30 +8,30 @@ const ruleTester = new RuleTester(); ruleTester.run('array-style (function)', arrayStyle, { valid: [ { - name: 'default option', + name: 'namespace import', code: dedent` import * as z from 'zod'; z.array(z.string()); `, }, { - name: '`function` option', + name: 'named import', code: dedent` - import * as z from 'zod'; - z.array(z.string()); + import { array, string } from 'zod'; + array(string()); `, }, { - name: 'named', + name: 'named z import', code: dedent` - import { array, string } from 'zod'; - array(string()); + import { z } from 'zod'; + z.array(z.string()); `, }, ], invalid: [ { - name: 'namespace', + name: 'namespace import', code: dedent` import * as z from 'zod'; z.string().array(); @@ -43,7 +43,7 @@ ruleTester.run('array-style (function)', arrayStyle, { `, }, { - name: 'named', + name: 'named import', code: dedent` import { string } from 'zod'; string().array(); @@ -52,6 +52,19 @@ ruleTester.run('array-style (function)', arrayStyle, { errors: [{ messageId: 'useFunction' }], output: null, }, + { + // https://github.com/marcalexiei/eslint-plugin-zod-x/issues/174 + name: 'named z import', + code: dedent` + import { z } from 'zod'; + z.string().array(); + `, + errors: [{ messageId: 'useFunction' }], + output: dedent` + import { z } from 'zod'; + z.array(z.string()); + `, + }, { name: 'with method', code: dedent` diff --git a/src/rules/array-style.ts b/src/rules/array-style.ts index 7699d32..dc5d69a 100644 --- a/src/rules/array-style.ts +++ b/src/rules/array-style.ts @@ -60,13 +60,13 @@ export const arrayStyle = ESLintUtils.RuleCreator(getRuleURL)< return; } - const { importType, schemaType } = zodSchema; + const { schemaDecl, schemaType } = zodSchema; if (style === 'method') { // match all z.array(z.string()) and convert them into // z.string().array() if (schemaType === 'array') { - if (importType === 'namespace') { + if (schemaDecl === 'namespace') { context.report({ node, messageId: 'useMethod', @@ -105,7 +105,7 @@ export const arrayStyle = ESLintUtils.RuleCreator(getRuleURL)< // if there is a param the array has already a schema inside node.arguments.length === 0 ) { - if (importType === 'namespace') { + if (schemaDecl === 'namespace') { context.report({ node, messageId: 'useFunction', diff --git a/src/rules/consistent-import-source.spec.ts b/src/rules/consistent-import-source.spec.ts index 84d278d..b2373ca 100644 --- a/src/rules/consistent-import-source.spec.ts +++ b/src/rules/consistent-import-source.spec.ts @@ -6,6 +6,8 @@ const ruleTester = new RuleTester(); ruleTester.run('consistent-import-source', consistentImportSource, { valid: [ + { code: 'import * as z from "zod"' }, + { code: 'import { z } from "zod"' }, { code: 'import z from "zod"' }, { code: 'import z from "zod"', @@ -26,6 +28,24 @@ ruleTester.run('consistent-import-source', consistentImportSource, { ], invalid: [ { + name: 'namespace', + code: 'import * as z from "zod/v4"', + errors: [ + { + messageId: 'sourceNotAllowed', + data: { source: 'zod/v4', sources: '"zod"' }, + suggestions: [ + { + messageId: 'replaceSource', + data: { valid: 'zod', invalid: 'zod/v4' }, + output: 'import * as z from "zod"', + }, + ], + }, + ], + }, + { + name: 'default', code: 'import z from "zod"', options: [{ sources: ['zod/v4'] }], errors: [ diff --git a/src/rules/consistent-object-schema-type.spec.ts b/src/rules/consistent-object-schema-type.spec.ts index fa6fd50..f6f9fd2 100644 --- a/src/rules/consistent-object-schema-type.spec.ts +++ b/src/rules/consistent-object-schema-type.spec.ts @@ -8,19 +8,26 @@ const ruleTester = new RuleTester(); ruleTester.run('consistent-object-schema-type', consistentObjectSchemaType, { valid: [ { - name: 'correct usage', + name: 'namespace import', code: dedent` import * as z from 'zod'; z.object({}) `, }, { - name: 'correct usage (named)', + name: 'named import', code: dedent` import { object } from 'zod'; object({}) `, }, + { + name: 'named z import', + code: dedent` + import { z } from 'zod'; + z.object({}) + `, + }, { name: 'nested', options: [{ allow: ['looseObject', 'strictObject'] }], diff --git a/src/rules/no-any-schema.spec.ts b/src/rules/no-any-schema.spec.ts index f9756f5..34c6ab1 100644 --- a/src/rules/no-any-schema.spec.ts +++ b/src/rules/no-any-schema.spec.ts @@ -14,6 +14,14 @@ ruleTester.run('no-any-schema', noAnySchema, { const schema = z.string(); `, }, + { + // https://github.com/marcalexiei/eslint-plugin-zod-x/issues/174 + name: 'named z import', + code: dedent` + import { z } from 'zod'; + const schema = z.string(); + `, + }, { name: 'nested schema declaration', code: dedent` @@ -48,6 +56,48 @@ ruleTester.run('no-any-schema', noAnySchema, { }, ], }, + { + name: 'named z import', + code: dedent` + import { z } from 'zod'; + const schema = z.any(); + `, + errors: [ + { + messageId: 'noZAny', + suggestions: [ + { + messageId: 'useUnknown', + output: dedent` + import { z } from 'zod'; + const schema = z.unknown(); + `, + }, + ], + }, + ], + }, + { + name: 'named z import with rename', + code: dedent` + import { z as pippo } from 'zod'; + const schema = pippo.any(); + `, + errors: [ + { + messageId: 'noZAny', + suggestions: [ + { + messageId: 'useUnknown', + output: dedent` + import { z as pippo } from 'zod'; + const schema = pippo.unknown(); + `, + }, + ], + }, + ], + }, { name: 'named import', code: dedent` @@ -79,6 +129,7 @@ ruleTester.run('no-any-schema', noAnySchema, { }, { // https://github.com/marcalexiei/eslint-plugin-zod-x/issues/143 + name: 'should correctly fix any schema with chained method', code: dedent` import * as z from 'zod'; export const aSchema = z.any().refine((value) => value) diff --git a/src/rules/no-empty-custom-schema.spec.ts b/src/rules/no-empty-custom-schema.spec.ts index d6778a0..bc7dc96 100644 --- a/src/rules/no-empty-custom-schema.spec.ts +++ b/src/rules/no-empty-custom-schema.spec.ts @@ -8,21 +8,21 @@ const ruleTester = new RuleTester(); ruleTester.run('no-empty-custom-schema', noEmptyCustomSchema, { valid: [ { - name: 'valid usage', + name: 'namespace', code: dedent` import * as z from 'zod'; z.custom((val) => typeof val === "string" ? /^\\d+px$/.test(val) : false); `, }, { - name: 'valid usage (named)', + name: 'named', code: dedent` import { custom } from 'zod'; custom((val) => typeof val === "string" ? /^\\d+px$/.test(val) : false); `, }, { - name: 'valid usage (named renamed)', + name: 'named renamed', code: dedent` import { custom as zCustom } from 'zod'; zCustom((val) => typeof val === "string" ? /^\\d+px$/.test(val) : false); @@ -49,7 +49,7 @@ ruleTester.run('no-empty-custom-schema', noEmptyCustomSchema, { ], invalid: [ { - name: 'invalid usage', + name: 'namespace', code: dedent` import * as z from 'zod'; z.custom(); @@ -57,13 +57,21 @@ ruleTester.run('no-empty-custom-schema', noEmptyCustomSchema, { errors: [{ messageId: 'noEmptyCustomSchema' }], }, { - name: 'invalid usage (named)', + name: 'named', code: dedent` import { custom } from 'zod'; custom(); `, errors: [{ messageId: 'noEmptyCustomSchema' }], }, + { + name: 'named z', + code: dedent` + import { z } from 'zod'; + z.custom(); + `, + errors: [{ messageId: 'noEmptyCustomSchema' }], + }, { name: 'type without function', code: dedent` diff --git a/src/rules/no-number-schema-with-int.spec.ts b/src/rules/no-number-schema-with-int.spec.ts index 4e691ba..e300434 100644 --- a/src/rules/no-number-schema-with-int.spec.ts +++ b/src/rules/no-number-schema-with-int.spec.ts @@ -8,19 +8,26 @@ const ruleTester = new RuleTester(); ruleTester.run('no-number-schema-with-int', noNumberSchemaWithInt, { valid: [ { - name: 'valid usage', + name: 'namespace import', code: dedent` import * as z from 'zod'; z.int(); `, }, { - name: 'valid usage (named)', + name: 'named import', code: dedent` import int from 'zod'; int(); `, }, + { + name: 'named z import', + code: dedent` + import { z } from 'zod'; + z.int(); + `, + }, { name: 'standard + chain method', code: dedent` @@ -49,7 +56,7 @@ ruleTester.run('no-number-schema-with-int', noNumberSchemaWithInt, { ], invalid: [ { - name: 'number + int', + name: 'number + int (namespace import)', code: ` import * as z from 'zod'; z.number().int(); @@ -61,7 +68,7 @@ ruleTester.run('no-number-schema-with-int', noNumberSchemaWithInt, { `, }, { - name: 'number + int (named)', + name: 'number + int (named import)', code: ` import { number } from 'zod'; number().int(); @@ -69,6 +76,18 @@ ruleTester.run('no-number-schema-with-int', noNumberSchemaWithInt, { errors: [{ messageId: 'removeNumber' }], output: null, }, + { + name: 'number + int (named z import)', + code: ` + import { z } from 'zod'; + z.number().int(); + `, + errors: [{ messageId: 'removeNumber' }], + output: ` + import { z } from 'zod'; + z.int(); + `, + }, { name: 'number + int + other method', code: ` diff --git a/src/rules/no-number-schema-with-int.ts b/src/rules/no-number-schema-with-int.ts index 02c94a6..27e9d36 100644 --- a/src/rules/no-number-schema-with-int.ts +++ b/src/rules/no-number-schema-with-int.ts @@ -67,7 +67,7 @@ export const noNumberSchemaWithInt = ESLintUtils.RuleCreator(getRuleURL)({ } // If it's a named import usage (e.g. `import { number } from 'zod'`), report but do not fix. - if (zodSchemaMeta.importType === 'named') { + if (zodSchemaMeta.schemaDecl === 'named') { context.report({ node, messageId: 'removeNumber', diff --git a/src/rules/no-optional-and-default-together.spec.ts b/src/rules/no-optional-and-default-together.spec.ts index f43cd3d..395a08f 100644 --- a/src/rules/no-optional-and-default-together.spec.ts +++ b/src/rules/no-optional-and-default-together.spec.ts @@ -24,6 +24,13 @@ ruleTester.run( string().default("Hello World") `, }, + { + name: 'schema with only default (named z)', + code: dedent` + import { z } from 'zod'; + z.string().default("Hello World") + `, + }, { name: 'schema with only optional', code: dedent` @@ -106,6 +113,16 @@ ruleTester.run( errors: [{ messageId: 'noOptionalAndDefaultTogether' }], output: null, }, + { + name: 'optional then default - preferredMethod: none (explicit)', + code: dedent` + import { z } from 'zod'; + z.string().optional().default("Hello World") + `, + options: [{ preferredMethod: 'none' }], + errors: [{ messageId: 'noOptionalAndDefaultTogether' }], + output: null, + }, { name: 'optional then default - preferredMethod: none (explicit)', code: dedent` diff --git a/src/rules/no-throw-in-refine.spec.ts b/src/rules/no-throw-in-refine.spec.ts index bd88360..56c7f82 100644 --- a/src/rules/no-throw-in-refine.spec.ts +++ b/src/rules/no-throw-in-refine.spec.ts @@ -1,4 +1,5 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; +import dedent from 'dedent'; import { noThrowInRefine } from './no-throw-in-refine.js'; @@ -8,14 +9,14 @@ ruleTester.run('no-throw-in-refine', noThrowInRefine, { valid: [ { name: 'refine with arrow body shorthand', - code: ` + code: dedent` import * as z from 'zod'; z.number().min(0).refine((val) => true); `, }, { name: 'nested function not reported', - code: ` + code: dedent` import * as z from 'zod'; z.string().refine((val) => { const fn = () => { throw new Error("nested"); }; // nested function is fine @@ -31,7 +32,7 @@ ruleTester.run('no-throw-in-refine', noThrowInRefine, { invalid: [ { name: 'inside arrow function', - code: ` + code: dedent` import * as z from 'zod'; z.string().refine(() => { throw new Error(); }); `, @@ -39,7 +40,7 @@ ruleTester.run('no-throw-in-refine', noThrowInRefine, { }, { name: 'inside arrow function (named)', - code: ` + code: dedent` import { string } from 'zod'; string().refine(() => { throw new Error(); }); `, @@ -47,7 +48,7 @@ ruleTester.run('no-throw-in-refine', noThrowInRefine, { }, { name: 'inside arrow function within if', - code: ` + code: dedent` import * as z from 'zod'; z.number().refine((val) => { if (val < 0) throw new Error('Invalid'); @@ -57,7 +58,7 @@ ruleTester.run('no-throw-in-refine', noThrowInRefine, { }, { name: 'inside arrow function within else', - code: ` + code: dedent` import * as z from 'zod'; z.number().refine((val) => { if (val < 0) return true @@ -68,7 +69,7 @@ ruleTester.run('no-throw-in-refine', noThrowInRefine, { }, { name: 'inside arrow function within cycle', - code: ` + code: dedent` import * as z from 'zod'; z.number().refine((val) => { for (const it of val) { diff --git a/src/rules/no-unknown-schema.spec.ts b/src/rules/no-unknown-schema.spec.ts index 96436de..e4de21f 100644 --- a/src/rules/no-unknown-schema.spec.ts +++ b/src/rules/no-unknown-schema.spec.ts @@ -42,6 +42,14 @@ ruleTester.run('no-unknown-schema', noUnknownSchema, { `, errors: [{ messageId: 'noZUnknown' }], }, + { + name: 'namespace z import', + code: dedent` + import { z } from 'zod'; + const schema = z.unknown(); + `, + errors: [{ messageId: 'noZUnknown' }], + }, { name: 'namespace import within an object', code: dedent` diff --git a/src/rules/prefer-meta-last.spec.ts b/src/rules/prefer-meta-last.spec.ts index 79c464c..abf43af 100644 --- a/src/rules/prefer-meta-last.spec.ts +++ b/src/rules/prefer-meta-last.spec.ts @@ -8,15 +8,29 @@ const ruleTester = new RuleTester(); ruleTester.run('prefer-meta-last', preferMetaLast, { valid: [ { - name: 'valid usage', + name: 'namespace import', code: dedent` import * as z from 'zod'; z.string().meta({ description: "desc" }) `, }, + { + name: 'named import', + code: dedent` + import { string } from 'zod'; + string().meta({ description: "desc" }) + `, + }, + { + name: 'named z import', + code: dedent` + import { z } from 'zod'; + z.string().meta({ description: "desc" }) + `, + }, { name: 'No meta... no error', - code: ` + code: dedent` import * as z from 'zod'; z.string().min(5).max(10); `, @@ -30,7 +44,7 @@ ruleTester.run('prefer-meta-last', preferMetaLast, { }, { name: 'multiple chained meta at the end (still valid)', - code: ` + code: dedent` import * as z from 'zod'; z.string().min(5).max(10).meta({ a: 1 }).meta({ b: 2 }); `, @@ -38,7 +52,7 @@ ruleTester.run('prefer-meta-last', preferMetaLast, { { // https://github.com/marcalexiei/eslint-plugin-zod-x/issues/42 name: 'meta not belonging to zod', - code: ` + code: dedent` export const t = initTRPC .meta() .context() @@ -52,7 +66,7 @@ ruleTester.run('prefer-meta-last', preferMetaLast, { { // https://github.com/marcalexiei/eslint-plugin-zod-x/issues/70 name: 'inside object', - code: ` + code: dedent` import * as z from 'zod'; export const baseEventPayloadSchema = z.object({ type: z.string(), @@ -63,7 +77,7 @@ ruleTester.run('prefer-meta-last', preferMetaLast, { { // https://github.com/marcalexiei/eslint-plugin-zod-x/issues/42 name: 'inside looseObject', - code: ` + code: dedent` import * as z from 'zod'; export const baseEventPayloadSchema = z.looseObject({ type: z.string(), @@ -74,7 +88,7 @@ ruleTester.run('prefer-meta-last', preferMetaLast, { { // https://github.com/marcalexiei/eslint-plugin-zod-x/issues/42 name: 'inside strictObject', - code: ` + code: dedent` import * as z from 'zod'; export const baseEventPayloadSchema = z.strictObject({ type: z.string(), @@ -86,7 +100,7 @@ ruleTester.run('prefer-meta-last', preferMetaLast, { invalid: [ { - name: 'meta() before another method', + name: 'meta() before another method (namespace import)', code: dedent` import * as z from 'zod'; z.string().meta({ description: "desc" }).trim(); @@ -97,6 +111,30 @@ ruleTester.run('prefer-meta-last', preferMetaLast, { z.string().trim().meta({ description: "desc" }); `, }, + { + name: 'meta() before another method (named import)', + code: dedent` + import { string } from 'zod'; + string().meta({ description: "desc" }).trim(); + `, + errors: [{ messageId: 'metaNotLast' }], + output: dedent` + import { string } from 'zod'; + string().trim().meta({ description: "desc" }); + `, + }, + { + name: 'meta() before another method (named z import)', + code: dedent` + import { z } from 'zod'; + z.string().meta({ description: "desc" }).trim(); + `, + errors: [{ messageId: 'metaNotLast' }], + output: dedent` + import { z } from 'zod'; + z.string().trim().meta({ description: "desc" }); + `, + }, { name: 'meta() followed by transform()', code: dedent` diff --git a/src/rules/prefer-meta.spec.ts b/src/rules/prefer-meta.spec.ts index d41fb73..d662fe5 100644 --- a/src/rules/prefer-meta.spec.ts +++ b/src/rules/prefer-meta.spec.ts @@ -8,19 +8,26 @@ const ruleTester = new RuleTester(); ruleTester.run('prefer-meta', preferMeta, { valid: [ { - name: 'valid usage', + name: 'namespace import', code: dedent` import * as z from 'zod'; z.string().meta({ description: "desc" }); `, }, { - name: 'valid usage (named)', + name: 'named import', code: dedent` import { string } from 'zod'; string().meta({ description: "desc" }); `, }, + { + name: 'named z import', + code: dedent` + import { z } from 'zod'; + z.string().meta({ description: "desc" }); + `, + }, { name: 'valid usage (not last method)', code: dedent` @@ -45,7 +52,7 @@ ruleTester.run('prefer-meta', preferMeta, { { // https://github.com/marcalexiei/eslint-plugin-zod-x/issues/121 name: 'ignores non-zod describe methods', - code: ` + code: dedent` import { test } from "@playwright/test"; test.describe("test", () => {}); `, @@ -54,7 +61,7 @@ ruleTester.run('prefer-meta', preferMeta, { invalid: [ { - name: 'describe with string', + name: 'describe with string (namespace import)', code: dedent` import * as z from 'zod'; z.string().describe("desc").trim(); @@ -66,7 +73,7 @@ ruleTester.run('prefer-meta', preferMeta, { `, }, { - name: 'describe with string (named)', + name: 'describe with string (named import)', code: dedent` import { string } from 'zod'; string().describe("desc").trim(); @@ -77,6 +84,18 @@ ruleTester.run('prefer-meta', preferMeta, { string().meta({ description: "desc" }).trim(); `, }, + { + name: 'describe with string (named z import)', + code: dedent` + import { z } from 'zod'; + z.string().describe("desc").trim(); + `, + errors: [{ messageId: 'preferMeta' }], + output: dedent` + import { z } from 'zod'; + z.string().meta({ description: "desc" }).trim(); + `, + }, { name: 'describe with literal', code: dedent` diff --git a/src/rules/require-brand-type-parameter.spec.ts b/src/rules/require-brand-type-parameter.spec.ts index 31620e2..65bbbdb 100644 --- a/src/rules/require-brand-type-parameter.spec.ts +++ b/src/rules/require-brand-type-parameter.spec.ts @@ -8,19 +8,26 @@ const ruleTester = new RuleTester(); ruleTester.run('require-brand-type-parameter', requireBrandTypeParameter, { valid: [ { - name: 'correct usage', + name: 'namespace', code: dedent` import * as z from 'zod' z.string().min(1).brand<"aaa">(); `, }, { - name: 'correct usage (named)', + name: 'named import', code: dedent` import { string } from 'zod' string().min(1).brand<"aaa">(); `, }, + { + name: 'named z import', + code: dedent` + import { z } from 'zod' + z.string().min(1).brand<"aaa">(); + `, + }, { name: 'no error on other brand function', code: dedent` @@ -55,7 +62,7 @@ ruleTester.run('require-brand-type-parameter', requireBrandTypeParameter, { invalid: [ { - name: 'invalid', + name: 'namespace import', code: dedent` import * as z from 'zod'; z.string().min(1).brand(); @@ -76,7 +83,7 @@ ruleTester.run('require-brand-type-parameter', requireBrandTypeParameter, { ], }, { - name: 'invalid (named)', + name: 'named import', code: dedent` import { string } from 'zod'; string().min(1).brand(); @@ -96,6 +103,27 @@ ruleTester.run('require-brand-type-parameter', requireBrandTypeParameter, { }, ], }, + { + name: 'named z import', + code: dedent` + import { z } from 'zod'; + z.string().min(1).brand(); + `, + errors: [ + { + messageId: 'missingTypeParameter', + suggestions: [ + { + messageId: 'removeBrandFunction', + output: dedent` + import { z } from 'zod'; + z.string().min(1); + `, + }, + ], + }, + ], + }, { name: 'brand without type parameter in complex chain', code: dedent` @@ -118,7 +146,7 @@ ruleTester.run('require-brand-type-parameter', requireBrandTypeParameter, { ], }, { - name: 'brand without type parameter not as last method', + name: 'brand without type parameter not as last method (namespace import)', code: dedent` import * as z from 'zod'; z.string().min(1).brand().max(2) @@ -139,7 +167,7 @@ ruleTester.run('require-brand-type-parameter', requireBrandTypeParameter, { ], }, { - name: 'brand without type parameter not as last method (named)', + name: 'brand without type parameter not as last method (named import)', code: dedent` import { string } from 'zod'; string().min(1).brand().max(2) @@ -159,5 +187,26 @@ ruleTester.run('require-brand-type-parameter', requireBrandTypeParameter, { }, ], }, + { + name: 'brand without type parameter not as last method (named z import)', + code: dedent` + import { z } from 'zod'; + z.string().min(1).brand().max(2) + `, + errors: [ + { + messageId: 'missingTypeParameter', + suggestions: [ + { + messageId: 'removeBrandFunction', + output: dedent` + import { z } from 'zod'; + z.string().min(1).max(2) + `, + }, + ], + }, + ], + }, ], }); diff --git a/src/rules/require-error-message.spec.ts b/src/rules/require-error-message.spec.ts index c3f0f5e..c99223c 100644 --- a/src/rules/require-error-message.spec.ts +++ b/src/rules/require-error-message.spec.ts @@ -8,19 +8,26 @@ const ruleTester = new RuleTester(); ruleTester.run('prefer-strict-object (refine)', requireErrorMessage, { valid: [ { - name: 'object with error property', + name: 'object with error property (namespace import)', code: dedent` import * as z from 'zod'; z.string().refine(() => true, { error: "error msg" }); `, }, { - name: 'object with error property (named)', + name: 'object with error property (named import)', code: dedent` import { string } from 'zod'; string().refine(() => true, { error: "error msg" }); `, }, + { + name: 'object with error property (named z import)', + code: dedent` + import { z } from 'zod'; + z.string().refine(() => true, { error: "error msg" }); + `, + }, { name: 'string error message', code: dedent` diff --git a/src/rules/require-schema-suffix.spec.ts b/src/rules/require-schema-suffix.spec.ts index 8f4c1ac..1876943 100644 --- a/src/rules/require-schema-suffix.spec.ts +++ b/src/rules/require-schema-suffix.spec.ts @@ -97,6 +97,7 @@ ruleTester.run('require-schema-suffix', requireSchemaSuffix, { invalid: [ { + name: 'namespace', code: dedent` import * as z from 'zod'; const myVar = z.string(); @@ -110,6 +111,7 @@ ruleTester.run('require-schema-suffix', requireSchemaSuffix, { output: null, }, { + name: 'named', code: dedent` import { string } from 'zod'; const myVar = string(); @@ -123,6 +125,7 @@ ruleTester.run('require-schema-suffix', requireSchemaSuffix, { output: null, }, { + name: 'named with chained method', code: dedent` import { string } from 'zod'; const myVar = string().min(1); diff --git a/src/rules/schema-error-property-style.spec.ts b/src/rules/schema-error-property-style.spec.ts index 895f97f..d6fbeb3 100644 --- a/src/rules/schema-error-property-style.spec.ts +++ b/src/rules/schema-error-property-style.spec.ts @@ -8,19 +8,26 @@ const ruleTester = new RuleTester(); ruleTester.run('error-style (custom)', schemaErrorPropertyStyle, { valid: [ { - name: 'default option', + name: 'default option (namespace import)', code: dedent` import * as z from 'zod'; z.custom(() => true, { error: "my error" }) `, }, { - name: 'default option (named)', + name: 'default option (named import)', code: dedent` import { custom } from 'zod'; custom(() => true, { error: "my error" }) `, }, + { + name: 'default option (named z import)', + code: dedent` + import { z } from 'zod'; + z.custom(() => true, { error: "my error" }) + `, + }, { name: 'default option non-zod', code: dedent` @@ -81,11 +88,17 @@ ruleTester.run('error-style (refine)', schemaErrorPropertyStyle, { valid: [ { name: 'default option', - code: 'z.string().refine(() => true, { error: "my error" })', + code: dedent` + import * as z from 'zod'; + z.string().refine(() => true, { error: "my error" }); + `, }, { name: 'default with template string', - code: 'z.string().refine(() => true, `asd`)', + code: dedent` + import * as z from 'zod'; + z.string().refine(() => true, \`asd\`); + `, }, ], invalid: [ diff --git a/src/rules/schema-error-property-style.ts b/src/rules/schema-error-property-style.ts index d36b58b..0b74c4d 100644 --- a/src/rules/schema-error-property-style.ts +++ b/src/rules/schema-error-property-style.ts @@ -45,7 +45,6 @@ export const schemaErrorPropertyStyle = ESLintUtils.RuleCreator(getRuleURL)< ], create(context, [{ selector, example }]) { const { - // importDeclarationListener, detectZodSchemaRootNode, collectZodChainMethods, diff --git a/src/utils/detect-zod-schema-root-node.ts b/src/utils/detect-zod-schema-root-node.ts index 138a609..adca6d4 100644 --- a/src/utils/detect-zod-schema-root-node.ts +++ b/src/utils/detect-zod-schema-root-node.ts @@ -3,10 +3,21 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; interface DetectData { - importType: 'namespace' | 'named'; - schemaType: string; // the "factory" for the outer expression - methods: Array; // full chain in call order, e.g. ["number","int","min"] - node: TSESTree.CallExpression; // the outer call expression analyzed + /** + * How the schema is declared: + * - `namespace` -> `z.string()` + * - `named` -> `string()` + */ + schemaDecl: 'namespace' | 'named'; + + /** the "factory" for the outer expression */ + schemaType: string; + + /** full chain in call order, e.g. ["number","int","min"] */ + methods: Array; + + /** the outer call expression analyzed */ + node: TSESTree.CallExpression; } /** @@ -16,7 +27,7 @@ interface DetectData { export type DetectResult = | (DetectData & { // innerSchemas: Array<{ - // importType: 'namespace' | 'named'; + // schemaDecl: 'namespace' | 'named'; // schemaType: string; // methods: Array; // node: TSESTree.CallExpression; @@ -73,7 +84,7 @@ function isOutermostCallExpression(node: TSESTree.CallExpression): boolean { * This helper DOES NOT require the call to be outermost. * * Returns: - * { importType, schemaType, methods, node } if successful + * { schemaDecl, schemaType, methods, node } if successful * null otherwise */ function parseZodCallExpression( @@ -81,7 +92,7 @@ function parseZodCallExpression( zodNamespaces: Set, zodNamedImports: Set, ): { - importType: 'namespace' | 'named'; + schemaDecl: 'namespace' | 'named'; schemaType: string; methods: Array; node: TSESTree.CallExpression; @@ -134,7 +145,7 @@ function parseZodCallExpression( return null; } return { - importType: 'namespace', + schemaDecl: 'namespace', schemaType: factory, methods, node: call, @@ -153,7 +164,7 @@ function parseZodCallExpression( // If methods exist and the first item equals factory, that's odd but we'll prefer explicit factory: // Return schemaType as factory return { - importType: 'named', + schemaDecl: 'named', schemaType: factory, methods, node: call, @@ -260,7 +271,7 @@ export function detectZodSchemaRootNode( // } return { - importType: outer.importType, + schemaDecl: outer.schemaDecl, schemaType: outer.schemaType, methods: outer.methods, node: call, diff --git a/src/utils/track-zod-schema-imports.ts b/src/utils/track-zod-schema-imports.ts index 2c39485..85b5dd7 100644 --- a/src/utils/track-zod-schema-imports.ts +++ b/src/utils/track-zod-schema-imports.ts @@ -104,7 +104,15 @@ export function trackZodSchemaImports(): Result { break; case AST_NODE_TYPES.ImportSpecifier: - zodNamedImports.add(spec.local.name); + // If the user imports `z` via a named import, it acts as a namespace. + // Therefore, it must be recorded in the appropriate set. + // We check the imported identifier because the user may alias it. + if ('name' in spec.imported && spec.imported.name === 'z') { + zodNamespaces.add(spec.local.name); + } else { + zodNamedImports.add(spec.local.name); + } + break; // no default