@sarj/eslint-plugin 16.23.2 → 16.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.cjs +2157 -1994
  2. package/dist/index.js +235 -72
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -288,7 +288,7 @@ function isTypeOnly(node) {
288
288
  import { AST_NODE_TYPES as AST_NODE_TYPES4, ASTUtils as ASTUtils2 } from "@typescript-eslint/utils";
289
289
 
290
290
  // src/rules/_paths.ts
291
- import { posix } from "path";
291
+ import { posix, win32 } from "path";
292
292
  var LINE_TERMINATOR_RE = /[\n\r\u2028\u2029]/u;
293
293
  var SCRIPT_FILE_RE = /(?:^|[\\/])scripts[\\/]|\.mjs$/;
294
294
  var STORY_FILE_RE = /\.stories\.[cm]?[jt]sx?$/i;
@@ -373,7 +373,21 @@ function isScriptFile(filename) {
373
373
  return SCRIPT_FILE_RE.test(scopedFilename(filename));
374
374
  }
375
375
  function scopedFilename(input) {
376
- return (typeof input === "string" ? input : input.filename).replaceAll("\\", "/");
376
+ if (typeof input === "string") return input.replaceAll("\\", "/");
377
+ const filename = input.filename.replaceAll("\\", "/");
378
+ const cwd = input.cwd.replaceAll("\\", "/");
379
+ const fileWindows = windowsAbsolute(filename);
380
+ const cwdWindows = windowsAbsolute(cwd);
381
+ let scoped = filename;
382
+ if (fileWindows && cwdWindows) scoped = win32.relative(cwd, filename).replaceAll("\\", "/");
383
+ else if (!fileWindows && !cwdWindows && posix.isAbsolute(filename) && posix.isAbsolute(cwd)) {
384
+ scoped = posix.relative(cwd, filename);
385
+ }
386
+ if (windowsAbsolute(scoped) || posix.isAbsolute(scoped)) return scoped;
387
+ return `./${posix.normalize(scoped)}`;
388
+ }
389
+ function windowsAbsolute(filename) {
390
+ return /^[A-Za-z]:\//u.test(filename) || filename.startsWith("//");
377
391
  }
378
392
  function matchesAnyPattern(filename, patterns) {
379
393
  const normalized = filename.replaceAll("\\", "/");
@@ -822,7 +836,7 @@ function isSearchParamReceiver(call, services) {
822
836
  // package.json
823
837
  var package_default = {
824
838
  name: "@sarj/eslint-plugin",
825
- version: "16.23.2",
839
+ version: "16.26.0",
826
840
  packageManager: "npm@12.2.0",
827
841
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
828
842
  type: "module",
@@ -7261,32 +7275,39 @@ import { ASTUtils as ASTUtils21, AST_NODE_TYPES as AST_NODE_TYPES40 } from "@typ
7261
7275
 
7262
7276
  // src/rules/_sql.ts
7263
7277
  import { AST_NODE_TYPES as AST_NODE_TYPES39, ASTUtils as ASTUtils20 } from "@typescript-eslint/utils";
7264
- function stripSqlNoise(text) {
7265
- return scanSqlNoise(text);
7278
+ var SQL_IDENTIFIER_CONTINUATION = /[\w$\u0080-\u{10FFFF}]/u;
7279
+ var DOLLAR_DELIMITER = /\$(?:[A-Za-z_\u0080-\u{10FFFF}][A-Za-z_0-9\u0080-\u{10FFFF}]*)?\$/uy;
7280
+ var SQL_LINE_END = /[\r\n]/g;
7281
+ var CONTINUED_QUOTE = /[ \t\f\v]*(?:--[^\r\n]*)?[\r\n](?:[ \t\f\v\r\n]|--[^\r\n]*[\r\n])*'/y;
7282
+ function stripSqlNoise(text, options) {
7283
+ return scanSqlNoise(text, void 0, options?.preserveQuotedTokens === true);
7266
7284
  }
7267
7285
  function sqlNoiseInfo(text) {
7268
7286
  const singleQuotedRanges = [];
7269
7287
  const masked = scanSqlNoise(text, (start, end) => singleQuotedRanges.push([start, end]));
7270
7288
  return { text: masked, singleQuotedRanges };
7271
7289
  }
7272
- function scanSqlNoise(text, onSingleQuoted) {
7290
+ function scanSqlNoise(text, onSingleQuoted, preserveQuotedTokens = false) {
7273
7291
  const out = text.split("");
7274
7292
  let index = 0;
7275
7293
  while (index < text.length) {
7276
7294
  const dollarEnd = dollarQuotedSqlEnd(text, index);
7277
7295
  if (dollarEnd !== null) {
7278
7296
  maskSqlRange(text, out, index, dollarEnd);
7297
+ if (preserveQuotedTokens) out[index] = "?";
7279
7298
  index = dollarEnd;
7280
7299
  continue;
7281
7300
  }
7282
7301
  const character = text[index];
7283
7302
  if (character === "'" || character === '"') {
7303
+ const start = index;
7284
7304
  index = quotedSqlEnd(text, out, index, onSingleQuoted);
7305
+ if (preserveQuotedTokens) out[start] = "?";
7285
7306
  continue;
7286
7307
  }
7287
7308
  if (character === "-" && text[index + 1] === "-") {
7288
- const newline = text.indexOf("\n", index);
7289
- const end = newline === -1 ? text.length : newline;
7309
+ SQL_LINE_END.lastIndex = index;
7310
+ const end = SQL_LINE_END.exec(text)?.index ?? text.length;
7290
7311
  maskSqlRange(text, out, index, end);
7291
7312
  index = end;
7292
7313
  continue;
@@ -7300,8 +7321,9 @@ function scanSqlNoise(text, onSingleQuoted) {
7300
7321
  return out.join("");
7301
7322
  }
7302
7323
  function dollarQuotedSqlEnd(text, start) {
7303
- if (text[start] !== "$" || /[\w$]/u.test(text[start - 1] ?? "")) return null;
7304
- const delimiter = /^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/u.exec(text.slice(start))?.[0];
7324
+ if (text[start] !== "$" || SQL_IDENTIFIER_CONTINUATION.test(text[start - 1] ?? "")) return null;
7325
+ DOLLAR_DELIMITER.lastIndex = start;
7326
+ const delimiter = DOLLAR_DELIMITER.exec(text)?.[0];
7305
7327
  if (delimiter === void 0) return null;
7306
7328
  const closing = text.indexOf(delimiter, start + delimiter.length);
7307
7329
  return closing < 0 ? text.length : closing + delimiter.length;
@@ -7319,17 +7341,24 @@ function blockSqlCommentEnd(text, out, start) {
7319
7341
  index += 2;
7320
7342
  continue;
7321
7343
  }
7322
- if (text[index] !== "\n") out[index] = " ";
7344
+ if (text[index] !== "\n" && text[index] !== "\r") out[index] = " ";
7323
7345
  index += 1;
7324
7346
  }
7325
7347
  return index;
7326
7348
  }
7327
7349
  function quotedSqlEnd(text, out, start, onSingleQuoted) {
7328
7350
  const quote = text[start];
7351
+ const escapeString = quote === "'" && /[Ee]/u.test(text[start - 1] ?? "") && !SQL_IDENTIFIER_CONTINUATION.test(text[start - 2] ?? "");
7352
+ let segmentStart = start;
7329
7353
  out[start] = " ";
7330
7354
  let index = start + 1;
7331
7355
  while (index < text.length) {
7332
7356
  const character = text[index];
7357
+ if (character === "\\" && index + 1 < text.length) {
7358
+ maskSqlRange(text, out, index, index + 2);
7359
+ index += 2;
7360
+ continue;
7361
+ }
7333
7362
  if (character === quote) {
7334
7363
  out[index] = " ";
7335
7364
  if (index + 1 < text.length && text[index + 1] === quote) {
@@ -7338,17 +7367,29 @@ function quotedSqlEnd(text, out, start, onSingleQuoted) {
7338
7367
  continue;
7339
7368
  }
7340
7369
  index += 1;
7341
- if (quote === "'") onSingleQuoted?.(start, index);
7370
+ if (quote === "'") onSingleQuoted?.(segmentStart, index);
7371
+ const continuation = escapedSqlContinuation(text, index, escapeString);
7372
+ if (continuation !== null) {
7373
+ maskSqlRange(text, out, index, continuation);
7374
+ segmentStart = continuation - 1;
7375
+ index = continuation;
7376
+ continue;
7377
+ }
7342
7378
  break;
7343
7379
  }
7344
- if (character !== "\n") out[index] = " ";
7380
+ if (character !== "\n" && character !== "\r") out[index] = " ";
7345
7381
  index += 1;
7346
7382
  }
7347
7383
  return index;
7348
7384
  }
7385
+ function escapedSqlContinuation(text, start, escapeString) {
7386
+ if (!escapeString) return null;
7387
+ CONTINUED_QUOTE.lastIndex = start;
7388
+ return CONTINUED_QUOTE.test(text) ? CONTINUED_QUOTE.lastIndex : null;
7389
+ }
7349
7390
  function maskSqlRange(text, out, start, end) {
7350
7391
  for (let index = start; index < end; index += 1) {
7351
- if (text[index] !== "\n") out[index] = " ";
7392
+ if (text[index] !== "\n" && text[index] !== "\r") out[index] = " ";
7352
7393
  }
7353
7394
  }
7354
7395
  var SUBSTITUTION_MARKER = "?";
@@ -7414,7 +7455,7 @@ function markConsumed(node, consumed) {
7414
7455
  markConsumed(node.quasi, consumed);
7415
7456
  }
7416
7457
  }
7417
- function createSqlListener(handler) {
7458
+ function createSqlListener(handler, options) {
7418
7459
  const consumed = /* @__PURE__ */ new WeakSet();
7419
7460
  const visit = (node) => {
7420
7461
  if (consumed.has(node) || isDiscardedSqlText(node)) {
@@ -7425,7 +7466,7 @@ function createSqlListener(handler) {
7425
7466
  return;
7426
7467
  }
7427
7468
  markConsumed(node, consumed);
7428
- handler(stripSqlNoise(text), node);
7469
+ handler(stripSqlNoise(text, options), node, text);
7429
7470
  };
7430
7471
  return {
7431
7472
  BinaryExpression: (node) => {
@@ -8769,6 +8810,20 @@ import {
8769
8810
  AST_NODE_TYPES as AST_NODE_TYPES45,
8770
8811
  ASTUtils as ASTUtils24
8771
8812
  } from "@typescript-eslint/utils";
8813
+
8814
+ // src/rules/_stable-value-binding.ts
8815
+ import "@typescript-eslint/utils";
8816
+ function stableValueBinding(variable, acceptRead) {
8817
+ let initializer;
8818
+ return variable.references.every((reference) => {
8819
+ if (!reference.isWrite()) return acceptRead?.(reference) ?? true;
8820
+ if (reference.init !== true || initializer !== void 0 && initializer !== reference.identifier) return false;
8821
+ initializer = reference.identifier;
8822
+ return true;
8823
+ });
8824
+ }
8825
+
8826
+ // src/rules/no-detached-global-fetch.ts
8772
8827
  var NO_DETACHED_GLOBAL_FETCH_DOCUMENTATION = {
8773
8828
  summary: "Keep the ambient global fetch receiver-safe when storing or explicitly rebinding it.",
8774
8829
  rationale: "Calling a raw ambient fetch through a class or object property supplies that object as `this`; receiver-sensitive hosts can reject that invocation only after deployment.",
@@ -8837,14 +8892,9 @@ var no_detached_global_fetch_default = createRule({
8837
8892
  function isGlobalReceiver(node) {
8838
8893
  return node.type === AST_NODE_TYPES45.Identifier && GLOBAL_RECEIVERS.has(node.name) && resolvesToGlobal(node);
8839
8894
  }
8840
- function isStableAlias(variable) {
8841
- return !variable.references.some(
8842
- (reference) => reference.isWrite() && reference.init !== true
8843
- );
8844
- }
8845
8895
  function recordAlias(identifier) {
8846
8896
  const variable = bindingOf(identifier);
8847
- if (variable !== null && isStableAlias(variable)) aliases.add(variable);
8897
+ if (variable !== null && stableValueBinding(variable)) aliases.add(variable);
8848
8898
  }
8849
8899
  function staticMemberName7(node) {
8850
8900
  if (!node.computed) {
@@ -8857,7 +8907,7 @@ var no_detached_global_fetch_default = createRule({
8857
8907
  if (expression.type === AST_NODE_TYPES45.Identifier) {
8858
8908
  if (expression.name === "fetch" && resolvesToGlobal(expression)) return true;
8859
8909
  const variable = bindingOf(expression);
8860
- return variable !== null && aliases.has(variable) && isStableAlias(variable);
8910
+ return variable !== null && aliases.has(variable);
8861
8911
  }
8862
8912
  if (expression.type === AST_NODE_TYPES45.MemberExpression) {
8863
8913
  return isGlobalFetchMember(expression);
@@ -12610,18 +12660,18 @@ var no_router_refresh_polling_default = createRule({
12610
12660
  if (hookCallee.type !== AST_NODE_TYPES62.Identifier) return;
12611
12661
  const hook = ASTUtils39.findVariable(context.sourceCode.getScope(hookCallee), hookCallee.name);
12612
12662
  const router = ASTUtils39.findVariable(context.sourceCode.getScope(node.id), node.id.name);
12613
- if (hook !== null && router !== null && routerHooks.has(hook)) routers.add(router);
12663
+ if (hook !== null && router !== null && routerHooks.has(hook) && stableValueBinding(router)) routers.add(router);
12614
12664
  }
12615
12665
  },
12616
12666
  CallExpression(node) {
12617
12667
  const unwrappedNodeCallee = unwrapExpression(node.callee);
12618
12668
  const receiver = unwrappedNodeCallee.type === AST_NODE_TYPES62.MemberExpression ? unwrapExpression(unwrappedNodeCallee.object) : unwrappedNodeCallee;
12619
- if (unwrappedNodeCallee.type !== AST_NODE_TYPES62.MemberExpression || receiver.type !== AST_NODE_TYPES62.Identifier || ASTUtils39.getPropertyName(unwrappedNodeCallee) === null || (ASTUtils39.getPropertyName(unwrappedNodeCallee) ?? "") !== "refresh") return;
12669
+ if (unwrappedNodeCallee.type !== AST_NODE_TYPES62.MemberExpression || receiver.type !== AST_NODE_TYPES62.Identifier || ASTUtils39.getPropertyName(unwrappedNodeCallee) !== "refresh") return;
12620
12670
  const router = ASTUtils39.findVariable(
12621
12671
  context.sourceCode.getScope(receiver),
12622
12672
  receiver.name
12623
12673
  );
12624
- if (router === null || !routers.has(router) || router.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
12674
+ if (router === null || !routers.has(router)) return;
12625
12675
  const callback = enclosingIntervalCallback(context.sourceCode, node);
12626
12676
  if (callback !== null && !reportedCallbacks.has(callback)) {
12627
12677
  reportedCallbacks.add(callback);
@@ -13580,51 +13630,138 @@ var NO_SELECT_STAR_DOCUMENTATION = {
13580
13630
  rationale: "Wildcard projections couple row shape and query cost to unrelated schema changes.",
13581
13631
  remediation: "List every required column explicitly in the projection.",
13582
13632
  category: "correctness",
13583
- 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."],
13633
+ 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. Escaped ordinary strings, brackets, and backticks are handled conservatively. This is a bounded lexical scan, not a complete SQL parser."],
13584
13634
  examples: [
13585
13635
  { 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 },
13586
13636
  { 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 },
13587
13637
  { 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 },
13588
- { 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 }
13638
+ { 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 },
13639
+ { "id": "wrapped-exists-outer-wildcard", "scenarioId": "projection-owner", "title": "The outer projection still selects every column", "outcome": "match", "files": [{ "path": "src/queries.ts", "source": 'db.query("SELECT EXISTS((SELECT * FROM jobs)), * FROM runs");' }], "focusPath": "src/queries.ts", "expectedCount": 1, "public": true },
13640
+ { "id": "quoted-column-arithmetic", "scenarioId": "projection-owner", "title": "Quoted arithmetic operands are not wildcards", "outcome": "no-match", "files": [{ "path": "src/queries.ts", "source": 'db.query("SELECT \\"units\\" * \\"price\\" AS total FROM entries");' }], "focusPath": "src/queries.ts", "expectedCount": 0, "public": true }
13589
13641
  ]
13590
13642
  };
13591
- var QUERY_SHAPE = /\bSELECT\b[\s\S]*?\bFROM\b/i;
13592
- var SELECT_KEYWORD = /\bSELECT\b/gi;
13593
- var FROM_KEYWORD = /^FROM\b/i;
13594
- var EXISTS_BEFORE = /\bEXISTS\s*\(\s*$/i;
13595
- var QUALIFIED_PREFIX = /\w\.$/;
13596
- function hasRealSelectStar(sql) {
13597
- const selects = [...sql.matchAll(SELECT_KEYWORD)].map((m) => m.index);
13598
- for (let pos = 0; pos < sql.length; pos++) {
13599
- if (sql[pos] !== "*" || !isProjectionStar(sql, pos)) {
13600
- continue;
13601
- }
13602
- const owning = selects.filter((start) => start < pos).at(-1);
13603
- if (owning !== void 0 && !EXISTS_BEFORE.test(sql.slice(0, owning))) {
13604
- return true;
13605
- }
13643
+ var QUERY_SELECT = /\bSELECT\b/i;
13644
+ var QUERY_FROM = /\bFROM\b/i;
13645
+ var SQL_TOKEN = /[A-Za-z_\u0080-\u{10FFFF}][A-Za-z0-9_$\u0080-\u{10FFFF}]*|\d+(?:\.\d+)?|\.[ \t\r\n\f\v]*\[(?:[^\]]|\]\])*\]|[-+*/<>=~!@#%^&|\x60?]+|[(),.;]/gu;
13646
+ var ASCII_WORD = /^[A-Za-z_]+$/u;
13647
+ var AMBIGUOUS_QUOTING = /[\x60[\]\\]/u;
13648
+ var CONSERVATIVE_SELECT = /\bSELECT\b/gi;
13649
+ var CONSERVATIVE_FROM = /FROM\b/iy;
13650
+ var ASCII_IDENTIFIER_CHARACTER = /[A-Za-z0-9_]/u;
13651
+ var SQL_WHITESPACE = /\s/u;
13652
+ var EXISTS_KEYWORD_LENGTH = 6;
13653
+ var PROJECTION_END = /\s*(?:[,)]|FROM(?![A-Za-z0-9_$\u0080-\u{10FFFF}])|$)/iyu;
13654
+ var SELECT_PREFIX_WORDS = /* @__PURE__ */ new Set([
13655
+ "ALL",
13656
+ "DISTINCT",
13657
+ "HIGH_PRIORITY",
13658
+ "PERCENT",
13659
+ "SQL_BIG_RESULT",
13660
+ "SQL_BUFFER_RESULT",
13661
+ "SQL_CACHE",
13662
+ "SQL_CALC_FOUND_ROWS",
13663
+ "SQL_NO_CACHE",
13664
+ "SQL_SMALL_RESULT",
13665
+ "STRAIGHT_JOIN",
13666
+ "TIES",
13667
+ "TOP",
13668
+ "WITH"
13669
+ ]);
13670
+ function hasQueryShape(sql) {
13671
+ return QUERY_SELECT.test(sql) && QUERY_FROM.test(sql);
13672
+ }
13673
+ function hasRealSelectStar(sql, original = sql, conservativeSource = sql) {
13674
+ const conservative = AMBIGUOUS_QUOTING.test(original) ? { source: conservativeSource, selects: [...conservativeSource.matchAll(CONSERVATIVE_SELECT)].map((match) => ({ offset: match.index, allowsStar: selectAllowsStar(conservativeSource, match.index) })) } : void 0;
13675
+ const strictSuffix = /[\x60[\]\\?]/u.test(original);
13676
+ const projections = /* @__PURE__ */ new Map();
13677
+ const existsScopes = /* @__PURE__ */ new Set();
13678
+ let depth = 0;
13679
+ let previous = "";
13680
+ let prior = "";
13681
+ for (const match of sql.matchAll(SQL_TOKEN)) {
13682
+ const raw = match[0];
13683
+ const token = ASCII_WORD.test(raw) ? raw.toUpperCase() : raw;
13684
+ if (token === ")") {
13685
+ projections.delete(depth);
13686
+ existsScopes.delete(depth);
13687
+ depth = Math.max(0, depth - 1);
13688
+ }
13689
+ const projection = projections.get(depth);
13690
+ if (isKeyword(token, previous, "SELECT")) {
13691
+ projections.set(depth, { exists: existsScopes.size > 0, strictSuffix, conservative, prefix: [], star: false });
13692
+ } else if (isKeyword(token, previous, "FROM")) {
13693
+ if (projection?.star === true) return true;
13694
+ projections.delete(depth);
13695
+ } else if (token === ";") {
13696
+ projections.clear();
13697
+ existsScopes.clear();
13698
+ } else if (projection !== void 0) {
13699
+ observeProjection(sql, match, previous, projection);
13700
+ }
13701
+ if (token === "(") {
13702
+ depth += 1;
13703
+ if (isKeyword(previous, prior, "EXISTS")) existsScopes.add(depth);
13704
+ }
13705
+ prior = previous;
13706
+ previous = token;
13606
13707
  }
13607
13708
  return false;
13608
13709
  }
13609
- function isProjectionStar(sql, pos) {
13610
- if (QUALIFIED_PREFIX.test(sql.slice(0, pos))) {
13611
- return true;
13612
- }
13613
- let before = pos - 1;
13614
- while (before >= 0 && /\s/.test(sql[before] ?? "")) {
13615
- before -= 1;
13616
- }
13617
- let after = pos + 1;
13618
- while (after < sql.length && /\s/.test(sql[after] ?? "")) {
13619
- after += 1;
13620
- }
13621
- const beforeChar = before >= 0 ? sql[before] ?? "" : "";
13622
- const afterChar = after < sql.length ? sql[after] ?? "" : "";
13623
- const terminates = afterChar === "" || afterChar === "," || afterChar === ")" || FROM_KEYWORD.test(sql.slice(after));
13624
- if (!terminates) {
13625
- return false;
13626
- }
13627
- return !(beforeChar === "(" && afterChar === ")");
13710
+ function selectAllowsStar(source, offset) {
13711
+ const opening = skipWhitespace(source, offset - 1, -1);
13712
+ if (source[opening] !== "(") return true;
13713
+ const wordEnd = skipWhitespace(source, opening - 1, -1);
13714
+ const wordStart = wordEnd - EXISTS_KEYWORD_LENGTH + 1;
13715
+ return wordStart < 0 || source.slice(wordStart, wordEnd + 1).toUpperCase() !== "EXISTS" || wordStart !== 0 && ASCII_IDENTIFIER_CHARACTER.test(source[wordStart - 1] ?? "");
13716
+ }
13717
+ function isKeyword(token, previous, keyword) {
13718
+ return token === keyword && previous !== ".";
13719
+ }
13720
+ function observeProjection(sql, match, previous, projection) {
13721
+ if (isProjectionStar(sql, match, previous, projection)) {
13722
+ if (projection.conservative === void 0 || conservativeProjectionStar(projection.conservative, match.index)) {
13723
+ projection.star = true;
13724
+ }
13725
+ } else if (projection.prefix.length < 2) {
13726
+ const raw = match[0];
13727
+ projection.prefix.push(ASCII_WORD.test(raw) ? raw.toUpperCase() : raw);
13728
+ }
13729
+ }
13730
+ function conservativeProjectionStar(query, offset) {
13731
+ const { source, selects } = query;
13732
+ let low = 0;
13733
+ let high = selects.length;
13734
+ while (low < high) {
13735
+ const middle = Math.floor((low + high) / 2);
13736
+ const start = selects[middle];
13737
+ if (start !== void 0 && start.offset < offset) low = middle + 1;
13738
+ else high = middle;
13739
+ }
13740
+ const owning = selects[low - 1];
13741
+ if (owning?.allowsStar !== true) return false;
13742
+ if (source[offset - 1] === "." && ASCII_IDENTIFIER_CHARACTER.test(source[offset - 2] ?? "")) return true;
13743
+ const before = skipWhitespace(source, offset - 1, -1);
13744
+ const after = skipWhitespace(source, offset + 1, 1);
13745
+ const afterChar = source[after] ?? "";
13746
+ CONSERVATIVE_FROM.lastIndex = after;
13747
+ return (afterChar === "" || afterChar === "," || afterChar === ")" || CONSERVATIVE_FROM.test(source)) && !(source[before] === "(" && afterChar === ")");
13748
+ }
13749
+ function skipWhitespace(source, offset, direction) {
13750
+ let cursor = offset;
13751
+ while (cursor >= 0 && cursor < source.length && SQL_WHITESPACE.test(source[cursor] ?? "")) cursor += direction;
13752
+ return cursor;
13753
+ }
13754
+ function isProjectionStar(sql, match, previous, projection) {
13755
+ if (match[0] !== "*" || projection.exists) return false;
13756
+ const { prefix } = projection;
13757
+ if (previous === ".") return true;
13758
+ if (!projection.strictSuffix && (prefix.length === 0 || previous === ",")) return true;
13759
+ if (!endsProjection(sql, match.index + 1)) return false;
13760
+ return previous === "SELECT" || previous === "," || SELECT_PREFIX_WORDS.has(previous) || previous === ")" && (prefix[0] === "DISTINCT" && prefix[1] === "ON" || prefix[0] === "TOP");
13761
+ }
13762
+ function endsProjection(sql, offset) {
13763
+ PROJECTION_END.lastIndex = offset;
13764
+ return PROJECTION_END.test(sql);
13628
13765
  }
13629
13766
  var no_select_star_default = createRule({
13630
13767
  name: "no-select-star",
@@ -13644,12 +13781,12 @@ var no_select_star_default = createRule({
13644
13781
  if (isTestFile(context)) {
13645
13782
  return {};
13646
13783
  }
13647
- return createSqlListener((sql, node) => {
13648
- if (!QUERY_SHAPE.test(sql) || !hasRealSelectStar(sql)) {
13649
- return;
13650
- }
13784
+ return createSqlListener((sql, node, original) => {
13785
+ if (!hasQueryShape(sql)) return;
13786
+ const conservativeSource = AMBIGUOUS_QUOTING.test(original) ? stripSqlNoise(original) : sql;
13787
+ if (!hasRealSelectStar(sql, original, conservativeSource)) return;
13651
13788
  context.report({ node, messageId: "noSelectStar" });
13652
- });
13789
+ }, { preserveQuotedTokens: true });
13653
13790
  }
13654
13791
  });
13655
13792
 
@@ -20652,16 +20789,42 @@ var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
20652
20789
  remediation: "On a supported Node runtime, consider hash(algorithm, value, encoding). Preserve the output encoding explicitly: digest() returns a Buffer, while hash defaults to hex. Keep createHash for streams or multiple updates.",
20653
20790
  category: "performance",
20654
20791
  limitations: [
20655
- "Only bindings and inline calls with statically proven provenance from crypto or node:crypto are analyzed; arbitrary assignments and dynamic module specifiers are excluded.",
20792
+ "Only direct imports, const bindings, and inline calls with statically proven provenance from crypto or node:crypto are analyzed; arbitrary assignments and dynamic module specifiers are excluded.",
20656
20793
  "Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid.",
20794
+ "Namespace bindings must remain read-only: visible writes, aliases, exports, inherited mutation or alias methods, unknown computed members, and other escaping uses are excluded; type-only references and direct member calls remain eligible.",
20657
20795
  "Runtime support and output encoding require manual review; no autofix or guaranteed speedup is promised."
20658
20796
  ],
20659
20797
  references: ["https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options"],
20660
20798
  examples: [
20661
20799
  { id: "one-shot-hash", title: "Use Node's one-shot hash API", outcome: "no-match", files: [{ path: "case.ts", source: "import { hash } from 'node:crypto'; export const digest = hash('sha256', 'value', 'hex');" }], focusPath: "case.ts", expectedCount: 0, public: true },
20662
- { id: "mutable-one-shot-chain", title: "Avoid mutable state for one value", outcome: "match", files: [{ path: "case.ts", source: "import { createHash } from 'node:crypto'; export const digest = createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 1, public: true }
20800
+ { id: "mutable-one-shot-chain", title: "Avoid mutable state for one value", outcome: "match", files: [{ path: "case.ts", source: "import { createHash } from 'node:crypto'; export const digest = createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 1, public: true },
20801
+ { id: "native-namespace", scenarioId: "stable-native-namespace", title: "An unchanged namespace retains native provenance", outcome: "match", files: [{ path: "case.ts", source: "const crypto = require('node:crypto'); export const digest = crypto.createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 1, public: true },
20802
+ { id: "replaced-namespace-member", scenarioId: "stable-native-namespace", title: "A replaced member has a different contract", outcome: "no-match", files: [{ path: "case.ts", source: "const crypto = require('node:crypto'); crypto.createHash = () => ({ update() { return this; }, digest() { return 'custom'; } }); export const digest = crypto.createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 0, public: true }
20663
20803
  ]
20664
20804
  };
20805
+ var UNSAFE_NAMESPACE_METHODS = /* @__PURE__ */ new Set(["__defineGetter__", "__defineSetter__", "valueOf"]);
20806
+ function stableNamespace(variable) {
20807
+ if (variable.defs.some((definition) => definition.type === "Variable" && definition.parent.parent.type === AST_NODE_TYPES99.ExportNamedDeclaration)) return false;
20808
+ return stableValueBinding(variable, (reference) => {
20809
+ if (reference.isTypeReference && !reference.isValueReference) return true;
20810
+ const receiver = outerExpression(reference.identifier);
20811
+ if (typeQueryReference(receiver)) return true;
20812
+ const member = receiver.parent;
20813
+ if (member?.type !== AST_NODE_TYPES99.MemberExpression || member.object !== receiver) {
20814
+ return member?.type === AST_NODE_TYPES99.UnaryExpression && member.operator === "typeof";
20815
+ }
20816
+ const use = outerExpression(member);
20817
+ const name = ASTUtils61.getPropertyName(member);
20818
+ return name !== null && !UNSAFE_NAMESPACE_METHODS.has(name) && use.parent?.type === AST_NODE_TYPES99.CallExpression && use.parent.callee === use;
20819
+ });
20820
+ }
20821
+ function typeQueryReference(node) {
20822
+ let current = node;
20823
+ while (current.parent?.type === AST_NODE_TYPES99.TSQualifiedName && current.parent.left === current) {
20824
+ current = current.parent;
20825
+ }
20826
+ return current.parent?.type === AST_NODE_TYPES99.TSTypeQuery && current.parent.exprName === current;
20827
+ }
20665
20828
  function isCryptoLoader(node, resolve2) {
20666
20829
  const unwrappedNodeCallee = node.type === "CallExpression" || node.type === "NewExpression" ? unwrapExpression(node.callee) : null;
20667
20830
  if (node.type !== AST_NODE_TYPES99.CallExpression || node.arguments.length !== 1) return false;
@@ -20704,7 +20867,7 @@ var prefer_node_crypto_hash_default = createRule({
20704
20867
  }
20705
20868
  function record(identifier, destination) {
20706
20869
  const variable = resolve2(identifier);
20707
- if (variable !== null) destination.add(variable);
20870
+ if (variable !== null && (destination === namespaceBindings ? stableNamespace(variable) : stableValueBinding(variable))) destination.add(variable);
20708
20871
  }
20709
20872
  return {
20710
20873
  ImportDeclaration(node) {
@@ -20846,11 +21009,11 @@ var prefer_node_fs_promises_default = createRule({
20846
21009
  };
20847
21010
  const isNamespace = (node) => {
20848
21011
  const binding = bindingOf(node);
20849
- return binding !== null && namespaces.has(binding) && !binding.references.some((reference) => reference.isWrite() && reference.init !== true);
21012
+ return binding !== null && namespaces.has(binding);
20850
21013
  };
20851
21014
  const recordNamespace = (node) => {
20852
21015
  const binding = bindingOf(node);
20853
- if (binding !== null) namespaces.add(binding);
21016
+ if (binding !== null && stableValueBinding(binding)) namespaces.add(binding);
20854
21017
  };
20855
21018
  return {
20856
21019
  ImportDeclaration(node) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.23.2",
3
+ "version": "16.26.0",
4
4
  "packageManager": "npm@12.2.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",