@veda-hq/eslint-plugin-shuddhi 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +67 -2
- package/package.json +64 -3
- package/src/error-handling/configs.js +122 -0
- package/src/error-handling/error-handling.js +990 -0
- package/src/error-handling/index.js +12 -0
- package/src/index.js +54 -0
- package/src/input-parsing/configs.js +116 -0
- package/src/input-parsing/env-through-schema.js +173 -0
- package/src/input-parsing/helpers.js +129 -0
- package/src/input-parsing/index.js +57 -0
- package/src/input-parsing/no-hand-rolled-argv.js +208 -0
- package/src/input-parsing/no-regex-structured-parse.js +167 -0
- package/src/input-parsing/prisma-json-through-schema.js +130 -0
- package/src/input-parsing/schema-parse-untrusted.js +226 -0
- package/src/input-parsing/schema-satisfies-contract.js +163 -0
- package/src/input-parsing/strict-scalar-parse.js +609 -0
- package/src/nest-prisma/helpers.js +336 -0
- package/src/nest-prisma/index.js +16 -0
- package/src/nest-prisma/nest-prisma-rules.js +577 -0
- package/src/nest-prisma/no-self-mock-sut.js +225 -0
- package/src/nest-prisma/require-orderby-findfirst.js +68 -0
- package/src/server-boundary/compare-and-set.js +664 -0
- package/src/server-boundary/configs.js +62 -0
- package/src/server-boundary/dto-field-constraints.js +313 -0
- package/src/server-boundary/dto-implements-contract.js +81 -0
- package/src/server-boundary/helpers.js +53 -0
- package/src/server-boundary/index.js +47 -0
- package/src/server-boundary/nest-body-dto.js +278 -0
- package/src/server-boundary/no-capability-literal.js +92 -0
- package/src/server-boundary/no-local-api-contract.js +95 -0
- package/src/test-lint/c05-candidates.js +1462 -0
- package/src/test-lint/c05.js +423 -0
- package/src/test-lint/configs.js +177 -0
- package/src/test-lint/detox.js +124 -0
- package/src/test-lint/helpers.js +341 -0
- package/src/test-lint/index.js +227 -0
- package/src/test-lint/process-hygiene.js +1125 -0
- package/src/test-lint/sql-fake.js +175 -0
- package/src/test-lint/test-structure.js +1166 -0
- package/src/test-lint/vacuous-assertion.js +751 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Veda Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @veda-hq/eslint-plugin-shuddhi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
शुद्धि, purification. ESLint rules for code that coding agents write and
|
|
4
|
+
reviewers keep sending back: errors swallowed or stringified unsafely, input
|
|
5
|
+
parsed by hand, NestJS and Prisma boundaries left open, and tests that pass
|
|
6
|
+
without proving anything.
|
|
7
|
+
|
|
8
|
+
Each rule closes a defect class that a code reviewer reported repeatedly after
|
|
9
|
+
push. Its suite holds those findings, reduced, as invalid cases, with the
|
|
10
|
+
corrected code and the near misses as valid ones.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add -D eslint @veda-hq/eslint-plugin-shuddhi
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
ESLint 9 flat config, Node 24. `typescript` is an optional peer: only the rules
|
|
19
|
+
that read types (`strict-scalar-parse`, `nest-body-dto`) need it, and only
|
|
20
|
+
where the config gives the parser a TypeScript project.
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// eslint.config.js
|
|
26
|
+
import shuddhi from '@veda-hq/eslint-plugin-shuddhi';
|
|
27
|
+
|
|
28
|
+
export default [
|
|
29
|
+
{
|
|
30
|
+
files: ['**/*.ts'],
|
|
31
|
+
plugins: { shuddhi },
|
|
32
|
+
rules: {
|
|
33
|
+
'shuddhi/no-swallowed-catch': 'error',
|
|
34
|
+
'shuddhi/no-vacuous-assertion': 'error',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The plugin can be bound under any namespace: a repository that had these
|
|
41
|
+
rules under its own prefix keeps its rule ids (and its bulk suppressions) by
|
|
42
|
+
binding the plugin, or one family's `rules`, under that prefix.
|
|
43
|
+
|
|
44
|
+
Each family also exports config-block helpers that turn its rules on for a set
|
|
45
|
+
of globs, with a `namespace` parameter:
|
|
46
|
+
|
|
47
|
+
| Family | Rules | Helper |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| error handling | `no-swallowed-catch`, `safe-log`, `no-raw-error-string`, `no-direct-child-process` | `errorHandlingBlocks` |
|
|
50
|
+
| input parsing | `no-hand-rolled-argv`, `env-through-schema`, `strict-scalar-parse`, `schema-parse-untrusted`, `schema-satisfies-contract`, `prisma-json-through-schema`, `no-regex-structured-parse` | `inputParsingConfigs` |
|
|
51
|
+
| server boundary | `nest-body-dto`, `dto-field-constraints`, `no-local-api-contract`, `dto-implements-contract`, `no-capability-literal`, `compare-and-set` | `boundaryBlocks` |
|
|
52
|
+
| NestJS and Prisma | `no-raw-sql-interpolation`, `nest-http-exception`, `prisma-transaction-multiwrite`, `nest-param-pipe`, `no-unbound-test-app`, `require-orderby-findfirst`, `no-self-mock-sut` | none |
|
|
53
|
+
| test lint | 41 rules on test structure, vacuous assertions, process hygiene, fixtures and fakes | `testLintBlocks` |
|
|
54
|
+
|
|
55
|
+
Every name of a repository's own helper (the safe-logging module, the child
|
|
56
|
+
process runner, the argument parser, the contracts package) is a rule option,
|
|
57
|
+
so the messages point at the helper your repository actually has.
|
|
58
|
+
|
|
59
|
+
## Checks
|
|
60
|
+
|
|
61
|
+
`pnpm check`: the suites under `node --test` with at least 95% of lines and
|
|
62
|
+
functions and 90% of branches covered, and a smoke import of the published
|
|
63
|
+
entry point. The rules are plain JavaScript, shipped as written; the suites
|
|
64
|
+
are TypeScript that Node runs by stripping types.
|
|
65
|
+
|
|
66
|
+
## Licence
|
|
67
|
+
|
|
68
|
+
MIT.
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veda-hq/eslint-plugin-shuddhi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "ESLint rules for code coding agents write: error handling, input parsing, NestJS and Prisma boundaries, and test quality. Each rule closes a defect class reviewers kept finding after push.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslintplugin",
|
|
8
|
+
"eslint-plugin",
|
|
9
|
+
"nestjs",
|
|
10
|
+
"prisma",
|
|
11
|
+
"vitest",
|
|
12
|
+
"testing",
|
|
13
|
+
"coding agents"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=24.15"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/veda-hq/veda.git",
|
|
23
|
+
"directory": "packages/eslint-plugin-shuddhi"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/veda-hq/veda/tree/main/packages/eslint-plugin-shuddhi#readme",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"provenance": true
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.js",
|
|
32
|
+
"./error-handling": "./src/error-handling/index.js",
|
|
33
|
+
"./error-handling/configs": "./src/error-handling/configs.js",
|
|
34
|
+
"./input-parsing": "./src/input-parsing/index.js",
|
|
35
|
+
"./nest-prisma": "./src/nest-prisma/index.js",
|
|
36
|
+
"./server-boundary": "./src/server-boundary/index.js",
|
|
37
|
+
"./server-boundary/configs": "./src/server-boundary/configs.js",
|
|
38
|
+
"./test-lint": "./src/test-lint/index.js",
|
|
39
|
+
"./test-lint/configs": "./src/test-lint/configs.js",
|
|
40
|
+
"./nest-prisma/helpers": "./src/nest-prisma/helpers.js"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"src",
|
|
44
|
+
"!src/**/*.test.ts",
|
|
45
|
+
"!src/**/fixtures"
|
|
46
|
+
],
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"eslint": "^9.24.0",
|
|
49
|
+
"typescript": ">=5.4 <6"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"typescript": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@typescript-eslint/rule-tester": "8.48.0",
|
|
58
|
+
"eslint": "9.39.1",
|
|
59
|
+
"typescript": "5.9.3",
|
|
60
|
+
"typescript-eslint": "8.48.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"test": "node --test --experimental-test-coverage --test-coverage-lines=95 --test-coverage-functions=95 --test-coverage-branches=90 --test-coverage-exclude=src/**/*.test.ts --test-coverage-exclude=src/**/fixtures/** src/**/*.test.ts",
|
|
64
|
+
"smoke": "node --input-type=module -e \"const p = (await import('@veda-hq/eslint-plugin-shuddhi')).default; if (Object.keys(p.rules).length === 0) throw new Error('no rules');\"",
|
|
65
|
+
"check": "pnpm test && pnpm smoke"
|
|
66
|
+
}
|
|
6
67
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flat-config blocks for the error-handling rules (catalogue ERR-01, ERR-02
|
|
3
|
+
* with OBS-03, ERR-03, ERR-08).
|
|
4
|
+
*
|
|
5
|
+
* The rule ids need the plugin bound under a namespace, so a workspace takes
|
|
6
|
+
* them through {@link errorHandlingBlocks} rather than a bare spread. The
|
|
7
|
+
* namespace defaults to `shuddhi`; a project that already has these rules
|
|
8
|
+
* under another namespace (and suppressions recorded against those ids)
|
|
9
|
+
* passes its own.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { rules } from './index.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The plugin object bound when the caller passes none. One module-level
|
|
16
|
+
* object, so calling the helpers twice binds the same plugin: flat config
|
|
17
|
+
* refuses two different objects under one namespace.
|
|
18
|
+
*/
|
|
19
|
+
const familyPlugin = { rules };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} ErrorHandlingOptions
|
|
23
|
+
* @property {string} [namespace] Plugin namespace the rule ids use. Default `shuddhi`.
|
|
24
|
+
* @property {string} [helperModule] Module named in the safe-log and
|
|
25
|
+
* no-raw-error-string messages as the home of `safeLog`, `safeInvoke` and
|
|
26
|
+
* `errorDetail`.
|
|
27
|
+
* @property {string} [helperSourcePattern] Regular expression over import
|
|
28
|
+
* specifiers: an import of `safeLog`/`safeInvoke` from a matching module is
|
|
29
|
+
* the real wrapper (safe-log).
|
|
30
|
+
* @property {string} [runChildPath] The one file allowed to use the async
|
|
31
|
+
* child_process APIs (the runner itself), as a path from `root`; also named
|
|
32
|
+
* in the no-direct-child-process message.
|
|
33
|
+
* @property {string} [root] Directory `runChildPath` is relative to. Defaults
|
|
34
|
+
* to ESLint's working directory.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** `'error'`, or `['error', options]` when any option was given. */
|
|
38
|
+
function entry(options) {
|
|
39
|
+
const given = Object.fromEntries(
|
|
40
|
+
Object.entries(options).filter(([, value]) => value !== undefined),
|
|
41
|
+
);
|
|
42
|
+
return Object.keys(given).length > 0 ? ['error', given] : 'error';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The error-handling rules as a rules object.
|
|
47
|
+
*
|
|
48
|
+
* @param {ErrorHandlingOptions} [options]
|
|
49
|
+
*/
|
|
50
|
+
export function errorHandling({
|
|
51
|
+
namespace = 'shuddhi',
|
|
52
|
+
helperModule,
|
|
53
|
+
helperSourcePattern,
|
|
54
|
+
runChildPath,
|
|
55
|
+
root,
|
|
56
|
+
} = {}) {
|
|
57
|
+
return {
|
|
58
|
+
// ERR-01. A catch that neither rethrows nor rejects needs
|
|
59
|
+
// `// fail-open: <reason>`; `no-empty` only ever saw empty blocks.
|
|
60
|
+
[`${namespace}/no-swallowed-catch`]: 'error',
|
|
61
|
+
// ERR-02 and OBS-03, one rule: injected loggers, and console on a catch
|
|
62
|
+
// path, go through safeLog/safeInvoke.
|
|
63
|
+
[`${namespace}/safe-log`]: entry({ helperModule, helperSourcePattern }),
|
|
64
|
+
// ERR-03. A caught value becomes text only through errorDetail().
|
|
65
|
+
[`${namespace}/no-raw-error-string`]: entry({ helperModule }),
|
|
66
|
+
// ERR-08. Async child processes only through the runner.
|
|
67
|
+
[`${namespace}/no-direct-child-process`]: entry({
|
|
68
|
+
allow: runChildPath === undefined ? undefined : [runChildPath],
|
|
69
|
+
runner: runChildPath,
|
|
70
|
+
root,
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Where the error-handling rules do not apply: tests, fixtures and mocks.
|
|
77
|
+
* A test's catch that captures an error to assert on it is the point of the
|
|
78
|
+
* test, and a test harness's direct spawn is not shipped tooling. Globs are
|
|
79
|
+
* relative to whichever workspace config uses them.
|
|
80
|
+
*/
|
|
81
|
+
const CODE_FILES = '**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}';
|
|
82
|
+
|
|
83
|
+
export const ERROR_HANDLING_EXEMPT = [
|
|
84
|
+
'**/*.test.*',
|
|
85
|
+
'**/*.spec.*',
|
|
86
|
+
'**/*.perf-test.*',
|
|
87
|
+
'**/*.fixture.*',
|
|
88
|
+
'**/__tests__/**',
|
|
89
|
+
'**/__mocks__/**',
|
|
90
|
+
'**/fixtures/**',
|
|
91
|
+
'**/test/**',
|
|
92
|
+
'**/e2e/**',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Config blocks that turn the error-handling rules on for `files` and off for
|
|
97
|
+
* {@link ERROR_HANDLING_EXEMPT}. Legacy hits are best held in the workspace's
|
|
98
|
+
* `eslint-suppressions.json` (ESLint bulk suppressions): counts only fall.
|
|
99
|
+
*
|
|
100
|
+
* @param {import('eslint').Linter.Config['files']} files
|
|
101
|
+
* @param {ErrorHandlingOptions & { plugin?: import('eslint').ESLint.Plugin }} [options]
|
|
102
|
+
* `plugin` is the object bound under `namespace`; pass the one your config
|
|
103
|
+
* already binds there, if any. Defaults to a plugin of this family's rules.
|
|
104
|
+
*/
|
|
105
|
+
export function errorHandlingBlocks(files, options = {}) {
|
|
106
|
+
const { plugin = familyPlugin, namespace = 'shuddhi', ...rest } = options;
|
|
107
|
+
const ruleSet = errorHandling({ namespace, ...rest });
|
|
108
|
+
return [
|
|
109
|
+
{ files, plugins: { [namespace]: plugin }, rules: ruleSet },
|
|
110
|
+
{
|
|
111
|
+
// Each exemption is ANDed with a code extension (a nested array is an
|
|
112
|
+
// intersection in flat config). A bare `**/test/**` or `**/*.test.*`
|
|
113
|
+
// here would make ESLint lint every .sh and .json under those paths,
|
|
114
|
+
// since any `files` match marks a file as lintable.
|
|
115
|
+
files: ERROR_HANDLING_EXEMPT.map(glob => [glob, CODE_FILES]),
|
|
116
|
+
plugins: { [namespace]: plugin },
|
|
117
|
+
rules: Object.fromEntries(
|
|
118
|
+
Object.keys(ruleSet).map(rule => [rule, 'off']),
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|