eslint-plugin-zod-utils 1.0.2 → 1.0.3

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
@@ -102,6 +102,27 @@ app.post("/users", {
102
102
  ```
103
103
 
104
104
  The rule understands `import { z } from "zod"`, `import * as zod from "zod"`, aliased `z` imports, and direct named schema factories such as `import { object, string } from "zod"`.
105
+ It also understands default imports such as `import z from "zod"`.
106
+
107
+ When type information is available through `@typescript-eslint/parser`, the rule also reports repeated derived schema creation from imported Zod schemas:
108
+
109
+ ```ts
110
+ import { UserSchema } from "./schemas";
111
+
112
+ function getPublicSchema() {
113
+ return UserSchema.pick({ id: true });
114
+ }
115
+ ```
116
+
117
+ ```ts
118
+ import { BaseSchema, TenantIdSchema } from "./schemas";
119
+
120
+ function getTenantSchema() {
121
+ return BaseSchema.extend({ tenantId: TenantIdSchema });
122
+ }
123
+ ```
124
+
125
+ This type-aware detection does not use schema-name conventions. If type information is unavailable, imported schema roots are not inferred from names such as `UserSchema` or `UserZodSchema`. The rule also does not assume a global `z` identifier refers to Zod.
105
126
 
106
127
  ## Development
107
128
 
package/dist/index.cjs CHANGED
@@ -88,6 +88,37 @@ var ZOD_FACTORY_IMPORTS = /* @__PURE__ */ new Set([
88
88
  "xid"
89
89
  ]);
90
90
  var ZOD_NAMESPACE_IMPORTS = /* @__PURE__ */ new Set(["z", "coerce", "iso"]);
91
+ var ZOD_EXECUTION_METHODS = /* @__PURE__ */ new Set([
92
+ "parse",
93
+ "parseAsync",
94
+ "safeParse",
95
+ "safeParseAsync"
96
+ ]);
97
+ var ZOD_SCHEMA_COMBINATOR_METHODS = /* @__PURE__ */ new Set([
98
+ "and",
99
+ "array",
100
+ "brand",
101
+ "catchall",
102
+ "deepPartial",
103
+ "describe",
104
+ "extend",
105
+ "merge",
106
+ "nullable",
107
+ "nullish",
108
+ "omit",
109
+ "optional",
110
+ "or",
111
+ "partial",
112
+ "passthrough",
113
+ "pick",
114
+ "readonly",
115
+ "refine",
116
+ "required",
117
+ "strict",
118
+ "strip",
119
+ "superRefine",
120
+ "transform"
121
+ ]);
91
122
  function findVariable(scope, name) {
92
123
  let currentScope = scope;
93
124
  while (currentScope) {
@@ -121,8 +152,33 @@ function getRootIdentifier(node) {
121
152
  return null;
122
153
  }
123
154
  }
155
+ function getRootMethodName(node) {
156
+ let current = node;
157
+ let rootMethodName = null;
158
+ while (true) {
159
+ if (current.type === "CallExpression") {
160
+ current = current.callee;
161
+ continue;
162
+ }
163
+ if (current.type === "MemberExpression") {
164
+ if (current.object.type === "Identifier" && !current.computed && current.property.type === "Identifier") {
165
+ rootMethodName = current.property.name;
166
+ }
167
+ current = current.object;
168
+ continue;
169
+ }
170
+ return rootMethodName;
171
+ }
172
+ }
173
+ function getCallMethodName(node) {
174
+ const { callee } = node;
175
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") {
176
+ return callee.property.name;
177
+ }
178
+ return null;
179
+ }
124
180
  function isZodImportSpecifier(node) {
125
- if (node.type !== "ImportNamespaceSpecifier" && node.type !== "ImportSpecifier") {
181
+ if (node.type !== "ImportDefaultSpecifier" && node.type !== "ImportNamespaceSpecifier" && node.type !== "ImportSpecifier") {
126
182
  return false;
127
183
  }
128
184
  const declaration = node.parent;
@@ -132,7 +188,7 @@ function isZodImportSpecifier(node) {
132
188
  if (declaration.source.value !== "zod") {
133
189
  return false;
134
190
  }
135
- if (node.type === "ImportNamespaceSpecifier") {
191
+ if (node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier") {
136
192
  return true;
137
193
  }
138
194
  if (node.importKind === "type" || declaration.importKind === "type") {
@@ -141,6 +197,39 @@ function isZodImportSpecifier(node) {
141
197
  const importedName = node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
142
198
  return ZOD_NAMESPACE_IMPORTS.has(importedName) || ZOD_FACTORY_IMPORTS.has(importedName);
143
199
  }
200
+ function hasFullTypeInformation(services) {
201
+ return services.program !== null;
202
+ }
203
+ function isZodDeclarationFile(fileName) {
204
+ return /(?:^|[/\\])node_modules[/\\]zod[/\\]/u.test(fileName);
205
+ }
206
+ function isZodSchemaType(type, services, seen = /* @__PURE__ */ new Set()) {
207
+ if (!hasFullTypeInformation(services)) {
208
+ return false;
209
+ }
210
+ if (seen.has(type)) {
211
+ return false;
212
+ }
213
+ seen.add(type);
214
+ const symbols = [type.getSymbol(), type.aliasSymbol];
215
+ if (symbols.some(
216
+ (symbol) => symbol?.getDeclarations()?.some(
217
+ (declaration) => isZodDeclarationFile(declaration.getSourceFile().fileName)
218
+ )
219
+ )) {
220
+ return true;
221
+ }
222
+ if (type.isUnionOrIntersection()) {
223
+ return type.types.some((subType) => isZodSchemaType(subType, services, seen));
224
+ }
225
+ if (type.getBaseTypes()?.some((baseType) => isZodSchemaType(baseType, services, seen))) {
226
+ return true;
227
+ }
228
+ const parseSymbol = type.getProperty("parse");
229
+ return parseSymbol?.getDeclarations()?.some(
230
+ (declaration) => isZodDeclarationFile(declaration.getSourceFile().fileName)
231
+ ) ?? false;
232
+ }
144
233
  var noInlineZodSchema = import_utils.ESLintUtils.RuleCreator(
145
234
  (ruleName2) => `https://www.npmjs.com/package/eslint-plugin-zod-utils#${ruleName2}`
146
235
  )({
@@ -158,7 +247,15 @@ var noInlineZodSchema = import_utils.ESLintUtils.RuleCreator(
158
247
  defaultOptions: [],
159
248
  create(context) {
160
249
  const sourceCode = context.sourceCode;
250
+ const parserServices = import_utils.ESLintUtils.getParserServices(context, true);
251
+ function isZodExecutionCall(node) {
252
+ const methodName = getCallMethodName(node);
253
+ return methodName !== null && ZOD_EXECUTION_METHODS.has(methodName);
254
+ }
161
255
  function isZodSchemaCall(node) {
256
+ if (isZodExecutionCall(node)) {
257
+ return false;
258
+ }
162
259
  const root = getRootIdentifier(node.callee);
163
260
  if (!root) {
164
261
  return false;
@@ -167,10 +264,28 @@ var noInlineZodSchema = import_utils.ESLintUtils.RuleCreator(
167
264
  const variable = findVariable(scope, root.name);
168
265
  return variable?.defs.some((definition) => isZodImportSpecifier(definition.node)) ?? false;
169
266
  }
170
- function hasZodCallAncestor(node) {
267
+ function isTypedZodSchemaCombinatorCall(node) {
268
+ const rootMethodName = getRootMethodName(node);
269
+ if (rootMethodName === null || !ZOD_SCHEMA_COMBINATOR_METHODS.has(rootMethodName)) {
270
+ return false;
271
+ }
272
+ const root = getRootIdentifier(node.callee);
273
+ if (!root) {
274
+ return false;
275
+ }
276
+ if (!hasFullTypeInformation(parserServices)) {
277
+ return false;
278
+ }
279
+ const type = parserServices.getTypeAtLocation(root);
280
+ return isZodSchemaType(type, parserServices);
281
+ }
282
+ function isSchemaCreationCall(node) {
283
+ return isZodSchemaCall(node) || isTypedZodSchemaCombinatorCall(node);
284
+ }
285
+ function hasSchemaCreationCallAncestor(node) {
171
286
  let current = node.parent;
172
287
  while (current) {
173
- if (current.type === "CallExpression" && isZodSchemaCall(current)) {
288
+ if (current.type === "CallExpression" && isSchemaCreationCall(current)) {
174
289
  return true;
175
290
  }
176
291
  current = current.parent;
@@ -192,10 +307,10 @@ var noInlineZodSchema = import_utils.ESLintUtils.RuleCreator(
192
307
  }
193
308
  return {
194
309
  CallExpression(node) {
195
- if (!isZodSchemaCall(node)) {
310
+ if (!isSchemaCreationCall(node)) {
196
311
  return;
197
312
  }
198
- if (hasZodCallAncestor(node)) {
313
+ if (hasSchemaCreationCallAncestor(node)) {
199
314
  return;
200
315
  }
201
316
  if (!isInsideRepeatedExecutionPath(node)) {
@@ -218,7 +333,7 @@ var rules = {
218
333
  var plugin = {
219
334
  meta: {
220
335
  name: "eslint-plugin-zod-utils",
221
- version: "0.1.0"
336
+ version: "1.0.3"
222
337
  },
223
338
  rules,
224
339
  configs: {}
package/dist/index.js CHANGED
@@ -60,6 +60,37 @@ var ZOD_FACTORY_IMPORTS = /* @__PURE__ */ new Set([
60
60
  "xid"
61
61
  ]);
62
62
  var ZOD_NAMESPACE_IMPORTS = /* @__PURE__ */ new Set(["z", "coerce", "iso"]);
63
+ var ZOD_EXECUTION_METHODS = /* @__PURE__ */ new Set([
64
+ "parse",
65
+ "parseAsync",
66
+ "safeParse",
67
+ "safeParseAsync"
68
+ ]);
69
+ var ZOD_SCHEMA_COMBINATOR_METHODS = /* @__PURE__ */ new Set([
70
+ "and",
71
+ "array",
72
+ "brand",
73
+ "catchall",
74
+ "deepPartial",
75
+ "describe",
76
+ "extend",
77
+ "merge",
78
+ "nullable",
79
+ "nullish",
80
+ "omit",
81
+ "optional",
82
+ "or",
83
+ "partial",
84
+ "passthrough",
85
+ "pick",
86
+ "readonly",
87
+ "refine",
88
+ "required",
89
+ "strict",
90
+ "strip",
91
+ "superRefine",
92
+ "transform"
93
+ ]);
63
94
  function findVariable(scope, name) {
64
95
  let currentScope = scope;
65
96
  while (currentScope) {
@@ -93,8 +124,33 @@ function getRootIdentifier(node) {
93
124
  return null;
94
125
  }
95
126
  }
127
+ function getRootMethodName(node) {
128
+ let current = node;
129
+ let rootMethodName = null;
130
+ while (true) {
131
+ if (current.type === "CallExpression") {
132
+ current = current.callee;
133
+ continue;
134
+ }
135
+ if (current.type === "MemberExpression") {
136
+ if (current.object.type === "Identifier" && !current.computed && current.property.type === "Identifier") {
137
+ rootMethodName = current.property.name;
138
+ }
139
+ current = current.object;
140
+ continue;
141
+ }
142
+ return rootMethodName;
143
+ }
144
+ }
145
+ function getCallMethodName(node) {
146
+ const { callee } = node;
147
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") {
148
+ return callee.property.name;
149
+ }
150
+ return null;
151
+ }
96
152
  function isZodImportSpecifier(node) {
97
- if (node.type !== "ImportNamespaceSpecifier" && node.type !== "ImportSpecifier") {
153
+ if (node.type !== "ImportDefaultSpecifier" && node.type !== "ImportNamespaceSpecifier" && node.type !== "ImportSpecifier") {
98
154
  return false;
99
155
  }
100
156
  const declaration = node.parent;
@@ -104,7 +160,7 @@ function isZodImportSpecifier(node) {
104
160
  if (declaration.source.value !== "zod") {
105
161
  return false;
106
162
  }
107
- if (node.type === "ImportNamespaceSpecifier") {
163
+ if (node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier") {
108
164
  return true;
109
165
  }
110
166
  if (node.importKind === "type" || declaration.importKind === "type") {
@@ -113,6 +169,39 @@ function isZodImportSpecifier(node) {
113
169
  const importedName = node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
114
170
  return ZOD_NAMESPACE_IMPORTS.has(importedName) || ZOD_FACTORY_IMPORTS.has(importedName);
115
171
  }
172
+ function hasFullTypeInformation(services) {
173
+ return services.program !== null;
174
+ }
175
+ function isZodDeclarationFile(fileName) {
176
+ return /(?:^|[/\\])node_modules[/\\]zod[/\\]/u.test(fileName);
177
+ }
178
+ function isZodSchemaType(type, services, seen = /* @__PURE__ */ new Set()) {
179
+ if (!hasFullTypeInformation(services)) {
180
+ return false;
181
+ }
182
+ if (seen.has(type)) {
183
+ return false;
184
+ }
185
+ seen.add(type);
186
+ const symbols = [type.getSymbol(), type.aliasSymbol];
187
+ if (symbols.some(
188
+ (symbol) => symbol?.getDeclarations()?.some(
189
+ (declaration) => isZodDeclarationFile(declaration.getSourceFile().fileName)
190
+ )
191
+ )) {
192
+ return true;
193
+ }
194
+ if (type.isUnionOrIntersection()) {
195
+ return type.types.some((subType) => isZodSchemaType(subType, services, seen));
196
+ }
197
+ if (type.getBaseTypes()?.some((baseType) => isZodSchemaType(baseType, services, seen))) {
198
+ return true;
199
+ }
200
+ const parseSymbol = type.getProperty("parse");
201
+ return parseSymbol?.getDeclarations()?.some(
202
+ (declaration) => isZodDeclarationFile(declaration.getSourceFile().fileName)
203
+ ) ?? false;
204
+ }
116
205
  var noInlineZodSchema = ESLintUtils.RuleCreator(
117
206
  (ruleName2) => `https://www.npmjs.com/package/eslint-plugin-zod-utils#${ruleName2}`
118
207
  )({
@@ -130,7 +219,15 @@ var noInlineZodSchema = ESLintUtils.RuleCreator(
130
219
  defaultOptions: [],
131
220
  create(context) {
132
221
  const sourceCode = context.sourceCode;
222
+ const parserServices = ESLintUtils.getParserServices(context, true);
223
+ function isZodExecutionCall(node) {
224
+ const methodName = getCallMethodName(node);
225
+ return methodName !== null && ZOD_EXECUTION_METHODS.has(methodName);
226
+ }
133
227
  function isZodSchemaCall(node) {
228
+ if (isZodExecutionCall(node)) {
229
+ return false;
230
+ }
134
231
  const root = getRootIdentifier(node.callee);
135
232
  if (!root) {
136
233
  return false;
@@ -139,10 +236,28 @@ var noInlineZodSchema = ESLintUtils.RuleCreator(
139
236
  const variable = findVariable(scope, root.name);
140
237
  return variable?.defs.some((definition) => isZodImportSpecifier(definition.node)) ?? false;
141
238
  }
142
- function hasZodCallAncestor(node) {
239
+ function isTypedZodSchemaCombinatorCall(node) {
240
+ const rootMethodName = getRootMethodName(node);
241
+ if (rootMethodName === null || !ZOD_SCHEMA_COMBINATOR_METHODS.has(rootMethodName)) {
242
+ return false;
243
+ }
244
+ const root = getRootIdentifier(node.callee);
245
+ if (!root) {
246
+ return false;
247
+ }
248
+ if (!hasFullTypeInformation(parserServices)) {
249
+ return false;
250
+ }
251
+ const type = parserServices.getTypeAtLocation(root);
252
+ return isZodSchemaType(type, parserServices);
253
+ }
254
+ function isSchemaCreationCall(node) {
255
+ return isZodSchemaCall(node) || isTypedZodSchemaCombinatorCall(node);
256
+ }
257
+ function hasSchemaCreationCallAncestor(node) {
143
258
  let current = node.parent;
144
259
  while (current) {
145
- if (current.type === "CallExpression" && isZodSchemaCall(current)) {
260
+ if (current.type === "CallExpression" && isSchemaCreationCall(current)) {
146
261
  return true;
147
262
  }
148
263
  current = current.parent;
@@ -164,10 +279,10 @@ var noInlineZodSchema = ESLintUtils.RuleCreator(
164
279
  }
165
280
  return {
166
281
  CallExpression(node) {
167
- if (!isZodSchemaCall(node)) {
282
+ if (!isSchemaCreationCall(node)) {
168
283
  return;
169
284
  }
170
- if (hasZodCallAncestor(node)) {
285
+ if (hasSchemaCreationCallAncestor(node)) {
171
286
  return;
172
287
  }
173
288
  if (!isInsideRepeatedExecutionPath(node)) {
@@ -190,7 +305,7 @@ var rules = {
190
305
  var plugin = {
191
306
  meta: {
192
307
  name: "eslint-plugin-zod-utils",
193
- version: "0.1.0"
308
+ version: "1.0.3"
194
309
  },
195
310
  rules,
196
311
  configs: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-zod-utils",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "ESLint utilities for safer Zod schema usage.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -46,7 +46,8 @@
46
46
  "tsup": "^8.5.1",
47
47
  "typescript": "^6.0.3",
48
48
  "typescript-eslint": "^8.60.1",
49
- "vitest": "^4.1.8"
49
+ "vitest": "^4.1.8",
50
+ "zod": "^4.4.3"
50
51
  },
51
52
  "dependencies": {
52
53
  "@typescript-eslint/utils": "^8.60.1"