@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.
- package/dist/index.cjs +2157 -1994
- package/dist/index.js +235 -72
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
7265
|
-
|
|
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
|
-
|
|
7289
|
-
const end =
|
|
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] !== "$" ||
|
|
7304
|
-
|
|
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?.(
|
|
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 &&
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
13592
|
-
var
|
|
13593
|
-
var
|
|
13594
|
-
var
|
|
13595
|
-
var
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
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
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
if (
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
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 (!
|
|
13649
|
-
|
|
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)
|
|
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) {
|