@sarj/eslint-plugin 16.36.3 → 16.36.6

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 CHANGED
@@ -1928,7 +1928,7 @@ var NO_TEST_COMMENTS_DOCUMENTATION = {
1928
1928
  autofix: "safe",
1929
1929
  limitations: [
1930
1930
  "Checks actual line and block comment tokens in recognized maintained test files, including test fixtures; strings and template text remain unchanged. Generated/vendor files are excluded.",
1931
- "Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed. Ordinary prose embedded in a directive remains the owning tool's responsibility.",
1931
+ "Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed. Executable detect-secrets 1.5.0 allowlist/whitelist pragmas retain its case-sensitive delimiter, spacing, physical-line and next-line grammar; arbitrary pragma prose remains ordinary commentary. Rationale on an executable directive remains the owning tool's responsibility.",
1932
1932
  "The automatic fix removes comments and adjacent padding while retaining every line ending and necessary token boundary; it does not rename tests or remove executable statements. Bare integration/e2e directories and ambiguous hyphen/underscore test basenames require another concrete test-path signal or a runtime testing-framework import; ordinary production integration modules and rule implementations ending in -test are preserved."
1933
1933
  ],
1934
1934
  examples: [
@@ -1939,6 +1939,52 @@ var NO_TEST_COMMENTS_DOCUMENTATION = {
1939
1939
  var DIRECTIVE = /^(?:eslint(?:-\S+)?\b|(?:biome|oxlint|jshint|jslint|c8|v8|istanbul|vitest|prettier|dprint)\b|@(?:ts-(?:check|nocheck|expect-error|ignore)|vitest-environment|jest-environment|jsx\w*|license|preserve)\b|[#!]\s*__(?:PURE|NO_SIDE_EFFECTS)__|\/\s*<(?:reference|amd-module|amd-dependency)\b)/u;
1940
1940
  var LEGAL = /(?:^|\n)\s*\*?\s*(?:Copyright\b|SPDX-License-Identifier:|Licensed under\b|@license\b|@preserve\b)/iu;
1941
1941
  var TYPE_CONTRACT = /@(?:type|typedef|template|param|returns?|satisfies|callback|overload|implements|extends|augments|enum|this|import)\b/u;
1942
+ var DETECT_SECRETS_COMMENT_MARKERS = [
1943
+ { start: "#", end: "" },
1944
+ { start: "//", end: "" },
1945
+ { start: "/\\*", end: " *\\*/" },
1946
+ { start: "'", end: "" },
1947
+ { start: "--", end: "" },
1948
+ { start: "<!--[# \\t]*?", end: " *?-->" }
1949
+ ];
1950
+ var DETECT_SECRETS_PRAGMAS = DETECT_SECRETS_COMMENT_MARKERS.flatMap(({ start, end }) => [
1951
+ { nextLine: false, pattern: new RegExp(`[ \\t]*${start} *pragma: ?(?:allow|white)list[ -]secret[^\\n]*?${end}[ \\t]*(?![\\s\\S])`, "u") },
1952
+ { nextLine: true, pattern: new RegExp(`^[ \\t]*${start} *pragma: ?allowlist[ -]nextline[ -]secret[^\\n]*?${end}[ \\t]*(?![\\s\\S])`, "u") }
1953
+ ]);
1954
+ var PYTHON_WHITESPACE = new RegExp("\\p{White_Space}", "u");
1955
+ var PYTHON_EXTRA_WHITESPACE = /* @__PURE__ */ new Set(["", "", "", ""]);
1956
+ function hasDetectSecretsPragma(comment, text) {
1957
+ if (!comment.value.includes("pragma:")) return false;
1958
+ let lineStart = Math.max(text.lastIndexOf("\r", comment.range[0] - 1), text.lastIndexOf("\n", comment.range[0] - 1)) + 1;
1959
+ while (lineStart < comment.range[1]) {
1960
+ const nextCr = text.indexOf("\r", lineStart);
1961
+ const nextLf = text.indexOf("\n", lineStart);
1962
+ const lineEnd = Math.min(nextCr < 0 ? text.length : nextCr, nextLf < 0 ? text.length : nextLf);
1963
+ if (hasPragmaInReaderLine(comment, text.slice(lineStart, lineEnd), lineStart)) return true;
1964
+ lineStart = lineEnd + (text[lineEnd] === "\r" && text[lineEnd + 1] === "\n" ? 2 : 1);
1965
+ }
1966
+ return false;
1967
+ }
1968
+ function hasPragmaInReaderLine(comment, rawLine, lineStart) {
1969
+ const sameLine = stripPythonTrailingWhitespace(rawLine);
1970
+ for (const { nextLine, pattern } of DETECT_SECRETS_PRAGMAS) {
1971
+ const column = nextLine ? 0 : Math.max(0, comment.range[0] - lineStart);
1972
+ const match = pattern.exec((nextLine ? rawLine : sameLine).slice(column));
1973
+ if (match === null) continue;
1974
+ const pragmaStart = lineStart + column + match.index + match[0].indexOf("pragma:");
1975
+ if (pragmaStart >= comment.range[0] && pragmaStart < comment.range[1]) return true;
1976
+ }
1977
+ return false;
1978
+ }
1979
+ function stripPythonTrailingWhitespace(value) {
1980
+ let end = value.length;
1981
+ while (end > 0) {
1982
+ const character = value.charAt(end - 1);
1983
+ if (!PYTHON_WHITESPACE.test(character) && !PYTHON_EXTRA_WHITESPACE.has(character)) break;
1984
+ end--;
1985
+ }
1986
+ return value.slice(0, end);
1987
+ }
1942
1988
  var no_test_comments_default = createRule({
1943
1989
  name: "no-test-comments",
1944
1990
  documentation: NO_TEST_COMMENTS_DOCUMENTATION,
@@ -1975,7 +2021,7 @@ var no_test_comments_default = createRule({
1975
2021
  }
1976
2022
  for (const [index, comment] of comments.entries()) {
1977
2023
  const body3 = comment.value.trim().replace(/^\*\s*/u, "");
1978
- if (DIRECTIVE.test(body3) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value)) continue;
2024
+ if (DIRECTIVE.test(body3) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value) || hasDetectSecretsPragma(comment, context.sourceCode.text)) continue;
1979
2025
  context.report({
1980
2026
  node: comment,
1981
2027
  messageId: "testComment",
@@ -2268,8 +2314,13 @@ function commentSuppressesChecking(text, kind) {
2268
2314
 
2269
2315
  // src/rules/_closed-object-contract.ts
2270
2316
  var import_typescript2 = __toESM(require("typescript"), 1);
2271
- function closedObjectContract(type, checker) {
2272
- return visitContract(type, { checker, active: /* @__PURE__ */ new Set(), completed: /* @__PURE__ */ new WeakMap() });
2317
+ function closedObjectContract(type, checker, singlePropertyOrigins = false) {
2318
+ return visitContract(type, {
2319
+ checker,
2320
+ active: /* @__PURE__ */ new Set(),
2321
+ completed: /* @__PURE__ */ new WeakMap(),
2322
+ origins: singlePropertyOrigins ? /* @__PURE__ */ new Map() : void 0
2323
+ });
2273
2324
  }
2274
2325
  function visitContract(type, proof) {
2275
2326
  const known2 = proof.completed.get(type);
@@ -2283,10 +2334,20 @@ function visitContract(type, proof) {
2283
2334
  }
2284
2335
  function completeObject(type, proof) {
2285
2336
  if (!ordinaryObject(type, proof.checker)) return false;
2337
+ if (proof.origins && !singleOrigins(type, proof.origins)) return false;
2286
2338
  const declarations = type.getSymbol()?.getDeclarations()?.filter((declaration) => import_typescript2.default.isInterfaceDeclaration(declaration) || import_typescript2.default.isTypeLiteralNode(declaration));
2287
2339
  if (!declarations?.length) return false;
2288
2340
  return declarations.every((declaration) => completeDeclaration(declaration, proof));
2289
2341
  }
2342
+ function singleOrigins(type, origins) {
2343
+ for (const property of type.getProperties()) {
2344
+ const declarations = property.getDeclarations();
2345
+ const declaration = declarations?.[0];
2346
+ if (!declaration || declarations?.length !== 1 || origins.has(property.name) && origins.get(property.name) !== declaration) return false;
2347
+ origins.set(property.name, declaration);
2348
+ }
2349
+ return true;
2350
+ }
2290
2351
  function ordinaryObject(type, checker) {
2291
2352
  if ((type.flags & import_typescript2.default.TypeFlags.Object) === 0 || type.isUnion() || checker.getIndexInfosOfType(type).length > 0) return false;
2292
2353
  const object = type;
@@ -2605,6 +2666,9 @@ function directEval(node) {
2605
2666
  function primitiveKeyword(node) {
2606
2667
  return node !== void 0 && (node.kind === import_typescript6.default.SyntaxKind.StringKeyword || node.kind === import_typescript6.default.SyntaxKind.NumberKeyword || node.kind === import_typescript6.default.SyntaxKind.BooleanKeyword || node.kind === import_typescript6.default.SyntaxKind.BigIntKeyword);
2607
2668
  }
2669
+ function nullishPrimitiveUnion(node) {
2670
+ return node !== void 0 && import_typescript6.default.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === import_typescript6.default.SyntaxKind.UndefinedKeyword || import_typescript6.default.isLiteralTypeNode(part) && part.literal.kind === import_typescript6.default.SyntaxKind.NullKeyword);
2671
+ }
2608
2672
  function consumerAnnotation(node) {
2609
2673
  if (node.params.length !== 1) return void 0;
2610
2674
  const parameter = node.params[0];
@@ -2662,6 +2726,7 @@ function directCalls(binding3) {
2662
2726
  // src/rules/no-redundant-consumer-union.ts
2663
2727
  var CONFIG = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2664
2728
  var EXAMPLE = 'export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: "Ada", breed: "Collie"}; function label(value: Dog | Animal): string {return value.name;} label(dog);';
2729
+ var NULLABLE_EXAMPLE = EXAMPLE.replace("readonly name: string", "readonly name: string | null").replace("return value.name;", "return value.name ?? '';");
2665
2730
  var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2666
2731
  defaultLevel: "warning",
2667
2732
  summary: "Keep the covering object contract in a private scalar consumer union.",
@@ -2672,21 +2737,23 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2672
2737
  limitations: [
2673
2738
  "Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.",
2674
2739
  "Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single pure returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.",
2675
- "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, callbacks and recursive or nested object fields are unsupported.",
2740
+ "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.",
2676
2741
  "Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
2677
2742
  "General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, helpers, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."
2678
2743
  ],
2679
2744
  examples: [
2680
2745
  { id: "covered-alternative", title: "The private consumer needs the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE.replace("Dog | Animal", "Animal") }] },
2681
- { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2746
+ { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2747
+ { id: "nullable-covered-alternative", scenarioId: "nullable-readonly-contract", title: "A nullable name retains the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2748
+ { id: "nullable-domain-contract", scenarioId: "nullable-readonly-contract", title: "A literal breed keeps the authored domain contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("readonly breed: string", "readonly breed: 'Collie'") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2682
2749
  ]
2683
2750
  };
2684
2751
  function scalarFields(type, checker) {
2685
- if (!closedObjectContract(type, checker)) return null;
2752
+ if (!closedObjectContract(type, checker, true)) return null;
2686
2753
  const fields2 = /* @__PURE__ */ new Map();
2687
2754
  for (const property of type.getProperties()) {
2688
2755
  const declarations = property.getDeclarations();
2689
- if (!declarations?.length || !declarations.every((member3) => import_typescript7.default.isPropertySignature(member3) && member3.name && (import_typescript7.default.isIdentifier(member3.name) || import_typescript7.default.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === import_typescript7.default.SyntaxKind.ReadonlyKeyword && primitiveKeyword(member3.type))) return null;
2756
+ if (!declarations?.length || !declarations.every((member3) => import_typescript7.default.isPropertySignature(member3) && member3.name && (import_typescript7.default.isIdentifier(member3.name) || import_typescript7.default.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === import_typescript7.default.SyntaxKind.ReadonlyKeyword && (primitiveKeyword(member3.type) || nullishPrimitiveUnion(member3.type)))) return null;
2690
2757
  fields2.set(property.name, (property.flags & import_typescript7.default.SymbolFlags.Optional) === 0);
2691
2758
  }
2692
2759
  return fields2.size ? fields2 : null;
@@ -2773,7 +2840,7 @@ var import_typescript8 = __toESM(require("typescript"), 1);
2773
2840
  var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2774
2841
  var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
2775
2842
  var HELPER_EXAMPLE = 'export {}; const account = {name: "Ada", email: "a"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: {email: string; name: string}): string {return formatName(user.name);} label(account);';
2776
- var NULLABLE_EXAMPLE = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
2843
+ var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
2777
2844
  var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
2778
2845
  defaultLevel: "warning",
2779
2846
  summary: "Prefer the proven read-only subset for a private consumer parameter.",
@@ -2792,8 +2859,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
2792
2859
  { id: "used-fields", title: "Every required dependency is read", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE2.replace("return user.name;", "return user.name + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
2793
2860
  { id: "scalar-helper", scenarioId: "local-scalar-helper", title: "Only the name crosses a local scalar helper call", outcome: "match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("email: string;", "") }] },
2794
2861
  { id: "helper-and-other-field", scenarioId: "local-scalar-helper", title: "The consumer still requires a separately read field", outcome: "no-match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("return formatName(user.name);", "return formatName(user.name) + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
2795
- { id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("email: string | null;", "") }] },
2796
- { id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
2862
+ { id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2 }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("email: string | null;", "") }] },
2863
+ { id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
2797
2864
  ]
2798
2865
  };
2799
2866
  function inlineFields(node, reads) {
@@ -2807,9 +2874,6 @@ function inlineFields(node, reads) {
2807
2874
  }
2808
2875
  return fields2;
2809
2876
  }
2810
- function nullishPrimitiveUnion(node) {
2811
- return node !== void 0 && import_typescript8.default.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === import_typescript8.default.SyntaxKind.UndefinedKeyword || import_typescript8.default.isLiteralTypeNode(part) && part.literal.kind === import_typescript8.default.SyntaxKind.NullKeyword);
2812
- }
2813
2877
  var prefer_minimal_parameter_contract_default = createRule({
2814
2878
  name: "prefer-minimal-parameter-contract",
2815
2879
  documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
@@ -3961,7 +4025,7 @@ function isSearchParamReceiver(call, services) {
3961
4025
  // package.json
3962
4026
  var package_default = {
3963
4027
  name: "@sarj/eslint-plugin",
3964
- version: "16.36.3",
4028
+ version: "16.36.6",
3965
4029
  packageManager: "npm@12.2.1",
3966
4030
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
3967
4031
  type: "module",
package/dist/index.js CHANGED
@@ -1877,7 +1877,7 @@ var NO_TEST_COMMENTS_DOCUMENTATION = {
1877
1877
  autofix: "safe",
1878
1878
  limitations: [
1879
1879
  "Checks actual line and block comment tokens in recognized maintained test files, including test fixtures; strings and template text remain unchanged. Generated/vendor files are excluded.",
1880
- "Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed. Ordinary prose embedded in a directive remains the owning tool's responsibility.",
1880
+ "Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed. Executable detect-secrets 1.5.0 allowlist/whitelist pragmas retain its case-sensitive delimiter, spacing, physical-line and next-line grammar; arbitrary pragma prose remains ordinary commentary. Rationale on an executable directive remains the owning tool's responsibility.",
1881
1881
  "The automatic fix removes comments and adjacent padding while retaining every line ending and necessary token boundary; it does not rename tests or remove executable statements. Bare integration/e2e directories and ambiguous hyphen/underscore test basenames require another concrete test-path signal or a runtime testing-framework import; ordinary production integration modules and rule implementations ending in -test are preserved."
1882
1882
  ],
1883
1883
  examples: [
@@ -1888,6 +1888,52 @@ var NO_TEST_COMMENTS_DOCUMENTATION = {
1888
1888
  var DIRECTIVE = /^(?:eslint(?:-\S+)?\b|(?:biome|oxlint|jshint|jslint|c8|v8|istanbul|vitest|prettier|dprint)\b|@(?:ts-(?:check|nocheck|expect-error|ignore)|vitest-environment|jest-environment|jsx\w*|license|preserve)\b|[#!]\s*__(?:PURE|NO_SIDE_EFFECTS)__|\/\s*<(?:reference|amd-module|amd-dependency)\b)/u;
1889
1889
  var LEGAL = /(?:^|\n)\s*\*?\s*(?:Copyright\b|SPDX-License-Identifier:|Licensed under\b|@license\b|@preserve\b)/iu;
1890
1890
  var TYPE_CONTRACT = /@(?:type|typedef|template|param|returns?|satisfies|callback|overload|implements|extends|augments|enum|this|import)\b/u;
1891
+ var DETECT_SECRETS_COMMENT_MARKERS = [
1892
+ { start: "#", end: "" },
1893
+ { start: "//", end: "" },
1894
+ { start: "/\\*", end: " *\\*/" },
1895
+ { start: "'", end: "" },
1896
+ { start: "--", end: "" },
1897
+ { start: "<!--[# \\t]*?", end: " *?-->" }
1898
+ ];
1899
+ var DETECT_SECRETS_PRAGMAS = DETECT_SECRETS_COMMENT_MARKERS.flatMap(({ start, end }) => [
1900
+ { nextLine: false, pattern: new RegExp(`[ \\t]*${start} *pragma: ?(?:allow|white)list[ -]secret[^\\n]*?${end}[ \\t]*(?![\\s\\S])`, "u") },
1901
+ { nextLine: true, pattern: new RegExp(`^[ \\t]*${start} *pragma: ?allowlist[ -]nextline[ -]secret[^\\n]*?${end}[ \\t]*(?![\\s\\S])`, "u") }
1902
+ ]);
1903
+ var PYTHON_WHITESPACE = new RegExp("\\p{White_Space}", "u");
1904
+ var PYTHON_EXTRA_WHITESPACE = /* @__PURE__ */ new Set(["", "", "", ""]);
1905
+ function hasDetectSecretsPragma(comment, text) {
1906
+ if (!comment.value.includes("pragma:")) return false;
1907
+ let lineStart = Math.max(text.lastIndexOf("\r", comment.range[0] - 1), text.lastIndexOf("\n", comment.range[0] - 1)) + 1;
1908
+ while (lineStart < comment.range[1]) {
1909
+ const nextCr = text.indexOf("\r", lineStart);
1910
+ const nextLf = text.indexOf("\n", lineStart);
1911
+ const lineEnd = Math.min(nextCr < 0 ? text.length : nextCr, nextLf < 0 ? text.length : nextLf);
1912
+ if (hasPragmaInReaderLine(comment, text.slice(lineStart, lineEnd), lineStart)) return true;
1913
+ lineStart = lineEnd + (text[lineEnd] === "\r" && text[lineEnd + 1] === "\n" ? 2 : 1);
1914
+ }
1915
+ return false;
1916
+ }
1917
+ function hasPragmaInReaderLine(comment, rawLine, lineStart) {
1918
+ const sameLine = stripPythonTrailingWhitespace(rawLine);
1919
+ for (const { nextLine, pattern } of DETECT_SECRETS_PRAGMAS) {
1920
+ const column = nextLine ? 0 : Math.max(0, comment.range[0] - lineStart);
1921
+ const match = pattern.exec((nextLine ? rawLine : sameLine).slice(column));
1922
+ if (match === null) continue;
1923
+ const pragmaStart = lineStart + column + match.index + match[0].indexOf("pragma:");
1924
+ if (pragmaStart >= comment.range[0] && pragmaStart < comment.range[1]) return true;
1925
+ }
1926
+ return false;
1927
+ }
1928
+ function stripPythonTrailingWhitespace(value) {
1929
+ let end = value.length;
1930
+ while (end > 0) {
1931
+ const character = value.charAt(end - 1);
1932
+ if (!PYTHON_WHITESPACE.test(character) && !PYTHON_EXTRA_WHITESPACE.has(character)) break;
1933
+ end--;
1934
+ }
1935
+ return value.slice(0, end);
1936
+ }
1891
1937
  var no_test_comments_default = createRule({
1892
1938
  name: "no-test-comments",
1893
1939
  documentation: NO_TEST_COMMENTS_DOCUMENTATION,
@@ -1924,7 +1970,7 @@ var no_test_comments_default = createRule({
1924
1970
  }
1925
1971
  for (const [index, comment] of comments.entries()) {
1926
1972
  const body3 = comment.value.trim().replace(/^\*\s*/u, "");
1927
- if (DIRECTIVE.test(body3) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value)) continue;
1973
+ if (DIRECTIVE.test(body3) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value) || hasDetectSecretsPragma(comment, context.sourceCode.text)) continue;
1928
1974
  context.report({
1929
1975
  node: comment,
1930
1976
  messageId: "testComment",
@@ -2217,8 +2263,13 @@ function commentSuppressesChecking(text, kind) {
2217
2263
 
2218
2264
  // src/rules/_closed-object-contract.ts
2219
2265
  import ts2 from "typescript";
2220
- function closedObjectContract(type, checker) {
2221
- return visitContract(type, { checker, active: /* @__PURE__ */ new Set(), completed: /* @__PURE__ */ new WeakMap() });
2266
+ function closedObjectContract(type, checker, singlePropertyOrigins = false) {
2267
+ return visitContract(type, {
2268
+ checker,
2269
+ active: /* @__PURE__ */ new Set(),
2270
+ completed: /* @__PURE__ */ new WeakMap(),
2271
+ origins: singlePropertyOrigins ? /* @__PURE__ */ new Map() : void 0
2272
+ });
2222
2273
  }
2223
2274
  function visitContract(type, proof) {
2224
2275
  const known2 = proof.completed.get(type);
@@ -2232,10 +2283,20 @@ function visitContract(type, proof) {
2232
2283
  }
2233
2284
  function completeObject(type, proof) {
2234
2285
  if (!ordinaryObject(type, proof.checker)) return false;
2286
+ if (proof.origins && !singleOrigins(type, proof.origins)) return false;
2235
2287
  const declarations = type.getSymbol()?.getDeclarations()?.filter((declaration) => ts2.isInterfaceDeclaration(declaration) || ts2.isTypeLiteralNode(declaration));
2236
2288
  if (!declarations?.length) return false;
2237
2289
  return declarations.every((declaration) => completeDeclaration(declaration, proof));
2238
2290
  }
2291
+ function singleOrigins(type, origins) {
2292
+ for (const property of type.getProperties()) {
2293
+ const declarations = property.getDeclarations();
2294
+ const declaration = declarations?.[0];
2295
+ if (!declaration || declarations?.length !== 1 || origins.has(property.name) && origins.get(property.name) !== declaration) return false;
2296
+ origins.set(property.name, declaration);
2297
+ }
2298
+ return true;
2299
+ }
2239
2300
  function ordinaryObject(type, checker) {
2240
2301
  if ((type.flags & ts2.TypeFlags.Object) === 0 || type.isUnion() || checker.getIndexInfosOfType(type).length > 0) return false;
2241
2302
  const object = type;
@@ -2554,6 +2615,9 @@ function directEval(node) {
2554
2615
  function primitiveKeyword(node) {
2555
2616
  return node !== void 0 && (node.kind === ts6.SyntaxKind.StringKeyword || node.kind === ts6.SyntaxKind.NumberKeyword || node.kind === ts6.SyntaxKind.BooleanKeyword || node.kind === ts6.SyntaxKind.BigIntKeyword);
2556
2617
  }
2618
+ function nullishPrimitiveUnion(node) {
2619
+ return node !== void 0 && ts6.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === ts6.SyntaxKind.UndefinedKeyword || ts6.isLiteralTypeNode(part) && part.literal.kind === ts6.SyntaxKind.NullKeyword);
2620
+ }
2557
2621
  function consumerAnnotation(node) {
2558
2622
  if (node.params.length !== 1) return void 0;
2559
2623
  const parameter = node.params[0];
@@ -2611,6 +2675,7 @@ function directCalls(binding3) {
2611
2675
  // src/rules/no-redundant-consumer-union.ts
2612
2676
  var CONFIG = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2613
2677
  var EXAMPLE = 'export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: "Ada", breed: "Collie"}; function label(value: Dog | Animal): string {return value.name;} label(dog);';
2678
+ var NULLABLE_EXAMPLE = EXAMPLE.replace("readonly name: string", "readonly name: string | null").replace("return value.name;", "return value.name ?? '';");
2614
2679
  var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2615
2680
  defaultLevel: "warning",
2616
2681
  summary: "Keep the covering object contract in a private scalar consumer union.",
@@ -2621,21 +2686,23 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2621
2686
  limitations: [
2622
2687
  "Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.",
2623
2688
  "Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single pure returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.",
2624
- "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, callbacks and recursive or nested object fields are unsupported.",
2689
+ "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.",
2625
2690
  "Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
2626
2691
  "General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, helpers, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."
2627
2692
  ],
2628
2693
  examples: [
2629
2694
  { id: "covered-alternative", title: "The private consumer needs the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE.replace("Dog | Animal", "Animal") }] },
2630
- { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2695
+ { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2696
+ { id: "nullable-covered-alternative", scenarioId: "nullable-readonly-contract", title: "A nullable name retains the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2697
+ { id: "nullable-domain-contract", scenarioId: "nullable-readonly-contract", title: "A literal breed keeps the authored domain contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("readonly breed: string", "readonly breed: 'Collie'") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2631
2698
  ]
2632
2699
  };
2633
2700
  function scalarFields(type, checker) {
2634
- if (!closedObjectContract(type, checker)) return null;
2701
+ if (!closedObjectContract(type, checker, true)) return null;
2635
2702
  const fields2 = /* @__PURE__ */ new Map();
2636
2703
  for (const property of type.getProperties()) {
2637
2704
  const declarations = property.getDeclarations();
2638
- if (!declarations?.length || !declarations.every((member3) => ts7.isPropertySignature(member3) && member3.name && (ts7.isIdentifier(member3.name) || ts7.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === ts7.SyntaxKind.ReadonlyKeyword && primitiveKeyword(member3.type))) return null;
2705
+ if (!declarations?.length || !declarations.every((member3) => ts7.isPropertySignature(member3) && member3.name && (ts7.isIdentifier(member3.name) || ts7.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === ts7.SyntaxKind.ReadonlyKeyword && (primitiveKeyword(member3.type) || nullishPrimitiveUnion(member3.type)))) return null;
2639
2706
  fields2.set(property.name, (property.flags & ts7.SymbolFlags.Optional) === 0);
2640
2707
  }
2641
2708
  return fields2.size ? fields2 : null;
@@ -2722,7 +2789,7 @@ import ts8 from "typescript";
2722
2789
  var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2723
2790
  var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
2724
2791
  var HELPER_EXAMPLE = 'export {}; const account = {name: "Ada", email: "a"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: {email: string; name: string}): string {return formatName(user.name);} label(account);';
2725
- var NULLABLE_EXAMPLE = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
2792
+ var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
2726
2793
  var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
2727
2794
  defaultLevel: "warning",
2728
2795
  summary: "Prefer the proven read-only subset for a private consumer parameter.",
@@ -2741,8 +2808,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
2741
2808
  { id: "used-fields", title: "Every required dependency is read", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE2.replace("return user.name;", "return user.name + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
2742
2809
  { id: "scalar-helper", scenarioId: "local-scalar-helper", title: "Only the name crosses a local scalar helper call", outcome: "match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("email: string;", "") }] },
2743
2810
  { id: "helper-and-other-field", scenarioId: "local-scalar-helper", title: "The consumer still requires a separately read field", outcome: "no-match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("return formatName(user.name);", "return formatName(user.name) + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
2744
- { id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("email: string | null;", "") }] },
2745
- { id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
2811
+ { id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2 }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("email: string | null;", "") }] },
2812
+ { id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
2746
2813
  ]
2747
2814
  };
2748
2815
  function inlineFields(node, reads) {
@@ -2756,9 +2823,6 @@ function inlineFields(node, reads) {
2756
2823
  }
2757
2824
  return fields2;
2758
2825
  }
2759
- function nullishPrimitiveUnion(node) {
2760
- return node !== void 0 && ts8.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === ts8.SyntaxKind.UndefinedKeyword || ts8.isLiteralTypeNode(part) && part.literal.kind === ts8.SyntaxKind.NullKeyword);
2761
- }
2762
2826
  var prefer_minimal_parameter_contract_default = createRule({
2763
2827
  name: "prefer-minimal-parameter-contract",
2764
2828
  documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
@@ -3914,7 +3978,7 @@ function isSearchParamReceiver(call, services) {
3914
3978
  // package.json
3915
3979
  var package_default = {
3916
3980
  name: "@sarj/eslint-plugin",
3917
- version: "16.36.3",
3981
+ version: "16.36.6",
3918
3982
  packageManager: "npm@12.2.1",
3919
3983
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
3920
3984
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.36.3",
3
+ "version": "16.36.6",
4
4
  "packageManager": "npm@12.2.1",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",