eslint-plugin-zod 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,12 +39,14 @@
39
39
  | [no-empty-custom-schema](docs/rules/no-empty-custom-schema.md) | Disallow usage of `z.custom()` without arguments | ✅ | | | |
40
40
  | [no-number-schema-with-int](docs/rules/no-number-schema-with-int.md) | Disallow usage of `z.number().int()` as it is considered legacy | ✅ | 🔧 | | |
41
41
  | [no-optional-and-default-together](docs/rules/no-optional-and-default-together.md) | Disallow using both `.optional()` and `.default()` on the same Zod schema | ✅ | 🔧 | | |
42
+ | [no-string-schema-with-uuid](docs/rules/no-string-schema-with-uuid.md) | Disallow usage of `z.string().uuid()` in favor of the dedicated `z.uuid()` schema | ✅ | 🔧 | | |
42
43
  | [no-throw-in-refine](docs/rules/no-throw-in-refine.md) | Disallow throwing errors directly inside Zod refine callbacks | ✅ | | | |
43
44
  | [no-unknown-schema](docs/rules/no-unknown-schema.md) | Disallow usage of `z.unknown()` in Zod schemas | | | | |
44
45
  | [prefer-enum-over-literal-union](docs/rules/prefer-enum-over-literal-union.md) | Prefer `z.enum()` over `z.union()` when all members are string literals. | ✅ | 🔧 | | |
45
46
  | [prefer-meta](docs/rules/prefer-meta.md) | Enforce usage of `.meta()` over `.describe()` | ✅ | 🔧 | | |
46
47
  | [prefer-meta-last](docs/rules/prefer-meta-last.md) | Enforce `.meta()` as last method | ✅ | 🔧 | | |
47
48
  | [prefer-namespace-import](docs/rules/prefer-namespace-import.md) | Enforce importing zod as a namespace import (`import * as z from 'zod'`) | | 🔧 | | ❌ |
49
+ | [prefer-string-schema-with-trim](docs/rules/prefer-string-schema-with-trim.md) | Enforce `z.string().trim()` to prevent accidental leading/trailing whitespace | ✅ | 🔧 | | |
48
50
  | [require-brand-type-parameter](docs/rules/require-brand-type-parameter.md) | Require type parameter on `.brand()` functions | ✅ | | 💡 | |
49
51
  | [require-error-message](docs/rules/require-error-message.md) | Enforce that custom refinements include an error message | ✅ | 🔧 | | |
50
52
  | [require-schema-suffix](docs/rules/require-schema-suffix.md) | Require schema suffix when declaring a Zod schema | ✅ | | | |
package/dist/index.cjs CHANGED
@@ -9,12 +9,14 @@ const no_any_schema_js_1 = require("./rules/no-any-schema.cjs");
9
9
  const no_empty_custom_schema_js_1 = require("./rules/no-empty-custom-schema.cjs");
10
10
  const no_number_schema_with_int_js_1 = require("./rules/no-number-schema-with-int.cjs");
11
11
  const no_optional_and_default_together_js_1 = require("./rules/no-optional-and-default-together.cjs");
12
+ const no_string_schema_with_uuid_js_1 = require("./rules/no-string-schema-with-uuid.cjs");
12
13
  const no_throw_in_refine_js_1 = require("./rules/no-throw-in-refine.cjs");
13
14
  const no_unknown_schema_js_1 = require("./rules/no-unknown-schema.cjs");
14
15
  const prefer_enum_over_literal_union_js_1 = require("./rules/prefer-enum-over-literal-union.cjs");
15
16
  const prefer_meta_last_js_1 = require("./rules/prefer-meta-last.cjs");
16
17
  const prefer_meta_js_1 = require("./rules/prefer-meta.cjs");
17
18
  const prefer_namespace_import_js_1 = require("./rules/prefer-namespace-import.cjs");
19
+ const prefer_string_schema_with_trim_js_1 = require("./rules/prefer-string-schema-with-trim.cjs");
18
20
  const require_brand_type_parameter_js_1 = require("./rules/require-brand-type-parameter.cjs");
