@sarj/eslint-plugin 16.13.0 → 16.13.1
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 +46 -42
- package/dist/index.js +46 -42
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -52,7 +52,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
52
52
|
// package.json
|
|
53
53
|
var package_default = {
|
|
54
54
|
name: "@sarj/eslint-plugin",
|
|
55
|
-
version: "16.13.
|
|
55
|
+
version: "16.13.1",
|
|
56
56
|
packageManager: "npm@12.2.0",
|
|
57
57
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
58
58
|
type: "module",
|
|
@@ -5493,31 +5493,48 @@ function sqlTextOf(node) {
|
|
|
5493
5493
|
const right = sqlTextOf(node.right);
|
|
5494
5494
|
return left !== null && right !== null ? left + right : null;
|
|
5495
5495
|
}
|
|
5496
|
-
case import_utils32.AST_NODE_TYPES.ArrayExpression:
|
|
5497
|
-
|
|
5498
|
-
for (const element of node.elements) {
|
|
5499
|
-
if (element === null) {
|
|
5500
|
-
return null;
|
|
5501
|
-
}
|
|
5502
|
-
const part = sqlTextOf(element);
|
|
5503
|
-
if (part === null) {
|
|
5504
|
-
return null;
|
|
5505
|
-
}
|
|
5506
|
-
parts.push(part);
|
|
5507
|
-
}
|
|
5508
|
-
return parts.length > 0 ? parts.join(" ") : null;
|
|
5509
|
-
}
|
|
5496
|
+
case import_utils32.AST_NODE_TYPES.ArrayExpression:
|
|
5497
|
+
return fragmentArrayText(node);
|
|
5510
5498
|
default:
|
|
5511
5499
|
return null;
|
|
5512
5500
|
}
|
|
5513
5501
|
}
|
|
5514
|
-
function
|
|
5502
|
+
function fragmentArrayText(node) {
|
|
5503
|
+
const join4 = joinedFragmentCall(node);
|
|
5504
|
+
const separator = join4 === null ? " " : join4.arguments.length === 0 ? "," : join4.arguments.length === 1 ? staticString(join4.arguments[0]) : null;
|
|
5505
|
+
if (separator === null) return null;
|
|
5506
|
+
const parts = [];
|
|
5507
|
+
for (const element of node.elements) {
|
|
5508
|
+
if (element === null || join4 !== null && element.type === import_utils32.AST_NODE_TYPES.ArrayExpression) {
|
|
5509
|
+
return null;
|
|
5510
|
+
}
|
|
5511
|
+
const part = sqlTextOf(element);
|
|
5512
|
+
if (part === null) {
|
|
5513
|
+
return null;
|
|
5514
|
+
}
|
|
5515
|
+
parts.push(part);
|
|
5516
|
+
}
|
|
5517
|
+
return parts.length > 0 ? parts.join(separator) : null;
|
|
5518
|
+
}
|
|
5519
|
+
function joinedFragmentCall(node) {
|
|
5515
5520
|
const parent = node.parent;
|
|
5516
|
-
return parent?.type === import_utils32.AST_NODE_TYPES.MemberExpression && parent.object === node && !parent.computed && parent.property.type === import_utils32.AST_NODE_TYPES.Identifier && parent.property.name === "join" && parent.parent?.type === import_utils32.AST_NODE_TYPES.CallExpression;
|
|
5521
|
+
return parent?.type === import_utils32.AST_NODE_TYPES.MemberExpression && parent.object === node && !parent.computed && parent.property.type === import_utils32.AST_NODE_TYPES.Identifier && parent.property.name === "join" && parent.parent?.type === import_utils32.AST_NODE_TYPES.CallExpression && parent.parent.callee === parent ? parent.parent : null;
|
|
5517
5522
|
}
|
|
5518
5523
|
function markConsumed(node, consumed) {
|
|
5519
5524
|
consumed.add(node);
|
|
5520
|
-
|
|
5525
|
+
const value = unwrapExpression(node);
|
|
5526
|
+
if (value !== node) {
|
|
5527
|
+
markConsumed(value, consumed);
|
|
5528
|
+
return;
|
|
5529
|
+
}
|
|
5530
|
+
if (node.type === import_utils32.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
|
|
5531
|
+
markConsumed(node.left, consumed);
|
|
5532
|
+
markConsumed(node.right, consumed);
|
|
5533
|
+
} else if (node.type === import_utils32.AST_NODE_TYPES.ArrayExpression) {
|
|
5534
|
+
for (const element of node.elements) if (element !== null) markConsumed(element, consumed);
|
|
5535
|
+
} else if (node.type === import_utils32.AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
5536
|
+
markConsumed(node.quasi, consumed);
|
|
5537
|
+
}
|
|
5521
5538
|
}
|
|
5522
5539
|
function createSqlListener(handler) {
|
|
5523
5540
|
const consumed = /* @__PURE__ */ new WeakSet();
|
|
@@ -5537,9 +5554,12 @@ function createSqlListener(handler) {
|
|
|
5537
5554
|
visit(node);
|
|
5538
5555
|
},
|
|
5539
5556
|
ArrayExpression: (node) => {
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5557
|
+
const join4 = joinedFragmentCall(node);
|
|
5558
|
+
if (join4 === null) return;
|
|
5559
|
+
visit(node);
|
|
5560
|
+
markConsumed(node, consumed);
|
|
5561
|
+
const separator = join4.arguments[0];
|
|
5562
|
+
if (separator !== void 0 && staticString(separator) !== null) markConsumed(separator, consumed);
|
|
5543
5563
|
},
|
|
5544
5564
|
TemplateLiteral: (node) => {
|
|
5545
5565
|
visit(node);
|
|
@@ -5570,7 +5590,7 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
|
|
|
5570
5590
|
remediation: "Bind data values through SQL placeholders. Select non-bindable identifiers or clauses from fixed, reviewed fragments instead of interpolating runtime text.",
|
|
5571
5591
|
category: "security",
|
|
5572
5592
|
limitations: [
|
|
5573
|
-
"Template and concatenation fragments are inspected at recognized execution calls. Double-quoted identifiers, comments, and dollar strings are excluded; this is not a general SQL injection detector.",
|
|
5593
|
+
"Template and concatenation fragments are inspected at recognized execution calls, including runtime pieces surrounded by literal text inside a quoted value. Double-quoted identifiers, comments, and dollar strings are excluded; this is not a general SQL injection detector.",
|
|
5574
5594
|
"Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
|
|
5575
5595
|
"The bounded lexer recognizes doubled quotes, comments, and PostgreSQL dollar strings; dialect-specific escape modes and SQL generated through other APIs require separate security review."
|
|
5576
5596
|
],
|
|
@@ -5618,6 +5638,7 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
|
|
|
5618
5638
|
var DEFAULT_METHODS = ["prepare", "exec", "query"];
|
|
5619
5639
|
var CONSTANT_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
5620
5640
|
function isStaticFragment(expression) {
|
|
5641
|
+
expression = unwrapExpression(expression);
|
|
5621
5642
|
if (expression.type === import_utils33.AST_NODE_TYPES.Identifier) {
|
|
5622
5643
|
return CONSTANT_CASE_RE.test(expression.name);
|
|
5623
5644
|
}
|
|
@@ -5645,28 +5666,13 @@ function runtimeInterpolations(template) {
|
|
|
5645
5666
|
const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
|
|
5646
5667
|
offset += RUNTIME_MARKER.length;
|
|
5647
5668
|
if (isStaticFragment(expression)) return [];
|
|
5648
|
-
if (inValue
|
|
5669
|
+
if (inValue) {
|
|
5649
5670
|
return [{ expression, messageId: "dynamicSql" }];
|
|
5650
5671
|
}
|
|
5651
5672
|
return unquoted ? [{ expression, messageId: "dynamicFragment" }] : [];
|
|
5652
5673
|
}
|
|
5653
5674
|
);
|
|
5654
5675
|
}
|
|
5655
|
-
function endsWithSqlQuote(text) {
|
|
5656
|
-
return /'\s*$/u.test(text);
|
|
5657
|
-
}
|
|
5658
|
-
function startsWithSqlQuote(text) {
|
|
5659
|
-
return /^\s*'/u.test(text);
|
|
5660
|
-
}
|
|
5661
|
-
function staticLiteralText(node) {
|
|
5662
|
-
if (node.type === import_utils33.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
5663
|
-
return node.value;
|
|
5664
|
-
}
|
|
5665
|
-
if (node.type === import_utils33.AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
|
|
5666
|
-
return node.quasis[0]?.value.raw;
|
|
5667
|
-
}
|
|
5668
|
-
return void 0;
|
|
5669
|
-
}
|
|
5670
5676
|
function runtimeConcatOperands(node) {
|
|
5671
5677
|
if (node.type !== import_utils33.AST_NODE_TYPES.BinaryExpression || node.operator !== "+") {
|
|
5672
5678
|
return [];
|
|
@@ -5678,7 +5684,7 @@ function runtimeConcatOperands(node) {
|
|
|
5678
5684
|
if (!hasStringLiteral) {
|
|
5679
5685
|
return [];
|
|
5680
5686
|
}
|
|
5681
|
-
const parts = operands.map((operand) =>
|
|
5687
|
+
const parts = operands.map((operand) => staticString(operand) ?? RUNTIME_MARKER);
|
|
5682
5688
|
const statement = parts.join("");
|
|
5683
5689
|
const ranges = sqlSingleQuotedRanges(statement);
|
|
5684
5690
|
const visible = stripSqlNoise(statement);
|
|
@@ -5688,9 +5694,7 @@ function runtimeConcatOperands(node) {
|
|
|
5688
5694
|
const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
|
|
5689
5695
|
offset += parts[index]?.length ?? 0;
|
|
5690
5696
|
if (isStaticFragment(operand)) return [];
|
|
5691
|
-
|
|
5692
|
-
const after = operands[index + 1];
|
|
5693
|
-
if (inValue && before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "")) {
|
|
5697
|
+
if (inValue) {
|
|
5694
5698
|
return [{ expression: operand, messageId: "dynamicSql" }];
|
|
5695
5699
|
}
|
|
5696
5700
|
return unquoted ? [{ expression: operand, messageId: "dynamicFragment" }] : [];
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// package.json
|
|
2
2
|
var package_default = {
|
|
3
3
|
name: "@sarj/eslint-plugin",
|
|
4
|
-
version: "16.13.
|
|
4
|
+
version: "16.13.1",
|
|
5
5
|
packageManager: "npm@12.2.0",
|
|
6
6
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
7
7
|
type: "module",
|
|
@@ -5454,31 +5454,48 @@ function sqlTextOf(node) {
|
|
|
5454
5454
|
const right = sqlTextOf(node.right);
|
|
5455
5455
|
return left !== null && right !== null ? left + right : null;
|
|
5456
5456
|
}
|
|
5457
|
-
case AST_NODE_TYPES28.ArrayExpression:
|
|
5458
|
-
|
|
5459
|
-
for (const element of node.elements) {
|
|
5460
|
-
if (element === null) {
|
|
5461
|
-
return null;
|
|
5462
|
-
}
|
|
5463
|
-
const part = sqlTextOf(element);
|
|
5464
|
-
if (part === null) {
|
|
5465
|
-
return null;
|
|
5466
|
-
}
|
|
5467
|
-
parts.push(part);
|
|
5468
|
-
}
|
|
5469
|
-
return parts.length > 0 ? parts.join(" ") : null;
|
|
5470
|
-
}
|
|
5457
|
+
case AST_NODE_TYPES28.ArrayExpression:
|
|
5458
|
+
return fragmentArrayText(node);
|
|
5471
5459
|
default:
|
|
5472
5460
|
return null;
|
|
5473
5461
|
}
|
|
5474
5462
|
}
|
|
5475
|
-
function
|
|
5463
|
+
function fragmentArrayText(node) {
|
|
5464
|
+
const join4 = joinedFragmentCall(node);
|
|
5465
|
+
const separator = join4 === null ? " " : join4.arguments.length === 0 ? "," : join4.arguments.length === 1 ? staticString(join4.arguments[0]) : null;
|
|
5466
|
+
if (separator === null) return null;
|
|
5467
|
+
const parts = [];
|
|
5468
|
+
for (const element of node.elements) {
|
|
5469
|
+
if (element === null || join4 !== null && element.type === AST_NODE_TYPES28.ArrayExpression) {
|
|
5470
|
+
return null;
|
|
5471
|
+
}
|
|
5472
|
+
const part = sqlTextOf(element);
|
|
5473
|
+
if (part === null) {
|
|
5474
|
+
return null;
|
|
5475
|
+
}
|
|
5476
|
+
parts.push(part);
|
|
5477
|
+
}
|
|
5478
|
+
return parts.length > 0 ? parts.join(separator) : null;
|
|
5479
|
+
}
|
|
5480
|
+
function joinedFragmentCall(node) {
|
|
5476
5481
|
const parent = node.parent;
|
|
5477
|
-
return parent?.type === AST_NODE_TYPES28.MemberExpression && parent.object === node && !parent.computed && parent.property.type === AST_NODE_TYPES28.Identifier && parent.property.name === "join" && parent.parent?.type === AST_NODE_TYPES28.CallExpression;
|
|
5482
|
+
return parent?.type === AST_NODE_TYPES28.MemberExpression && parent.object === node && !parent.computed && parent.property.type === AST_NODE_TYPES28.Identifier && parent.property.name === "join" && parent.parent?.type === AST_NODE_TYPES28.CallExpression && parent.parent.callee === parent ? parent.parent : null;
|
|
5478
5483
|
}
|
|
5479
5484
|
function markConsumed(node, consumed) {
|
|
5480
5485
|
consumed.add(node);
|
|
5481
|
-
|
|
5486
|
+
const value = unwrapExpression(node);
|
|
5487
|
+
if (value !== node) {
|
|
5488
|
+
markConsumed(value, consumed);
|
|
5489
|
+
return;
|
|
5490
|
+
}
|
|
5491
|
+
if (node.type === AST_NODE_TYPES28.BinaryExpression && node.operator === "+") {
|
|
5492
|
+
markConsumed(node.left, consumed);
|
|
5493
|
+
markConsumed(node.right, consumed);
|
|
5494
|
+
} else if (node.type === AST_NODE_TYPES28.ArrayExpression) {
|
|
5495
|
+
for (const element of node.elements) if (element !== null) markConsumed(element, consumed);
|
|
5496
|
+
} else if (node.type === AST_NODE_TYPES28.TaggedTemplateExpression) {
|
|
5497
|
+
markConsumed(node.quasi, consumed);
|
|
5498
|
+
}
|
|
5482
5499
|
}
|
|
5483
5500
|
function createSqlListener(handler) {
|
|
5484
5501
|
const consumed = /* @__PURE__ */ new WeakSet();
|
|
@@ -5498,9 +5515,12 @@ function createSqlListener(handler) {
|
|
|
5498
5515
|
visit(node);
|
|
5499
5516
|
},
|
|
5500
5517
|
ArrayExpression: (node) => {
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5518
|
+
const join4 = joinedFragmentCall(node);
|
|
5519
|
+
if (join4 === null) return;
|
|
5520
|
+
visit(node);
|
|
5521
|
+
markConsumed(node, consumed);
|
|
5522
|
+
const separator = join4.arguments[0];
|
|
5523
|
+
if (separator !== void 0 && staticString(separator) !== null) markConsumed(separator, consumed);
|
|
5504
5524
|
},
|
|
5505
5525
|
TemplateLiteral: (node) => {
|
|
5506
5526
|
visit(node);
|
|
@@ -5531,7 +5551,7 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
|
|
|
5531
5551
|
remediation: "Bind data values through SQL placeholders. Select non-bindable identifiers or clauses from fixed, reviewed fragments instead of interpolating runtime text.",
|
|
5532
5552
|
category: "security",
|
|
5533
5553
|
limitations: [
|
|
5534
|
-
"Template and concatenation fragments are inspected at recognized execution calls. Double-quoted identifiers, comments, and dollar strings are excluded; this is not a general SQL injection detector.",
|
|
5554
|
+
"Template and concatenation fragments are inspected at recognized execution calls, including runtime pieces surrounded by literal text inside a quoted value. Double-quoted identifiers, comments, and dollar strings are excluded; this is not a general SQL injection detector.",
|
|
5535
5555
|
"Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
|
|
5536
5556
|
"The bounded lexer recognizes doubled quotes, comments, and PostgreSQL dollar strings; dialect-specific escape modes and SQL generated through other APIs require separate security review."
|
|
5537
5557
|
],
|
|
@@ -5579,6 +5599,7 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
|
|
|
5579
5599
|
var DEFAULT_METHODS = ["prepare", "exec", "query"];
|
|
5580
5600
|
var CONSTANT_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
5581
5601
|
function isStaticFragment(expression) {
|
|
5602
|
+
expression = unwrapExpression(expression);
|
|
5582
5603
|
if (expression.type === AST_NODE_TYPES29.Identifier) {
|
|
5583
5604
|
return CONSTANT_CASE_RE.test(expression.name);
|
|
5584
5605
|
}
|
|
@@ -5606,28 +5627,13 @@ function runtimeInterpolations(template) {
|
|
|
5606
5627
|
const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
|
|
5607
5628
|
offset += RUNTIME_MARKER.length;
|
|
5608
5629
|
if (isStaticFragment(expression)) return [];
|
|
5609
|
-
if (inValue
|
|
5630
|
+
if (inValue) {
|
|
5610
5631
|
return [{ expression, messageId: "dynamicSql" }];
|
|
5611
5632
|
}
|
|
5612
5633
|
return unquoted ? [{ expression, messageId: "dynamicFragment" }] : [];
|
|
5613
5634
|
}
|
|
5614
5635
|
);
|
|
5615
5636
|
}
|
|
5616
|
-
function endsWithSqlQuote(text) {
|
|
5617
|
-
return /'\s*$/u.test(text);
|
|
5618
|
-
}
|
|
5619
|
-
function startsWithSqlQuote(text) {
|
|
5620
|
-
return /^\s*'/u.test(text);
|
|
5621
|
-
}
|
|
5622
|
-
function staticLiteralText(node) {
|
|
5623
|
-
if (node.type === AST_NODE_TYPES29.Literal && typeof node.value === "string") {
|
|
5624
|
-
return node.value;
|
|
5625
|
-
}
|
|
5626
|
-
if (node.type === AST_NODE_TYPES29.TemplateLiteral && node.expressions.length === 0) {
|
|
5627
|
-
return node.quasis[0]?.value.raw;
|
|
5628
|
-
}
|
|
5629
|
-
return void 0;
|
|
5630
|
-
}
|
|
5631
5637
|
function runtimeConcatOperands(node) {
|
|
5632
5638
|
if (node.type !== AST_NODE_TYPES29.BinaryExpression || node.operator !== "+") {
|
|
5633
5639
|
return [];
|
|
@@ -5639,7 +5645,7 @@ function runtimeConcatOperands(node) {
|
|
|
5639
5645
|
if (!hasStringLiteral) {
|
|
5640
5646
|
return [];
|
|
5641
5647
|
}
|
|
5642
|
-
const parts = operands.map((operand) =>
|
|
5648
|
+
const parts = operands.map((operand) => staticString(operand) ?? RUNTIME_MARKER);
|
|
5643
5649
|
const statement = parts.join("");
|
|
5644
5650
|
const ranges = sqlSingleQuotedRanges(statement);
|
|
5645
5651
|
const visible = stripSqlNoise(statement);
|
|
@@ -5649,9 +5655,7 @@ function runtimeConcatOperands(node) {
|
|
|
5649
5655
|
const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
|
|
5650
5656
|
offset += parts[index]?.length ?? 0;
|
|
5651
5657
|
if (isStaticFragment(operand)) return [];
|
|
5652
|
-
|
|
5653
|
-
const after = operands[index + 1];
|
|
5654
|
-
if (inValue && before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "")) {
|
|
5658
|
+
if (inValue) {
|
|
5655
5659
|
return [{ expression: operand, messageId: "dynamicSql" }];
|
|
5656
5660
|
}
|
|
5657
5661
|
return unquoted ? [{ expression: operand, messageId: "dynamicFragment" }] : [];
|