@sarj/eslint-plugin 16.4.5 → 16.4.7

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
@@ -262,6 +262,7 @@ function deepFreeze(value) {
262
262
  }
263
263
 
264
264
  // src/rules/_paths.ts
265
+ var LINE_TERMINATOR_RE = /[\n\r\u2028\u2029]/u;
265
266
  var SCRIPT_FILE_RE = /(?:^|[\\/])scripts[\\/]|\.mjs$/;
266
267
  var STORY_FILE_RE = /\.stories\.[cm]?[jt]sx?$/i;
267
268
  var STORY_TREE_RE = /(^|\/)stories(?:[_-][^/]*)?\//i;
@@ -311,8 +312,8 @@ function leadingCommentBodies(sourceText) {
311
312
  const bodies = [];
312
313
  let index = 0;
313
314
  if (sourceText.startsWith("#!")) {
314
- const shebangEnd = sourceText.indexOf("\n");
315
- index = shebangEnd === -1 ? sourceText.length : shebangEnd + 1;
315
+ const shebangEnd = commentLineEnd(sourceText, 2);
316
+ index = shebangEnd === sourceText.length ? sourceText.length : shebangEnd + 1;
316
317
  }
317
318
  while (index < sourceText.length) {
318
319
  while (/\s/u.test(sourceText[index] ?? "")) index++;
@@ -326,7 +327,7 @@ function leadingCommentBodies(sourceText) {
326
327
  const end = sourceText.indexOf("*/", index + 2);
327
328
  if (end === -1) break;
328
329
  bodies.push(
329
- ...sourceText.slice(index + 2, end).split("\n").map((line) => line.replace(/^\s*\*?\s?/u, "").trim()).filter((line) => line.length > 0)
330
+ ...sourceText.slice(index + 2, end).split(LINE_TERMINATOR_RE).map((line) => line.replace(/^\s*\*?\s?/u, "").trim()).filter((line) => line.length > 0)
330
331
  );
331
332
  index = end + 2;
332
333
  continue;
@@ -345,8 +346,8 @@ function isScriptFile(filename) {
345
346
  return SCRIPT_FILE_RE.test(filename);
346
347
  }
347
348
  function commentLineEnd(sourceText, start) {
348
- const end = sourceText.indexOf("\n", start);
349
- return end === -1 ? sourceText.length : end;
349
+ const end = sourceText.slice(start).search(LINE_TERMINATOR_RE);
350
+ return end === -1 ? sourceText.length : start + end;
350
351
  }
351
352
 
352
353
  // src/rules/require-explicit-contract-implementation.ts
@@ -5473,7 +5474,9 @@ var NO_ENUM_DOCUMENTATION = {
5473
5474
  focusPath: "src/status.ts",
5474
5475
  expectedCount: 1,
5475
5476
  public: true
5476
- }
5477
+ },
5478
+ { id: "authored-after-comment", scenarioId: "native-header-boundary", title: "Keep executable authored code in scope", outcome: "match", files: [{ path: "src/status.ts", source: '// ordinary header\u2028const marker="auto-generated file"; enum Mode {Start} console.log(Mode.Start, marker);' }], focusPath: "src/status.ts", expectedCount: 1, public: true },
5479
+ { id: "generated-header", scenarioId: "native-header-boundary", title: "Respect a native generated banner", outcome: "no-match", files: [{ path: "src/status.ts", source: '// auto-generated file\u2028const marker="auto-generated file"; enum Mode {Start} console.log(Mode.Start, marker);' }], focusPath: "src/status.ts", expectedCount: 0, public: true }
5477
5480
  ]
5478
5481
  };
5479
5482
  function matchesAnyPattern(filename, patterns) {
@@ -6833,7 +6836,9 @@ var NO_JSON_STRINGIFY_ERROR_DOCUMENTATION = {
6833
6836
  limitations: ["The rule uses stable local catch bindings and unshadowed built-in constructors, not runtime type information. Custom replacers are left to their serializer contract; a catch value is not guaranteed to be an Error."],
6834
6837
  examples: [
6835
6838
  { id: "explicit-error-message", title: "Narrow an unknown catch value before selecting fields", outcome: "no-match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err instanceof Error ? err.message : String(err) }); }" }], focusPath: "src/report.ts", expectedCount: 0, public: true },
6836
- { id: "stringified-error", title: "Do not stringify an Error object", outcome: "match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err }); }" }], focusPath: "src/report.ts", expectedCount: 1, public: true }
6839
+ { id: "stringified-error", title: "Do not stringify an Error object", outcome: "match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err }); }" }], focusPath: "src/report.ts", expectedCount: 1, public: true },
6840
+ { id: "shadowed-error-binding", scenarioId: "binding-provenance", title: "Keep native binding provenance", outcome: "match", files: [{ path: "src/error.ts", source: 'function probe(){try{throw {id:1};}catch(err){if(err instanceof Error)return err.message;{const err=new Error("lost");return JSON.stringify(err);}}}\nconsole.log(probe());' }], focusPath: "src/error.ts", expectedCount: 1, public: true },
6841
+ { id: "escaped-fallback-binding", scenarioId: "binding-provenance", title: "Keep native binding provenance", outcome: "no-match", files: [{ path: "src/error.ts", source: "function probe(){try{throw {id:1};}catch(err){if(err instanceof Error)return err.message;return JSON.stringify(\\u0065rr);}}\nconsole.log(probe());" }], focusPath: "src/error.ts", expectedCount: 0, public: true }
6837
6842
  ]
6838
6843
  };
6839
6844
  var SAFE_STRING_PROPS = /* @__PURE__ */ new Set(["message", "stack", "name"]);
@@ -6863,25 +6868,25 @@ var BUILTIN_ERROR_CONSTRUCTORS = /* @__PURE__ */ new Set([
6863
6868
  "TypeError",
6864
6869
  "URIError"
6865
6870
  ]);
