@sarj/eslint-plugin 3.0.0 → 4.1.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 +48 -6
- package/dist/index.cjs +209 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -8
- package/dist/index.d.ts +14 -8
- package/dist/index.js +209 -143
- package/dist/index.js.map +1 -1
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -12,10 +12,47 @@ import sarj from "@sarj/eslint-plugin";
|
|
|
12
12
|
export default [...sarj.configs.recommended];
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
51 rules. Each rule's source under `src/rules/` carries its own `@fileoverview` rationale plus `meta.docs.description` + `meta.messages` — read the file for the full reasoning, including the false positives it deliberately does not fire on.
|
|
16
16
|
|
|
17
17
|
Presets: `recommended` (warn-first), `strict` (every rule at error), `style-guide` (formatting/naming subset).
|
|
18
18
|
|
|
19
|
+
## New in 4.1.0 — `no-hand-rolled-sleep`
|
|
20
|
+
|
|
21
|
+
`new Promise((resolve) => setTimeout(resolve, ms))` is `node:timers/promises`'s
|
|
22
|
+
`setTimeout` rewritten by hand, minus the `AbortSignal` — so it is a capability
|
|
23
|
+
loss, not verbosity. The hand-rolled sleep holds a live timer that nothing can
|
|
24
|
+
clear, and its mirror image, the `Promise.race([work, rejectAfter(ms)])` timeout
|
|
25
|
+
arm, leaks the timer the other way: when `work` wins, nothing clears it and it
|
|
26
|
+
keeps the event loop alive until it fires.
|
|
27
|
+
|
|
28
|
+
Shipped only after confirming nothing already enabled reports this position. The
|
|
29
|
+
enabled set was resolved with `ESLint#calculateConfigForFile` against the shipped
|
|
30
|
+
`eslint.strict.mjs` (204 rules before this one) and a file containing every
|
|
31
|
+
shape was linted through it: no report. `eslint-plugin-unicorn` 72 has no
|
|
32
|
+
promisified-timer rule among its 341; `unicorn/prefer-abort-signal-timeout` covers the
|
|
33
|
+
`AbortController` + `setTimeout` idiom and not the race arm; core
|
|
34
|
+
`no-promise-executor-return` fires on the concise-arrow spelling only and its
|
|
35
|
+
remedy ("add braces") entrenches the hand-rolled sleep. The polling-loop variant
|
|
36
|
+
(`while (!done) await sleep(ms)`) is deliberately absent — core `no-await-in-loop`
|
|
37
|
+
is already enabled and reports that exact position.
|
|
38
|
+
|
|
39
|
+
Measured over 1,471 files containing `setTimeout` across 15 OSS repos (hono,
|
|
40
|
+
tRPC, drizzle-orm, undici, vitest, got, cal.com, documenso, dub, formbricks,
|
|
41
|
+
midday, openstatus, papermark, unkey, zod) and seven internal ones: 85 + 11
|
|
42
|
+
sleeps and 2 + 1 leaky race arms at the default settings, **0 false positives**.
|
|
43
|
+
|
|
44
|
+
`checkClientModules` is the option that matters. A browser or React Native
|
|
45
|
+
bundle cannot import `node:timers/promises` and the web platform ships no
|
|
46
|
+
equivalent, so client modules are skipped by default — 79% of the internal
|
|
47
|
+
corpus's occurrences live in `.tsx` components where the fix cannot be applied.
|
|
48
|
+
Turn it on only in a tree where every file resolves `node:` builtins. The race
|
|
49
|
+
message is reported everywhere regardless: `AbortSignal.timeout` is on the web
|
|
50
|
+
platform too.
|
|
51
|
+
|
|
52
|
+
| Rule | What it catches | Preset |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `no-hand-rolled-sleep` | `new Promise((r) => setTimeout(r, ms))` in any spelling, and an uncleared `Promise.race`/`Promise.any` timeout arm. Options: `checkClientModules` (default `false`), `allowIn`. | warn / error |
|
|
55
|
+
|
|
19
56
|
## New in 2.14.0 — `no-tautological-expect`
|
|
20
57
|
|
|
21
58
|
The TS half of SARJ057. An `expect(...)` whose operands are all literals has
|
|
@@ -125,7 +162,7 @@ Glob patterns whose files opt out (generated code already opts out by default).
|
|
|
125
162
|
|
|
126
163
|
## Configurable rules
|
|
127
164
|
|
|
128
|
-
Most rules take no options. These
|
|
165
|
+
Most rules take no options. These do, because they encode a codebase's
|
|
129
166
|
architecture rather than a language fact — the defaults describe one convention
|
|
130
167
|
and every repo gets to name its own.
|
|
131
168
|
|
|
@@ -135,10 +172,15 @@ and every repo gets to name its own.
|
|
|
135
172
|
| `no-dynamic-sql` | `methods` | `["prepare", "exec", "query"]` | Statement-taking methods to inspect |
|
|
136
173
|
| `no-storage-in-stateless-modules` | `modules` | `[]` (rule off) | Directories declared stateless |
|
|
137
174
|
| `no-storage-in-stateless-modules` | `methods` | `["prepare", "put", "getWithMetadata"]` | Storage methods to flag |
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
175
|
+
| `no-hand-rolled-sleep` | `checkClientModules` | `false` | Also report the sleep form in browser/React Native modules |
|
|
176
|
+
| `no-hand-rolled-sleep` | `allowIn` | `[]` | Glob patterns for a sanctioned sleep wrapper module |
|
|
177
|
+
|
|
178
|
+
The path options on the first three rules are **regular-expression sources
|
|
179
|
+
matched against the absolute filename**, not globs — so they can express both
|
|
180
|
+
path separators. `allowIn` is the exception, on `no-hand-rolled-sleep` as on
|
|
181
|
+
`require-fetch-timeout`: it takes minimatch-ish **globs**, also matched against
|
|
182
|
+
the absolute path, so anchor them with a `**/` prefix. Supplying an option
|
|
183
|
+
**replaces** the default rather than extending it.
|
|
142
184
|
|
|
143
185
|
`no-storage-in-stateless-modules` is a **no-op until `modules` is set**. The
|
|
144
186
|
method names alone (`put`, `prepare`) carry no type information, so the rule is
|
package/dist/index.cjs
CHANGED
|
@@ -7177,126 +7177,8 @@ var prefer_non_nullable_collection_default = import_utils51.ESLintUtils.RuleCrea
|
|
|
7177
7177
|
}
|
|
7178
7178
|
});
|
|
7179
7179
|
|
|
7180
|
-
// src/rules/primary-export-file-name.ts
|
|
7181
|
-
var import_utils52 = require("@typescript-eslint/utils");
|
|
7182
|
-
var CONVENTIONAL_BUCKET_EXPORTS = /* @__PURE__ */ new Set(["cn"]);
|
|
7183
|
-
var GENERIC_ENTRYPOINTS = /* @__PURE__ */ new Set(["main", "run", "cli", "setup", "teardown", "execute"]);
|
|
7184
|
-
var ACRONYM_OVERRIDES = [
|
|
7185
|
-
[/OAuth/g, "Oauth"],
|
|
7186
|
-
[/GraphQL/g, "Graphql"],
|
|
7187
|
-
[/gRPC/g, "Grpc"]
|
|
7188
|
-
];
|
|
7189
|
-
var CAMEL_BOUNDARY_RE = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g;
|
|
7190
|
-
var basename = (filename) => filename.split(/[/\\]/).pop() ?? filename;
|
|
7191
|
-
var stemOf = (base) => base.replace(/\.(test|spec|config|d|stories)\.[cm]?[jt]sx?$/i, "").replace(/\.[cm]?[jt]sx?$/i, "");
|
|
7192
|
-
var kebabCase = (name) => {
|
|
7193
|
-
let normalized = name;
|
|
7194
|
-
for (const [pattern, replacement] of ACRONYM_OVERRIDES) {
|
|
7195
|
-
normalized = normalized.replace(pattern, replacement);
|
|
7196
|
-
}
|
|
7197
|
-
return normalized.replace(CAMEL_BOUNDARY_RE, "-").toLowerCase();
|
|
7198
|
-
};
|
|
7199
|
-
var isFunctionOrClass = (node) => node.type === import_utils52.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils52.AST_NODE_TYPES.FunctionExpression;
|
|
7200
|
-
var findPrimaryExport = (body) => {
|
|
7201
|
-
let exportCount = 0;
|
|
7202
|
-
let primaryCandidate = null;
|
|
7203
|
-
for (const statement of body) {
|
|
7204
|
-
if (statement.type === import_utils52.AST_NODE_TYPES.ExportAllDeclaration) {
|
|
7205
|
-
return null;
|
|
7206
|
-
}
|
|
7207
|
-
if (statement.type === import_utils52.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
7208
|
-
exportCount += 1;
|
|
7209
|
-
const decl = statement.declaration;
|
|
7210
|
-
if ((decl.type === import_utils52.AST_NODE_TYPES.FunctionDeclaration || decl.type === import_utils52.AST_NODE_TYPES.ClassDeclaration) && decl.id !== null) {
|
|
7211
|
-
primaryCandidate = { name: decl.id.name, node: statement, isDefault: true };
|
|
7212
|
-
} else if (decl.type === import_utils52.AST_NODE_TYPES.Identifier) {
|
|
7213
|
-
primaryCandidate = { name: decl.name, node: statement, isDefault: true };
|
|
7214
|
-
}
|
|
7215
|
-
} else if (statement.type === import_utils52.AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
7216
|
-
if (statement.source !== null) {
|
|
7217
|
-
return null;
|
|
7218
|
-
}
|
|
7219
|
-
const decl = statement.declaration;
|
|
7220
|
-
if (decl === null) {
|
|
7221
|
-
exportCount += statement.specifiers.length;
|
|
7222
|
-
if (statement.specifiers.length === 1 && primaryCandidate === null) {
|
|
7223
|
-
const spec = statement.specifiers[0];
|
|
7224
|
-
if (spec && spec.exported.type === import_utils52.AST_NODE_TYPES.Identifier) {
|
|
7225
|
-
primaryCandidate = { name: spec.exported.name, node: statement, isDefault: false };
|
|
7226
|
-
}
|
|
7227
|
-
}
|
|
7228
|
-
} else if (decl.type === import_utils52.AST_NODE_TYPES.FunctionDeclaration || decl.type === import_utils52.AST_NODE_TYPES.ClassDeclaration) {
|
|
7229
|
-
if (decl.id !== null) {
|
|
7230
|
-
exportCount += 1;
|
|
7231
|
-
if (primaryCandidate === null) {
|
|
7232
|
-
primaryCandidate = { name: decl.id.name, node: statement, isDefault: false };
|
|
7233
|
-
}
|
|
7234
|
-
}
|
|
7235
|
-
} else if (decl.type === import_utils52.AST_NODE_TYPES.VariableDeclaration) {
|
|
7236
|
-
for (const declarator of decl.declarations) {
|
|
7237
|
-
if (declarator.id.type === import_utils52.AST_NODE_TYPES.Identifier) {
|
|
7238
|
-
exportCount += 1;
|
|
7239
|
-
if (primaryCandidate === null && declarator.init && isFunctionOrClass(declarator.init)) {
|
|
7240
|
-
primaryCandidate = { name: declarator.id.name, node: statement, isDefault: false };
|
|
7241
|
-
}
|
|
7242
|
-
}
|
|
7243
|
-
}
|
|
7244
|
-
} else if (decl.type === import_utils52.AST_NODE_TYPES.TSTypeAliasDeclaration || decl.type === import_utils52.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
7245
|
-
exportCount += 1;
|
|
7246
|
-
if (primaryCandidate === null) {
|
|
7247
|
-
primaryCandidate = { name: decl.id.name, node: statement, isDefault: false };
|
|
7248
|
-
}
|
|
7249
|
-
}
|
|
7250
|
-
}
|
|
7251
|
-
}
|
|
7252
|
-
if (primaryCandidate === null || exportCount > 2) {
|
|
7253
|
-
return null;
|
|
7254
|
-
}
|
|
7255
|
-
return { ...primaryCandidate, count: exportCount };
|
|
7256
|
-
};
|
|
7257
|
-
var primary_export_file_name_default = import_utils52.ESLintUtils.RuleCreator(
|
|
7258
|
-
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7259
|
-
)({
|
|
7260
|
-
name: "primary-export-file-name",
|
|
7261
|
-
meta: {
|
|
7262
|
-
type: "suggestion",
|
|
7263
|
-
docs: {
|
|
7264
|
-
description: "Enforce semantic naming alignment: a file with a single primary export must be named after that export."
|
|
7265
|
-
},
|
|
7266
|
-
schema: [],
|
|
7267
|
-
messages: {
|
|
7268
|
-
primaryExportMismatch: "Module stem '{{stem}}' does not match its primary export '{{name}}' \u2014 rename the file to '{{expected}}{{ext}}' to describe its responsibility."
|
|
7269
|
-
}
|
|
7270
|
-
},
|
|
7271
|
-
defaultOptions: [],
|
|
7272
|
-
create(context) {
|
|
7273
|
-
const base = basename(context.filename);
|
|
7274
|
-
if (base.endsWith(".d.ts") || base.startsWith("index.") || base.startsWith("_")) return {};
|
|
7275
|
-
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
7276
|
-
const stem3 = stemOf(base);
|
|
7277
|
-
if (stem3 === "page" || stem3 === "layout" || stem3 === "loading" || stem3 === "error" || stem3 === "not-found" || stem3 === "route" || stem3 === "__root" || stem3 === "config" || stem3 === "constants" || stem3 === "types" || stem3.startsWith("$") || stem3.startsWith("[")) {
|
|
7278
|
-
return {};
|
|
7279
|
-
}
|
|
7280
|
-
return {
|
|
7281
|
-
Program(node) {
|
|
7282
|
-
const primary = findPrimaryExport(node.body);
|
|
7283
|
-
if (primary === null) return;
|
|
7284
|
-
if (CONVENTIONAL_BUCKET_EXPORTS.has(primary.name) || GENERIC_ENTRYPOINTS.has(primary.name)) return;
|
|
7285
|
-
const expectedStem = kebabCase(primary.name);
|
|
7286
|
-
if (stem3 === expectedStem) return;
|
|
7287
|
-
const ext = base.endsWith(".tsx") ? ".tsx" : ".ts";
|
|
7288
|
-
context.report({
|
|
7289
|
-
node: primary.node,
|
|
7290
|
-
messageId: "primaryExportMismatch",
|
|
7291
|
-
data: { stem: stem3, name: primary.name, expected: expectedStem, ext }
|
|
7292
|
-
});
|
|
7293
|
-
}
|
|
7294
|
-
};
|
|
7295
|
-
}
|
|
7296
|
-
});
|
|
7297
|
-
|
|
7298
7180
|
// src/rules/no-implicit-attribute-access.ts
|
|
7299
|
-
var
|
|
7181
|
+
var import_utils52 = require("@typescript-eslint/utils");
|
|
7300
7182
|
var EXCLUDED_BASES = /* @__PURE__ */ new Set([
|
|
7301
7183
|
"environ",
|
|
7302
7184
|
"headers",
|
|
@@ -7312,15 +7194,15 @@ var EXCLUDED_BASES = /* @__PURE__ */ new Set([
|
|
|
7312
7194
|
"sessionStorage"
|
|
7313
7195
|
]);
|
|
7314
7196
|
function getBaseName(node) {
|
|
7315
|
-
if (node.type ===
|
|
7197
|
+
if (node.type === import_utils52.AST_NODE_TYPES.Identifier) {
|
|
7316
7198
|
return node.name;
|
|
7317
7199
|
}
|
|
7318
|
-
if (node.type ===
|
|
7200
|
+
if (node.type === import_utils52.AST_NODE_TYPES.MemberExpression && node.property.type === import_utils52.AST_NODE_TYPES.Identifier) {
|
|
7319
7201
|
return node.property.name;
|
|
7320
7202
|
}
|
|
7321
7203
|
return null;
|
|
7322
7204
|
}
|
|
7323
|
-
var no_implicit_attribute_access_default =
|
|
7205
|
+
var no_implicit_attribute_access_default = import_utils52.ESLintUtils.RuleCreator(
|
|
7324
7206
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7325
7207
|
)({
|
|
7326
7208
|
name: "no-implicit-attribute-access",
|
|
@@ -7341,7 +7223,7 @@ var no_implicit_attribute_access_default = import_utils53.ESLintUtils.RuleCreato
|
|
|
7341
7223
|
if (!node.computed) {
|
|
7342
7224
|
return;
|
|
7343
7225
|
}
|
|
7344
|
-
if (node.property.type ===
|
|
7226
|
+
if (node.property.type === import_utils52.AST_NODE_TYPES.Literal && typeof node.property.value === "string") {
|
|
7345
7227
|
const baseName = getBaseName(node.object);
|
|
7346
7228
|
if (!baseName || !EXCLUDED_BASES.has(baseName)) {
|
|
7347
7229
|
context.report({
|
|
@@ -7354,11 +7236,11 @@ var no_implicit_attribute_access_default = import_utils53.ESLintUtils.RuleCreato
|
|
|
7354
7236
|
},
|
|
7355
7237
|
CallExpression(node) {
|
|
7356
7238
|
const callee = node.callee;
|
|
7357
|
-
if (callee.type ===
|
|
7358
|
-
const method = callee.property.type ===
|
|
7239
|
+
if (callee.type === import_utils52.AST_NODE_TYPES.MemberExpression) {
|
|
7240
|
+
const method = callee.property.type === import_utils52.AST_NODE_TYPES.Identifier ? callee.property.name : null;
|
|
7359
7241
|
if (method === "get") {
|
|
7360
7242
|
const arg = node.arguments[0];
|
|
7361
|
-
if (arg && arg.type ===
|
|
7243
|
+
if (arg && arg.type === import_utils52.AST_NODE_TYPES.Literal && typeof arg.value === "string") {
|
|
7362
7244
|
const baseName = getBaseName(callee.object);
|
|
7363
7245
|
if (!baseName || !EXCLUDED_BASES.has(baseName)) {
|
|
7364
7246
|
context.report({
|
|
@@ -7376,8 +7258,8 @@ var no_implicit_attribute_access_default = import_utils53.ESLintUtils.RuleCreato
|
|
|
7376
7258
|
});
|
|
7377
7259
|
|
|
7378
7260
|
// src/rules/strict-test-assertions.ts
|
|
7379
|
-
var
|
|
7380
|
-
var strict_test_assertions_default =
|
|
7261
|
+
var import_utils53 = require("@typescript-eslint/utils");
|
|
7262
|
+
var strict_test_assertions_default = import_utils53.ESLintUtils.RuleCreator.withoutDocs({
|
|
7381
7263
|
meta: {
|
|
7382
7264
|
type: "suggestion",
|
|
7383
7265
|
docs: {
|
|
@@ -7395,9 +7277,9 @@ var strict_test_assertions_default = import_utils54.ESLintUtils.RuleCreator.with
|
|
|
7395
7277
|
let currentObject = null;
|
|
7396
7278
|
let sequence = [];
|
|
7397
7279
|
function getExpectObject(expr) {
|
|
7398
|
-
if (expr.type ===
|
|
7280
|
+
if (expr.type === import_utils53.AST_NODE_TYPES.CallExpression && expr.callee.type === import_utils53.AST_NODE_TYPES.MemberExpression && expr.callee.object.type === import_utils53.AST_NODE_TYPES.CallExpression && expr.callee.object.callee.type === import_utils53.AST_NODE_TYPES.Identifier && expr.callee.object.callee.name === "expect" && expr.callee.object.arguments.length === 1) {
|
|
7399
7281
|
const arg = expr.callee.object.arguments.at(0);
|
|
7400
|
-
if (arg?.type ===
|
|
7282
|
+
if (arg?.type === import_utils53.AST_NODE_TYPES.MemberExpression) {
|
|
7401
7283
|
return arg.object;
|
|
7402
7284
|
}
|
|
7403
7285
|
}
|
|
@@ -7413,12 +7295,12 @@ var strict_test_assertions_default = import_utils54.ESLintUtils.RuleCreator.with
|
|
|
7413
7295
|
if (!currentObject) return null;
|
|
7414
7296
|
const objectText = context.sourceCode.getText(currentObject);
|
|
7415
7297
|
const props = sequence.map((stmt) => {
|
|
7416
|
-
if (stmt.expression.type !==
|
|
7298
|
+
if (stmt.expression.type !== import_utils53.AST_NODE_TYPES.CallExpression || stmt.expression.callee.type !== import_utils53.AST_NODE_TYPES.MemberExpression || stmt.expression.callee.object.type !== import_utils53.AST_NODE_TYPES.CallExpression) {
|
|
7417
7299
|
return null;
|
|
7418
7300
|
}
|
|
7419
7301
|
const actual = stmt.expression.callee.object.arguments.at(0);
|
|
7420
7302
|
const expected = stmt.expression.arguments.at(0);
|
|
7421
|
-
if (actual?.type ===
|
|
7303
|
+
if (actual?.type === import_utils53.AST_NODE_TYPES.MemberExpression && actual.property.type === import_utils53.AST_NODE_TYPES.Identifier && expected) {
|
|
7422
7304
|
const propName2 = actual.property.name;
|
|
7423
7305
|
const valText = context.sourceCode.getText(expected);
|
|
7424
7306
|
return `${propName2}: ${valText}`;
|
|
@@ -7436,7 +7318,7 @@ var strict_test_assertions_default = import_utils54.ESLintUtils.RuleCreator.with
|
|
|
7436
7318
|
}
|
|
7437
7319
|
}
|
|
7438
7320
|
for (const statement of body) {
|
|
7439
|
-
if (statement.type ===
|
|
7321
|
+
if (statement.type === import_utils53.AST_NODE_TYPES.ExpressionStatement) {
|
|
7440
7322
|
const obj = getExpectObject(statement.expression);
|
|
7441
7323
|
if (obj && currentObject && context.sourceCode.getText(obj) === context.sourceCode.getText(currentObject)) {
|
|
7442
7324
|
sequence.push(statement);
|
|
@@ -7470,8 +7352,8 @@ var strict_test_assertions_default = import_utils54.ESLintUtils.RuleCreator.with
|
|
|
7470
7352
|
});
|
|
7471
7353
|
|
|
7472
7354
|
// src/rules/no-async-callback-in-waitfor.ts
|
|
7473
|
-
var
|
|
7474
|
-
var no_async_callback_in_waitfor_default =
|
|
7355
|
+
var import_utils54 = require("@typescript-eslint/utils");
|
|
7356
|
+
var no_async_callback_in_waitfor_default = import_utils54.ESLintUtils.RuleCreator(
|
|
7475
7357
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7476
7358
|
)({
|
|
7477
7359
|
name: "no-async-callback-in-waitfor",
|
|
@@ -7492,9 +7374,9 @@ var no_async_callback_in_waitfor_default = import_utils55.ESLintUtils.RuleCreato
|
|
|
7492
7374
|
}
|
|
7493
7375
|
return {
|
|
7494
7376
|
CallExpression(node) {
|
|
7495
|
-
if (node.callee.type ===
|
|
7377
|
+
if (node.callee.type === import_utils54.AST_NODE_TYPES.Identifier && node.callee.name === "waitFor") {
|
|
7496
7378
|
const callback = node.arguments[0];
|
|
7497
|
-
if (callback && (callback.type ===
|
|
7379
|
+
if (callback && (callback.type === import_utils54.AST_NODE_TYPES.ArrowFunctionExpression || callback.type === import_utils54.AST_NODE_TYPES.FunctionExpression) && callback.async) {
|
|
7498
7380
|
context.report({
|
|
7499
7381
|
node: callback,
|
|
7500
7382
|
messageId: "noAsyncCallbackInWaitFor"
|
|
@@ -7506,6 +7388,183 @@ var no_async_callback_in_waitfor_default = import_utils55.ESLintUtils.RuleCreato
|
|
|
7506
7388
|
}
|
|
7507
7389
|
});
|
|
7508
7390
|
|
|
7391
|
+
// src/rules/no-hand-rolled-sleep.ts
|
|
7392
|
+
var import_utils55 = require("@typescript-eslint/utils");
|
|
7393
|
+
var GLOBAL_OBJECTS2 = /* @__PURE__ */ new Set([
|
|
7394
|
+
"globalThis",
|
|
7395
|
+
"window",
|
|
7396
|
+
"self",
|
|
7397
|
+
"global"
|
|
7398
|
+
]);
|
|
7399
|
+
var CLIENT_ONLY_MODULES = /^(react|react-dom|react-native|svelte|vue|preact|solid-js)(\/|$)|^next\/(navigation|router|link|image)$/;
|
|
7400
|
+
var RACE_METHODS = /* @__PURE__ */ new Set(["race", "any"]);
|
|
7401
|
+
function matchesAnyPattern3(filename, patterns) {
|
|
7402
|
+
for (const pattern of patterns) {
|
|
7403
|
+
const regexSource = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLESTAR::").replace(/\*/g, "[^/\\\\]*").replace(/::DOUBLESTAR::/g, ".*");
|
|
7404
|
+
if (new RegExp(`^${regexSource}$`).test(filename)) {
|
|
7405
|
+
return true;
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
return false;
|
|
7409
|
+
}
|
|
7410
|
+
function isSetTimeoutCallee(callee) {
|
|
7411
|
+
if (callee.type === import_utils55.AST_NODE_TYPES.Identifier) {
|
|
7412
|
+
return callee.name === "setTimeout";
|
|
7413
|
+
}
|
|
7414
|
+
return callee.type === import_utils55.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils55.AST_NODE_TYPES.Identifier && callee.property.name === "setTimeout" && callee.object.type === import_utils55.AST_NODE_TYPES.Identifier && GLOBAL_OBJECTS2.has(callee.object.name);
|
|
7415
|
+
}
|
|
7416
|
+
function soleCall(fn) {
|
|
7417
|
+
if (fn.body.type !== import_utils55.AST_NODE_TYPES.BlockStatement) {
|
|
7418
|
+
return fn.body.type === import_utils55.AST_NODE_TYPES.CallExpression ? fn.body : null;
|
|
7419
|
+
}
|
|
7420
|
+
if (fn.body.body.length !== 1) {
|
|
7421
|
+
return null;
|
|
7422
|
+
}
|
|
7423
|
+
const [only] = fn.body.body;
|
|
7424
|
+
if (only?.type !== import_utils55.AST_NODE_TYPES.ExpressionStatement) {
|
|
7425
|
+
return null;
|
|
7426
|
+
}
|
|
7427
|
+
return only.expression.type === import_utils55.AST_NODE_TYPES.CallExpression ? only.expression : null;
|
|
7428
|
+
}
|
|
7429
|
+
function isTimedDelay(delay) {
|
|
7430
|
+
if (delay === void 0) {
|
|
7431
|
+
return false;
|
|
7432
|
+
}
|
|
7433
|
+
if (delay.type === import_utils55.AST_NODE_TYPES.Literal && typeof delay.value === "number") {
|
|
7434
|
+
return delay.value !== 0;
|
|
7435
|
+
}
|
|
7436
|
+
return true;
|
|
7437
|
+
}
|
|
7438
|
+
function settlesWithoutValue(callback, name) {
|
|
7439
|
+
if (callback.type === import_utils55.AST_NODE_TYPES.Identifier) {
|
|
7440
|
+
return callback.name === name;
|
|
7441
|
+
}
|
|
7442
|
+
if (callback.type !== import_utils55.AST_NODE_TYPES.ArrowFunctionExpression && callback.type !== import_utils55.AST_NODE_TYPES.FunctionExpression) {
|
|
7443
|
+
return false;
|
|
7444
|
+
}
|
|
7445
|
+
const call = soleCall(callback);
|
|
7446
|
+
return call !== null && call.arguments.length === 0 && call.callee.type === import_utils55.AST_NODE_TYPES.Identifier && call.callee.name === name;
|
|
7447
|
+
}
|
|
7448
|
+
function rejectsInCallback(callback, name) {
|
|
7449
|
+
if (callback.type === import_utils55.AST_NODE_TYPES.Identifier) {
|
|
7450
|
+
return callback.name === name;
|
|
7451
|
+
}
|
|
7452
|
+
if (callback.type !== import_utils55.AST_NODE_TYPES.ArrowFunctionExpression && callback.type !== import_utils55.AST_NODE_TYPES.FunctionExpression) {
|
|
7453
|
+
return false;
|
|
7454
|
+
}
|
|
7455
|
+
const call = soleCall(callback);
|
|
7456
|
+
return call !== null && call.callee.type === import_utils55.AST_NODE_TYPES.Identifier && call.callee.name === name;
|
|
7457
|
+
}
|
|
7458
|
+
function parameterName(fn, index) {
|
|
7459
|
+
const parameter = fn.params[index];
|
|
7460
|
+
return parameter?.type === import_utils55.AST_NODE_TYPES.Identifier ? parameter.name : null;
|
|
7461
|
+
}
|
|
7462
|
+
function isRaceArm(node) {
|
|
7463
|
+
const array = node.parent;
|
|
7464
|
+
if (array?.type !== import_utils55.AST_NODE_TYPES.ArrayExpression) {
|
|
7465
|
+
return false;
|
|
7466
|
+
}
|
|
7467
|
+
const call = array.parent;
|
|
7468
|
+
return call?.type === import_utils55.AST_NODE_TYPES.CallExpression && call.arguments[0] === array && call.callee.type === import_utils55.AST_NODE_TYPES.MemberExpression && !call.callee.computed && call.callee.object.type === import_utils55.AST_NODE_TYPES.Identifier && call.callee.object.name === "Promise" && call.callee.property.type === import_utils55.AST_NODE_TYPES.Identifier && RACE_METHODS.has(call.callee.property.name);
|
|
7469
|
+
}
|
|
7470
|
+
var no_hand_rolled_sleep_default = import_utils55.ESLintUtils.RuleCreator(
|
|
7471
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7472
|
+
)({
|
|
7473
|
+
name: "no-hand-rolled-sleep",
|
|
7474
|
+
meta: {
|
|
7475
|
+
type: "problem",
|
|
7476
|
+
docs: {
|
|
7477
|
+
description: "Disallow hand-rolled promisified timers (`new Promise((r) => setTimeout(r, ms))`) and hand-rolled `Promise.race` timeout arms; the stdlib forms are cancellable, these are not."
|
|
7478
|
+
},
|
|
7479
|
+
schema: [
|
|
7480
|
+
{
|
|
7481
|
+
type: "object",
|
|
7482
|
+
additionalProperties: false,
|
|
7483
|
+
properties: {
|
|
7484
|
+
allowIn: {
|
|
7485
|
+
description: "Glob patterns for modules exempt from the rule (e.g. a single sanctioned `sleep` utility). Matched against the ABSOLUTE file path, so anchor with a `**/` prefix (e.g. `**/lib/sleep.ts`).",
|
|
7486
|
+
type: "array",
|
|
7487
|
+
items: { type: "string" }
|
|
7488
|
+
},
|
|
7489
|
+
checkClientModules: {
|
|
7490
|
+
description: "Also report the sleep form in browser/React Native modules. Off by default: those bundles cannot import `node:timers/promises` and the web platform has no equivalent, so the fix is impossible to follow. Turn on only where every file can resolve `node:` builtins.",
|
|
7491
|
+
type: "boolean"
|
|
7492
|
+
}
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
],
|
|
7496
|
+
messages: {
|
|
7497
|
+
handRolledSleep: 'Hand-rolled sleep: `new Promise((resolve) => setTimeout(resolve, ms))` cannot be cancelled, so an aborted request or a lost race still waits out the full delay. Use `import { setTimeout as sleep } from "node:timers/promises"` and pass `{ signal }`.',
|
|
7498
|
+
handRolledTimeoutRace: "Hand-rolled timeout arm: when the other promise wins, this timer is never cleared and keeps the event loop alive until it fires. Use `AbortSignal.timeout(ms)` and pass the signal to the operation."
|
|
7499
|
+
}
|
|
7500
|
+
},
|
|
7501
|
+
defaultOptions: [{}],
|
|
7502
|
+
create(context, [optionsArg]) {
|
|
7503
|
+
const { filename, sourceCode } = context;
|
|
7504
|
+
if (isTestFile(filename) || isScriptFile(filename) || isGeneratedFile(filename, sourceCode.getText())) {
|
|
7505
|
+
return {};
|
|
7506
|
+
}
|
|
7507
|
+
const allowIn = optionsArg?.allowIn ?? [];
|
|
7508
|
+
if (allowIn.length > 0 && matchesAnyPattern3(filename, allowIn)) {
|
|
7509
|
+
return {};
|
|
7510
|
+
}
|
|
7511
|
+
const checkClientModules = optionsArg?.checkClientModules ?? false;
|
|
7512
|
+
function isClientModule() {
|
|
7513
|
+
if (/\.[cm]?[jt]sx$/.test(filename)) {
|
|
7514
|
+
return true;
|
|
7515
|
+
}
|
|
7516
|
+
const program = sourceCode.ast;
|
|
7517
|
+
for (const statement of program.body) {
|
|
7518
|
+
if (statement.type === import_utils55.AST_NODE_TYPES.ExpressionStatement && statement.expression.type === import_utils55.AST_NODE_TYPES.Literal && statement.expression.value === "use client") {
|
|
7519
|
+
return true;
|
|
7520
|
+
}
|
|
7521
|
+
if (statement.type === import_utils55.AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && CLIENT_ONLY_MODULES.test(statement.source.value)) {
|
|
7522
|
+
return true;
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
return false;
|
|
7526
|
+
}
|
|
7527
|
+
let clientModule = null;
|
|
7528
|
+
const reportsSleepHere = () => {
|
|
7529
|
+
if (checkClientModules) {
|
|
7530
|
+
return true;
|
|
7531
|
+
}
|
|
7532
|
+
clientModule ??= isClientModule();
|
|
7533
|
+
return !clientModule;
|
|
7534
|
+
};
|
|
7535
|
+
return {
|
|
7536
|
+
NewExpression(node) {
|
|
7537
|
+
if (node.callee.type !== import_utils55.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
|
|
7538
|
+
return;
|
|
7539
|
+
}
|
|
7540
|
+
const executor = node.arguments[0];
|
|
7541
|
+
if (executor?.type !== import_utils55.AST_NODE_TYPES.ArrowFunctionExpression && executor?.type !== import_utils55.AST_NODE_TYPES.FunctionExpression) {
|
|
7542
|
+
return;
|
|
7543
|
+
}
|
|
7544
|
+
const call = soleCall(executor);
|
|
7545
|
+
if (call === null || !isSetTimeoutCallee(call.callee)) {
|
|
7546
|
+
return;
|
|
7547
|
+
}
|
|
7548
|
+
const [callback, delay] = call.arguments;
|
|
7549
|
+
if (callback === void 0 || !isTimedDelay(delay)) {
|
|
7550
|
+
return;
|
|
7551
|
+
}
|
|
7552
|
+
const resolveName = parameterName(executor, 0);
|
|
7553
|
+
if (resolveName !== null && settlesWithoutValue(callback, resolveName)) {
|
|
7554
|
+
if (reportsSleepHere()) {
|
|
7555
|
+
context.report({ node, messageId: "handRolledSleep" });
|
|
7556
|
+
}
|
|
7557
|
+
return;
|
|
7558
|
+
}
|
|
7559
|
+
const rejectName = parameterName(executor, 1);
|
|
7560
|
+
if (rejectName !== null && isRaceArm(node) && rejectsInCallback(callback, rejectName)) {
|
|
7561
|
+
context.report({ node, messageId: "handRolledTimeoutRace" });
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
};
|
|
7565
|
+
}
|
|
7566
|
+
});
|
|
7567
|
+
|
|
7509
7568
|
// src/rules/prefer-setup-file-mocks.ts
|
|
7510
7569
|
var import_utils56 = require("@typescript-eslint/utils");
|
|
7511
7570
|
var prefer_setup_file_mocks_default = import_utils56.ESLintUtils.RuleCreator(
|
|
@@ -7567,7 +7626,6 @@ var rules = {
|
|
|
7567
7626
|
"no-unsafe-mock-casting": no_unsafe_mock_casting_default,
|
|
7568
7627
|
"prefer-string-literal-union": prefer_string_literal_union_default,
|
|
7569
7628
|
"prefer-zod-enum": prefer_zod_enum_default,
|
|
7570
|
-
"primary-export-file-name": primary_export_file_name_default,
|
|
7571
7629
|
"no-silent-promise-catch": no_silent_promise_catch_default,
|
|
7572
7630
|
"require-fetch-timeout": require_fetch_timeout_default,
|
|
7573
7631
|
"no-offset-pagination": no_offset_pagination_default,
|
|
@@ -7575,6 +7633,7 @@ var rules = {
|
|
|
7575
7633
|
"no-repeated-string-literal": no_repeated_string_literal_default,
|
|
7576
7634
|
"no-select-star": no_select_star_default,
|
|
7577
7635
|
"no-sleep-in-test-body": no_sleep_in_test_body_default,
|
|
7636
|
+
"no-hand-rolled-sleep": no_hand_rolled_sleep_default,
|
|
7578
7637
|
"no-conditional-in-test": no_conditional_in_test_default,
|
|
7579
7638
|
"prefer-constant-time-secret-compare": prefer_constant_time_secret_compare_default,
|
|
7580
7639
|
"store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
|
|
@@ -7597,7 +7656,7 @@ var rules = {
|
|
|
7597
7656
|
var plugin = {
|
|
7598
7657
|
meta: {
|
|
7599
7658
|
name: "@sarj/eslint-plugin",
|
|
7600
|
-
version: "
|
|
7659
|
+
version: "4.1.0"
|
|
7601
7660
|
},
|
|
7602
7661
|
rules,
|
|
7603
7662
|
configs: {
|
|
@@ -7631,7 +7690,6 @@ var plugin = {
|
|
|
7631
7690
|
"@sarj/no-cors-wildcard-with-credentials": "warn",
|
|
7632
7691
|
"@sarj/no-secret-in-log": "warn",
|
|
7633
7692
|
"@sarj/no-unsafe-mock-casting": "warn",
|
|
7634
|
-
"@sarj/primary-export-file-name": "warn",
|
|
7635
7693
|
"@sarj/prefer-string-literal-union": "warn",
|
|
7636
7694
|
"@sarj/prefer-zod-enum": "warn",
|
|
7637
7695
|
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
@@ -7645,6 +7703,10 @@ var plugin = {
|
|
|
7645
7703
|
"@sarj/store-insert-requires-on-conflict": "warn",
|
|
7646
7704
|
"@sarj/no-offset-pagination": "warn",
|
|
7647
7705
|
"@sarj/no-select-star": "warn",
|
|
7706
|
+
// Uncancellable hand-rolled timers. Verified against the shipped
|
|
7707
|
+
// strict config (205 enabled rules) that nothing already reports this
|
|
7708
|
+
// position; `unicorn` 72 has no promisified-timer rule at all.
|
|
7709
|
+
"@sarj/no-hand-rolled-sleep": "warn",
|
|
7648
7710
|
"@sarj/no-sleep-in-test-body": "warn",
|
|
7649
7711
|
"@sarj/no-conditional-in-test": "warn",
|
|
7650
7712
|
"@sarj/no-repeated-string-literal": "warn",
|
|
@@ -7661,7 +7723,8 @@ var plugin = {
|
|
|
7661
7723
|
// Anti-comment-verbosity family (2026-07), from a 37,918-comment,
|
|
7662
7724
|
// nine-repo measurement study. Each is a deletion-class finding, so each
|
|
7663
7725
|
// was validated against pydantic / trio / attrs as well as the maintained
|
|
7664
|
-
// repos: `no-restated-comment` 0 hits in
|
|
7726
|
+
// repos: `no-restated-comment` 0 hits in the flagship first-party
|
|
7727
|
+
// repo and 4 in the three famous
|
|
7665
7728
|
// corpora combined; `trailing-value-narration` 18 hits, 18 true
|
|
7666
7729
|
// positives; `jsdoc-restates-signature` 36 hits, 0 measured false
|
|
7667
7730
|
// positives, and it offers a suggestion rather than a `--fix` because a
|
|
@@ -7672,7 +7735,7 @@ var plugin = {
|
|
|
7672
7735
|
// The TS half of SARJ057 (2026-07). Python has caught the
|
|
7673
7736
|
// assertion-FREE test since 0.15.0 (SARJ043) and had no TS
|
|
7674
7737
|
// counterpart, which is how `expect(true).toBe(true); // placeholder`
|
|
7675
|
-
// survived in
|
|
7738
|
+
// survived in a first-party repo: the file HAS an assertion.
|
|
7676
7739
|
// Measured across 5,819 .ts/.tsx files (1,003 of them test files) in
|
|
7677
7740
|
// six internal repos plus got / hono / swr / trpc: 3 hits, 3 true
|
|
7678
7741
|
// positives, 0 false positives.
|
|
@@ -7721,7 +7784,6 @@ var plugin = {
|
|
|
7721
7784
|
"@sarj/no-cors-wildcard-with-credentials": "error",
|
|
7722
7785
|
"@sarj/no-secret-in-log": "error",
|
|
7723
7786
|
"@sarj/no-unsafe-mock-casting": "error",
|
|
7724
|
-
"@sarj/primary-export-file-name": "error",
|
|
7725
7787
|
// Promoted to error 2026-07-25 — strict means strict (user directive).
|
|
7726
7788
|
"@sarj/prefer-string-literal-union": "error",
|
|
7727
7789
|
"@sarj/prefer-zod-enum": "error",
|
|
@@ -7733,6 +7795,10 @@ var plugin = {
|
|
|
7733
7795
|
"@sarj/store-insert-requires-on-conflict": "error",
|
|
7734
7796
|
"@sarj/no-offset-pagination": "error",
|
|
7735
7797
|
"@sarj/no-select-star": "error",
|
|
7798
|
+
// Uncancellable hand-rolled timers. Verified against the shipped
|
|
7799
|
+
// strict config (205 enabled rules) that nothing already reports this
|
|
7800
|
+
// position; `unicorn` 72 has no promisified-timer rule at all.
|
|
7801
|
+
"@sarj/no-hand-rolled-sleep": "error",
|
|
7736
7802
|
"@sarj/no-sleep-in-test-body": "error",
|
|
7737
7803
|
"@sarj/no-conditional-in-test": "error",
|
|
7738
7804
|
"@sarj/no-repeated-string-literal": "error",
|