@sarj/eslint-plugin 16.23.3 → 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 +2142 -1993
- package/dist/index.js +219 -70
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -836,7 +836,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
836
836
|
// package.json
|
|
837
837
|
var package_default = {
|
|
838
838
|
name: "@sarj/eslint-plugin",
|
|
839
|
-
version: "16.
|
|
839
|
+
version: "16.26.0",
|
|
840
840
|
packageManager: "npm@12.2.0",
|
|
841
841
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
842
842
|
type: "module",
|
|
@@ -7275,32 +7275,39 @@ import { ASTUtils as ASTUtils21, AST_NODE_TYPES as AST_NODE_TYPES40 } from "@typ
|
|
|
7275
7275
|
|
|
7276
7276
|
// src/rules/_sql.ts
|
|
7277
7277
|
import { AST_NODE_TYPES as AST_NODE_TYPES39, ASTUtils as ASTUtils20 } from "@typescript-eslint/utils";
|
|
7278
|
-
|
|
7279
|
-
|
|
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);
|
|
7280
7284
|
}
|
|
7281
7285
|
function sqlNoiseInfo(text) {
|
|
7282
7286
|
const singleQuotedRanges = [];
|
|
7283
7287
|
const masked = scanSqlNoise(text, (start, end) => singleQuotedRanges.push([start, end]));
|
|
7284
7288
|
return { text: masked, singleQuotedRanges };
|
|
7285
7289
|
}
|
|
7286
|
-
function scanSqlNoise(text, onSingleQuoted) {
|
|
7290
|
+
function scanSqlNoise(text, onSingleQuoted, preserveQuotedTokens = false) {
|
|
7287
7291
|
const out = text.split("");
|
|
7288
7292
|
let index = 0;
|
|
7289
7293
|
while (index < text.length) {
|
|
7290
7294
|
const dollarEnd = dollarQuotedSqlEnd(text, index);
|
|
7291
7295
|
if (dollarEnd !== null) {
|
|
7292
7296
|
maskSqlRange(text, out, index, dollarEnd);
|
|
7297
|
+
if (preserveQuotedTokens) out[index] = "?";
|
|
7293
7298
|
index = dollarEnd;
|
|
7294
7299
|
continue;
|
|
7295
7300
|
}
|
|
7296
7301
|
const character = text[index];
|
|
7297
7302
|
if (character === "'" || character === '"') {
|
|
7303
|
+
const start = index;
|
|
7298
7304
|
index = quotedSqlEnd(text, out, index, onSingleQuoted);
|
|
7305
|
+
if (preserveQuotedTokens) out[start] = "?";
|
|
7299
7306
|
continue;
|
|
7300
7307
|
}
|
|
7301
7308
|
if (character === "-" && text[index + 1] === "-") {
|
|
7302
|
-
|
|
7303
|
-
const end =
|
|
7309
|
+
SQL_LINE_END.lastIndex = index;
|
|
7310
|
+
const end = SQL_LINE_END.exec(text)?.index ?? text.length;
|
|
7304
7311
|
maskSqlRange(text, out, index, end);
|
|
7305
7312
|
index = end;
|
|
7306
7313
|
continue;
|
|
@@ -7314,8 +7321,9 @@ function scanSqlNoise(text, onSingleQuoted) {
|
|
|
7314
7321
|
return out.join("");
|
|
7315
7322
|
}
|
|
7316
7323
|
function dollarQuotedSqlEnd(text, start) {
|
|
7317
|
-
if (text[start] !== "$" ||
|
|
7318
|
-
|
|
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];
|
|
7319
7327
|
if (delimiter === void 0) return null;
|
|
7320
7328
|
const closing = text.indexOf(delimiter, start + delimiter.length);
|
|
7321
7329
|
return closing < 0 ? text.length : closing + delimiter.length;
|
|
@@ -7333,17 +7341,24 @@ function blockSqlCommentEnd(text, out, start) {
|
|
|
7333
7341
|
index += 2;
|
|
7334
7342
|
continue;
|
|
7335
7343
|
}
|
|
7336
|
-
if (text[index] !== "\n") out[index] = " ";
|
|
7344
|
+
if (text[index] !== "\n" && text[index] !== "\r") out[index] = " ";
|
|
7337
7345
|
index += 1;
|
|
7338
7346
|
}
|
|
7339
7347
|
return index;
|
|
7340
7348
|
}
|
|
7341
7349
|
function quotedSqlEnd(text, out, start, onSingleQuoted) {
|
|
7342
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;
|
|
7343
7353
|
out[start] = " ";
|
|
7344
7354
|
let index = start + 1;
|
|
7345
7355
|
while (index < text.length) {
|
|
7346
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
|
+
}
|
|
7347
7362
|
if (character === quote) {
|
|
7348
7363
|
out[index] = " ";
|
|
7349
7364
|
if (index + 1 < text.length && text[index + 1] === quote) {
|
|
@@ -7352,17 +7367,29 @@ function quotedSqlEnd(text, out, start, onSingleQuoted) {
|
|
|
7352
7367
|
continue;
|
|
7353
7368
|
}
|
|
7354
7369
|
index += 1;
|
|
7355
|
-
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
|
+
}
|
|
7356
7378
|
break;
|
|
7357
7379
|
}
|
|
7358
|
-
if (character !== "\n") out[index] = " ";
|
|
7380
|
+
if (character !== "\n" && character !== "\r") out[index] = " ";
|
|
7359
7381
|
index += 1;
|
|
7360
7382
|
}
|
|
7361
7383
|
return index;
|
|
7362
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
|
+
}
|
|
7363
7390
|
function maskSqlRange(text, out, start, end) {
|
|
7364
7391
|
for (let index = start; index < end; index += 1) {
|
|
7365
|
-
if (text[index] !== "\n") out[index] = " ";
|
|
7392
|
+
if (text[index] !== "\n" && text[index] !== "\r") out[index] = " ";
|
|
7366
7393
|
}
|
|
7367
7394
|
}
|
|
7368
7395
|
var SUBSTITUTION_MARKER = "?";
|
|
@@ -7428,7 +7455,7 @@ function markConsumed(node, consumed) {
|
|
|
7428
7455
|
markConsumed(node.quasi, consumed);
|
|
7429
7456
|
}
|
|
7430
7457
|
}
|
|
7431
|
-
function createSqlListener(handler) {
|
|
7458
|
+
function createSqlListener(handler, options) {
|
|
7432
7459
|
const consumed = /* @__PURE__ */ new WeakSet();
|
|
7433
7460
|
const visit = (node) => {
|
|
7434
7461
|
if (consumed.has(node) || isDiscardedSqlText(node)) {
|
|
@@ -7439,7 +7466,7 @@ function createSqlListener(handler) {
|
|
|
7439
7466
|
return;
|
|
7440
7467
|
}
|
|
7441
7468
|
markConsumed(node, consumed);
|
|
7442
|
-
handler(stripSqlNoise(text), node);
|
|
7469
|
+
handler(stripSqlNoise(text, options), node, text);
|
|
7443
7470
|
};
|
|
7444
7471
|
return {
|
|
7445
7472
|
BinaryExpression: (node) => {
|
|
@@ -8783,6 +8810,20 @@ import {
|
|
|
8783
8810
|
AST_NODE_TYPES as AST_NODE_TYPES45,
|
|
8784
8811
|
ASTUtils as ASTUtils24
|
|
8785
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
|
|
8786
8827
|
var NO_DETACHED_GLOBAL_FETCH_DOCUMENTATION = {
|
|
8787
8828
|
summary: "Keep the ambient global fetch receiver-safe when storing or explicitly rebinding it.",
|
|
8788
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.",
|
|
@@ -8851,14 +8892,9 @@ var no_detached_global_fetch_default = createRule({
|
|
|
8851
8892
|
function isGlobalReceiver(node) {
|
|
8852
8893
|
return node.type === AST_NODE_TYPES45.Identifier && GLOBAL_RECEIVERS.has(node.name) && resolvesToGlobal(node);
|
|
8853
8894
|
}
|
|
8854
|
-
function isStableAlias(variable) {
|
|
8855
|
-
return !variable.references.some(
|
|
8856
|
-
(reference) => reference.isWrite() && reference.init !== true
|
|
8857
|
-
);
|
|
8858
|
-
}
|
|
8859
8895
|
function recordAlias(identifier) {
|
|
8860
8896
|
const variable = bindingOf(identifier);
|
|
8861
|
-
if (variable !== null &&
|
|
8897
|
+
if (variable !== null && stableValueBinding(variable)) aliases.add(variable);
|
|
8862
8898
|
}
|
|
8863
8899
|
function staticMemberName7(node) {
|
|
8864
8900
|
if (!node.computed) {
|
|
@@ -8871,7 +8907,7 @@ var no_detached_global_fetch_default = createRule({
|
|
|
8871
8907
|
if (expression.type === AST_NODE_TYPES45.Identifier) {
|
|
8872
8908
|
if (expression.name === "fetch" && resolvesToGlobal(expression)) return true;
|
|
8873
8909
|
const variable = bindingOf(expression);
|
|
8874
|
-
return variable !== null && aliases.has(variable)
|
|
8910
|
+
return variable !== null && aliases.has(variable);
|
|
8875
8911
|
}
|
|
8876
8912
|
if (expression.type === AST_NODE_TYPES45.MemberExpression) {
|
|
8877
8913
|
return isGlobalFetchMember(expression);
|
|
@@ -12624,18 +12660,18 @@ var no_router_refresh_polling_default = createRule({
|
|
|
12624
12660
|
if (hookCallee.type !== AST_NODE_TYPES62.Identifier) return;
|
|
12625
12661
|
const hook = ASTUtils39.findVariable(context.sourceCode.getScope(hookCallee), hookCallee.name);
|
|
12626
12662
|
const router = ASTUtils39.findVariable(context.sourceCode.getScope(node.id), node.id.name);
|
|
12627
|
-
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);
|
|
12628
12664
|
}
|
|
12629
12665
|
},
|
|
12630
12666
|
CallExpression(node) {
|
|
12631
12667
|
const unwrappedNodeCallee = unwrapExpression(node.callee);
|
|
12632
12668
|
const receiver = unwrappedNodeCallee.type === AST_NODE_TYPES62.MemberExpression ? unwrapExpression(unwrappedNodeCallee.object) : unwrappedNodeCallee;
|
|
12633
|
-
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;
|
|
12634
12670
|
const router = ASTUtils39.findVariable(
|
|
12635
12671
|
context.sourceCode.getScope(receiver),
|
|
12636
12672
|
receiver.name
|
|
12637
12673
|
);
|
|
12638
|
-
if (router === null || !routers.has(router)
|
|
12674
|
+
if (router === null || !routers.has(router)) return;
|
|
12639
12675
|
const callback = enclosingIntervalCallback(context.sourceCode, node);
|
|
12640
12676
|
if (callback !== null && !reportedCallbacks.has(callback)) {
|
|
12641
12677
|
reportedCallbacks.add(callback);
|
|
@@ -13594,51 +13630,138 @@ var NO_SELECT_STAR_DOCUMENTATION = {
|
|
|
13594
13630
|
rationale: "Wildcard projections couple row shape and query cost to unrelated schema changes.",
|
|
13595
13631
|
remediation: "List every required column explicitly in the projection.",
|
|
13596
13632
|
category: "correctness",
|
|
13597
|
-
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."],
|
|
13598
13634
|
examples: [
|
|
13599
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 },
|
|
13600
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 },
|
|
13601
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 },
|
|
13602
|
-
{ 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 }
|
|
13603
13641
|
]
|
|
13604
13642
|
};
|
|
13605
|
-
var
|
|
13606
|
-
var
|
|
13607
|
-
var
|
|
13608
|
-
var
|
|
13609
|
-
var
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
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;
|
|
13620
13707
|
}
|
|
13621
13708
|
return false;
|
|
13622
13709
|
}
|
|
13623
|
-
function
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
|
|
13637
|
-
|
|
13638
|
-
if (
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
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);
|
|
13642
13765
|
}
|
|
13643
13766
|
var no_select_star_default = createRule({
|
|
13644
13767
|
name: "no-select-star",
|
|
@@ -13658,12 +13781,12 @@ var no_select_star_default = createRule({
|
|
|
13658
13781
|
if (isTestFile(context)) {
|
|
13659
13782
|
return {};
|
|
13660
13783
|
}
|
|
13661
|
-
return createSqlListener((sql, node) => {
|
|
13662
|
-
if (!
|
|
13663
|
-
|
|
13664
|
-
|
|
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;
|
|
13665
13788
|
context.report({ node, messageId: "noSelectStar" });
|
|
13666
|
-
});
|
|
13789
|
+
}, { preserveQuotedTokens: true });
|
|
13667
13790
|
}
|
|
13668
13791
|
});
|
|
13669
13792
|
|
|
@@ -20666,16 +20789,42 @@ var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
|
|
|
20666
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.",
|
|
20667
20790
|
category: "performance",
|
|
20668
20791
|
limitations: [
|
|
20669
|
-
"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.",
|
|
20670
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.",
|
|
20671
20795
|
"Runtime support and output encoding require manual review; no autofix or guaranteed speedup is promised."
|
|
20672
20796
|
],
|
|
20673
20797
|
references: ["https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options"],
|
|
20674
20798
|
examples: [
|
|
20675
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 },
|
|
20676
|
-
{ 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 }
|
|
20677
20803
|
]
|
|
20678
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
|
+
}
|
|
20679
20828
|
function isCryptoLoader(node, resolve2) {
|
|
20680
20829
|
const unwrappedNodeCallee = node.type === "CallExpression" || node.type === "NewExpression" ? unwrapExpression(node.callee) : null;
|
|
20681
20830
|
if (node.type !== AST_NODE_TYPES99.CallExpression || node.arguments.length !== 1) return false;
|
|
@@ -20718,7 +20867,7 @@ var prefer_node_crypto_hash_default = createRule({
|
|
|
20718
20867
|
}
|
|
20719
20868
|
function record(identifier, destination) {
|
|
20720
20869
|
const variable = resolve2(identifier);
|
|
20721
|
-
if (variable !== null) destination.add(variable);
|
|
20870
|
+
if (variable !== null && (destination === namespaceBindings ? stableNamespace(variable) : stableValueBinding(variable))) destination.add(variable);
|
|
20722
20871
|
}
|
|
20723
20872
|
return {
|
|
20724
20873
|
ImportDeclaration(node) {
|
|
@@ -20860,11 +21009,11 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
20860
21009
|
};
|
|
20861
21010
|
const isNamespace = (node) => {
|
|
20862
21011
|
const binding = bindingOf(node);
|
|
20863
|
-
return binding !== null && namespaces.has(binding)
|
|
21012
|
+
return binding !== null && namespaces.has(binding);
|
|
20864
21013
|
};
|
|
20865
21014
|
const recordNamespace = (node) => {
|
|
20866
21015
|
const binding = bindingOf(node);
|
|
20867
|
-
if (binding !== null) namespaces.add(binding);
|
|
21016
|
+
if (binding !== null && stableValueBinding(binding)) namespaces.add(binding);
|
|
20868
21017
|
};
|
|
20869
21018
|
return {
|
|
20870
21019
|
ImportDeclaration(node) {
|