├── .README
├── README.md
└── rules
│ ├── prefer-enum.md
│ └── require-strict.md
├── .eslintrc
├── .github
└── workflows
│ ├── feature.yaml
│ └── main.yaml
├── .gitignore
├── .npmignore
├── .releaserc
├── LICENSE
├── README.md
├── bin
└── readmeAssertions.ts
├── package-lock.json
├── package.json
├── src
├── index.ts
└── rules
│ ├── preferEnum.ts
│ └── requireStrict.ts
├── test
└── rules
│ ├── assertions
│ ├── preferEnum.ts
│ └── requireStrict.ts
│ └── index.ts
└── tsconfig.json
/.README/README.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-zod
2 |
3 | [](https://github.com/gajus/canonical)
4 | [](https://www.npmjs.org/package/eslint-plugin-zod)
5 | [](https://twitter.com/kuizinas)
6 |
7 | [Zod](https://github.com/colinhacks/zod) linting rules for ESLint.
8 |
9 | {"gitdown": "contents"}
10 |
11 | ## Installation
12 |
13 | 1. Install [ESLint](https://www.github.com/eslint/eslint).
14 | 1. Install [`eslint-plugin-zod`](https://github.com/gajus/eslint-plugin-zod) plugin.
15 |
16 |
17 |
18 | ```sh
19 | npm install eslint --save-dev
20 | npm install eslint-plugin-zod --save-dev
21 | ```
22 |
23 | ## Configuration
24 |
25 | 1. Add `plugins` section and specify `eslint-plugin-zod` as a plugin.
26 | 1. Enable rules.
27 |
28 |
29 |
30 | ```json
31 | {
32 | "plugins": [
33 | "zod"
34 | ],
35 | "rules": {
36 | "zod/prefer-enum": 2,
37 | "zod/require-strict": 2
38 | }
39 | }
40 |
41 | ```
42 |
43 | ## Rules
44 |
45 |
46 |
47 | {"gitdown": "include", "file": "./rules/prefer-enum.md"}
48 | {"gitdown": "include", "file": "./rules/require-strict.md"}
49 |
--------------------------------------------------------------------------------
/.README/rules/prefer-enum.md:
--------------------------------------------------------------------------------
1 | ### `prefer-enum`
2 |
3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._
4 |
5 | Prefers `z.enum` over a union of literals.
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.README/rules/require-strict.md:
--------------------------------------------------------------------------------
1 | ### `require-strict`
2 |
3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._
4 |
5 | Requires that objects are initialized with `.strict()`.
6 |
7 | #### Options
8 |
9 | |configuration|format|default|description|
10 | |---|---|---|---|
11 | |`allowPassthrough`|boolean|`true`|Ignores objects explicitly set to `allowPassthrough()`.|
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "canonical",
4 | "canonical/typescript"
5 | ],
6 | "parserOptions": {
7 | "project": "./tsconfig.json"
8 | },
9 | "rules": {
10 | "no-template-curly-in-string": 0
11 | },
12 | "root": true
13 | }
--------------------------------------------------------------------------------
/.github/workflows/feature.yaml:
--------------------------------------------------------------------------------
1 | jobs:
2 | test:
3 | runs-on: ubuntu-latest
4 | environment: release
5 | name: Test
6 | steps:
7 | - name: setup repository
8 | uses: actions/checkout@v3
9 | with:
10 | fetch-depth: 0
11 | - name: setup node.js
12 | uses: actions/setup-node@v3
13 | with:
14 | node-version: "18"
15 | - run: npm install
16 | - run: npm run lint
17 | - run: npm run test
18 | - run: npm run build
19 | timeout-minutes: 10
20 | name: Test and build
21 | on:
22 | pull_request:
23 | branches:
24 | - main
25 | types:
26 | - opened
27 | - synchronize
28 | - reopened
29 | - ready_for_review
30 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | jobs:
2 | test:
3 | runs-on: ubuntu-latest
4 | environment: release
5 | name: Test
6 | steps:
7 | - name: setup repository
8 | uses: actions/checkout@v3
9 | with:
10 | fetch-depth: 0
11 | - name: setup node.js
12 | uses: actions/setup-node@v3
13 | with:
14 | node-version: "18"
15 | - run: npm install
16 | - run: npm run lint
17 | - run: npm run test
18 | - run: npm run build
19 | - run: npx semantic-release
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
23 | name: Test, build and release
24 | on:
25 | push:
26 | branches:
27 | - main
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | *.log
5 | .*
6 | !.babelrc
7 | !.eslintrc
8 | !.github
9 | !.gitignore
10 | !.npmignore
11 | !.README
12 | !.releaserc
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | bin
3 | !/dist/src
4 | test
5 | coverage
6 | *.tgz
7 | .*
8 | *.log
9 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "main"
4 | ],
5 | "plugins": [
6 | "@semantic-release/commit-analyzer",
7 | "@semantic-release/github",
8 | "@semantic-release/npm"
9 | ]
10 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022, Gajus Kuizinas (https://gajus.com/)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * Neither the name of the Gajus Kuizinas (https://gajus.com/) nor the
12 | names of its contributors may be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL GAJUS KUIZINAS BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # eslint-plugin-zod
4 |
5 | [](https://github.com/gajus/canonical)
6 | [](https://www.npmjs.org/package/eslint-plugin-zod)
7 | [](https://twitter.com/kuizinas)
8 |
9 | [Zod](https://github.com/colinhacks/zod) linting rules for ESLint.
10 |
11 | * [eslint-plugin-zod](#user-content-eslint-plugin-zod)
12 | * [Installation](#user-content-eslint-plugin-zod-installation)
13 | * [Configuration](#user-content-eslint-plugin-zod-configuration)
14 | * [Rules](#user-content-eslint-plugin-zod-rules)
15 | * [`prefer-enum`](#user-content-eslint-plugin-zod-rules-prefer-enum)
16 | * [`require-strict`](#user-content-eslint-plugin-zod-rules-require-strict)
17 |
18 |
19 |
20 |
21 | ## Installation
22 |
23 | 1. Install [ESLint](https://www.github.com/eslint/eslint).
24 | 1. Install [`eslint-plugin-zod`](https://github.com/gajus/eslint-plugin-zod) plugin.
25 |
26 |
27 |
28 | ```sh
29 | npm install eslint --save-dev
30 | npm install eslint-plugin-zod --save-dev
31 | ```
32 |
33 |
34 |
35 | ## Configuration
36 |
37 | 1. Add `plugins` section and specify `eslint-plugin-zod` as a plugin.
38 | 1. Enable rules.
39 |
40 |
41 |
42 | ```json
43 | {
44 | "plugins": [
45 | "zod"
46 | ],
47 | "rules": {
48 | "zod/prefer-enum": 2,
49 | "zod/require-strict": 2
50 | }
51 | }
52 |
53 | ```
54 |
55 |
56 |
57 | ## Rules
58 |
59 |
60 |
61 |
62 |
63 | ### prefer-enum
64 |
65 | _The `--fix` option on the command line automatically fixes problems reported by this rule._
66 |
67 | Prefers `z.enum` over a union of literals.
68 |
69 |
70 |
71 |
72 |
73 | ### require-strict
74 |
75 | _The `--fix` option on the command line automatically fixes problems reported by this rule._
76 |
77 | Requires that objects are initialized with `.strict()`.
78 |
79 |
80 |
81 | #### Options
82 |
83 | |configuration|format|default|description|
84 | |---|---|---|---|
85 | |`allowPassthrough`|boolean|`true`|Ignores objects explicitly set to `allowPassthrough()`.|
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/bin/readmeAssertions.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | /* eslint-disable @typescript-eslint/no-require-imports */
3 |
4 | /**
5 | * @file This script is used to inline assertions into the README.md documents.
6 | */
7 |
8 | import fs from 'fs';
9 | import path from 'path';
10 | import glob from 'glob';
11 | import _ from 'lodash';
12 |
13 | type EslintError = {
14 | message: string,
15 | };
16 |
17 | type Setup = {
18 | code: string,
19 | errors: EslintError[],
20 | options: unknown[],
21 | output: string,
22 | };
23 |
24 | const formatCodeSnippet = (setup: Setup) => {
25 | const paragraphs: string[] = [];
26 |
27 | paragraphs.push(setup.code);
28 |
29 | if (setup.options) {
30 | paragraphs.push('// Options: ' + JSON.stringify(setup.options));
31 | }
32 |
33 | if (setup.errors) {
34 | for (const message of setup.errors) {
35 | paragraphs.push('// Message: ' + message.message);
36 | }
37 | }
38 |
39 | if (setup.output) {
40 | paragraphs.push('// Fixed code: \n// ' + setup.output.split('\n').join('\n// '));
41 | }
42 |
43 | return paragraphs.join('\n');
44 | };
45 |
46 | const getAssertions = () => {
47 | const assertionFiles = glob.sync(path.resolve(__dirname, '../test/rules/assertions/*.ts'));
48 |
49 | const assertionNames = _.map(assertionFiles, (filePath) => {
50 | return path.basename(filePath, '.ts');
51 | });
52 |
53 | const assertionCodes = _.map(assertionFiles, (filePath) => {
54 | const codes = require(filePath);
55 |
56 | return {
57 | invalid: _.map(codes.invalid, formatCodeSnippet),
58 | valid: _.map(codes.valid, formatCodeSnippet),
59 | };
60 | });
61 |
62 | return _.zipObject(assertionNames, assertionCodes);
63 | };
64 |
65 | const updateDocuments = (assertions) => {
66 | const readmeDocumentPath = path.join(__dirname, '../README.md');
67 |
68 | let documentBody = fs.readFileSync(readmeDocumentPath, 'utf8');
69 |
70 | documentBody = documentBody.replace(//giu, (assertionsBlock) => {
71 | let exampleBody = '';
72 |
73 | const ruleName = /assertions ([a-z]+)/iu.exec(assertionsBlock)?.[1];
74 |
75 | if (!ruleName) {
76 | throw new Error('Rule name not found.');
77 | }
78 |
79 | const ruleAssertions = assertions[ruleName];
80 |
81 | if (!ruleAssertions) {
82 | throw new Error('No assertions available for rule "' + ruleName + '".');
83 | }
84 |
85 | if (ruleAssertions.invalid.length) {
86 | exampleBody += 'The following patterns are considered problems:\n\n```js\n' + ruleAssertions.invalid.join('\n\n') + '\n```\n\n';
87 | }
88 |
89 | if (ruleAssertions.valid.length) {
90 | exampleBody += 'The following patterns are not considered problems:\n\n```js\n' + ruleAssertions.valid.join('\n\n') + '\n```\n\n';
91 | }
92 |
93 | return exampleBody;
94 | });
95 |
96 | fs.writeFileSync(readmeDocumentPath, documentBody);
97 | };
98 |
99 | updateDocuments(getAssertions());
100 |
101 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": {
3 | "email": "gajus@gajus.com",
4 | "name": "Gajus Kuizinas",
5 | "url": "http://gajus.com"
6 | },
7 | "description": "Zod linting rules for ESLint.",
8 | "devDependencies": {
9 | "@semantic-release/commit-analyzer": "^9.0.2",
10 | "@semantic-release/github": "^8.0.6",
11 | "@semantic-release/npm": "^9.0.1",
12 | "@types/mocha": "^10.0.0",
13 | "@types/node": "^18.11.9",
14 | "eslint": "^8.27.0",
15 | "eslint-config-canonical": "37.0.3",
16 | "gitdown": "^3.1.5",
17 | "glob": "^8.0.3",
18 | "lodash": "^4.17.21",
19 | "mocha": "^10.1.0",
20 | "semantic-release": "^19.0.5",
21 | "tsx": "^3.12.1",
22 | "typescript": "^4.9.3"
23 | },
24 | "engines": {
25 | "node": ">=12"
26 | },
27 | "keywords": [
28 | "eslint",
29 | "plugin",
30 | "zod"
31 | ],
32 | "license": "BSD-3-Clause",
33 | "main": "./dist/src/index.js",
34 | "name": "eslint-plugin-zod",
35 | "peerDependencies": {
36 | "eslint": ">=8.1.0"
37 | },
38 | "repository": {
39 | "type": "git",
40 | "url": "https://github.com/gajus/eslint-plugin-zod"
41 | },
42 | "scripts": {
43 | "build": "npm run documentation && tsc",
44 | "documentation": "gitdown ./.README/README.md --output-file ./README.md && npm run documentation-add-assertions",
45 | "documentation-add-assertions": "tsx ./bin/readmeAssertions",
46 | "lint": "eslint ./src ./test && tsc --noEmit",
47 | "test": "mocha --require tsx test/**/*"
48 | },
49 | "version": "0.0.0-development"
50 | }
51 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import preferEnum from './rules/preferEnum';
2 | import requireStrict from './rules/requireStrict';
3 |
4 | const rules = {
5 | 'prefer-enum': preferEnum,
6 | 'require-strict': requireStrict,
7 | };
8 |
9 | export = {
10 | rules,
11 | rulesConfig: {
12 | 'prefer-enum': 0,
13 | 'require-strict': 0,
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/rules/preferEnum.ts:
--------------------------------------------------------------------------------
1 | const create = (context) => {
2 | return {
3 | CallExpression (node) {
4 | if (node.callee.object?.name !== 'z') {
5 | return;
6 | }
7 |
8 | if (node.callee.property.name !== 'union') {
9 | return;
10 | }
11 |
12 | const members = node.arguments[0].elements;
13 |
14 | const allMembersAreLiterals = members.every((member) => {
15 | return member.type === 'CallExpression' && member.callee.object.name === 'z' && member.callee.property.name === 'literal';
16 | });
17 |
18 | if (!allMembersAreLiterals) {
19 | return;
20 | }
21 |
22 | context.report({
23 | message: 'Use z.enum().',
24 | node,
25 | });
26 | },
27 | };
28 | };
29 |
30 | export = {
31 | create,
32 | meta: {
33 | docs: {
34 | description: 'Prefers `z.enum` over a union of literals.',
35 | url: 'https://github.com/gajus/eslint-plugin-zod#eslint-plugin-zod-rules-prefer-enum',
36 | },
37 | fixable: 'code',
38 | schema: [
39 | {
40 | additionalProperties: false,
41 | properties: {},
42 | type: 'object',
43 | },
44 | ],
45 | type: 'problem',
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/rules/requireStrict.ts:
--------------------------------------------------------------------------------
1 | const defaultOptions = {
2 | allowPassthrough: true,
3 | };
4 |
5 | const create = (context) => {
6 | return {
7 | CallExpression (node) {
8 | if (node.callee.object?.name !== 'z') {
9 | return;
10 | }
11 |
12 | if (node.callee.property.name !== 'object') {
13 | return;
14 | }
15 |
16 | const {
17 | allowPassthrough,
18 | } = context.options[0] ?? defaultOptions;
19 |
20 | if (!node.parent.property) {
21 | context.report({
22 | fix: (fixer) => {
23 | return fixer.insertTextAfter(node, '.strict()');
24 | },
25 | message: 'Add a strict() call to the schema.',
26 | node,
27 | });
28 | } else if (
29 | // z.object().strict()
30 | node.parent?.property?.name !== 'strict' &&
31 | // z.object().merge().strict()
32 | node.parent?.parent?.parent?.property?.name !== 'strict'
33 | ) {
34 | if (node.parent?.property?.name === 'passthrough' && allowPassthrough) {
35 | return;
36 | }
37 |
38 | if (node.parent?.property?.name === 'and') {
39 | // Ignore .and() calls
40 | return;
41 | }
42 |
43 | // As far as I can think, in cases where the property name is not-strict,
44 | // e.g. passthrough, we should not add a strict() call.
45 | context.report({
46 | message: 'Add a strict() call to the schema.',
47 | node,
48 | });
49 | }
50 | },
51 | };
52 | };
53 |
54 | export = {
55 | create,
56 | meta: {
57 | docs: {
58 | description: 'Requires that objects are initialized with .strict().',
59 | url: 'https://github.com/gajus/eslint-plugin-zod#eslint-plugin-zod-rules-require-strict',
60 | },
61 | fixable: 'code',
62 | schema: [
63 | {
64 | additionalProperties: false,
65 | properties: {
66 | allowPassthrough: {
67 | default: true,
68 | type: 'boolean',
69 | },
70 | },
71 | type: 'object',
72 | },
73 | ],
74 | type: 'problem',
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/test/rules/assertions/preferEnum.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | invalid: [
3 | {
4 | code: 'z.union([z.literal(\'foo\'), z.literal(\'bar\')])',
5 | errors: [
6 | {
7 | message: 'Use z.enum().',
8 | },
9 | ],
10 | },
11 | ],
12 | valid: [
13 | {
14 | code: 'z.enum([\'foo\', \'bar\'])',
15 | },
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/test/rules/assertions/requireStrict.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | invalid: [
3 | {
4 | code: 'z.object()',
5 | errors: [
6 | {
7 | message: 'Add a strict() call to the schema.',
8 | },
9 | ],
10 | output: 'z.object().strict()',
11 | },
12 | {
13 | code: 'z.object().passthrough()',
14 | errors: [
15 | {
16 | message: 'Add a strict() call to the schema.',
17 | },
18 | ],
19 | options: [
20 | {
21 | allowPassthrough: false,
22 | },
23 | ],
24 | },
25 | ],
26 | valid: [
27 | {
28 | code: 'foo()',
29 | },
30 | {
31 | // Not a Zod schema
32 | code: 'b.object()',
33 | },
34 | {
35 | code: 'z.object().strict()',
36 | },
37 | {
38 | code: 'z.object().merge().strict()',
39 | },
40 | {
41 | // Ignore .and() calls
42 | code: 'z.object().and()',
43 | },
44 | {
45 | code: 'z.object().passthrough()',
46 | },
47 | {
48 | code: 'z.object().passthrough()',
49 | options: [
50 | {
51 | allowPassthrough: true,
52 | },
53 | ],
54 | },
55 | ],
56 | };
57 |
--------------------------------------------------------------------------------
/test/rules/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | /* eslint-disable @typescript-eslint/no-require-imports */
3 |
4 | import {
5 | RuleTester,
6 | } from 'eslint';
7 | import {
8 | camelCase,
9 | } from 'lodash';
10 | import plugin from '../../src';
11 |
12 | const ruleTester = new RuleTester({
13 | parserOptions: {
14 | ecmaVersion: 'latest',
15 | },
16 | });
17 |
18 | const reportingRules = [
19 | 'prefer-enum',
20 | 'require-strict',
21 | ];
22 |
23 | for (const ruleName of reportingRules) {
24 | const assertions = require('./assertions/' + camelCase(ruleName));
25 |
26 | ruleTester.run(ruleName, plugin.rules[ruleName], assertions.default);
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "declaration": true,
5 | "esModuleInterop": true,
6 | "module": "commonjs",
7 | "lib": [
8 | "es2021"
9 | ],
10 | "moduleResolution": "node",
11 | "noImplicitAny": false,
12 | "noImplicitReturns": true,
13 | "useUnknownInCatchVariables": false,
14 | "outDir": "dist",
15 | "skipLibCheck": true,
16 | "strict": true,
17 | "target": "es2018"
18 | },
19 | "exclude": [
20 | "dist",
21 | "node_modules"
22 | ],
23 | "include": [
24 | "bin",
25 | "src",
26 | "test"
27 | ]
28 | }
--------------------------------------------------------------------------------