@sarj/eslint-plugin 15.1.0 → 15.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/dist/index.cjs +332 -63
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +358 -85
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1010,11 +1010,11 @@ function isTrivialInitializer(node) {
|
|
|
1010
1010
|
}
|
|
1011
1011
|
function restatesStatementHead(body2, statement) {
|
|
1012
1012
|
if (statement === null) return false;
|
|
1013
|
-
const
|
|
1014
|
-
const opener =
|
|
1015
|
-
if (opener === void 0 ||
|
|
1013
|
+
const words2 = body2.match(/[A-Za-z][\w$]*/g) ?? [];
|
|
1014
|
+
const opener = words2[0];
|
|
1015
|
+
if (opener === void 0 || words2.length > NARRATION_MAX_WORDS) return false;
|
|
1016
1016
|
if (!NARRATION_VERB_RE.test(opener)) return false;
|
|
1017
|
-
const content =
|
|
1017
|
+
const content = words2.slice(1).map(normalizeToken).filter((word) => !NARRATION_STOPWORDS.has(word));
|
|
1018
1018
|
if (content.length < NARRATION_MIN_CONTENT) return false;
|
|
1019
1019
|
const head = statement.split("(")[0] ?? statement;
|
|
1020
1020
|
const code = headTokens(head);
|
|
@@ -1282,8 +1282,8 @@ function isRedundantNarration(body2, statementBelow, standalone, isolatedEnumera
|
|
|
1282
1282
|
if (META_COMMENTARY_RE.test(t) && !justified) return true;
|
|
1283
1283
|
if (isBareDeferral(t) && !justified) return true;
|
|
1284
1284
|
if (HELPER_OPENER_RE.test(t) || LETS_RE.test(t)) return true;
|
|
1285
|
-
const
|
|
1286
|
-
if (
|
|
1285
|
+
const words2 = t.split(/\s+/);
|
|
1286
|
+
if (words2.length > 1 && words2.length <= 4 && DUMMY_TRANSLATION_RE.test(t) && !/[():=]/.test(t)) {
|
|
1287
1287
|
const lowerT = t.toLowerCase();
|
|
1288
1288
|
if (!RATIONALE_WORDS.some((word) => lowerT.includes(word)) && restatesWholeStatement(t, statementBelow)) {
|
|
1289
1289
|
return true;
|
|
@@ -1365,8 +1365,8 @@ function isWeakWalkthroughComment(body2, statement) {
|
|
|
1365
1365
|
if (normalized.length === 0 || normalized.endsWith("?") || normalized.split(/\s+/).length > WALL_MAX_WORDS || isDirective(normalized) || isProtected(normalized) || !WALL_NARRATION_RE.test(normalized)) {
|
|
1366
1366
|
return false;
|
|
1367
1367
|
}
|
|
1368
|
-
const
|
|
1369
|
-
const described =
|
|
1368
|
+
const words2 = contentTokens(normalized);
|
|
1369
|
+
const described = words2.slice(1);
|
|
1370
1370
|
if (described.length === 0) return false;
|
|
1371
1371
|
const code = codeTokens(statement);
|
|
1372
1372
|
const matched = described.filter((word) => restates([word], code)).length;
|
|
@@ -2710,7 +2710,7 @@ var no_fat_try_blocks_default = createRule({
|
|
|
2710
2710
|
const sourceCode = context.sourceCode;
|
|
2711
2711
|
return {
|
|
2712
2712
|
TryStatement(node) {
|
|
2713
|
-
if (node.finalizer !== null) {
|
|
2713
|
+
if (node.finalizer !== null && node.handler === null) {
|
|
2714
2714
|
return;
|
|
2715
2715
|
}
|
|
2716
2716
|
if (handlerRethrows(node.handler)) {
|
|
@@ -2950,15 +2950,32 @@ var noHandRolledSpinnerDocumentation = {
|
|
|
2950
2950
|
rationale: "One-off loading indicators duplicate a shared primitive and let accessibility and styling diverge.",
|
|
2951
2951
|
remediation: "Render the design-system Spinner component instead.",
|
|
2952
2952
|
category: "maintainability",
|
|
2953
|
-
limitations: ["Only static className values on div and span elements are inspected."],
|
|
2953
|
+
limitations: ["Only static className values on div and span elements are inspected; tests, stories, generated files, and the design-system implementation are excluded."],
|
|
2954
2954
|
examples: [
|
|
2955
2955
|
{ id: "design-system-spinner", title: "Use the shared spinner", outcome: "no-match", files: [{ path: "src/loading-state.tsx", source: '<Spinner className="size-4" />' }], focusPath: "src/loading-state.tsx", expectedCount: 0, public: true },
|
|
2956
2956
|
{ id: "border-ring-spinner", title: "Do not rebuild a spinner", outcome: "match", files: [{ path: "src/loading-state.tsx", source: '<div className="size-4 animate-spin rounded-full border-2 border-t-transparent" />' }], focusPath: "src/loading-state.tsx", expectedCount: 1, public: true }
|
|
2957
2957
|
]
|
|
2958
2958
|
};
|
|
2959
2959
|
var DESIGN_SYSTEM_PATH = /(?:^|[/\\])components[/\\]ui[/\\]/u;
|
|
2960
|
-
var
|
|
2961
|
-
var
|
|
2960
|
+
var DIRECTIONAL_BORDER = /^border-([trblsexy])-(.+)$/u;
|
|
2961
|
+
var CSS_LENGTH = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:cap|ch|cm|dvh|dvw|em|ex|ic|in|lh|lvh|lvw|mm|pc|pt|px|q|rcap|rch|rem|rex|ric|rlh|svh|svw|vb|vh|vi|vmax|vmin|vw|%)$/u;
|
|
2962
|
+
var ARBITRARY_LENGTH_FUNCTION = /^(?:calc|clamp|max|min)\(.+\)$/u;
|
|
2963
|
+
function isBorderWidthValue(value) {
|
|
2964
|
+
if (/^\d+$/u.test(value)) return true;
|
|
2965
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2966
|
+
const arbitrary = value.slice(1, -1);
|
|
2967
|
+
const length = arbitrary.startsWith("length:") ? arbitrary.slice("length:".length) : arbitrary;
|
|
2968
|
+
return CSS_LENGTH.test(length) || ARBITRARY_LENGTH_FUNCTION.test(length) || arbitrary.startsWith("length:") && /^var\(.+\)$/u.test(length);
|
|
2969
|
+
}
|
|
2970
|
+
return value.startsWith("(length:") && value.endsWith(")") && value.length > "(length:)".length;
|
|
2971
|
+
}
|
|
2972
|
+
function isBorderWidth(token) {
|
|
2973
|
+
return token === "border" || token.startsWith("border-") && isBorderWidthValue(token.slice("border-".length));
|
|
2974
|
+
}
|
|
2975
|
+
function isContrastingEdge(token) {
|
|
2976
|
+
const match = DIRECTIONAL_BORDER.exec(token);
|
|
2977
|
+
return match?.[2] !== void 0 && !isBorderWidthValue(match[2]);
|
|
2978
|
+
}
|
|
2962
2979
|
function staticClassName(attribute) {
|
|
2963
2980
|
const value = attribute.value;
|
|
2964
2981
|
if (value?.type === import_utils13.AST_NODE_TYPES.Literal && typeof value.value === "string") {
|
|
@@ -2987,7 +3004,7 @@ var no_hand_rolled_spinner_default = createRule({
|
|
|
2987
3004
|
},
|
|
2988
3005
|
defaultOptions: [],
|
|
2989
3006
|
create(context) {
|
|
2990
|
-
if (DESIGN_SYSTEM_PATH.test(context.filename)) {
|
|
3007
|
+
if (DESIGN_SYSTEM_PATH.test(context.filename) || isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
|
|
2991
3008
|
return {};
|
|
2992
3009
|
}
|
|
2993
3010
|
return {
|
|
@@ -3002,7 +3019,7 @@ var no_hand_rolled_spinner_default = createRule({
|
|
|
3002
3019
|
const className = staticClassName(classNameAttribute);
|
|
3003
3020
|
if (className === null) return;
|
|
3004
3021
|
const classes = className.split(/\s+/u);
|
|
3005
|
-
if (classes.includes("animate-spin") && classes.includes("rounded-full") && classes.some(
|
|
3022
|
+
if (classes.includes("animate-spin") && classes.includes("rounded-full") && classes.some(isBorderWidth) && classes.some(isContrastingEdge)) {
|
|
3006
3023
|
context.report({ node, messageId: "handRolledSpinner" });
|
|
3007
3024
|
}
|
|
3008
3025
|
}
|
|
@@ -3068,9 +3085,9 @@ function nameWords(name) {
|
|
|
3068
3085
|
return name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2").split(/[^A-Za-z0-9]+/u).filter(Boolean).map((word) => word.toLowerCase());
|
|
3069
3086
|
}
|
|
3070
3087
|
function isStrongSecurityName(name) {
|
|
3071
|
-
const
|
|
3072
|
-
return
|
|
3073
|
-
(word, index) => word === "api" &&
|
|
3088
|
+
const words2 = nameWords(name);
|
|
3089
|
+
return words2.some((word) => STRONG_SECURITY_WORDS.has(word)) || words2.some(
|
|
3090
|
+
(word, index) => word === "api" && words2[index + 1] === "key" || word === "auth" && words2[index + 1] === "id" || word === "verification" && words2[index + 1] === "code"
|
|
3074
3091
|
);
|
|
3075
3092
|
}
|
|
3076
3093
|
function isNonSecurityName(name) {
|
|
@@ -4032,7 +4049,7 @@ var VALUE_TAG_RE = /@(example|deprecated|see|remarks|throws|internal|public|alph
|
|
|
4032
4049
|
var BOUNDARY_RE = /(?<=[.!?])["'`)\]]*\s+(?=[A-Z0-9`])/;
|
|
4033
4050
|
var BULLET_RE = /^\s*(?:[-*+] |\d+[.)] )/;
|
|
4034
4051
|
var HEADING_RE = /^[A-Za-z][A-Za-z ]+:$/;
|
|
4035
|
-
var TECHNICAL_ANCHOR_RE = /https?:\/\/|`[^`\n]+`|:[a-z][a-z0-9_-]*:|(["'])[^"'\n]+\1|\d|\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b|\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b|(?:^|\s)(?:[\w.-]+\/)+[\w.-]+|\b[\w.-]+\.(?:py|pyi|js|jsx|ts|tsx|json|ya?ml|toml|csv|parquet|md)\b|->|=>|==|!=|<=|>=|\|/mu;
|
|
4052
|
+
var TECHNICAL_ANCHOR_RE = /https?:\/\/|`[^`\n]+`|:[a-z][a-z0-9_-]*:|(["'])[^"'\n]+\1|\bv?\d+\.\d+(?:\.\d+)?\b|\b\d+(?:\.\d+)?\s?(?:ns|us|ms|s|sec|secs|seconds?|mins?|minutes?|hours?|days?|bytes?|kib|mib|gib|kb|mb|gb|hz|khz|mhz|px|%)\b|\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b|\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b|(?:^|\s)(?:[\w.-]+\/)+[\w.-]+|\b[\w.-]+\.(?:py|pyi|js|jsx|ts|tsx|json|ya?ml|toml|csv|parquet|md)\b|->|=>|==|!=|<=|>=|\|/mu;
|
|
4036
4053
|
function body(comment) {
|
|
4037
4054
|
return comment.value.replace(/^\*/, "").split("\n").map((line) => line.replace(/^\s*\*?\s?/, "")).join("\n").trim();
|
|
4038
4055
|
}
|
|
@@ -6848,22 +6865,25 @@ var noStorageInStatelessModulesDocumentation = {
|
|
|
6848
6865
|
rationale: "Private storage in a stateless workflow creates another source of truth that can silently diverge.",
|
|
6849
6866
|
remediation: "Read from the system of record or derive state from an artifact the workflow already produces.",
|
|
6850
6867
|
category: "architecture",
|
|
6851
|
-
limitations: ["The rule is disabled until module path patterns are configured
|
|
6868
|
+
limitations: ["The rule is disabled until module path patterns are configured, recognizes only configured storage method names, and requires storage-like receiver evidence for the overloaded `put` method."],
|
|
6852
6869
|
examples: [
|
|
6853
6870
|
{ id: "system-of-record", title: "Read from the system of record", outcome: "no-match", files: [{ path: "src/engineer-digest/post.ts", source: "const issues = await linear.listIssues();" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 0, public: true },
|
|
6854
6871
|
{ id: "private-storage", title: "Do not write private state in a stateless module", outcome: "match", files: [{ path: "src/engineer-digest/post.ts", source: "await kv.put('digest:last', timestamp);" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 1, public: true }
|
|
6855
6872
|
]
|
|
6856
6873
|
};
|
|
6857
6874
|
function compile2(patterns) {
|
|
6858
|
-
|
|
6859
|
-
for (const pattern of patterns) {
|
|
6860
|
-
try {
|
|
6861
|
-
compiled.push(new RegExp(pattern));
|
|
6862
|
-
} catch {
|
|
6863
|
-
}
|
|
6864
|
-
}
|
|
6865
|
-
return compiled;
|
|
6875
|
+
return patterns.map((pattern) => new RegExp(pattern));
|
|
6866
6876
|
}
|
|
6877
|
+
var STORAGE_RECEIVER_WORDS = /* @__PURE__ */ new Set([
|
|
6878
|
+
"bucket",
|
|
6879
|
+
"cache",
|
|
6880
|
+
"kv",
|
|
6881
|
+
"namespace",
|
|
6882
|
+
"r2",
|
|
6883
|
+
"redis",
|
|
6884
|
+
"storage",
|
|
6885
|
+
"store"
|
|
6886
|
+
]);
|
|
6867
6887
|
function storageMethodName(node, methods) {
|
|
6868
6888
|
const callee = node.callee;
|
|
6869
6889
|
if (callee.type !== import_utils35.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils35.AST_NODE_TYPES.Identifier) {
|
|
@@ -6876,8 +6896,31 @@ function storageMethodName(node, methods) {
|
|
|
6876
6896
|
if (node.arguments.length < (MIN_ARGUMENTS.get(name) ?? 1)) {
|
|
6877
6897
|
return null;
|
|
6878
6898
|
}
|
|
6899
|
+
if (name === "put" && !isStorageLikeReceiver(callee.object)) {
|
|
6900
|
+
return null;
|
|
6901
|
+
}
|
|
6879
6902
|
return name;
|
|
6880
6903
|
}
|
|
6904
|
+
function isStorageLikeReceiver(node) {
|
|
6905
|
+
if (node.type === import_utils35.AST_NODE_TYPES.Identifier) {
|
|
6906
|
+
return isStorageIdentifier(node.name);
|
|
6907
|
+
}
|
|
6908
|
+
if (node.type !== import_utils35.AST_NODE_TYPES.MemberExpression) {
|
|
6909
|
+
return false;
|
|
6910
|
+
}
|
|
6911
|
+
if (!node.computed && node.property.type === import_utils35.AST_NODE_TYPES.Identifier && isStorageIdentifier(node.property.name)) {
|
|
6912
|
+
return true;
|
|
6913
|
+
}
|
|
6914
|
+
return isStorageLikeReceiver(node.object);
|
|
6915
|
+
}
|
|
6916
|
+
function isStorageIdentifier(name) {
|
|
6917
|
+
return identifierWords(name).some(
|
|
6918
|
+
(word) => STORAGE_RECEIVER_WORDS.has(word.toLowerCase())
|
|
6919
|
+
);
|
|
6920
|
+
}
|
|
6921
|
+
function identifierWords(name) {
|
|
6922
|
+
return name.match(/[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|\d+/gu) ?? [name];
|
|
6923
|
+
}
|
|
6881
6924
|
var no_storage_in_stateless_modules_default = createRule({
|
|
6882
6925
|
name: "no-storage-in-stateless-modules",
|
|
6883
6926
|
documentation: noStorageInStatelessModulesDocumentation,
|
|
@@ -7279,12 +7322,86 @@ var no_tautological_expect_default = createRule({
|
|
|
7279
7322
|
});
|
|
7280
7323
|
|
|
7281
7324
|
// src/rules/no-typed-doc-sections.ts
|
|
7325
|
+
var TYPED_TAG_RE2 = /^\s*@(arg|argument|param|return|returns|yield|yields)\b(.*)$/iu;
|
|
7326
|
+
var PARAM_TAGS2 = /* @__PURE__ */ new Set(["arg", "argument", "param"]);
|
|
7327
|
+
var PARAMETER_FILLER = /* @__PURE__ */ new Set([
|
|
7328
|
+
"a",
|
|
7329
|
+
"an",
|
|
7330
|
+
"argument",
|
|
7331
|
+
"given",
|
|
7332
|
+
"input",
|
|
7333
|
+
"parameter",
|
|
7334
|
+
"passed",
|
|
7335
|
+
"provided",
|
|
7336
|
+
"the",
|
|
7337
|
+
"value"
|
|
7338
|
+
]);
|
|
7339
|
+
var RESULT_FILLER = /* @__PURE__ */ new Set([
|
|
7340
|
+
"a",
|
|
7341
|
+
"an",
|
|
7342
|
+
"array",
|
|
7343
|
+
"boolean",
|
|
7344
|
+
"generator",
|
|
7345
|
+
"number",
|
|
7346
|
+
"object",
|
|
7347
|
+
"output",
|
|
7348
|
+
"promise",
|
|
7349
|
+
"result",
|
|
7350
|
+
"return",
|
|
7351
|
+
"returned",
|
|
7352
|
+
"returns",
|
|
7353
|
+
"string",
|
|
7354
|
+
"the",
|
|
7355
|
+
"value"
|
|
7356
|
+
]);
|
|
7357
|
+
function hasVacuousTypedTag(text) {
|
|
7358
|
+
const tags = typedTags(text);
|
|
7359
|
+
return tags.length > 0 && tags.some(isVacuousTag);
|
|
7360
|
+
}
|
|
7361
|
+
function typedTags(text) {
|
|
7362
|
+
const tags = [];
|
|
7363
|
+
for (const raw of text.split("\n")) {
|
|
7364
|
+
const match = TYPED_TAG_RE2.exec(raw);
|
|
7365
|
+
if (match !== null) {
|
|
7366
|
+
tags.push({ kind: (match[1] ?? "").toLowerCase(), payload: (match[2] ?? "").trim() });
|
|
7367
|
+
} else if (tags.length > 0 && raw.trim().length > 0 && !raw.trim().startsWith("@")) {
|
|
7368
|
+
const last = tags.at(-1);
|
|
7369
|
+
last.payload = `${last.payload} ${raw.trim()}`.trim();
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
return tags.map(({ kind, payload }) => {
|
|
7373
|
+
let rest = payload.replace(/^\{[^}\n]+\}\s*/u, "").trim();
|
|
7374
|
+
if (!PARAM_TAGS2.has(kind)) {
|
|
7375
|
+
return { kind, name: null, description: rest.replace(/^-\s*/u, "").trim() };
|
|
7376
|
+
}
|
|
7377
|
+
const match = /^(\[[^\]]+\]|[A-Za-z_$][\w$.[\]-]*)(?:\s+-\s*|\s+)?(.*)$/u.exec(rest);
|
|
7378
|
+
if (match === null) return { kind, name: null, description: "" };
|
|
7379
|
+
const rawName = (match[1] ?? "").replace(/^\[/u, "").replace(/\]$/u, "").split("=")[0] ?? "";
|
|
7380
|
+
rest = (match[2] ?? "").trim();
|
|
7381
|
+
return { kind, name: rawName, description: rest };
|
|
7382
|
+
});
|
|
7383
|
+
}
|
|
7384
|
+
function isVacuousTag(tag) {
|
|
7385
|
+
const description = words(tag.description).map(canonicalWord);
|
|
7386
|
+
if (description.length === 0) return true;
|
|
7387
|
+
if (tag.name === null) return description.every((word) => RESULT_FILLER.has(word));
|
|
7388
|
+
const nameWords2 = new Set(words(tag.name).map(canonicalWord));
|
|
7389
|
+
return description.every((word) => PARAMETER_FILLER.has(word) || nameWords2.has(word));
|
|
7390
|
+
}
|
|
7391
|
+
function words(text) {
|
|
7392
|
+
return text.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2").toLowerCase().match(/[a-z][a-z0-9]*/gu) ?? [];
|
|
7393
|
+
}
|
|
7394
|
+
function canonicalWord(word) {
|
|
7395
|
+
if (["identifier", "identifiers", "ids"].includes(word)) return "id";
|
|
7396
|
+
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
7397
|
+
return word;
|
|
7398
|
+
}
|
|
7282
7399
|
var noTypedDocSectionsDocumentation = {
|
|
7283
7400
|
summary: "Reject typed-signature repetition while preserving behavior that types cannot express.",
|
|
7284
7401
|
rationale: "Parameter and return tags repeat typed signatures and can drift without adding runtime behavior or constraints.",
|
|
7285
7402
|
remediation: "Remove repeated parameter and return tags; retain documentation for behavior, failures, and external contracts.",
|
|
7286
7403
|
category: "maintainability",
|
|
7287
|
-
limitations: ["
|
|
7404
|
+
limitations: ["Description-free or name-restating parameter and return tags are reported only when the documented function has corresponding explicit TypeScript types."],
|
|
7288
7405
|
examples: [
|
|
7289
7406
|
{
|
|
7290
7407
|
id: "behavioral-documentation",
|
|
@@ -7322,7 +7439,7 @@ var no_typed_doc_sections_default = createRule({
|
|
|
7322
7439
|
return {
|
|
7323
7440
|
Program() {
|
|
7324
7441
|
for (const group of proseGroups(context.filename, context.sourceCode, true)) {
|
|
7325
|
-
if (group.hasTypedTags && documentsTypedFunction(context.sourceCode, group.comment)) {
|
|
7442
|
+
if (group.hasTypedTags && hasVacuousTypedTag(group.text) && documentsTypedFunction(context.sourceCode, group.comment)) {
|
|
7326
7443
|
context.report({ node: group.comment, messageId: "typedSection" });
|
|
7327
7444
|
}
|
|
7328
7445
|
}
|
|
@@ -7423,28 +7540,32 @@ var STOPWORDS3 = /* @__PURE__ */ new Set([
|
|
|
7423
7540
|
"with"
|
|
7424
7541
|
]);
|
|
7425
7542
|
var DIRECTIVE_RE5 = /^\s*(?:eslint\b|eslint-|sarj-noqa\b|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|todo\b|fixme\b|hack\b|xxx\b)/i;
|
|
7543
|
+
var UNIT_NAME_SUFFIX_RE = /(?:_(?:NS|US|MS|S|SEC|SECS|SECOND|SECONDS|MIN|MINS|MINUTE|MINUTES|HOUR|HOURS|DAY|DAYS|BYTE|BYTES|KB|MB|GB|HZ|KHZ|MHZ|PX)|(?:Ns|Us|Ms|Sec|Secs|Second|Seconds|Min|Mins|Minute|Minutes|Hour|Hours|Day|Days|Byte|Bytes|Kb|Mb|Gb|Hz|Khz|Mhz|Px))$/u;
|
|
7426
7544
|
function narratesValue(body2, code) {
|
|
7427
7545
|
if (body2.length === 0 || DIRECTIVE_RE5.test(body2) || hasExternalReference(body2)) return false;
|
|
7428
7546
|
const codeNumbers = numbersIn(code);
|
|
7429
7547
|
if (codeNumbers.size === 0) return false;
|
|
7430
|
-
const
|
|
7431
|
-
if (
|
|
7548
|
+
const words2 = (body2.match(WORD_RE3) ?? []).map((word) => word.toLowerCase());
|
|
7549
|
+
if (words2.length === 0) return false;
|
|
7432
7550
|
const commentNumbers = numbersIn(body2);
|
|
7433
7551
|
if (commentNumbers.size === 0) return false;
|
|
7434
7552
|
for (const number of commentNumbers) {
|
|
7435
7553
|
if (!codeNumbers.has(number)) return false;
|
|
7436
7554
|
}
|
|
7437
|
-
if (!
|
|
7555
|
+
if (!words2.some((word) => UNIT_WORDS.has(word))) return false;
|
|
7438
7556
|
const identifiers = codeTokens(code);
|
|
7439
7557
|
const stems = /* @__PURE__ */ new Set();
|
|
7440
7558
|
for (const token of identifiers) stems.add(stem(token));
|
|
7441
|
-
return
|
|
7559
|
+
return words2.every(
|
|
7442
7560
|
(word) => STOPWORDS3.has(word) || UNIT_WORDS.has(word) || commentNumbers.has(word) || identifiers.has(word) || stems.has(stem(word))
|
|
7443
7561
|
);
|
|
7444
7562
|
}
|
|
7445
7563
|
function numbersIn(text) {
|
|
7446
7564
|
return new Set(text.match(NUMBER_RE) ?? []);
|
|
7447
7565
|
}
|
|
7566
|
+
function nameAlreadyCarriesUnit(code) {
|
|
7567
|
+
return (code.match(/[A-Za-z_$][\w$]*/gu) ?? []).some((identifier) => UNIT_NAME_SUFFIX_RE.test(identifier));
|
|
7568
|
+
}
|
|
7448
7569
|
var no_trailing_value_narration_default = createRule({
|
|
7449
7570
|
name: "no-trailing-value-narration",
|
|
7450
7571
|
documentation: noTrailingValueNarrationDocumentation,
|
|
@@ -7455,6 +7576,7 @@ var no_trailing_value_narration_default = createRule({
|
|
|
7455
7576
|
},
|
|
7456
7577
|
schema: [],
|
|
7457
7578
|
messages: {
|
|
7579
|
+
deleteNarration: "Trailing comment restates the literal and the identifier already names its unit \u2014 delete the comment so it cannot drift.",
|
|
7458
7580
|
narratesValue: "Trailing comment restates the literal on this line \u2014 put the unit in the name (STALE_TIME_MS) so it cannot drift."
|
|
7459
7581
|
}
|
|
7460
7582
|
},
|
|
@@ -7487,7 +7609,10 @@ var no_trailing_value_narration_default = createRule({
|
|
|
7487
7609
|
const code = line.slice(0, comment.loc.start.column);
|
|
7488
7610
|
const body2 = comment.value.replace(/^\*+/, "").replace(/\*+$/, "").trim();
|
|
7489
7611
|
if (narratesValue(body2, code)) {
|
|
7490
|
-
context.report({
|
|
7612
|
+
context.report({
|
|
7613
|
+
node: comment,
|
|
7614
|
+
messageId: nameAlreadyCarriesUnit(code) ? "deleteNarration" : "narratesValue"
|
|
7615
|
+
});
|
|
7491
7616
|
}
|
|
7492
7617
|
}
|
|
7493
7618
|
}
|
|
@@ -8528,15 +8653,23 @@ var no_zod_native_enum_default = createRule({
|
|
|
8528
8653
|
} catch {
|
|
8529
8654
|
services = null;
|
|
8530
8655
|
}
|
|
8531
|
-
const
|
|
8532
|
-
const
|
|
8656
|
+
const zodImportedBindings = /* @__PURE__ */ new Map();
|
|
8657
|
+
const zodNamespaceBindings = /* @__PURE__ */ new Set();
|
|
8658
|
+
function resolvedBinding(identifier) {
|
|
8659
|
+
return import_utils45.ASTUtils.findVariable(
|
|
8660
|
+
sourceCode.getScope(identifier),
|
|
8661
|
+
identifier.name
|
|
8662
|
+
);
|
|
8663
|
+
}
|
|
8533
8664
|
function isZodMemberCall(node, api) {
|
|
8534
8665
|
const callee = node.callee;
|
|
8535
|
-
if (callee.type === import_utils45.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils45.AST_NODE_TYPES.Identifier &&
|
|
8536
|
-
|
|
8666
|
+
if (callee.type === import_utils45.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils45.AST_NODE_TYPES.Identifier && callee.property.type === import_utils45.AST_NODE_TYPES.Identifier) {
|
|
8667
|
+
const binding = resolvedBinding(callee.object);
|
|
8668
|
+
return binding !== null && zodNamespaceBindings.has(binding) && callee.property.name === api;
|
|
8537
8669
|
}
|
|
8538
8670
|
if (callee.type === import_utils45.AST_NODE_TYPES.Identifier) {
|
|
8539
|
-
|
|
8671
|
+
const binding = resolvedBinding(callee);
|
|
8672
|
+
return binding !== null && zodImportedBindings.get(binding) === api;
|
|
8540
8673
|
}
|
|
8541
8674
|
return false;
|
|
8542
8675
|
}
|
|
@@ -8571,10 +8704,14 @@ var no_zod_native_enum_default = createRule({
|
|
|
8571
8704
|
}
|
|
8572
8705
|
for (const spec of node.specifiers) {
|
|
8573
8706
|
if (spec.type === import_utils45.AST_NODE_TYPES.ImportNamespaceSpecifier || spec.type === import_utils45.AST_NODE_TYPES.ImportDefaultSpecifier || spec.type === import_utils45.AST_NODE_TYPES.ImportSpecifier && (spec.imported.type === import_utils45.AST_NODE_TYPES.Identifier ? spec.imported.name === "z" : spec.imported.value === "z")) {
|
|
8574
|
-
|
|
8707
|
+
const binding = resolvedBinding(spec.local);
|
|
8708
|
+
if (binding !== null) zodNamespaceBindings.add(binding);
|
|
8575
8709
|
}
|
|
8576
8710
|
if (spec.type === import_utils45.AST_NODE_TYPES.ImportSpecifier && spec.imported.type === import_utils45.AST_NODE_TYPES.Identifier) {
|
|
8577
|
-
|
|
8711
|
+
const binding = resolvedBinding(spec.local);
|
|
8712
|
+
if (binding !== null) {
|
|
8713
|
+
zodImportedBindings.set(binding, spec.imported.name);
|
|
8714
|
+
}
|
|
8578
8715
|
}
|
|
8579
8716
|
}
|
|
8580
8717
|
},
|
|
@@ -12356,6 +12493,9 @@ var requireAssertNeverDocumentation = {
|
|
|
12356
12493
|
};
|
|
12357
12494
|
var isRuntimeHandlingStatement = (statement) => {
|
|
12358
12495
|
if (statement.type === import_utils62.AST_NODE_TYPES.EmptyStatement) return false;
|
|
12496
|
+
if (statement.type === import_utils62.AST_NODE_TYPES.BreakStatement) {
|
|
12497
|
+
return statement.label !== null;
|
|
12498
|
+
}
|
|
12359
12499
|
if (statement.type === import_utils62.AST_NODE_TYPES.TSTypeAliasDeclaration || statement.type === import_utils62.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
12360
12500
|
return false;
|
|
12361
12501
|
}
|
|
@@ -12385,7 +12525,12 @@ function isExhaustiveFiniteSwitch(node, services) {
|
|
|
12385
12525
|
const discriminant = services.esTreeNodeToTSNodeMap.get(node.discriminant);
|
|
12386
12526
|
const discriminantType = checker.getTypeAtLocation(discriminant);
|
|
12387
12527
|
const constituents = discriminantType.isUnion() ? discriminantType.types : [discriminantType];
|
|
12388
|
-
if (constituents.length
|
|
12528
|
+
if (!discriminantType.isUnion() || constituents.length < 2) return false;
|
|
12529
|
+
if (constituents.every(
|
|
12530
|
+
(constituent) => (constituent.flags & import_typescript.default.TypeFlags.BooleanLiteral) !== 0
|
|
12531
|
+
)) {
|
|
12532
|
+
return false;
|
|
12533
|
+
}
|
|
12389
12534
|
const expected = /* @__PURE__ */ new Set();
|
|
12390
12535
|
for (const constituent of constituents) {
|
|
12391
12536
|
const key = finiteTypeKey(constituent, checker);
|
|
@@ -13192,28 +13337,26 @@ var requireZodFormValidationDocumentation = {
|
|
|
13192
13337
|
rationale: "FormData values are untrusted strings or files and need runtime validation before use.",
|
|
13193
13338
|
remediation: "Read the value inside a Zod schema's `parse` or `safeParse` input.",
|
|
13194
13339
|
category: "security",
|
|
13340
|
+
limitations: [
|
|
13341
|
+
"Tests are excluded; imported schema-shaped names are trusted when their implementation is outside the linted file.",
|
|
13342
|
+
"Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer."
|
|
13343
|
+
],
|
|
13195
13344
|
examples: [
|
|
13196
13345
|
{ id: "validated-form-value", title: "Validate the form value", outcome: "no-match", files: [{ path: "src/action.ts", source: "const input = UserSchema.parse({ name: formData.get('name') });" }], focusPath: "src/action.ts", expectedCount: 0, public: true },
|
|
13197
13346
|
{ id: "raw-form-value", title: "Do not use a raw form value", outcome: "match", files: [{ path: "src/action.ts", source: "const name = formData.get('name');" }], focusPath: "src/action.ts", expectedCount: 1, public: true }
|
|
13198
13347
|
]
|
|
13199
13348
|
};
|
|
13200
|
-
var
|
|
13201
|
-
|
|
13202
|
-
|
|
13203
|
-
|
|
13204
|
-
|
|
13205
|
-
|
|
13206
|
-
|
|
13207
|
-
if (method !== "parse" && method !== "safeParse" && method !== "parseAsync" && method !== "safeParseAsync") {
|
|
13208
|
-
return false;
|
|
13209
|
-
}
|
|
13210
|
-
return looksLikeZodSchema(callee.object);
|
|
13211
|
-
};
|
|
13212
|
-
var looksLikeZodSchema = (node) => {
|
|
13349
|
+
var ZOD_PARSE_METHODS = /* @__PURE__ */ new Set([
|
|
13350
|
+
"parse",
|
|
13351
|
+
"safeParse",
|
|
13352
|
+
"parseAsync",
|
|
13353
|
+
"safeParseAsync"
|
|
13354
|
+
]);
|
|
13355
|
+
var zodReceiverRoot = (node) => {
|
|
13213
13356
|
let current = node;
|
|
13214
13357
|
while (true) {
|
|
13215
13358
|
if (current.type === import_utils66.AST_NODE_TYPES.Identifier) {
|
|
13216
|
-
return current
|
|
13359
|
+
return current;
|
|
13217
13360
|
}
|
|
13218
13361
|
if (current.type === import_utils66.AST_NODE_TYPES.CallExpression) {
|
|
13219
13362
|
current = current.callee;
|
|
@@ -13223,7 +13366,7 @@ var looksLikeZodSchema = (node) => {
|
|
|
13223
13366
|
current = current.object;
|
|
13224
13367
|
continue;
|
|
13225
13368
|
}
|
|
13226
|
-
return
|
|
13369
|
+
return null;
|
|
13227
13370
|
}
|
|
13228
13371
|
};
|
|
13229
13372
|
var isFormDataMethodCall = (node) => {
|
|
@@ -13253,6 +13396,34 @@ var require_zod_form_validation_default = createRule({
|
|
|
13253
13396
|
if (isTestFile(context.filename)) {
|
|
13254
13397
|
return {};
|
|
13255
13398
|
}
|
|
13399
|
+
const zodBindings = /* @__PURE__ */ new Set();
|
|
13400
|
+
const resolvedBinding = (identifier) => import_utils66.ASTUtils.findVariable(
|
|
13401
|
+
context.sourceCode.getScope(identifier),
|
|
13402
|
+
identifier.name
|
|
13403
|
+
);
|
|
13404
|
+
const isProvablyNonZodLocal = (identifier) => {
|
|
13405
|
+
const binding = resolvedBinding(identifier);
|
|
13406
|
+
if (binding === null || zodBindings.has(binding) || binding.defs.length !== 1) {
|
|
13407
|
+
return false;
|
|
13408
|
+
}
|
|
13409
|
+
const definition = binding.defs[0];
|
|
13410
|
+
if (definition?.type !== "Variable" || definition.node.type !== import_utils66.AST_NODE_TYPES.VariableDeclarator) {
|
|
13411
|
+
return false;
|
|
13412
|
+
}
|
|
13413
|
+
const init = definition.node.init;
|
|
13414
|
+
return init?.type === import_utils66.AST_NODE_TYPES.ObjectExpression || init?.type === import_utils66.AST_NODE_TYPES.ArrayExpression || init?.type === import_utils66.AST_NODE_TYPES.Literal || init?.type === import_utils66.AST_NODE_TYPES.ArrowFunctionExpression || init?.type === import_utils66.AST_NODE_TYPES.FunctionExpression;
|
|
13415
|
+
};
|
|
13416
|
+
const isZodParseCall = (node) => {
|
|
13417
|
+
if (node.type !== import_utils66.AST_NODE_TYPES.CallExpression) return false;
|
|
13418
|
+
const callee = node.callee;
|
|
13419
|
+
if (callee.type !== import_utils66.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils66.AST_NODE_TYPES.Identifier || !ZOD_PARSE_METHODS.has(callee.property.name)) {
|
|
13420
|
+
return false;
|
|
13421
|
+
}
|
|
13422
|
+
const root = zodReceiverRoot(callee.object);
|
|
13423
|
+
if (root === null) return false;
|
|
13424
|
+
const binding = resolvedBinding(root);
|
|
13425
|
+
return binding !== null && zodBindings.has(binding) || (root.name === "z" || ZOD_SCHEMA_NAME_RE.test(root.name)) && !isProvablyNonZodLocal(root);
|
|
13426
|
+
};
|
|
13256
13427
|
const isFormSourceIdentifier = (node) => {
|
|
13257
13428
|
if (node.type !== import_utils66.AST_NODE_TYPES.Identifier) return false;
|
|
13258
13429
|
if (/formdata/i.test(node.name)) return true;
|
|
@@ -13278,14 +13449,15 @@ var require_zod_form_validation_default = createRule({
|
|
|
13278
13449
|
}
|
|
13279
13450
|
return isFormSourceIdentifier(callee.object);
|
|
13280
13451
|
};
|
|
13281
|
-
const
|
|
13452
|
+
const zodParseAncestor = (node) => {
|
|
13282
13453
|
let parent = node.parent;
|
|
13283
13454
|
while (parent !== null && parent !== void 0) {
|
|
13284
|
-
if (isZodParseCall(parent)) return
|
|
13455
|
+
if (isZodParseCall(parent)) return parent;
|
|
13285
13456
|
parent = parent.parent;
|
|
13286
13457
|
}
|
|
13287
|
-
return
|
|
13458
|
+
return null;
|
|
13288
13459
|
};
|
|
13460
|
+
const hasZodParseAncestor = (node) => zodParseAncestor(node) !== null;
|
|
13289
13461
|
const isInstanceofNarrowing = (node) => {
|
|
13290
13462
|
const parent = node.parent;
|
|
13291
13463
|
return parent !== null && parent !== void 0 && parent.type === import_utils66.AST_NODE_TYPES.BinaryExpression && parent.operator === "instanceof" && parent.left === node && parent.right.type === import_utils66.AST_NODE_TYPES.Identifier && (parent.right.name === "File" || parent.right.name === "Blob");
|
|
@@ -13302,14 +13474,111 @@ var require_zod_form_validation_default = createRule({
|
|
|
13302
13474
|
}
|
|
13303
13475
|
return null;
|
|
13304
13476
|
};
|
|
13477
|
+
const containingStatement = (node) => {
|
|
13478
|
+
let current = node;
|
|
13479
|
+
while (current.parent !== void 0) {
|
|
13480
|
+
const parent = current.parent;
|
|
13481
|
+
if (parent.type === import_utils66.AST_NODE_TYPES.BlockStatement || parent.type === import_utils66.AST_NODE_TYPES.Program) {
|
|
13482
|
+
return current;
|
|
13483
|
+
}
|
|
13484
|
+
current = parent;
|
|
13485
|
+
}
|
|
13486
|
+
return null;
|
|
13487
|
+
};
|
|
13488
|
+
const zodParseMethod = (call) => {
|
|
13489
|
+
const callee = call.callee;
|
|
13490
|
+
return callee.type === import_utils66.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils66.AST_NODE_TYPES.Identifier ? callee.property.name : null;
|
|
13491
|
+
};
|
|
13492
|
+
const hasConditionalAncestorBeforeStatement = (node, statement) => {
|
|
13493
|
+
let current = node.parent;
|
|
13494
|
+
while (current !== void 0 && current !== statement) {
|
|
13495
|
+
if (current.type === import_utils66.AST_NODE_TYPES.LogicalExpression || current.type === import_utils66.AST_NODE_TYPES.ConditionalExpression) {
|
|
13496
|
+
return true;
|
|
13497
|
+
}
|
|
13498
|
+
current = current.parent;
|
|
13499
|
+
}
|
|
13500
|
+
return false;
|
|
13501
|
+
};
|
|
13502
|
+
const isAwaitedBeforeStatement = (node, statement) => {
|
|
13503
|
+
let current = node.parent;
|
|
13504
|
+
while (current !== void 0 && current !== statement) {
|
|
13505
|
+
if (current.type === import_utils66.AST_NODE_TYPES.AwaitExpression) return true;
|
|
13506
|
+
current = current.parent;
|
|
13507
|
+
}
|
|
13508
|
+
return false;
|
|
13509
|
+
};
|
|
13510
|
+
const guaranteedValidationStatement = (declarator, reference) => {
|
|
13511
|
+
const parse2 = zodParseAncestor(reference);
|
|
13512
|
+
if (parse2 === null) return null;
|
|
13513
|
+
const declarationStatement = containingStatement(declarator);
|
|
13514
|
+
const validationStatement = containingStatement(parse2);
|
|
13515
|
+
if (declarationStatement === null || validationStatement === null || declarationStatement.parent !== validationStatement.parent || validationStatement.range[0] <= declarationStatement.range[1] || hasConditionalAncestorBeforeStatement(parse2, validationStatement)) {
|
|
13516
|
+
return null;
|
|
13517
|
+
}
|
|
13518
|
+
if (validationStatement.type !== import_utils66.AST_NODE_TYPES.VariableDeclaration && validationStatement.type !== import_utils66.AST_NODE_TYPES.ExpressionStatement) {
|
|
13519
|
+
return null;
|
|
13520
|
+
}
|
|
13521
|
+
const method = zodParseMethod(parse2);
|
|
13522
|
+
if (method === "parse") return validationStatement;
|
|
13523
|
+
if (method === "parseAsync" && isAwaitedBeforeStatement(parse2, validationStatement)) {
|
|
13524
|
+
return validationStatement;
|
|
13525
|
+
}
|
|
13526
|
+
return null;
|
|
13527
|
+
};
|
|
13528
|
+
const isSafePrevalidationInspection = (identifier) => {
|
|
13529
|
+
const parent = identifier.parent;
|
|
13530
|
+
if (parent.type === import_utils66.AST_NODE_TYPES.UnaryExpression && parent.operator === "typeof") {
|
|
13531
|
+
return true;
|
|
13532
|
+
}
|
|
13533
|
+
if (parent.type !== import_utils66.AST_NODE_TYPES.BinaryExpression || parent.left !== identifier) {
|
|
13534
|
+
return false;
|
|
13535
|
+
}
|
|
13536
|
+
if (parent.operator === "instanceof") {
|
|
13537
|
+
return parent.right.type === import_utils66.AST_NODE_TYPES.Identifier && (parent.right.name === "File" || parent.right.name === "Blob");
|
|
13538
|
+
}
|
|
13539
|
+
return ["===", "!==", "==", "!="].includes(parent.operator) && (parent.right.type === import_utils66.AST_NODE_TYPES.Literal && parent.right.value === null || parent.right.type === import_utils66.AST_NODE_TYPES.Identifier && parent.right.name === "undefined");
|
|
13540
|
+
};
|
|
13541
|
+
const statementWithinBlock = (node, block) => {
|
|
13542
|
+
let current = node;
|
|
13543
|
+
while (current.parent !== void 0 && current.parent !== block) {
|
|
13544
|
+
current = current.parent;
|
|
13545
|
+
}
|
|
13546
|
+
return current.parent === block ? current : null;
|
|
13547
|
+
};
|
|
13305
13548
|
const bindingIsValidated = (declarator) => {
|
|
13306
13549
|
const variable = context.sourceCode.getDeclaredVariables(declarator)[0];
|
|
13307
13550
|
if (variable === void 0) return false;
|
|
13308
|
-
|
|
13309
|
-
(
|
|
13551
|
+
const references = variable.references.filter((reference) => !reference.isWriteOnly()).map((reference) => reference.identifier).filter(
|
|
13552
|
+
(identifier) => identifier.type === import_utils66.AST_NODE_TYPES.Identifier
|
|
13553
|
+
);
|
|
13554
|
+
if (references.length === 0) return false;
|
|
13555
|
+
if (references.some(isInstanceofNarrowing)) return true;
|
|
13556
|
+
const validationStatements = references.map((reference) => guaranteedValidationStatement(declarator, reference)).filter(
|
|
13557
|
+
(statement) => statement !== null
|
|
13310
13558
|
);
|
|
13559
|
+
const declarationStatement = containingStatement(declarator);
|
|
13560
|
+
const declarationBlock = declarationStatement?.parent;
|
|
13561
|
+
return references.every((reference) => {
|
|
13562
|
+
if (zodParseAncestor(reference) !== null || isSafePrevalidationInspection(reference)) {
|
|
13563
|
+
return true;
|
|
13564
|
+
}
|
|
13565
|
+
if (declarationBlock === void 0) return false;
|
|
13566
|
+
const useStatement = statementWithinBlock(reference, declarationBlock);
|
|
13567
|
+
return useStatement !== null && validationStatements.some(
|
|
13568
|
+
(statement) => statement.range[1] < useStatement.range[0]
|
|
13569
|
+
);
|
|
13570
|
+
});
|
|
13311
13571
|
};
|
|
13312
13572
|
return {
|
|
13573
|
+
ImportDeclaration(node) {
|
|
13574
|
+
if (!isZodModule(node.source.value)) return;
|
|
13575
|
+
for (const specifier of node.specifiers) {
|
|
13576
|
+
if (specifier.type === import_utils66.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils66.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils66.AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === import_utils66.AST_NODE_TYPES.Identifier ? specifier.imported.name === "z" : specifier.imported.value === "z")) {
|
|
13577
|
+
const binding = resolvedBinding(specifier.local);
|
|
13578
|
+
if (binding !== null) zodBindings.add(binding);
|
|
13579
|
+
}
|
|
13580
|
+
}
|
|
13581
|
+
},
|
|
13313
13582
|
CallExpression(node) {
|
|
13314
13583
|
if (!isFormDataGetCall(node)) return;
|
|
13315
13584
|
if (hasZodParseAncestor(node) || isInstanceofNarrowing(node)) return;
|
|
@@ -14069,7 +14338,7 @@ var rules = {
|
|
|
14069
14338
|
};
|
|
14070
14339
|
var meta = {
|
|
14071
14340
|
name: "@sarj/eslint-plugin",
|
|
14072
|
-
version: "15.
|
|
14341
|
+
version: "15.2.0"
|
|
14073
14342
|
};
|
|
14074
14343
|
var applicationOnlyRules = [
|
|
14075
14344
|
"no-restricted-library-load",
|