6866
- function identifierIsProvenError(identifier, scope) {
6867
- const variable = import_utils41.ASTUtils.findVariable(scope, identifier.name);
6871
+ function identifierIsProvenError(identifier, sourceCode) {
6872
+ const variable = import_utils41.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
6868
6873
  if (variable === null || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
6869
6874
  const definition = variable.defs[0];
6870
6875
  if (definition?.type === "CatchClause") return true;
6871
6876
  if (definition?.type !== "Variable") return false;
6872
6877
  const initializer = definition.node.init;
6873
- return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && isGlobalIdentifier(initializer.callee.name, scope);
6878
+ return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && isGlobalIdentifier(initializer.callee.name, sourceCode.getScope(initializer.callee));
6874
6879
  }
6875
6880
  function isGlobalIdentifier(name, scope) {
6876
6881
  const binding = import_utils41.ASTUtils.findVariable(scope, name);
6877
6882
  return binding === null || binding.defs.length === 0;
6878
6883
  }
6879
- function positiveErrorSubject(test) {
6880
- return instanceofErrorSubject(test) ?? typeGuardSubject(test);
6884
+ function positiveErrorSubject(test, sourceCode) {
6885
+ return instanceofErrorSubject(test, sourceCode) ?? typeGuardSubject(test);
6881
6886
  }
6882
6887
  var TYPE_GUARD_PATTERN = /^(is|has)[A-Z]/;
6883
- function instanceofErrorSubject(test) {
6884
- if (test.type === "BinaryExpression" && test.operator === "instanceof" && test.right.type === "Identifier" && test.right.name === "Error") {
6888
+ function instanceofErrorSubject(test, sourceCode) {
6889
+ if (test.type === "BinaryExpression" && test.operator === "instanceof" && test.right.type === "Identifier" && test.right.name === "Error" && isGlobalIdentifier("Error", sourceCode.getScope(test.right))) {
6885
6890
  return test.left;
6886
6891
  }
6887
6892
  return null;
@@ -6895,36 +6900,34 @@ function typeGuardSubject(test) {
6895
6900
  return null;
6896
6901
  }
6897
6902
  function isNarrowedByEarlyReturn(node, argExpr, sourceCode) {
6898
- const argText = sourceCode.getText(argExpr);
6899
6903
  let current = node.parent;
6900
6904
  while (current) {
6901
6905
  if (current.type === "BlockStatement" || current.type === "Program") {
6902
- if (hasEarlierErrorGuard(current.body, node, argText, sourceCode)) return true;
6906
+ if (hasEarlierErrorGuard(current.body, node, argExpr, sourceCode)) return true;
6903
6907
  }
6904
6908
  current = current.parent;
6905
6909
  }
6906
6910
  return false;
6907
6911
  }
6908
6912
  function isGuardedByInstanceofError(node, argExpr, sourceCode) {
6909
- const argText = sourceCode.getText(argExpr);
6910
- const sameSubject = (subject) => sourceCode.getText(subject) === argText;
6913
+ const sameSubject = (subject) => sameSubjectBinding(subject, argExpr, sourceCode);
6911
6914
  let current = node.parent;
6912
6915
  while (current) {
6913
6916
  if (current.type === "ConditionalExpression") {
6914
- const subject = positiveErrorSubject(current.test);
6917
+ const subject = positiveErrorSubject(current.test, sourceCode);
6915
6918
  if (subject && sameSubject(subject) && nodeWithin(node, current.alternate)) {
6916
6919
  return true;
6917
6920
  }
6918
- const negated = negatedInstanceofErrorSubject(current.test);
6921
+ const negated = negatedInstanceofErrorSubject(current.test, sourceCode);
6919
6922
  if (negated && sameSubject(negated) && nodeWithin(node, current.consequent)) {
6920
6923
  return true;
6921
6924
  }
6922
6925
  } else if (current.type === "IfStatement") {
6923
- const subject = positiveErrorSubject(current.test);
6926
+ const subject = positiveErrorSubject(current.test, sourceCode);
6924
6927
  if (subject && sameSubject(subject) && nodeWithin(node, current.alternate)) {
6925
6928
  return true;
6926
6929
  }
6927
- const negated = negatedInstanceofErrorSubject(current.test);
6930
+ const negated = negatedInstanceofErrorSubject(current.test, sourceCode);
6928
6931
  if (negated && sameSubject(negated) && nodeWithin(node, current.consequent)) {
6929
6932
  return true;
6930
6933
  }
@@ -6933,12 +6936,21 @@ function isGuardedByInstanceofError(node, argExpr, sourceCode) {
6933
6936
  }
6934
6937
  return false;
6935
6938
  }
6936
- function negatedInstanceofErrorSubject(test) {
6939
+ function negatedInstanceofErrorSubject(test, sourceCode) {
6937
6940
  if (test.type === "UnaryExpression" && test.operator === "!") {
6938
- return positiveErrorSubject(test.argument);
6941
+ return positiveErrorSubject(test.argument, sourceCode);
6939
6942
  }
6940
6943
  return null;
6941
6944
  }
6945
+ function sameSubjectBinding(subject, value, sourceCode) {
6946
+ const left = unwrapExpression(subject);
6947
+ const right = unwrapExpression(value);
6948
+ if (left.type === "Identifier" && right.type === "Identifier") {
6949
+ const binding = import_utils41.ASTUtils.findVariable(sourceCode.getScope(left), left.name);
6950
+ return binding !== null && binding === import_utils41.ASTUtils.findVariable(sourceCode.getScope(right), right.name);
6951
+ }
6952
+ return sourceCode.getText(subject) === sourceCode.getText(value);
6953
+ }
6942
6954
  function nodeWithin(node, container) {
6943
6955
  return container !== null && node.range[0] >= container.range[0] && node.range[1] <= container.range[1];
6944
6956
  }
@@ -6966,21 +6978,21 @@ function directLiteralValues(argument) {
6966
6978
  }
6967
6979
  return argument.type === "SpreadElement" ? [] : [argument];
6968
6980
  }
6969
- function expressionSuggestsError(expression, scope) {
6981
+ function expressionSuggestsError(expression, sourceCode) {
6970
6982
  expression = unwrapExpression(expression);
6971
6983
  const unwrappedExpressionCallee = expression.type === "CallExpression" || expression.type === "NewExpression" ? unwrapExpression(expression.callee) : null;
6972
6984
  if (expression.type === "Identifier") {
6973
- return identifierIsProvenError(expression, scope);
6985
+ return identifierIsProvenError(expression, sourceCode);
6974
6986
  }
6975
6987
  if (expression.type === "NewExpression" && unwrappedExpressionCallee?.type === "Identifier") {
6976
- return BUILTIN_ERROR_CONSTRUCTORS.has(unwrappedExpressionCallee.name) && isGlobalIdentifier(unwrappedExpressionCallee.name, scope);
6988
+ return BUILTIN_ERROR_CONSTRUCTORS.has(unwrappedExpressionCallee.name) && isGlobalIdentifier(unwrappedExpressionCallee.name, sourceCode.getScope(unwrappedExpressionCallee));
6977
6989
  }
6978
- return expression.type === "MemberExpression" && memberSuggestsError(expression, scope);
6990
+ return expression.type === "MemberExpression" && memberSuggestsError(expression, sourceCode);
6979
6991
  }
6980
- function memberSuggestsError(member, scope) {
6992
+ function memberSuggestsError(member, sourceCode) {
6981
6993
  const propName2 = import_utils41.ASTUtils.getPropertyName(member);
6982
6994
  const base = member.object;
6983
- const baseSuggestsError = base.type === "Identifier" && identifierIsProvenError(base, scope);
6995
+ const baseSuggestsError = base.type === "Identifier" && identifierIsProvenError(base, sourceCode);
6984
6996
  if (baseSuggestsError) {
6985
6997
  if (propName2 === null) {
6986
6998
  return true;
@@ -7020,7 +7032,7 @@ var no_json_stringify_error_default = createRule({
7020
7032
  const replacer = node.arguments[1] === void 0 ? void 0 : unwrapExpression(node.arguments[1]);
7021
7033
  if (replacer !== void 0 && !(replacer.type === "Literal" && replacer.value === null) && !(replacer.type === "Identifier" && replacer.name === "undefined" && isGlobalIdentifier("undefined", scope))) return;
7022
7034
  const unsafeValue = directLiteralValues(firstArg).find(
7023
- (value) => expressionSuggestsError(value, scope) && !isGuardedByInstanceofError(node, value, context.sourceCode) && !isNarrowedByEarlyReturn(node, value, context.sourceCode)
7035
+ (value) => expressionSuggestsError(value, context.sourceCode) && !isGuardedByInstanceofError(node, value, context.sourceCode) && !isNarrowedByEarlyReturn(node, value, context.sourceCode)
7024
7036
  );
7025
7037
  if (unsafeValue === void 0) {
7026
7038
  return;
@@ -7033,14 +7045,14 @@ var no_json_stringify_error_default = createRule({
7033
7045
  };
7034
7046
  }
7035
7047
  });
7036
- function hasEarlierErrorGuard(statements, node, argText, sourceCode) {
7048
+ function hasEarlierErrorGuard(statements, node, argExpr, sourceCode) {
7037
7049
  for (const stmt of statements) {
7038
7050
  if (stmt.range[0] >= node.range[0]) {
7039
7051
  break;
7040
7052
  }
7041
7053
  if (stmt.type === "IfStatement" && stmt.alternate === null && branchTerminates(stmt.consequent)) {
7042
- const subject = positiveErrorSubject(stmt.test);
7043
- if (subject && sourceCode.getText(subject) === argText) {
7054
+ const subject = positiveErrorSubject(stmt.test, sourceCode);
7055
+ if (subject && sameSubjectBinding(subject, argExpr, sourceCode)) {
7044
7056
  return true;
7045
7057
  }
7046
7058
  }
@@ -7196,7 +7208,7 @@ var import_utils43 = require("@typescript-eslint/utils");
7196
7208
  // src/rules/_zod.ts
7197
7209
  var ZOD_SCHEMA_NAME_RE = /Schema$|^Z[A-Z]/;
7198
7210
  function isZodModule(source) {
7199
- return /(^|[/@-])zod([/-]|$)/.test(source);
7211
+ return source === "zod" || source.startsWith("zod/");
7200
7212
  }
7201
7213
 
7202
7214
  // src/rules/no-impossible-zod-literal-bounds.ts
@@ -7583,6 +7595,7 @@ var NO_REDUNDANT_OPTIONAL_ARRAY_DEFAULT_DOCUMENTATION = {
7583
7595
  autofix: "safe",
7584
7596
  limitations: [
7585
7597
  "Only syntactically local Zod array chains with adjacent `.optional().default(...)` calls are checked.",
7598
+ "Imports must name the official zod package or its subpaths; relative modules and unproven npm aliases are excluded.",
7586
7599
  "String, scalar, tuple, set, record, aliased, composed, and dynamically constructed schemas are excluded.",
7587
7600
  "The reversed `.default(...).optional()` order is preserved because its outer optional can return undefined instead of the default.",
7588
7601
  "Defaults must be literal arrays or synchronous functions returning one literal array; dynamic or undefined fallbacks are excluded because Zod 3 reparses the fallback.",
@@ -7623,6 +7636,30 @@ var NO_REDUNDANT_OPTIONAL_ARRAY_DEFAULT_DOCUMENTATION = {
7623
7636
  focusPath: "src/schema.ts",
7624
7637
  expectedCount: 1,
7625
7638
  public: true
7639
+ },
7640
+ {
7641
+ id: "unrelated-relative-builder",
7642
+ title: "Preserve an unrelated builder in a directory named zod",
7643
+ outcome: "no-match",
7644
+ files: [
7645
+ { path: "src/my-zod/index.ts", source: 'export const z = { string: () => "string", array: (_element: string) => ({ optional: () => ({ default: (value: readonly string[]) => ({ value, optional: true }) }), default: (value: readonly string[]) => ({ value, optional: false }) }) };' },
7646
+ { path: "src/schema.ts", source: 'import { z } from "./my-zod/index.js"; export const Items = z.array(z.string()).optional().default([]);' }
7647
+ ],
7648
+ focusPath: "src/schema.ts",
7649
+ expectedCount: 0,
7650
+ public: true,
7651
+ scenarioId: "module-identity"
7652
+ },
7653
+ {
7654
+ id: "official-zod-subpath-array",
7655
+ title: "Official Zod subpath constructors retain the safe fix",
7656
+ outcome: "match",
7657
+ files: [{ path: "src/schema.ts", source: 'import { array, string } from "zod/v4"; const Items = array(string()).optional().default([]);' }],
7658
+ fixedFiles: [{ path: "src/schema.ts", source: 'import { array, string } from "zod/v4"; const Items = array(string()).default([]);' }],
7659
+ focusPath: "src/schema.ts",
7660
+ expectedCount: 1,
7661
+ public: true,
7662
+ scenarioId: "module-identity"
7626
7663
  }
7627
7664
  ]
7628
7665
  };
@@ -8815,11 +8852,12 @@ var NO_OFFSET_PAGINATION_DOCUMENTATION = {
8815
8852
  limitations: ["A SELECT/FROM query shape or adjacent LIMIT/OFFSET fragment is required. Isolated OFFSET fragments and test files are excluded; this lexical context does not prove a database execution sink. Performance and concurrent-write behavior depend on indexes, ordering, isolation, and dialect; bounded pages and random page access can justify OFFSET."],
8816
8853
  examples: [
8817
8854
  { id: "keyset-pagination", title: "Page from a stable cursor", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id FROM runs WHERE id > ? ORDER BY id LIMIT ?`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
8818
- { id: "offset-pagination", title: "Do not page by offset", outcome: "match", files: [{ path: "src/runs.ts", source: "db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
8855
+ { id: "offset-pagination", title: "Do not page by offset", outcome: "match", files: [{ path: "src/runs.ts", source: "db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);" }], focusPath: "src/runs.ts", expectedCount: 1, public: true },
8856
+ { id: "decoded-query", scenarioId: "native-decoded-sql", title: "Inspect native-decoded query text", outcome: "match", files: [{ path: "src/page.ts", source: 'db.query("SELECT id FROM runs ORDER BY id LIMIT 1 OFF\\u0053ET 1");' }], focusPath: "src/page.ts", expectedCount: 1, public: true },
8857
+ { id: "decoded-comment-data", scenarioId: "native-decoded-sql", title: "Ignore native-decoded comment data", outcome: "no-match", files: [{ path: "src/page.ts", source: 'db.query("SELECT id FROM runs /* OFF\\x53ET 1 */");' }], focusPath: "src/page.ts", expectedCount: 0, public: true }
8819
8858
  ]
8820
8859
  };
8821
8860
  var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
8822
- var OFFSET_GATE = /offset/i;
8823
8861
  var PAGINATION_CONTEXT = /\bSELECT\b[\s\S]*\bFROM\b[\s\S]*\bOFFSET\b|\bLIMIT\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)\s+OFFSET\b/i;
8824
8862
  var no_offset_pagination_default = createRule({
8825
8863
  name: "no-offset-pagination",
@@ -8836,7 +8874,7 @@ var no_offset_pagination_default = createRule({
8836
8874
  },
8837
8875
  defaultOptions: [],
8838
8876
  create(context) {
8839
- if (isTestFile(context.filename) || !OFFSET_GATE.test(context.sourceCode.text)) {
8877
+ if (isTestFile(context.filename)) {
8840
8878
  return {};
8841
8879
  }
8842
8880
  return createSqlListener((sql, node) => {
@@ -10953,7 +10991,9 @@ var NO_SELECT_STAR_DOCUMENTATION = {
10953
10991
  limitations: ["Only statically visible embedded SQL is checked; quoted strings (including PostgreSQL dollar strings), comments, function arguments such as COUNT(*), and stars inside EXISTS are excluded. This is a bounded lexical scan, not a complete SQL parser."],
10954
10992
  examples: [
10955
10993
  { id: "explicit-projection", title: "Select the required columns", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id, status FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
10956
- { id: "wildcard-projection", title: "Do not select every column", outcome: "match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT * FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
10994
+ { id: "wildcard-projection", title: "Do not select every column", outcome: "match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT * FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 1, public: true },
10995
+ { id: "decoded-query", scenarioId: "native-decoded-sql", title: "Inspect native-decoded query text", outcome: "match", files: [{ path: "src/page.ts", source: 'db.query("SEL\\u0045CT * FROM runs");' }], focusPath: "src/page.ts", expectedCount: 1, public: true },
10996
+ { id: "decoded-comment-data", scenarioId: "native-decoded-sql", title: "Ignore native-decoded comment data", outcome: "no-match", files: [{ path: "src/page.ts", source: 'db.query("SEL\\x45CT id FROM runs /* SELECT * FROM runs */");' }], focusPath: "src/page.ts", expectedCount: 0, public: true }
10957
10997
  ]
10958
10998
  };
10959
10999
  var QUERY_SHAPE = /\bSELECT\b[\s\S]*?\bFROM\b/i;
@@ -10961,7 +11001,6 @@ var SELECT_KEYWORD = /\bSELECT\b/gi;
10961
11001
  var FROM_KEYWORD = /^FROM\b/i;
10962
11002
  var EXISTS_BEFORE = /\bEXISTS\s*\(\s*$/i;
10963
11003
  var QUALIFIED_PREFIX = /\w\.$/;
10964
- var SELECT_GATE = /select/i;
10965
11004
  function hasRealSelectStar(sql) {
10966
11005
  const selects = [...sql.matchAll(SELECT_KEYWORD)].map((m) => m.index);
10967
11006
  for (let pos = 0; pos < sql.length; pos++) {
@@ -11010,7 +11049,7 @@ var no_select_star_default = createRule({
11010
11049
  },
11011
11050
  defaultOptions: [],
11012
11051
  create(context) {
11013
- if (isTestFile(context.filename) || !SELECT_GATE.test(context.sourceCode.text)) {
11052
+ if (isTestFile(context.filename)) {
11014
11053
  return {};
11015
11054
  }
11016
11055
  return createSqlListener((sql, node) => {
@@ -18934,16 +18973,20 @@ var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
18934
18973
  ],
18935
18974
  examples: [
18936
18975
  { id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "switch (kind) { case 'a': return a(); case 'b': return b(); case 'c': return c(); default: return fallback(); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
18937
- { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "if (kind === 'a') return a(); else if (kind === 'b') return b(); else if (kind === 'c') return c();" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
18976
+ { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "if (kind === 'a') return a(); else if (kind === 'b') return b(); else if (kind === 'c') return c();" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
18977
+ { id: "escaped-middle", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){if(kind==='a')return 1;else if(\\u006bind==='b')return 2;else if(kind==='c')return 3;return 0;}
18978
+ console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 1, public: true },
18979
+ { id: "different-discriminants", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "no-match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){const other="other";if(kind==='a')return 1;else if(other==='b')return 2;else if(kind==='c')return 3;return 0;}
18980
+ console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 0, public: true }
18938
18981
  ]
18939
18982
  };
18940
- function discriminantText(sourceCode, test) {
18983
+ function discriminantName(test) {
18941
18984
  if (test.type !== import_utils110.AST_NODE_TYPES.BinaryExpression || test.operator !== "===") return null;
18942
18985
  const leftIsCase = isCaseValue(test.left);
18943
18986
  const rightIsCase = isCaseValue(test.right);
18944
18987
  if (leftIsCase === rightIsCase) return null;
18945
18988
  const discriminant = leftIsCase ? test.right : test.left;
18946
- return discriminant.type === import_utils110.AST_NODE_TYPES.Identifier ? sourceCode.getText(discriminant) : null;
18989
+ return discriminant.type === import_utils110.AST_NODE_TYPES.Identifier ? discriminant.name : null;
18947
18990
  }
18948
18991
  function isCaseValue(node) {
18949
18992
  if (node.type === import_utils110.AST_NODE_TYPES.Literal) return true;
@@ -18967,12 +19010,12 @@ var prefer_switch_for_repeated_equality_default = createRule({
18967
19010
  return {
18968
19011
  IfStatement(node) {
18969
19012
  if (node.parent.type === import_utils110.AST_NODE_TYPES.IfStatement && node.parent.alternate === node) return;
18970
- const first = discriminantText(context.sourceCode, node.test);
19013
+ const first = discriminantName(node.test);
18971
19014
  if (first === null) return;
18972
19015
  let count = 1;
18973
19016
  let current = node.alternate;
18974
19017
  while (current?.type === import_utils110.AST_NODE_TYPES.IfStatement) {
18975
- if (discriminantText(context.sourceCode, current.test) !== first) return;
19018
+ if (discriminantName(current.test) !== first) return;
18976
19019
  count += 1;
18977
19020
  current = current.alternate;
18978
19021
  }
@@ -22700,7 +22743,9 @@ var STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION = {
22700
22743
  limitations: ["Only the nearest statically named callable is considered; top-level inserts and anonymous callbacks are excluded. Recognized conflict syntax does not prove idempotence or concurrency safety: WHERE NOT EXISTS can race, and INSERT OR REPLACE can delete an existing row. Review unique constraints and dialect semantics manually."],
22701
22744
  examples: [
22702
22745
  { id: "conflict-safe-insert", title: "Review the conflict policy for a replayed insert", outcome: "no-match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(); }" }], focusPath: "src/store.ts", expectedCount: 0, public: true },
22703
- { id: "bare-insert", title: "Review a bare insert in a replay-named callable", outcome: "match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?)`).run(); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
22746
+ { id: "bare-insert", title: "Review a bare insert in a replay-named callable", outcome: "match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?)`).run(); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true },
22747
+ { id: "decoded-query", scenarioId: "native-decoded-sql", title: "Inspect native-decoded query text", outcome: "match", files: [{ path: "src/page.ts", source: `function seed(){db.query("INS\\u0045RT INTO runs(id,note) VALUES(4,'new')");}` }], focusPath: "src/page.ts", expectedCount: 1, public: true },
22748
+ { id: "decoded-comment-data", scenarioId: "native-decoded-sql", title: "Ignore native-decoded comment data", outcome: "no-match", files: [{ path: "src/page.ts", source: `function seed(){db.query("SELECT id FROM runs /* INS\\x45RT INTO runs(id,note) VALUES(4,'new') */");}` }], focusPath: "src/page.ts", expectedCount: 0, public: true }
22704
22749
  ]
22705
22750
  };
22706
22751
  var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
@@ -22720,7 +22765,6 @@ function owningCallableName(node) {
22720
22765
  }
22721
22766
  return null;
22722
22767
  }
22723
- var INSERT_GATE = /insert/i;
22724
22768
  var store_insert_requires_on_conflict_default = createRule({
22725
22769
  name: "store-insert-requires-on-conflict",
22726
22770
  documentation: STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION,
@@ -22736,7 +22780,7 @@ var store_insert_requires_on_conflict_default = createRule({
22736
22780
  },
22737
22781
  defaultOptions: [],
22738
22782
  create(context) {
22739
- if (isTestFile(context.filename) || !INSERT_GATE.test(context.sourceCode.text)) {
22783
+ if (isTestFile(context.filename)) {
22740
22784
  return {};
22741
22785
  }
22742
22786
  return createSqlListener((sql, node) => {
@@ -24289,7 +24333,7 @@ var RULES = {
24289
24333
  };
24290
24334
  var meta = {
24291
24335
  name: "@sarj/eslint-plugin",
24292
- version: "16.4.5"
24336
+ version: "16.4.7"
24293
24337
  };
24294
24338
  var APPLICATION_ONLY_RULES = [];
24295
24339
  var LIBRARY_IMPORT_POLICY = ["error", {
package/dist/index.d.cts CHANGED
@@ -927,7 +927,7 @@ type FlatPreset = {
927
927
  declare const PLUGIN: {
928
928
  readonly meta: {
929
929
  readonly name: "@sarj/eslint-plugin";
930
- readonly version: "16.4.5";
930
+ readonly version: "16.4.7";
931
931
  };
932
932
  readonly rules: {
933
933
  readonly "require-explicit-contract-implementation": DocumentedRule<readonly [], "declareActualContract">;
package/dist/index.d.ts CHANGED
@@ -927,7 +927,7 @@ type FlatPreset = {
927
927
  declare const PLUGIN: {
928
928
  readonly meta: {
929
929
  readonly name: "@sarj/eslint-plugin";
930
- readonly version: "16.4.5";
930
+ readonly version: "16.4.7";
931
931
  };
932
932
  readonly rules: {
933
933
  readonly "require-explicit-contract-implementation": DocumentedRule<readonly [], "declareActualContract">;
package/dist/index.js CHANGED
@@ -211,6 +211,7 @@ function deepFreeze(value) {
211
211
  }
212
212
 
213
213
  // src/rules/_paths.ts
214
+ var LINE_TERMINATOR_RE = /[\n\r\u2028\u2029]/u;
214
215
  var SCRIPT_FILE_RE = /(?:^|[\\/])scripts[\\/]|\.mjs$/;
215
216
  var STORY_FILE_RE = /\.stories\.[cm]?[jt]sx?$/i;
216
217
  var STORY_TREE_RE = /(^|\/)stories(?:[_-][^/]*)?\//i;
@@ -260,8 +261,8 @@ function leadingCommentBodies(sourceText) {
260
261
  const bodies = [];
261
262
  let index = 0;
262
263
  if (sourceText.startsWith("#!")) {
263
- const shebangEnd = sourceText.indexOf("\n");
264
- index = shebangEnd === -1 ? sourceText.length : shebangEnd + 1;
264
+ const shebangEnd = commentLineEnd(sourceText, 2);
265
+ index = shebangEnd === sourceText.length ? sourceText.length : shebangEnd + 1;
265
266
  }
266
267
  while (index < sourceText.length) {
267
268
  while (/\s/u.test(sourceText[index] ?? "")) index++;
@@ -275,7 +276,7 @@ function leadingCommentBodies(sourceText) {
275
276
  const end = sourceText.indexOf("*/", index + 2);
276
277
  if (end === -1) break;
277
278
  bodies.push(
278
- ...sourceText.slice(index + 2, end).split("\n").map((line) => line.replace(/^\s*\*?\s?/u, "").trim()).filter((line) => line.length > 0)
279
+ ...sourceText.slice(index + 2, end).split(LINE_TERMINATOR_RE).map((line) => line.replace(/^\s*\*?\s?/u, "").trim()).filter((line) => line.length > 0)
279
280
  );
280
281
  index = end + 2;
281
282
  continue;
@@ -294,8 +295,8 @@ function isScriptFile(filename) {
294
295
  return SCRIPT_FILE_RE.test(filename);
295
296
  }
296
297
  function commentLineEnd(sourceText, start) {
297
- const end = sourceText.indexOf("\n", start);
298
- return end === -1 ? sourceText.length : end;
298
+ const end = sourceText.slice(start).search(LINE_TERMINATOR_RE);
299
+ return end === -1 ? sourceText.length : start + end;
299
300
  }
300
301
 
301
302
  // src/rules/require-explicit-contract-implementation.ts
@@ -5437,7 +5438,9 @@ var NO_ENUM_DOCUMENTATION = {
5437
5438
  focusPath: "src/status.ts",
5438
5439
  expectedCount: 1,
5439
5440
  public: true
5440
- }
5441
+ },
5442
+ { id: "authored-after-comment", scenarioId: "native-header-boundary", title: "Keep executable authored code in scope", outcome: "match", files: [{ path: "src/status.ts", source: '// ordinary header\u2028const marker="auto-generated file"; enum Mode {Start} console.log(Mode.Start, marker);' }], focusPath: "src/status.ts", expectedCount: 1, public: true },
5443
+ { id: "generated-header", scenarioId: "native-header-boundary", title: "Respect a native generated banner", outcome: "no-match", files: [{ path: "src/status.ts", source: '// auto-generated file\u2028const marker="auto-generated file"; enum Mode {Start} console.log(Mode.Start, marker);' }], focusPath: "src/status.ts", expectedCount: 0, public: true }
5441
5444
  ]
5442
5445
  };
5443
5446
  function matchesAnyPattern(filename, patterns) {
@@ -6800,7 +6803,9 @@ var NO_JSON_STRINGIFY_ERROR_DOCUMENTATION = {
6800
6803
  limitations: ["The rule uses stable local catch bindings and unshadowed built-in constructors, not runtime type information. Custom replacers are left to their serializer contract; a catch value is not guaranteed to be an Error."],
6801
6804
  examples: [
6802
6805
  { id: "explicit-error-message", title: "Narrow an unknown catch value before selecting fields", outcome: "no-match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err instanceof Error ? err.message : String(err) }); }" }], focusPath: "src/report.ts", expectedCount: 0, public: true },
6803
- { id: "stringified-error", title: "Do not stringify an Error object", outcome: "match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err }); }" }], focusPath: "src/report.ts", expectedCount: 1, public: true }
6806
+ { id: "stringified-error", title: "Do not stringify an Error object", outcome: "match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err }); }" }], focusPath: "src/report.ts", expectedCount: 1, public: true },
6807
+ { id: "shadowed-error-binding", scenarioId: "binding-provenance", title: "Keep native binding provenance", outcome: "match", files: [{ path: "src/error.ts", source: 'function probe(){try{throw {id:1};}catch(err){if(err instanceof Error)return err.message;{const err=new Error("lost");return JSON.stringify(err);}}}\nconsole.log(probe());' }], focusPath: "src/error.ts", expectedCount: 1, public: true },
6808
+ { id: "escaped-fallback-binding", scenarioId: "binding-provenance", title: "Keep native binding provenance", outcome: "no-match", files: [{ path: "src/error.ts", source: "function probe(){try{throw {id:1};}catch(err){if(err instanceof Error)return err.message;return JSON.stringify(\\u0065rr);}}\nconsole.log(probe());" }], focusPath: "src/error.ts", expectedCount: 0, public: true }
6804
6809
  ]
6805
6810
  };
6806
6811
  var SAFE_STRING_PROPS = /* @__PURE__ */ new Set(["message", "stack", "name"]);
@@ -6830,25 +6835,25 @@ var BUILTIN_ERROR_CONSTRUCTORS = /* @__PURE__ */ new Set([
6830
6835
  "TypeError",
6831
6836
  "URIError"
6832
6837
  ]);
6833
- function identifierIsProvenError(identifier, scope) {
6834
- const variable = ASTUtils14.findVariable(scope, identifier.name);
6838
+ function identifierIsProvenError(identifier, sourceCode) {
6839
+ const variable = ASTUtils14.findVariable(sourceCode.getScope(identifier), identifier.name);
6835
6840
  if (variable === null || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
6836
6841
  const definition = variable.defs[0];
6837
6842
  if (definition?.type === "CatchClause") return true;
6838
6843
  if (definition?.type !== "Variable") return false;
6839
6844
  const initializer = definition.node.init;
6840
- return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && isGlobalIdentifier(initializer.callee.name, scope);
6845
+ return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && isGlobalIdentifier(initializer.callee.name, sourceCode.getScope(initializer.callee));
6841
6846
  }
6842
6847
  function isGlobalIdentifier(name, scope) {
6843
6848
  const binding = ASTUtils14.findVariable(scope, name);
6844
6849
  return binding === null || binding.defs.length === 0;
6845
6850
  }
6846
- function positiveErrorSubject(test) {
6847
- return instanceofErrorSubject(test) ?? typeGuardSubject(test);
6851
+ function positiveErrorSubject(test, sourceCode) {
6852
+ return instanceofErrorSubject(test, sourceCode) ?? typeGuardSubject(test);
6848
6853
  }
6849
6854
  var TYPE_GUARD_PATTERN = /^(is|has)[A-Z]/;
6850
- function instanceofErrorSubject(test) {
6851
- if (test.type === "BinaryExpression" && test.operator === "instanceof" && test.right.type === "Identifier" && test.right.name === "Error") {
6855
+ function instanceofErrorSubject(test, sourceCode) {
6856
+ if (test.type === "BinaryExpression" && test.operator === "instanceof" && test.right.type === "Identifier" && test.right.name === "Error" && isGlobalIdentifier("Error", sourceCode.getScope(test.right))) {
6852
6857
  return test.left;
6853
6858
  }
6854
6859
  return null;
@@ -6862,36 +6867,34 @@ function typeGuardSubject(test) {
6862
6867
  return null;
6863
6868
  }
6864
6869
  function isNarrowedByEarlyReturn(node, argExpr, sourceCode) {
6865
- const argText = sourceCode.getText(argExpr);
6866
6870
  let current = node.parent;
6867
6871
  while (current) {
6868
6872
  if (current.type === "BlockStatement" || current.type === "Program") {
6869
- if (hasEarlierErrorGuard(current.body, node, argText, sourceCode)) return true;
6873
+ if (hasEarlierErrorGuard(current.body, node, argExpr, sourceCode)) return true;
6870
6874
  }
6871
6875
  current = current.parent;
6872
6876
  }
6873
6877
  return false;
6874
6878
  }
6875
6879
  function isGuardedByInstanceofError(node, argExpr, sourceCode) {
6876
- const argText = sourceCode.getText(argExpr);
6877
- const sameSubject = (subject) => sourceCode.getText(subject) === argText;
6880
+ const sameSubject = (subject) => sameSubjectBinding(subject, argExpr, sourceCode);
6878
6881
  let current = node.parent;
6879
6882
  while (current) {
6880
6883
  if (current.type === "ConditionalExpression") {
6881
- const subject = positiveErrorSubject(current.test);
6884
+ const subject = positiveErrorSubject(current.test, sourceCode);
6882
6885
  if (subject && sameSubject(subject) && nodeWithin(node, current.alternate)) {
6883
6886
  return true;
6884
6887
  }
6885
- const negated = negatedInstanceofErrorSubject(current.test);
6888
+ const negated = negatedInstanceofErrorSubject(current.test, sourceCode);
6886
6889
  if (negated && sameSubject(negated) && nodeWithin(node, current.consequent)) {
6887
6890
  return true;
6888
6891
  }
6889
6892
  } else if (current.type === "IfStatement") {
6890
- const subject = positiveErrorSubject(current.test);
6893
+ const subject = positiveErrorSubject(current.test, sourceCode);
6891
6894
  if (subject && sameSubject(subject) && nodeWithin(node, current.alternate)) {
6892
6895
  return true;
6893
6896
  }
6894
- const negated = negatedInstanceofErrorSubject(current.test);
6897
+ const negated = negatedInstanceofErrorSubject(current.test, sourceCode);
6895
6898
  if (negated && sameSubject(negated) && nodeWithin(node, current.consequent)) {
6896
6899
  return true;
6897
6900
  }
@@ -6900,12 +6903,21 @@ function isGuardedByInstanceofError(node, argExpr, sourceCode) {
6900
6903
  }
6901
6904
  return false;
6902
6905
  }
6903
- function negatedInstanceofErrorSubject(test) {
6906
+ function negatedInstanceofErrorSubject(test, sourceCode) {
6904
6907
  if (test.type === "UnaryExpression" && test.operator === "!") {
6905
- return positiveErrorSubject(test.argument);
6908
+ return positiveErrorSubject(test.argument, sourceCode);
6906
6909
  }
6907
6910
  return null;
6908
6911
  }
6912
+ function sameSubjectBinding(subject, value, sourceCode) {
6913
+ const left = unwrapExpression(subject);
6914
+ const right = unwrapExpression(value);
6915
+ if (left.type === "Identifier" && right.type === "Identifier") {
6916
+ const binding = ASTUtils14.findVariable(sourceCode.getScope(left), left.name);
6917
+ return binding !== null && binding === ASTUtils14.findVariable(sourceCode.getScope(right), right.name);
6918
+ }
6919
+ return sourceCode.getText(subject) === sourceCode.getText(value);
6920
+ }
6909
6921
  function nodeWithin(node, container) {
6910
6922
  return container !== null && node.range[0] >= container.range[0] && node.range[1] <= container.range[1];
6911
6923
  }
@@ -6933,21 +6945,21 @@ function directLiteralValues(argument) {
6933
6945
  }
6934
6946
  return argument.type === "SpreadElement" ? [] : [argument];
6935
6947
  }
6936
- function expressionSuggestsError(expression, scope) {
6948
+ function expressionSuggestsError(expression, sourceCode) {
6937
6949
  expression = unwrapExpression(expression);
6938
6950
  const unwrappedExpressionCallee = expression.type === "CallExpression" || expression.type === "NewExpression" ? unwrapExpression(expression.callee) : null;
6939
6951
  if (expression.type === "Identifier") {
6940
- return identifierIsProvenError(expression, scope);
6952
+ return identifierIsProvenError(expression, sourceCode);
6941
6953
  }
6942
6954
  if (expression.type === "NewExpression" && unwrappedExpressionCallee?.type === "Identifier") {
6943
- return BUILTIN_ERROR_CONSTRUCTORS.has(unwrappedExpressionCallee.name) && isGlobalIdentifier(unwrappedExpressionCallee.name, scope);
6955
+ return BUILTIN_ERROR_CONSTRUCTORS.has(unwrappedExpressionCallee.name) && isGlobalIdentifier(unwrappedExpressionCallee.name, sourceCode.getScope(unwrappedExpressionCallee));
6944
6956
  }
6945
- return expression.type === "MemberExpression" && memberSuggestsError(expression, scope);
6957
+ return expression.type === "MemberExpression" && memberSuggestsError(expression, sourceCode);
6946
6958
  }
6947
- function memberSuggestsError(member, scope) {
6959
+ function memberSuggestsError(member, sourceCode) {
6948
6960
  const propName2 = ASTUtils14.getPropertyName(member);
6949
6961
  const base = member.object;
6950
- const baseSuggestsError = base.type === "Identifier" && identifierIsProvenError(base, scope);
6962
+ const baseSuggestsError = base.type === "Identifier" && identifierIsProvenError(base, sourceCode);
6951
6963
  if (baseSuggestsError) {
6952
6964
  if (propName2 === null) {
6953
6965
  return true;
@@ -6987,7 +6999,7 @@ var no_json_stringify_error_default = createRule({
6987
6999
  const replacer = node.arguments[1] === void 0 ? void 0 : unwrapExpression(node.arguments[1]);
6988
7000
  if (replacer !== void 0 && !(replacer.type === "Literal" && replacer.value === null) && !(replacer.type === "Identifier" && replacer.name === "undefined" && isGlobalIdentifier("undefined", scope))) return;
6989
7001
  const unsafeValue = directLiteralValues(firstArg).find(
6990
- (value) => expressionSuggestsError(value, scope) && !isGuardedByInstanceofError(node, value, context.sourceCode) && !isNarrowedByEarlyReturn(node, value, context.sourceCode)
7002
+ (value) => expressionSuggestsError(value, context.sourceCode) && !isGuardedByInstanceofError(node, value, context.sourceCode) && !isNarrowedByEarlyReturn(node, value, context.sourceCode)
6991
7003
  );
6992
7004
  if (unsafeValue === void 0) {
6993
7005
  return;
@@ -7000,14 +7012,14 @@ var no_json_stringify_error_default = createRule({
7000
7012
  };
7001
7013
  }
7002
7014
  });
7003
- function hasEarlierErrorGuard(statements, node, argText, sourceCode) {
7015
+ function hasEarlierErrorGuard(statements, node, argExpr, sourceCode) {
7004
7016
  for (const stmt of statements) {
7005
7017
  if (stmt.range[0] >= node.range[0]) {
7006
7018
  break;
7007
7019
  }
7008
7020
  if (stmt.type === "IfStatement" && stmt.alternate === null && branchTerminates(stmt.consequent)) {
7009
- const subject = positiveErrorSubject(stmt.test);
7010
- if (subject && sourceCode.getText(subject) === argText) {
7021
+ const subject = positiveErrorSubject(stmt.test, sourceCode);
7022
+ if (subject && sameSubjectBinding(subject, argExpr, sourceCode)) {
7011
7023
  return true;
7012
7024
  }
7013
7025
  }
@@ -7170,7 +7182,7 @@ import {
7170
7182
  // src/rules/_zod.ts
7171
7183
  var ZOD_SCHEMA_NAME_RE = /Schema$|^Z[A-Z]/;
7172
7184
  function isZodModule(source) {
7173
- return /(^|[/@-])zod([/-]|$)/.test(source);
7185
+ return source === "zod" || source.startsWith("zod/");
7174
7186
  }
7175
7187
 
7176
7188
  // src/rules/no-impossible-zod-literal-bounds.ts
@@ -7562,6 +7574,7 @@ var NO_REDUNDANT_OPTIONAL_ARRAY_DEFAULT_DOCUMENTATION = {
7562
7574
  autofix: "safe",
7563
7575
  limitations: [
7564
7576
  "Only syntactically local Zod array chains with adjacent `.optional().default(...)` calls are checked.",
7577
+ "Imports must name the official zod package or its subpaths; relative modules and unproven npm aliases are excluded.",
7565
7578
  "String, scalar, tuple, set, record, aliased, composed, and dynamically constructed schemas are excluded.",
7566
7579
  "The reversed `.default(...).optional()` order is preserved because its outer optional can return undefined instead of the default.",
7567
7580
  "Defaults must be literal arrays or synchronous functions returning one literal array; dynamic or undefined fallbacks are excluded because Zod 3 reparses the fallback.",
@@ -7602,6 +7615,30 @@ var NO_REDUNDANT_OPTIONAL_ARRAY_DEFAULT_DOCUMENTATION = {
7602
7615
  focusPath: "src/schema.ts",
7603
7616
  expectedCount: 1,
7604
7617
  public: true
7618
+ },
7619
+ {
7620
+ id: "unrelated-relative-builder",
7621
+ title: "Preserve an unrelated builder in a directory named zod",
7622
+ outcome: "no-match",
7623
+ files: [
7624
+ { path: "src/my-zod/index.ts", source: 'export const z = { string: () => "string", array: (_element: string) => ({ optional: () => ({ default: (value: readonly string[]) => ({ value, optional: true }) }), default: (value: readonly string[]) => ({ value, optional: false }) }) };' },
7625
+ { path: "src/schema.ts", source: 'import { z } from "./my-zod/index.js"; export const Items = z.array(z.string()).optional().default([]);' }
7626
+ ],
7627
+ focusPath: "src/schema.ts",
7628
+ expectedCount: 0,
7629
+ public: true,
7630
+ scenarioId: "module-identity"
7631
+ },
7632
+ {
7633
+ id: "official-zod-subpath-array",
7634
+ title: "Official Zod subpath constructors retain the safe fix",
7635
+ outcome: "match",
7636
+ files: [{ path: "src/schema.ts", source: 'import { array, string } from "zod/v4"; const Items = array(string()).optional().default([]);' }],
7637
+ fixedFiles: [{ path: "src/schema.ts", source: 'import { array, string } from "zod/v4"; const Items = array(string()).default([]);' }],
7638
+ focusPath: "src/schema.ts",
7639
+ expectedCount: 1,
7640
+ public: true,
7641
+ scenarioId: "module-identity"
7605
7642
  }
7606
7643
  ]
7607
7644
  };
@@ -8799,11 +8836,12 @@ var NO_OFFSET_PAGINATION_DOCUMENTATION = {
8799
8836
  limitations: ["A SELECT/FROM query shape or adjacent LIMIT/OFFSET fragment is required. Isolated OFFSET fragments and test files are excluded; this lexical context does not prove a database execution sink. Performance and concurrent-write behavior depend on indexes, ordering, isolation, and dialect; bounded pages and random page access can justify OFFSET."],
8800
8837
  examples: [
8801
8838
  { id: "keyset-pagination", title: "Page from a stable cursor", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id FROM runs WHERE id > ? ORDER BY id LIMIT ?`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
8802
- { id: "offset-pagination", title: "Do not page by offset", outcome: "match", files: [{ path: "src/runs.ts", source: "db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
8839
+ { id: "offset-pagination", title: "Do not page by offset", outcome: "match", files: [{ path: "src/runs.ts", source: "db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);" }], focusPath: "src/runs.ts", expectedCount: 1, public: true },
8840
+ { id: "decoded-query", scenarioId: "native-decoded-sql", title: "Inspect native-decoded query text", outcome: "match", files: [{ path: "src/page.ts", source: 'db.query("SELECT id FROM runs ORDER BY id LIMIT 1 OFF\\u0053ET 1");' }], focusPath: "src/page.ts", expectedCount: 1, public: true },
8841
+ { id: "decoded-comment-data", scenarioId: "native-decoded-sql", title: "Ignore native-decoded comment data", outcome: "no-match", files: [{ path: "src/page.ts", source: 'db.query("SELECT id FROM runs /* OFF\\x53ET 1 */");' }], focusPath: "src/page.ts", expectedCount: 0, public: true }
8803
8842
  ]
8804
8843
  };
8805
8844
  var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
8806
- var OFFSET_GATE = /offset/i;
8807
8845
  var PAGINATION_CONTEXT = /\bSELECT\b[\s\S]*\bFROM\b[\s\S]*\bOFFSET\b|\bLIMIT\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)\s+OFFSET\b/i;
8808
8846
  var no_offset_pagination_default = createRule({
8809
8847
  name: "no-offset-pagination",
@@ -8820,7 +8858,7 @@ var no_offset_pagination_default = createRule({
8820
8858
  },
8821
8859
  defaultOptions: [],
8822
8860
  create(context) {
8823
- if (isTestFile(context.filename) || !OFFSET_GATE.test(context.sourceCode.text)) {
8861
+ if (isTestFile(context.filename)) {
8824
8862
  return {};
8825
8863
  }
8826
8864
  return createSqlListener((sql, node) => {
@@ -10937,7 +10975,9 @@ var NO_SELECT_STAR_DOCUMENTATION = {
10937
10975
  limitations: ["Only statically visible embedded SQL is checked; quoted strings (including PostgreSQL dollar strings), comments, function arguments such as COUNT(*), and stars inside EXISTS are excluded. This is a bounded lexical scan, not a complete SQL parser."],
10938
10976
  examples: [
10939
10977
  { id: "explicit-projection", title: "Select the required columns", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id, status FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
10940
- { id: "wildcard-projection", title: "Do not select every column", outcome: "match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT * FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
10978
+ { id: "wildcard-projection", title: "Do not select every column", outcome: "match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT * FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 1, public: true },
10979
+ { id: "decoded-query", scenarioId: "native-decoded-sql", title: "Inspect native-decoded query text", outcome: "match", files: [{ path: "src/page.ts", source: 'db.query("SEL\\u0045CT * FROM runs");' }], focusPath: "src/page.ts", expectedCount: 1, public: true },
10980
+ { id: "decoded-comment-data", scenarioId: "native-decoded-sql", title: "Ignore native-decoded comment data", outcome: "no-match", files: [{ path: "src/page.ts", source: 'db.query("SEL\\x45CT id FROM runs /* SELECT * FROM runs */");' }], focusPath: "src/page.ts", expectedCount: 0, public: true }
10941
10981
  ]
10942
10982
  };
10943
10983
  var QUERY_SHAPE = /\bSELECT\b[\s\S]*?\bFROM\b/i;
@@ -10945,7 +10985,6 @@ var SELECT_KEYWORD = /\bSELECT\b/gi;
10945
10985
  var FROM_KEYWORD = /^FROM\b/i;
10946
10986
  var EXISTS_BEFORE = /\bEXISTS\s*\(\s*$/i;
10947
10987
  var QUALIFIED_PREFIX = /\w\.$/;
10948
- var SELECT_GATE = /select/i;
10949
10988
  function hasRealSelectStar(sql) {
10950
10989
  const selects = [...sql.matchAll(SELECT_KEYWORD)].map((m) => m.index);
10951
10990
  for (let pos = 0; pos < sql.length; pos++) {
@@ -10994,7 +11033,7 @@ var no_select_star_default = createRule({
10994
11033
  },
10995
11034
  defaultOptions: [],
10996
11035
  create(context) {
10997
- if (isTestFile(context.filename) || !SELECT_GATE.test(context.sourceCode.text)) {
11036
+ if (isTestFile(context.filename)) {
10998
11037
  return {};
10999
11038
  }
11000
11039
  return createSqlListener((sql, node) => {
@@ -18945,16 +18984,20 @@ var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
18945
18984
  ],
18946
18985
  examples: [
18947
18986
  { id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "switch (kind) { case 'a': return a(); case 'b': return b(); case 'c': return c(); default: return fallback(); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
18948
- { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "if (kind === 'a') return a(); else if (kind === 'b') return b(); else if (kind === 'c') return c();" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
18987
+ { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "if (kind === 'a') return a(); else if (kind === 'b') return b(); else if (kind === 'c') return c();" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
18988
+ { id: "escaped-middle", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){if(kind==='a')return 1;else if(\\u006bind==='b')return 2;else if(kind==='c')return 3;return 0;}
18989
+ console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 1, public: true },
18990
+ { id: "different-discriminants", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "no-match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){const other="other";if(kind==='a')return 1;else if(other==='b')return 2;else if(kind==='c')return 3;return 0;}
18991
+ console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 0, public: true }
18949
18992
  ]
18950
18993
  };
18951
- function discriminantText(sourceCode, test) {
18994
+ function discriminantName(test) {
18952
18995
  if (test.type !== AST_NODE_TYPES95.BinaryExpression || test.operator !== "===") return null;
18953
18996
  const leftIsCase = isCaseValue(test.left);
18954
18997
  const rightIsCase = isCaseValue(test.right);
18955
18998
  if (leftIsCase === rightIsCase) return null;
18956
18999
  const discriminant = leftIsCase ? test.right : test.left;
18957
- return discriminant.type === AST_NODE_TYPES95.Identifier ? sourceCode.getText(discriminant) : null;
19000
+ return discriminant.type === AST_NODE_TYPES95.Identifier ? discriminant.name : null;
18958
19001
  }
18959
19002
  function isCaseValue(node) {
18960
19003
  if (node.type === AST_NODE_TYPES95.Literal) return true;
@@ -18978,12 +19021,12 @@ var prefer_switch_for_repeated_equality_default = createRule({
18978
19021
  return {
18979
19022
  IfStatement(node) {
18980
19023
  if (node.parent.type === AST_NODE_TYPES95.IfStatement && node.parent.alternate === node) return;
18981
- const first = discriminantText(context.sourceCode, node.test);
19024
+ const first = discriminantName(node.test);
18982
19025
  if (first === null) return;
18983
19026
  let count = 1;
18984
19027
  let current = node.alternate;
18985
19028
  while (current?.type === AST_NODE_TYPES95.IfStatement) {
18986
- if (discriminantText(context.sourceCode, current.test) !== first) return;
19029
+ if (discriminantName(current.test) !== first) return;
18987
19030
  count += 1;
18988
19031
  current = current.alternate;
18989
19032
  }
@@ -22721,7 +22764,9 @@ var STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION = {
22721
22764
  limitations: ["Only the nearest statically named callable is considered; top-level inserts and anonymous callbacks are excluded. Recognized conflict syntax does not prove idempotence or concurrency safety: WHERE NOT EXISTS can race, and INSERT OR REPLACE can delete an existing row. Review unique constraints and dialect semantics manually."],
22722
22765
  examples: [
22723
22766
  { id: "conflict-safe-insert", title: "Review the conflict policy for a replayed insert", outcome: "no-match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(); }" }], focusPath: "src/store.ts", expectedCount: 0, public: true },
22724
- { id: "bare-insert", title: "Review a bare insert in a replay-named callable", outcome: "match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?)`).run(); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
22767
+ { id: "bare-insert", title: "Review a bare insert in a replay-named callable", outcome: "match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?)`).run(); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true },
22768
+ { id: "decoded-query", scenarioId: "native-decoded-sql", title: "Inspect native-decoded query text", outcome: "match", files: [{ path: "src/page.ts", source: `function seed(){db.query("INS\\u0045RT INTO runs(id,note) VALUES(4,'new')");}` }], focusPath: "src/page.ts", expectedCount: 1, public: true },
22769
+ { id: "decoded-comment-data", scenarioId: "native-decoded-sql", title: "Ignore native-decoded comment data", outcome: "no-match", files: [{ path: "src/page.ts", source: `function seed(){db.query("SELECT id FROM runs /* INS\\x45RT INTO runs(id,note) VALUES(4,'new') */");}` }], focusPath: "src/page.ts", expectedCount: 0, public: true }
22725
22770
  ]
22726
22771
  };
22727
22772
  var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
@@ -22741,7 +22786,6 @@ function owningCallableName(node) {
22741
22786
  }
22742
22787
  return null;
22743
22788
  }
22744
- var INSERT_GATE = /insert/i;
22745
22789
  var store_insert_requires_on_conflict_default = createRule({
22746
22790
  name: "store-insert-requires-on-conflict",
22747
22791
  documentation: STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION,
@@ -22757,7 +22801,7 @@ var store_insert_requires_on_conflict_default = createRule({
22757
22801
  },
22758
22802
  defaultOptions: [],
22759
22803
  create(context) {
22760
- if (isTestFile(context.filename) || !INSERT_GATE.test(context.sourceCode.text)) {
22804
+ if (isTestFile(context.filename)) {
22761
22805
  return {};
22762
22806
  }
22763
22807
  return createSqlListener((sql, node) => {
@@ -24313,7 +24357,7 @@ var RULES = {
24313
24357
  };
24314
24358
  var meta = {
24315
24359
  name: "@sarj/eslint-plugin",
24316
- version: "16.4.5"
24360
+ version: "16.4.7"
24317
24361
  };
24318
24362
  var APPLICATION_ONLY_RULES = [];
24319
24363
  var LIBRARY_IMPORT_POLICY = ["error", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.4.5",
3
+ "version": "16.4.7",
4
4
  "packageManager": "npm@12.1.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",