@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 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.0",
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
- const parts = [];
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 isJoinedFragmentArray(node) {
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
- forEachOwnAstChild(node, (child) => markConsumed(child, consumed));
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
- if (isJoinedFragmentArray(node)) {
5541
- visit(node);
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 && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "")) {
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) => staticLiteralText(operand) ?? RUNTIME_MARKER);
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
- const before = operands[index - 1];
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.0",
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
- const parts = [];
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 isJoinedFragmentArray(node) {
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
- forEachOwnAstChild(node, (child) => markConsumed(child, consumed));
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
- if (isJoinedFragmentArray(node)) {
5502
- visit(node);
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 && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "")) {
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) => staticLiteralText(operand) ?? RUNTIME_MARKER);
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
- const before = operands[index - 1];
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" }] : [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.13.0",
3
+ "version": "16.13.1",
4
4
  "packageManager": "npm@12.2.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",