19
21
  const require_error_message_js_1 = require("./rules/require-error-message.cjs");
20
22
  const require_schema_suffix_js_1 = require("./rules/require-schema-suffix.cjs");
@@ -32,6 +34,7 @@ const eslintPluginZod = {
32
34
  'no-any-schema': no_any_schema_js_1.noAnySchema,
33
35
  'no-empty-custom-schema': no_empty_custom_schema_js_1.noEmptyCustomSchema,
34
36
  'no-number-schema-with-int': no_number_schema_with_int_js_1.noNumberSchemaWithInt,
37
+ 'no-string-schema-with-uuid': no_string_schema_with_uuid_js_1.noStringSchemaWithUuid,
35
38
  'no-optional-and-default-together': no_optional_and_default_together_js_1.noOptionalAndDefaultTogether,
36
39
  'no-throw-in-refine': no_throw_in_refine_js_1.noThrowInRefine,
37
40
  'no-unknown-schema': no_unknown_schema_js_1.noUnknownSchema,
@@ -39,6 +42,7 @@ const eslintPluginZod = {
39
42
  'prefer-meta': prefer_meta_js_1.preferMeta,
40
43
  'prefer-meta-last': prefer_meta_last_js_1.preferMetaLast,
41
44
  'prefer-namespace-import': prefer_namespace_import_js_1.preferNamespaceImport,
45
+ 'prefer-string-schema-with-trim': prefer_string_schema_with_trim_js_1.preferStringSchemaWithTrim,
42
46
  'require-brand-type-parameter': require_brand_type_parameter_js_1.requireBrandTypeParameter,
43
47
  'require-error-message': require_error_message_js_1.requireErrorMessage,
44
48
  'require-schema-suffix': require_schema_suffix_js_1.requireSchemaSuffix,
@@ -57,11 +61,13 @@ const recommendedConfig = {
57
61
  'zod/no-any-schema': 'error',
58
62
  'zod/no-empty-custom-schema': 'error',
59
63
  'zod/no-number-schema-with-int': 'error',
64
+ 'zod/no-string-schema-with-uuid': 'error',
60
65
  'zod/no-optional-and-default-together': 'error',
61
66
  'zod/no-throw-in-refine': 'error',
62
67
  'zod/prefer-enum-over-literal-union': 'error',
63
68
  'zod/prefer-meta': 'error',
64
69
  'zod/prefer-meta-last': 'error',
70
+ 'zod/prefer-string-schema-with-trim': 'error',
65
71
  'zod/require-brand-type-parameter': 'error',
66
72
  'zod/require-error-message': 'error',
67
73
  'zod/require-schema-suffix': 'error',
package/dist/index.js CHANGED
@@ -7,12 +7,14 @@ import { noAnySchema } from "./rules/no-any-schema.js";
7
7
  import { noEmptyCustomSchema } from "./rules/no-empty-custom-schema.js";
8
8
  import { noNumberSchemaWithInt } from "./rules/no-number-schema-with-int.js";
9
9
  import { noOptionalAndDefaultTogether } from "./rules/no-optional-and-default-together.js";
10
+ import { noStringSchemaWithUuid } from "./rules/no-string-schema-with-uuid.js";
10
11
  import { noThrowInRefine } from "./rules/no-throw-in-refine.js";
11
12
  import { noUnknownSchema } from "./rules/no-unknown-schema.js";
12
13
  import { preferEnumOverLiteralUnion } from "./rules/prefer-enum-over-literal-union.js";
13
14
  import { preferMetaLast } from "./rules/prefer-meta-last.js";
14
15
  import { preferMeta } from "./rules/prefer-meta.js";
15
16
  import { preferNamespaceImport } from "./rules/prefer-namespace-import.js";
17
+ import { preferStringSchemaWithTrim } from "./rules/prefer-string-schema-with-trim.js";
16
18
  import { requireBrandTypeParameter } from "./rules/require-brand-type-parameter.js";
17
19
  import { requireErrorMessage } from "./rules/require-error-message.js";
18
20
  import { requireSchemaSuffix } from "./rules/require-schema-suffix.js";
@@ -30,6 +32,7 @@ const eslintPluginZod = {
30
32
  'no-any-schema': noAnySchema,
31
33
  'no-empty-custom-schema': noEmptyCustomSchema,
32
34
  'no-number-schema-with-int': noNumberSchemaWithInt,
35
+ 'no-string-schema-with-uuid': noStringSchemaWithUuid,
33
36
  'no-optional-and-default-together': noOptionalAndDefaultTogether,
34
37
  'no-throw-in-refine': noThrowInRefine,
35
38
  'no-unknown-schema': noUnknownSchema,
@@ -37,6 +40,7 @@ const eslintPluginZod = {
37
40
  'prefer-meta': preferMeta,
38
41
  'prefer-meta-last': preferMetaLast,
39
42
  'prefer-namespace-import': preferNamespaceImport,
43
+ 'prefer-string-schema-with-trim': preferStringSchemaWithTrim,
40
44
  'require-brand-type-parameter': requireBrandTypeParameter,
41
45
  'require-error-message': requireErrorMessage,
42
46
  'require-schema-suffix': requireSchemaSuffix,
@@ -55,11 +59,13 @@ const recommendedConfig = {
55
59
  'zod/no-any-schema': 'error',
56
60
  'zod/no-empty-custom-schema': 'error',
57
61
  'zod/no-number-schema-with-int': 'error',
62
+ 'zod/no-string-schema-with-uuid': 'error',
58
63
  'zod/no-optional-and-default-together': 'error',
59
64
  'zod/no-throw-in-refine': 'error',
60
65
  'zod/prefer-enum-over-literal-union': 'error',
61
66
  'zod/prefer-meta': 'error',
62
67
  'zod/prefer-meta-last': 'error',
68
+ 'zod/prefer-string-schema-with-trim': 'error',
63
69
  'zod/require-brand-type-parameter': 'error',
64
70
  'zod/require-error-message': 'error',
65
71
  'zod/require-schema-suffix': 'error',
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noNumberSchemaWithInt = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const meta_js_1 = require("../meta.cjs");
6
+ const build_zod_chain_replacement_fix_js_1 = require("../utils/build-zod-chain-replacement-fix.cjs");
6
7
  const track_zod_schema_imports_js_1 = require("../utils/track-zod-schema-imports.cjs");
7
8
  exports.noNumberSchemaWithInt = utils_1.ESLintUtils.RuleCreator(meta_js_1.getRuleURL)({
8
9
  name: 'no-number-schema-with-int',
@@ -29,13 +30,11 @@ exports.noNumberSchemaWithInt = utils_1.ESLintUtils.RuleCreator(meta_js_1.getRul
29
30
  return;
30
31
  }
31
32
  const methods = collectZodChainMethods(node);
32
- const numberIndex = methods.findIndex((m) => m.name === 'number');
33
33
  const intIndex = methods.findIndex((m) => m.name === 'int');
34
- if (numberIndex === -1 || intIndex === -1) {
34
+ if (intIndex === -1) {
35
35
  return;
36
36
  }
37
- const numberNode = methods[numberIndex].node;
38
- const intNode = methods[intIndex].node;
37
+ const numberIndex = methods.findIndex((m) => m.name === 'number');
39
38
  if (zodSchemaMeta.schemaDecl === 'named') {
40
39
  context.report({
41
40
  node,
@@ -47,18 +46,14 @@ exports.noNumberSchemaWithInt = utils_1.ESLintUtils.RuleCreator(meta_js_1.getRul
47
46
  node,
48
47
  messageId: 'removeNumber',
49
48
  fix(fixer) {
50
- const numberCallee = numberNode.callee;
51
- const prefixObj = numberCallee.object;
52
- const prefixText = sourceCode.getText(prefixObj);
53
- const methodsBetween = methods.slice(numberIndex + 1, intIndex);
54
- const betweenSuffixes = methodsBetween.map((m) => {
55
- const callee = m.node.callee;
56
- const objText = sourceCode.getText(callee.object);
57
- const fullText = sourceCode.getText(m.node);
58
- return fullText.slice(objText.length);
49
+ return (0, build_zod_chain_replacement_fix_js_1.buildZodChainReplacementFix)({
50
+ sourceCode,
51
+ fixer,
52
+ methods,
53
+ fromIndex: numberIndex,
54
+ toIndex: intIndex,
55
+ toMethodName: 'int',
59
56
  });
60
- const replacement = `${prefixText}.int()${betweenSuffixes.join('')}`;
61
- return fixer.replaceTextRange([numberNode.range[0], intNode.range[1]], replacement);
62
57
  },
63
58
  });
64
59
  },
@@ -1,5 +1,6 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
2
  import { getRuleURL } from "../meta.js";
3
+ import { buildZodChainReplacementFix } from "../utils/build-zod-chain-replacement-fix.js";
3
4
  import { trackZodSchemaImports } from "../utils/track-zod-schema-imports.js";
4
5
  export const noNumberSchemaWithInt = ESLintUtils.RuleCreator(getRuleURL)({
5
6
  name: 'no-number-schema-with-int',
@@ -26,13 +27,11 @@ export const noNumberSchemaWithInt = ESLintUtils.RuleCreator(getRuleURL)({
26
27
  return;
27
28
  }
28
29
  const methods = collectZodChainMethods(node);
29
- const numberIndex = methods.findIndex((m) => m.name === 'number');
30
30
  const intIndex = methods.findIndex((m) => m.name === 'int');
31
- if (numberIndex === -1 || intIndex === -1) {
31
+ if (intIndex === -1) {
32
32
  return;
33
33
  }
34
- const numberNode = methods[numberIndex].node;
35
- const intNode = methods[intIndex].node;
34
+ const numberIndex = methods.findIndex((m) => m.name === 'number');
36
35
  if (zodSchemaMeta.schemaDecl === 'named') {
37
36
  context.report({
38
37
  node,
@@ -44,18 +43,14 @@ export const noNumberSchemaWithInt = ESLintUtils.RuleCreator(getRuleURL)({
44
43
  node,
45
44
  messageId: 'removeNumber',
46
45
  fix(fixer) {
47
- const numberCallee = numberNode.callee;
48
- const prefixObj = numberCallee.object;
49
- const prefixText = sourceCode.getText(prefixObj);
50
- const methodsBetween = methods.slice(numberIndex + 1, intIndex);
51
- const betweenSuffixes = methodsBetween.map((m) => {
52
- const callee = m.node.callee;
53
- const objText = sourceCode.getText(callee.object);
54
- const fullText = sourceCode.getText(m.node);
55
- return fullText.slice(objText.length);
46
+ return buildZodChainReplacementFix({
47
+ sourceCode,
48
+ fixer,
49
+ methods,
50
+ fromIndex: numberIndex,
51
+ toIndex: intIndex,
52
+ toMethodName: 'int',
56
53
  });
57
- const replacement = `${prefixText}.int()${betweenSuffixes.join('')}`;
58
- return fixer.replaceTextRange([numberNode.range[0], intNode.range[1]], replacement);
59
54
  },
60
55
  });
61
56
  },
@@ -35,11 +35,11 @@ exports.noOptionalAndDefaultTogether = utils_1.ESLintUtils.RuleCreator(meta_js_1
35
35
  defaultOptions: [defaultOptions],
36
36
  create(context, [{ preferredMethod }]) {
37
37
  const { sourceCode } = context;
38
- const { importDeclarationListener, detectZodSchemaRootNode: isZodSchema, collectZodChainMethods, } = (0, track_zod_schema_imports_js_1.trackZodSchemaImports)();
38
+ const { importDeclarationListener, detectZodSchemaRootNode, collectZodChainMethods, } = (0, track_zod_schema_imports_js_1.trackZodSchemaImports)();
39
39
  return {
40
40
  ImportDeclaration: importDeclarationListener,
41
41
  CallExpression(node) {
42
- if (!isZodSchema(node)) {
42
+ if (!detectZodSchemaRootNode(node)) {
43
43
  return;
44
44
  }
45
45
  const methods = collectZodChainMethods(node);
@@ -32,11 +32,11 @@ export const noOptionalAndDefaultTogether = ESLintUtils.RuleCreator(getRuleURL)(
32
32
  defaultOptions: [defaultOptions],
33
33
  create(context, [{ preferredMethod }]) {
34
34
  const { sourceCode } = context;
35
- const { importDeclarationListener, detectZodSchemaRootNode: isZodSchema, collectZodChainMethods, } = trackZodSchemaImports();
35
+ const { importDeclarationListener, detectZodSchemaRootNode, collectZodChainMethods, } = trackZodSchemaImports();
36
36
  return {
37
37
  ImportDeclaration: importDeclarationListener,
38
38
  CallExpression(node) {
39
- if (!isZodSchema(node)) {
39
+ if (!detectZodSchemaRootNode(node)) {
40
40
  return;
41
41
  }
42
42
  const methods = collectZodChainMethods(node);
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noStringSchemaWithUuid = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const meta_js_1 = require("../meta.cjs");
6
+ const build_zod_chain_replacement_fix_js_1 = require("../utils/build-zod-chain-replacement-fix.cjs");
7
+ const track_zod_schema_imports_js_1 = require("../utils/track-zod-schema-imports.cjs");
8
+ exports.noStringSchemaWithUuid = utils_1.ESLintUtils.RuleCreator(meta_js_1.getRuleURL)({
9
+ name: 'no-string-schema-with-uuid',
10
+ meta: {
11
+ fixable: 'code',
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Disallow usage of `z.string().uuid()` in favor of the dedicated `z.uuid()` schema',
15
+ url: 'https://zod.dev/api#uuids',
16
+ },
17
+ messages: {
18
+ useUuid: '`z.string().uuid()` is redundant. Use `z.uuid()` instead.',
19
+ },
20
+ schema: [],
21
+ },
22
+ defaultOptions: [],
23
+ create(context) {
24
+ const { sourceCode } = context;
25
+ const { importDeclarationListener, detectZodSchemaRootNode, collectZodChainMethods, } = (0, track_zod_schema_imports_js_1.trackZodSchemaImports)();
26
+ return {
27
+ ImportDeclaration: importDeclarationListener,
28
+ CallExpression(node) {
29
+ const zodSchemaMeta = detectZodSchemaRootNode(node);
30
+ if ((zodSchemaMeta === null || zodSchemaMeta === void 0 ? void 0 : zodSchemaMeta.schemaType) !== 'string') {
31
+ return;
32
+ }
33
+ const methods = collectZodChainMethods(node);
34
+ const uuidIndex = methods.findIndex((m) => m.name === 'uuid');
35
+ if (uuidIndex === -1) {
36
+ return;
37
+ }
38
+ const stringIndex = methods.findIndex((m) => m.name === 'string');
39
+ if (zodSchemaMeta.schemaDecl === 'named') {
40
+ context.report({
41
+ node,
42
+ messageId: 'useUuid',
43
+ });
44
+ return;
45
+ }
46
+ context.report({
47
+ node,
48
+ messageId: 'useUuid',
49
+ fix(fixer) {
50
+ return (0, build_zod_chain_replacement_fix_js_1.buildZodChainReplacementFix)({
51
+ sourceCode,
52
+ fixer,
53
+ methods,
54
+ fromIndex: stringIndex,
55
+ toIndex: uuidIndex,
56
+ toMethodName: 'uuid',
57
+ });
58
+ },
59
+ });
60
+ },
61
+ };
62
+ },
63
+ });
@@ -0,0 +1,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const noStringSchemaWithUuid: ESLintUtils.RuleModule<"useUuid", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -0,0 +1,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const noStringSchemaWithUuid: ESLintUtils.RuleModule<"useUuid", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -0,0 +1,60 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import { getRuleURL } from "../meta.js";
3
+ import { buildZodChainReplacementFix } from "../utils/build-zod-chain-replacement-fix.js";
4
+ import { trackZodSchemaImports } from "../utils/track-zod-schema-imports.js";
5
+ export const noStringSchemaWithUuid = ESLintUtils.RuleCreator(getRuleURL)({
6
+ name: 'no-string-schema-with-uuid',
7
+ meta: {
8
+ fixable: 'code',
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Disallow usage of `z.string().uuid()` in favor of the dedicated `z.uuid()` schema',
12
+ url: 'https://zod.dev/api#uuids',
13
+ },
14
+ messages: {
15
+ useUuid: '`z.string().uuid()` is redundant. Use `z.uuid()` instead.',
16
+ },
17
+ schema: [],
18
+ },
19
+ defaultOptions: [],
20
+ create(context) {
21
+ const { sourceCode } = context;
22
+ const { importDeclarationListener, detectZodSchemaRootNode, collectZodChainMethods, } = trackZodSchemaImports();
23
+ return {
24
+ ImportDeclaration: importDeclarationListener,
25
+ CallExpression(node) {
26
+ const zodSchemaMeta = detectZodSchemaRootNode(node);
27
+ if ((zodSchemaMeta === null || zodSchemaMeta === void 0 ? void 0 : zodSchemaMeta.schemaType) !== 'string') {
28
+ return;
29
+ }
30
+ const methods = collectZodChainMethods(node);
31
+ const uuidIndex = methods.findIndex((m) => m.name === 'uuid');
32
+ if (uuidIndex === -1) {
33
+ return;
34
+ }
35
+ const stringIndex = methods.findIndex((m) => m.name === 'string');
36
+ if (zodSchemaMeta.schemaDecl === 'named') {
37
+ context.report({
38
+ node,
39
+ messageId: 'useUuid',
40
+ });
41
+ return;
42
+ }
43
+ context.report({
44
+ node,
45
+ messageId: 'useUuid',
46
+ fix(fixer) {
47
+ return buildZodChainReplacementFix({
48
+ sourceCode,
49
+ fixer,
50
+ methods,
51
+ fromIndex: stringIndex,
52
+ toIndex: uuidIndex,
53
+ toMethodName: 'uuid',
54
+ });
55
+ },
56
+ });
57
+ },
58
+ };
59
+ },
60
+ });
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferStringSchemaWithTrim = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const meta_js_1 = require("../meta.cjs");
6
+ const track_zod_schema_imports_js_1 = require("../utils/track-zod-schema-imports.cjs");
7
+ exports.preferStringSchemaWithTrim = utils_1.ESLintUtils.RuleCreator(meta_js_1.getRuleURL)({
8
+ name: 'prefer-string-schema-with-trim',
9
+ meta: {
10
+ type: 'problem',
11
+ fixable: 'code',
12
+ docs: {
13
+ description: 'Enforce `z.string().trim()` to prevent accidental leading/trailing whitespace',
14
+ },
15
+ messages: {
16
+ addTrim: '`z.string()` schemas should use `.trim()`.',
17
+ },
18
+ schema: [],
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ const { importDeclarationListener, detectZodSchemaRootNode, collectZodChainMethods, } = (0, track_zod_schema_imports_js_1.trackZodSchemaImports)();
23
+ return {
24
+ ImportDeclaration: importDeclarationListener,
25
+ CallExpression(node) {
26
+ const zodSchemaMeta = detectZodSchemaRootNode(node);
27
+ if ((zodSchemaMeta === null || zodSchemaMeta === void 0 ? void 0 : zodSchemaMeta.schemaType) !== 'string') {
28
+ return;
29
+ }
30
+ const methods = collectZodChainMethods(zodSchemaMeta.node);
31
+ if (methods.some((it) => it.name === 'trim')) {
32
+ return;
33
+ }
34
+ if (zodSchemaMeta.schemaDecl === 'named') {
35
+ context.report({
36
+ node,
37
+ messageId: 'addTrim',
38
+ });
39
+ return;
40
+ }
41
+ context.report({
42
+ node,
43
+ messageId: 'addTrim',
44
+ fix(fixer) {
45
+ const lastMethod = methods.at(0);
46
+ return fixer.insertTextAfter(lastMethod.node, '.trim()');
47
+ },
48
+ });
49
+ },
50
+ };
51
+ },
52
+ });
@@ -0,0 +1,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const preferStringSchemaWithTrim: ESLintUtils.RuleModule<"addTrim", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -0,0 +1,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const preferStringSchemaWithTrim: ESLintUtils.RuleModule<"addTrim", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -0,0 +1,49 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import { getRuleURL } from "../meta.js";
3
+ import { trackZodSchemaImports } from "../utils/track-zod-schema-imports.js";
4
+ export const preferStringSchemaWithTrim = ESLintUtils.RuleCreator(getRuleURL)({
5
+ name: 'prefer-string-schema-with-trim',
6
+ meta: {
7
+ type: 'problem',
8
+ fixable: 'code',
9
+ docs: {
10
+ description: 'Enforce `z.string().trim()` to prevent accidental leading/trailing whitespace',
11
+ },
12
+ messages: {
13
+ addTrim: '`z.string()` schemas should use `.trim()`.',
14
+ },
15
+ schema: [],
16
+ },
17
+ defaultOptions: [],
18
+ create(context) {
19
+ const { importDeclarationListener, detectZodSchemaRootNode, collectZodChainMethods, } = trackZodSchemaImports();
20
+ return {
21
+ ImportDeclaration: importDeclarationListener,
22
+ CallExpression(node) {
23
+ const zodSchemaMeta = detectZodSchemaRootNode(node);
24
+ if ((zodSchemaMeta === null || zodSchemaMeta === void 0 ? void 0 : zodSchemaMeta.schemaType) !== 'string') {
25
+ return;
26
+ }
27
+ const methods = collectZodChainMethods(zodSchemaMeta.node);
28
+ if (methods.some((it) => it.name === 'trim')) {
29
+ return;
30
+ }
31
+ if (zodSchemaMeta.schemaDecl === 'named') {
32
+ context.report({
33
+ node,
34
+ messageId: 'addTrim',
35
+ });
36
+ return;
37
+ }
38
+ context.report({
39
+ node,
40
+ messageId: 'addTrim',
41
+ fix(fixer) {
42
+ const lastMethod = methods.at(0);
43
+ return fixer.insertTextAfter(lastMethod.node, '.trim()');
44
+ },
45
+ });
46
+ },
47
+ };
48
+ },
49
+ });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildZodChainReplacementFix = buildZodChainReplacementFix;
4
+ function buildZodChainReplacementFix(opts) {
5
+ var _a, _b;
6
+ const { sourceCode, fixer, methods, fromIndex, toIndex, toMethodName } = opts;
7
+ const fromNode = (_a = methods[fromIndex]) === null || _a === void 0 ? void 0 : _a.node;
8
+ const toNode = (_b = methods[toIndex]) === null || _b === void 0 ? void 0 : _b.node;
9
+ if (!fromNode || !toNode) {
10
+ return null;
11
+ }
12
+ const fromCallee = fromNode.callee;
13
+ const prefixObj = fromCallee.object;
14
+ const prefixText = sourceCode.getText(prefixObj);
15
+ const methodsBetween = methods.slice(fromIndex + 1, toIndex);
16
+ const betweenSuffixes = methodsBetween.map((m) => {
17
+ const betweenCallee = m.node.callee;
18
+ const objText = sourceCode.getText(betweenCallee.object);
19
+ const fullText = sourceCode.getText(m.node);
20
+ return fullText.slice(objText.length);
21
+ });
22
+ let replacement = `${prefixText}.${toMethodName}(`;
23
+ if (toNode.arguments.length) {
24
+ const argsText = toNode.arguments
25
+ .map((arg) => sourceCode.getText(arg))
26
+ .join(', ');
27
+ replacement += argsText;
28
+ }
29
+ replacement += `)${betweenSuffixes.join('')}`;
30
+ return fixer.replaceTextRange([fromNode.range[0], toNode.range[1]], replacement);
31
+ }
@@ -0,0 +1,12 @@
1
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
+ export declare function buildZodChainReplacementFix(opts: {
3
+ sourceCode: TSESLint.SourceCode;
4
+ fixer: TSESLint.RuleFixer;
5
+ methods: Array<{
6
+ name: string;
7
+ node: TSESTree.CallExpression;
8
+ }>;
9
+ fromIndex: number;
10
+ toIndex: number;
11
+ toMethodName: string;
12
+ }): TSESLint.RuleFix | null;
@@ -0,0 +1,12 @@
1
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
+ export declare function buildZodChainReplacementFix(opts: {
3
+ sourceCode: TSESLint.SourceCode;
4
+ fixer: TSESLint.RuleFixer;
5
+ methods: Array<{
6
+ name: string;
7
+ node: TSESTree.CallExpression;
8
+ }>;
9
+ fromIndex: number;
10
+ toIndex: number;
11
+ toMethodName: string;
12
+ }): TSESLint.RuleFix | null;
@@ -0,0 +1,28 @@
1
+ export function buildZodChainReplacementFix(opts) {
2
+ var _a, _b;
3
+ const { sourceCode, fixer, methods, fromIndex, toIndex, toMethodName } = opts;
4
+ const fromNode = (_a = methods[fromIndex]) === null || _a === void 0 ? void 0 : _a.node;
5
+ const toNode = (_b = methods[toIndex]) === null || _b === void 0 ? void 0 : _b.node;
6
+ if (!fromNode || !toNode) {
7
+ return null;
8
+ }
9
+ const fromCallee = fromNode.callee;
10
+ const prefixObj = fromCallee.object;
11
+ const prefixText = sourceCode.getText(prefixObj);
12
+ const methodsBetween = methods.slice(fromIndex + 1, toIndex);
13
+ const betweenSuffixes = methodsBetween.map((m) => {
14
+ const betweenCallee = m.node.callee;
15
+ const objText = sourceCode.getText(betweenCallee.object);
16
+ const fullText = sourceCode.getText(m.node);
17
+ return fullText.slice(objText.length);
18
+ });
19
+ let replacement = `${prefixText}.${toMethodName}(`;
20
+ if (toNode.arguments.length) {
21
+ const argsText = toNode.arguments
22
+ .map((arg) => sourceCode.getText(arg))
23
+ .join(', ');
24
+ replacement += argsText;
25
+ }
26
+ replacement += `)${betweenSuffixes.join('')}`;
27
+ return fixer.replaceTextRange([fromNode.range[0], toNode.range[1]], replacement);
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-zod",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "type": "module",
5
5
  "description": "ESLint plugin that adds custom linting rules to enforce best practices when using Zod",
6
6
  "engines": {
@@ -62,19 +62,19 @@
62
62
  "@marcalexiei/prettier-config": "1.1.4",
63
63
  "@types/esquery": "1.5.4",
64
64
  "@types/node": "24.10.1",
65
- "@typescript-eslint/rule-tester": "8.54.0",
66
- "@vitest/eslint-plugin": "1.6.6",
65
+ "@typescript-eslint/rule-tester": "8.56.1",
66
+ "@vitest/eslint-plugin": "1.6.9",
67
67
  "dedent": "1.7.1",
68
68
  "eslint": "9.39.2",
69
- "eslint-doc-generator": "3.0.2",
69
+ "eslint-doc-generator": "3.2.0",
70
70
  "eslint-import-resolver-typescript": "4.4.4",
71
- "eslint-plugin-eslint-plugin": "7.3.0",
71
+ "eslint-plugin-eslint-plugin": "7.3.1",
72
72
  "eslint-plugin-import-x": "4.16.1",
73
- "eslint-plugin-n": "17.23.2",
74
- "knip": "5.83.0",
73
+ "eslint-plugin-n": "17.24.0",
74
+ "knip": "5.85.0",
75
75
  "prettier": "3.8.1",
76
76
  "typescript": "5.9.3",
77
- "typescript-eslint": "8.54.0",
77
+ "typescript-eslint": "8.56.1",
78
78
  "vitest": "4.0.18",
79
79
  "zshy": "0.7.0"
80
80
  },