@agjs/tsforge 0.1.19 → 0.2.1
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/package.json +6 -2
- package/scripts/browser-check.ts +41 -5
- package/scripts/build-rules-md.ts +78 -21
- package/scripts/cli-metrics.ts +10 -0
- package/scripts/sweep.ts +53 -23
- package/scripts/web-sweep.ts +292 -0
- package/src/browser/index.ts +3 -0
- package/src/browser/oracle.ts +215 -8
- package/src/cli.ts +22 -4
- package/src/config/index.ts +8 -0
- package/src/config/profiles.ts +150 -0
- package/src/config/tsforge-config.ts +64 -5
- package/src/detect-gate.ts +144 -13
- package/src/eval/eval.types.ts +9 -0
- package/src/eval/failure-class.ts +263 -0
- package/src/eval/index.ts +8 -0
- package/src/eval/metrics.ts +7 -0
- package/src/eval/parse-log.ts +105 -0
- package/src/eval/report.ts +19 -0
- package/src/eval/score.ts +10 -0
- package/src/loop/feedback/meta-rule-docs.ts +48 -0
- package/src/loop/feedback/rule-docs.ts +150 -0
- package/src/loop/loop.types.ts +4 -0
- package/src/loop/rule-docs.generated.json +131 -1
- package/src/loop/ttsr-defaults.ts +175 -4
- package/src/loop/turn.ts +3 -0
- package/src/meta-rules/registry.ts +32 -0
- package/src/meta-rules/rules/ci/no-github-context-in-shell.ts +40 -0
- package/src/meta-rules/rules/ci/no-pull-request-target-untrusted-checkout.ts +42 -0
- package/src/meta-rules/rules/ci/workflow-permissions-explicit.ts +49 -0
- package/src/meta-rules/rules/ci/workflow-permissions-least-privilege.ts +44 -0
- package/src/meta-rules/rules/config/next-image-remote-patterns-no-wildcards.ts +77 -0
- package/src/meta-rules/rules/config/next-instrumentation-present.ts +66 -0
- package/src/meta-rules/rules/config/next-proxy-over-middleware.ts +64 -0
- package/src/meta-rules/rules/config/tsconfig-recommended-flags.ts +75 -0
- package/src/meta-rules/rules/supply-chain/dependency-overrides-require-comment.ts +61 -0
- package/src/meta-rules/rules/supply-chain/fastify-security-plugins.ts +54 -0
- package/src/meta-rules/rules/supply-chain/lockfile-required.ts +51 -0
- package/src/meta-rules/rules/supply-chain/migrations-must-be-checked-in.ts +49 -0
- package/src/meta-rules/rules/supply-chain/no-git-or-tarball-dependencies.ts +70 -0
- package/src/meta-rules/rules/supply-chain/package-manager-field-required.ts +31 -0
- package/src/meta-rules/rules/supply-chain/production-must-not-use-drizzle-push.ts +75 -0
- package/src/meta-rules/rules/supply-chain/single-package-manager.ts +30 -0
- package/src/meta-rules/utils/lockfiles.ts +105 -0
- package/src/meta-rules/utils/workflow-yaml.ts +86 -0
- package/src/rule-packs/authorization/index.ts +26 -0
- package/src/rule-packs/authorization/rules/id-param-requires-object-authz.ts +87 -0
- package/src/rule-packs/authorization/rules/mutating-route-requires-authz.ts +116 -0
- package/src/rule-packs/authorization/rules/server-action-requires-authz.ts +101 -0
- package/src/rule-packs/authorization/utils.ts +285 -0
- package/src/rule-packs/boundary-utils.ts +13 -0
- package/src/rule-packs/code-flow/index.ts +4 -1
- package/src/rule-packs/code-flow/rules/no-throw-literal.ts +67 -0
- package/src/rule-packs/drizzle/index.ts +7 -0
- package/src/rule-packs/drizzle/rules/update-delete-account-scoped-must-filter-scope.ts +106 -0
- package/src/rule-packs/drizzle/rules/update-delete-must-have-where.ts +73 -0
- package/src/rule-packs/drizzle/utils.ts +133 -1
- package/src/rule-packs/fastify/index.ts +38 -0
- package/src/rule-packs/fastify/rules/error-handler-must-set-status.ts +78 -0
- package/src/rule-packs/fastify/rules/prefer-return-over-reply-send.ts +104 -0
- package/src/rule-packs/fastify/rules/require-fp-for-shared-plugins.ts +106 -0
- package/src/rule-packs/fastify/rules/require-plugin-name.ts +54 -0
- package/src/rule-packs/fastify/rules/require-response-schema.ts +62 -0
- package/src/rule-packs/fastify/rules/require-route-schema.ts +104 -0
- package/src/rule-packs/fastify/rules/test-inject-must-close-app.ts +44 -0
- package/src/rule-packs/fastify/utils/fastifyChain.ts +231 -0
- package/src/rule-packs/index.ts +10 -0
- package/src/rule-packs/jwt-cookies/index.ts +10 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-maxage-or-expires.ts +132 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-samesite.ts +151 -0
- package/src/rule-packs/jwt-cookies/rules/jwt-must-verify-not-decode.ts +124 -0
- package/src/rule-packs/module-boundaries/index.ts +3 -0
- package/src/rule-packs/module-boundaries/rules/no-react-in-services.ts +111 -0
- package/src/rule-packs/nextjs/index.ts +32 -0
- package/src/rule-packs/nextjs/rules/await-dynamic-request-apis.ts +65 -0
- package/src/rule-packs/nextjs/rules/error-boundary-require-use-client.ts +38 -0
- package/src/rule-packs/nextjs/rules/mutation-should-revalidate-cache.ts +152 -0
- package/src/rule-packs/nextjs/rules/no-html-img-element.ts +45 -0
- package/src/rule-packs/nextjs/rules/no-internal-api-fetch.ts +126 -0
- package/src/rule-packs/nextjs/rules/no-secret-props-to-client.ts +118 -0
- package/src/rule-packs/nextjs/rules/no-sensitive-next-public-env.ts +72 -0
- package/src/rule-packs/nextjs/rules/prefer-lazy-use-state-init.ts +85 -0
- package/src/rule-packs/nextjs/rules/server-action-requires-authz-and-validation.ts +178 -0
- package/src/rule-packs/nextjs/rules/server-only-modules-import-server-only.ts +87 -0
- package/src/rule-packs/nextjs/utils.ts +18 -0
- package/src/rule-packs/react-component-architecture/index.ts +18 -0
- package/src/rule-packs/react-component-architecture/rules/dangerous-html-requires-sanitize.ts +83 -0
- package/src/rule-packs/react-component-architecture/rules/no-anonymous-useEffect.ts +61 -0
- package/src/rule-packs/react-component-architecture/rules/no-component-invocation.ts +55 -0
- package/src/rule-packs/react-component-architecture/rules/no-derived-state-in-effect.ts +204 -0
- package/src/rule-packs/react-component-architecture/rules/no-nested-component.ts +152 -0
- package/src/rule-packs/react-component-architecture/rules/no-react-fc.ts +57 -0
- package/src/rule-packs/rule-catalog.types.ts +21 -0
- package/src/rule-packs/rule-metadata.ts +163 -0
- package/src/rule-packs/runtime-boundaries/index.ts +33 -0
- package/src/rule-packs/runtime-boundaries/rules/no-prototype-polluting-merge.ts +113 -0
- package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-fetch-url.ts +69 -0
- package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-redirect.ts +79 -0
- package/src/rule-packs/runtime-boundaries/rules/upload-must-set-limits.ts +126 -0
- package/src/rule-packs/runtime-boundaries/rules/webhook-must-verify-signature-before-parse.ts +87 -0
- package/src/rule-packs/security/index.ts +35 -0
- package/src/rule-packs/security/rules/catch-must-handle.ts +126 -0
- package/src/rule-packs/security/rules/no-auth-token-in-storage.ts +107 -0
- package/src/rule-packs/security/rules/no-child-process-exec.ts +72 -0
- package/src/rule-packs/security/rules/no-dynamic-regexp.ts +56 -0
- package/src/rule-packs/security/rules/no-inner-html-assignment.ts +42 -0
- package/src/rule-packs/security/rules/no-spawn-with-shell.ts +106 -0
- package/src/rule-packs/structured-logging/index.ts +6 -0
- package/src/rule-packs/structured-logging/rules/caught-error-log-requires-cause.ts +234 -0
- package/src/rule-packs/structured-logging/rules/logger-not-console.ts +146 -0
- package/src/rule-packs/test-conventions/index.ts +9 -0
- package/src/rule-packs/test-conventions/rules/fake-timers-must-be-restored.ts +143 -0
- package/src/rule-packs/test-conventions/rules/no-conditional-expect.ts +77 -0
- package/src/rule-packs/test-conventions/rules/no-real-network-in-unit-tests.ts +174 -0
- package/src/rule-packs/typescript-core/index.ts +30 -0
- package/src/rule-packs/typescript-core/rules/exported-functions-require-return-type.ts +74 -0
- package/src/rule-packs/typescript-core/rules/fetch-must-check-ok.ts +106 -0
- package/src/rule-packs/typescript-core/rules/json-parse-must-validate.ts +97 -0
- package/src/rule-packs/typescript-core/rules/no-unsafe-boundary-cast.ts +70 -0
- package/src/stack-detection/packs.ts +57 -0
- package/strict.type-aware.eslint.config.mjs +33 -0
- package/strict.web.eslint.config.mjs +32 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { walkSome } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_AUTHZ_FUNCTIONS = [
|
|
7
|
+
"requireUser",
|
|
8
|
+
"authorize",
|
|
9
|
+
"requireAuth",
|
|
10
|
+
"assertAuthorized",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export interface IAuthzOptions {
|
|
14
|
+
readonly authzFunctions?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type AuthzRuleOptions = [IAuthzOptions];
|
|
18
|
+
|
|
19
|
+
export const authzOptionSchema: JSONSchema4 = {
|
|
20
|
+
type: "object",
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
authzFunctions: {
|
|
24
|
+
type: "array",
|
|
25
|
+
items: { type: "string" },
|
|
26
|
+
uniqueItems: true,
|
|
27
|
+
minItems: 1,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const MUTATING_HTTP_METHODS = new Set([
|
|
33
|
+
"POST",
|
|
34
|
+
"PUT",
|
|
35
|
+
"PATCH",
|
|
36
|
+
"DELETE",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const DB_MUTATION_METHODS = new Set(["update", "insert", "delete"]);
|
|
40
|
+
const DB_QUERY_METHODS = new Set([
|
|
41
|
+
"select",
|
|
42
|
+
"query",
|
|
43
|
+
"findFirst",
|
|
44
|
+
"findMany",
|
|
45
|
+
"findUnique",
|
|
46
|
+
"get",
|
|
47
|
+
"execute",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export function defaultAuthzOptions(): IAuthzOptions {
|
|
51
|
+
return { authzFunctions: [...DEFAULT_AUTHZ_FUNCTIONS] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveAuthzFunctions(options: IAuthzOptions): Set<string> {
|
|
55
|
+
return new Set(options.authzFunctions ?? DEFAULT_AUTHZ_FUNCTIONS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function calleeName(callee: TSESTree.Node): string | null {
|
|
59
|
+
if (callee.type === AST_NODE_TYPES.Identifier) {
|
|
60
|
+
return callee.name;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
65
|
+
!callee.computed &&
|
|
66
|
+
callee.property.type === AST_NODE_TYPES.Identifier
|
|
67
|
+
) {
|
|
68
|
+
return callee.property.name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isAuthzCall(
|
|
75
|
+
node: TSESTree.CallExpression,
|
|
76
|
+
authzNames: Set<string>
|
|
77
|
+
): boolean {
|
|
78
|
+
const name = calleeName(node.callee);
|
|
79
|
+
|
|
80
|
+
return name !== null && authzNames.has(name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function containsAuthzCall(
|
|
84
|
+
root: TSESTree.Node,
|
|
85
|
+
authzNames: Set<string>
|
|
86
|
+
): boolean {
|
|
87
|
+
return walkSome(
|
|
88
|
+
root,
|
|
89
|
+
(node) =>
|
|
90
|
+
node.type === AST_NODE_TYPES.CallExpression &&
|
|
91
|
+
isAuthzCall(node, authzNames)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isRouteHandlerFile(filename: string): boolean {
|
|
96
|
+
const base = filename.split(/[\\/]/).pop() ?? "";
|
|
97
|
+
|
|
98
|
+
return /^route\.(?:tsx|ts|jsx|js)$/.test(base);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function hasUseServerDirective(program: TSESTree.Program): boolean {
|
|
102
|
+
for (const stmt of program.body) {
|
|
103
|
+
if (
|
|
104
|
+
stmt.type !== AST_NODE_TYPES.ExpressionStatement ||
|
|
105
|
+
stmt.expression.type !== AST_NODE_TYPES.Literal ||
|
|
106
|
+
typeof stmt.expression.value !== "string"
|
|
107
|
+
) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (stmt.expression.value === "use server") {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function dbReceiverName(callee: TSESTree.MemberExpression): string | null {
|
|
120
|
+
const object = callee.object;
|
|
121
|
+
|
|
122
|
+
if (object.type === AST_NODE_TYPES.Identifier) {
|
|
123
|
+
return object.name;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
object.type === AST_NODE_TYPES.MemberExpression &&
|
|
128
|
+
!object.computed &&
|
|
129
|
+
object.property.type === AST_NODE_TYPES.Identifier
|
|
130
|
+
) {
|
|
131
|
+
return object.property.name;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function looksLikeDbReceiver(name: string | null): boolean {
|
|
138
|
+
if (name === null) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return name === "db" || name === "tx" || name.endsWith("Db");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isDbMutationCall(node: TSESTree.CallExpression): boolean {
|
|
146
|
+
const callee = node.callee;
|
|
147
|
+
|
|
148
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const method = calleeName(callee);
|
|
153
|
+
|
|
154
|
+
if (method === null || !DB_MUTATION_METHODS.has(method)) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return looksLikeDbReceiver(dbReceiverName(callee));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function isDbQueryCall(node: TSESTree.CallExpression): boolean {
|
|
162
|
+
const callee = node.callee;
|
|
163
|
+
|
|
164
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const method = calleeName(callee);
|
|
169
|
+
|
|
170
|
+
if (method === null || !DB_QUERY_METHODS.has(method)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return looksLikeDbReceiver(dbReceiverName(callee));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function isParamsIdRead(node: TSESTree.Node): boolean {
|
|
178
|
+
if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const object = node.object;
|
|
183
|
+
const property = node.property;
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
object.type !== AST_NODE_TYPES.Identifier ||
|
|
187
|
+
object.name !== "params" ||
|
|
188
|
+
property.type !== AST_NODE_TYPES.Identifier ||
|
|
189
|
+
property.name !== "id"
|
|
190
|
+
) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type FunctionLike =
|
|
198
|
+
| TSESTree.FunctionDeclaration
|
|
199
|
+
| TSESTree.FunctionExpression
|
|
200
|
+
| TSESTree.ArrowFunctionExpression;
|
|
201
|
+
|
|
202
|
+
export function getExportedMutatingHandlerName(
|
|
203
|
+
node: TSESTree.Node
|
|
204
|
+
): string | null {
|
|
205
|
+
if (node.type === AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
206
|
+
const declaration = node.declaration;
|
|
207
|
+
|
|
208
|
+
if (declaration === null) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (declaration.type === AST_NODE_TYPES.FunctionDeclaration) {
|
|
213
|
+
return getMutatingHandlerNameFromFunction(declaration);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (declaration.type === AST_NODE_TYPES.VariableDeclaration) {
|
|
217
|
+
for (const declarator of declaration.declarations) {
|
|
218
|
+
const name = getMutatingHandlerNameFromVariableDeclarator(declarator);
|
|
219
|
+
|
|
220
|
+
if (name !== null) {
|
|
221
|
+
return name;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
node.type === AST_NODE_TYPES.FunctionDeclaration &&
|
|
231
|
+
node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration
|
|
232
|
+
) {
|
|
233
|
+
return getMutatingHandlerNameFromFunction(node);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getMutatingHandlerNameFromFunction(
|
|
240
|
+
node: TSESTree.FunctionDeclaration
|
|
241
|
+
): string | null {
|
|
242
|
+
if (node.id === null) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!MUTATING_HTTP_METHODS.has(node.id.name)) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return node.id.name;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getMutatingHandlerNameFromVariableDeclarator(
|
|
254
|
+
node: TSESTree.VariableDeclarator
|
|
255
|
+
): string | null {
|
|
256
|
+
if (node.id.type !== AST_NODE_TYPES.Identifier) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!MUTATING_HTTP_METHODS.has(node.id.name)) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const init = node.init;
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
init === null ||
|
|
268
|
+
(init.type !== AST_NODE_TYPES.FunctionExpression &&
|
|
269
|
+
init.type !== AST_NODE_TYPES.ArrowFunctionExpression)
|
|
270
|
+
) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return node.id.name;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getFunctionLikeBody(
|
|
278
|
+
node: FunctionLike
|
|
279
|
+
): TSESTree.BlockStatement | TSESTree.Expression | null {
|
|
280
|
+
if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
281
|
+
return node.body;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return node.body;
|
|
285
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
export function isExpression(node: TSESTree.Node): node is TSESTree.Expression {
|
|
4
|
+
return node.type !== AST_NODE_TYPES.SpreadElement;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isStringLiteral(node: TSESTree.Expression): boolean {
|
|
8
|
+
return node.type === AST_NODE_TYPES.Literal && typeof node.value === "string";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isIdentifierNamed(node: TSESTree.Node, name: string): boolean {
|
|
12
|
+
return node.type === AST_NODE_TYPES.Identifier && node.name === name;
|
|
13
|
+
}
|
|
@@ -2,12 +2,14 @@ import type { TSESLint } from "@typescript-eslint/utils";
|
|
|
2
2
|
|
|
3
3
|
import { noBareDateNowRule } from "./rules/no-bare-date-now";
|
|
4
4
|
import { noTemplateTrimEmptyTernaryRule } from "./rules/no-template-trim-empty-ternary";
|
|
5
|
+
import { noThrowLiteralRule } from "./rules/no-throw-literal";
|
|
5
6
|
import { preferEarlyReturnRule } from "./rules/prefer-early-return";
|
|
6
7
|
import type { IRulePack } from "../rule-packs.types";
|
|
7
8
|
|
|
8
9
|
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
9
10
|
"no-bare-date-now": noBareDateNowRule,
|
|
10
11
|
"no-template-trim-empty-ternary": noTemplateTrimEmptyTernaryRule,
|
|
12
|
+
"no-throw-literal": noThrowLiteralRule,
|
|
11
13
|
"prefer-early-return": preferEarlyReturnRule,
|
|
12
14
|
};
|
|
13
15
|
|
|
@@ -18,7 +20,8 @@ export const codeFlowPack: IRulePack = {
|
|
|
18
20
|
rulesConfig: {
|
|
19
21
|
"no-bare-date-now": "error",
|
|
20
22
|
"no-template-trim-empty-ternary": "error",
|
|
21
|
-
"
|
|
23
|
+
"no-throw-literal": "error",
|
|
24
|
+
"prefer-early-return": "warn",
|
|
22
25
|
},
|
|
23
26
|
};
|
|
24
27
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
|
|
5
|
+
export const RULE_NAME = "no-throw-literal";
|
|
6
|
+
|
|
7
|
+
type MessageIds = "throwLiteral";
|
|
8
|
+
|
|
9
|
+
function isErrorConstruction(node: TSESTree.Expression): boolean {
|
|
10
|
+
if (node.type === AST_NODE_TYPES.NewExpression) {
|
|
11
|
+
const callee = node.callee;
|
|
12
|
+
|
|
13
|
+
if (callee.type === AST_NODE_TYPES.Identifier && callee.name === "Error") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
19
|
+
!callee.computed &&
|
|
20
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
21
|
+
callee.property.name === "Error"
|
|
22
|
+
) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const noThrowLiteralRule = createRule<[], MessageIds>({
|
|
31
|
+
name: RULE_NAME,
|
|
32
|
+
meta: {
|
|
33
|
+
type: "problem",
|
|
34
|
+
docs: {
|
|
35
|
+
description:
|
|
36
|
+
"Disallow throwing primitive literals (strings, numbers) — throw Error instances so error handlers can propagate status and stack traces correctly.",
|
|
37
|
+
},
|
|
38
|
+
schema: [],
|
|
39
|
+
messages: {
|
|
40
|
+
throwLiteral:
|
|
41
|
+
"Do not throw a literal value — throw an `Error` instance (e.g. `throw new Error('...')`) so framework error handlers can propagate it correctly.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultOptions: [],
|
|
45
|
+
create(context) {
|
|
46
|
+
return {
|
|
47
|
+
ThrowStatement(node: TSESTree.ThrowStatement) {
|
|
48
|
+
const argument = node.argument;
|
|
49
|
+
|
|
50
|
+
if (argument === null) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isErrorConstruction(argument)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
argument.type === AST_NODE_TYPES.Literal ||
|
|
60
|
+
argument.type === AST_NODE_TYPES.TemplateLiteral
|
|
61
|
+
) {
|
|
62
|
+
context.report({ node, messageId: "throwLiteral" });
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -8,6 +8,8 @@ import { schemaFilesMustNotImportDriverRule } from "./rules/schema-files-must-no
|
|
|
8
8
|
import { schemaFilesMustOnlyExportSchemaRule } from "./rules/schema-files-must-only-export-schema";
|
|
9
9
|
import { tablesMustHaveTimestampsRule } from "./rules/tables-must-have-timestamps";
|
|
10
10
|
import { timestampMustSpecifyModeRule } from "./rules/timestamp-must-specify-mode";
|
|
11
|
+
import { updateDeleteAccountScopedMustFilterScopeRule } from "./rules/update-delete-account-scoped-must-filter-scope";
|
|
12
|
+
import { updateDeleteMustHaveWhereRule } from "./rules/update-delete-must-have-where";
|
|
11
13
|
import type { IRulePack } from "../rule-packs.types";
|
|
12
14
|
|
|
13
15
|
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
@@ -19,6 +21,9 @@ const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
|
19
21
|
"schema-files-must-only-export-schema": schemaFilesMustOnlyExportSchemaRule,
|
|
20
22
|
"tables-must-have-timestamps": tablesMustHaveTimestampsRule,
|
|
21
23
|
"timestamp-must-specify-mode": timestampMustSpecifyModeRule,
|
|
24
|
+
"update-delete-account-scoped-must-filter-scope":
|
|
25
|
+
updateDeleteAccountScopedMustFilterScopeRule,
|
|
26
|
+
"update-delete-must-have-where": updateDeleteMustHaveWhereRule,
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
export const drizzlePack: IRulePack = {
|
|
@@ -35,6 +40,8 @@ export const drizzlePack: IRulePack = {
|
|
|
35
40
|
"schema-files-must-only-export-schema": "warn",
|
|
36
41
|
"tables-must-have-timestamps": "warn",
|
|
37
42
|
"timestamp-must-specify-mode": "error",
|
|
43
|
+
"update-delete-account-scoped-must-filter-scope": "error",
|
|
44
|
+
"update-delete-must-have-where": "error",
|
|
38
45
|
},
|
|
39
46
|
};
|
|
40
47
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
import { matchesAnyGlobPattern } from "../../utils";
|
|
5
|
+
import {
|
|
6
|
+
chainContainsWhereWithScope,
|
|
7
|
+
identifyUpdateDeleteQuery,
|
|
8
|
+
} from "../utils";
|
|
9
|
+
|
|
10
|
+
export const RULE_NAME = "update-delete-account-scoped-must-filter-scope";
|
|
11
|
+
|
|
12
|
+
export interface IUpdateDeleteAccountScopedMustFilterScopeOptions {
|
|
13
|
+
readonly tables?: readonly string[];
|
|
14
|
+
readonly scopeColumn?: string;
|
|
15
|
+
readonly alternateScopeColumns?: readonly string[];
|
|
16
|
+
readonly allowFiles?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type RuleOptions = [IUpdateDeleteAccountScopedMustFilterScopeOptions];
|
|
20
|
+
type MessageIds = "missingScopeFilter";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SCOPE_COLUMN = "accountId";
|
|
23
|
+
|
|
24
|
+
const optionSchema: JSONSchema4 = {
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
tables: {
|
|
29
|
+
type: "array",
|
|
30
|
+
items: { type: "string" },
|
|
31
|
+
uniqueItems: true,
|
|
32
|
+
minItems: 1,
|
|
33
|
+
},
|
|
34
|
+
scopeColumn: { type: "string", minLength: 1 },
|
|
35
|
+
alternateScopeColumns: {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: { type: "string", minLength: 1 },
|
|
38
|
+
uniqueItems: true,
|
|
39
|
+
},
|
|
40
|
+
allowFiles: {
|
|
41
|
+
type: "array",
|
|
42
|
+
items: { type: "string", minLength: 1 },
|
|
43
|
+
uniqueItems: true,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const updateDeleteAccountScopedMustFilterScopeRule = createRule<
|
|
49
|
+
RuleOptions,
|
|
50
|
+
MessageIds
|
|
51
|
+
>({
|
|
52
|
+
name: RULE_NAME,
|
|
53
|
+
meta: {
|
|
54
|
+
type: "problem",
|
|
55
|
+
docs: {
|
|
56
|
+
description:
|
|
57
|
+
"Require Drizzle `.update()` / `.delete()` against account-scoped tables to filter by a scope column in `.where()`.",
|
|
58
|
+
},
|
|
59
|
+
schema: [optionSchema],
|
|
60
|
+
messages: {
|
|
61
|
+
missingScopeFilter:
|
|
62
|
+
"Drizzle `.{{kind}}()` on account-scoped table `{{table}}` is missing a `{{scopeColumn}}` filter in `.where()` — tenant data can leak across accounts.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
defaultOptions: [{}],
|
|
66
|
+
create(context, [options]) {
|
|
67
|
+
const tables = new Set(options.tables ?? []);
|
|
68
|
+
const scopeColumn = options.scopeColumn ?? DEFAULT_SCOPE_COLUMN;
|
|
69
|
+
const alternateScopeColumns = options.alternateScopeColumns ?? [];
|
|
70
|
+
const allowFiles = options.allowFiles ?? [];
|
|
71
|
+
|
|
72
|
+
if (tables.size === 0) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (matchesAnyGlobPattern(context.filename, allowFiles)) {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const scopeColumns = [scopeColumn, ...alternateScopeColumns];
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
CallExpression(node) {
|
|
84
|
+
const query = identifyUpdateDeleteQuery(node);
|
|
85
|
+
|
|
86
|
+
if (query === null || !tables.has(query.table)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (chainContainsWhereWithScope(node, scopeColumns)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
context.report({
|
|
95
|
+
node,
|
|
96
|
+
messageId: "missingScopeFilter",
|
|
97
|
+
data: {
|
|
98
|
+
kind: query.kind,
|
|
99
|
+
table: query.table,
|
|
100
|
+
scopeColumn,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
import { matchesAnyGlobPattern } from "../../utils";
|
|
5
|
+
import { chainContainsWhere, identifyUpdateDeleteQuery } from "../utils";
|
|
6
|
+
|
|
7
|
+
export const RULE_NAME = "update-delete-must-have-where";
|
|
8
|
+
|
|
9
|
+
export interface IUpdateDeleteMustHaveWhereOptions {
|
|
10
|
+
readonly allowFiles?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type RuleOptions = [IUpdateDeleteMustHaveWhereOptions];
|
|
14
|
+
type MessageIds = "missingWhere";
|
|
15
|
+
|
|
16
|
+
const optionSchema: JSONSchema4 = {
|
|
17
|
+
type: "object",
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: {
|
|
20
|
+
allowFiles: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: { type: "string", minLength: 1 },
|
|
23
|
+
uniqueItems: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const updateDeleteMustHaveWhereRule = createRule<
|
|
29
|
+
RuleOptions,
|
|
30
|
+
MessageIds
|
|
31
|
+
>({
|
|
32
|
+
name: RULE_NAME,
|
|
33
|
+
meta: {
|
|
34
|
+
type: "problem",
|
|
35
|
+
docs: {
|
|
36
|
+
description:
|
|
37
|
+
"Require every Drizzle `.update()` and `.delete()` call to include a `.where()` clause — unscoped writes affect every row.",
|
|
38
|
+
},
|
|
39
|
+
schema: [optionSchema],
|
|
40
|
+
messages: {
|
|
41
|
+
missingWhere:
|
|
42
|
+
"Drizzle `.{{kind}}()` on `{{table}}` is missing `.where()` — unscoped writes can mutate every row in the table.",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultOptions: [{}],
|
|
46
|
+
create(context, [options]) {
|
|
47
|
+
const allowFiles = options.allowFiles ?? [];
|
|
48
|
+
|
|
49
|
+
if (matchesAnyGlobPattern(context.filename, allowFiles)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
CallExpression(node) {
|
|
55
|
+
const query = identifyUpdateDeleteQuery(node);
|
|
56
|
+
|
|
57
|
+
if (query === null) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (chainContainsWhere(node)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: "missingWhere",
|
|
68
|
+
data: { kind: query.kind, table: query.table },
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|