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