@sarj/eslint-plugin 15.26.1 → 15.26.3

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
@@ -652,6 +652,25 @@ var no_broad_return_type_default = createRule({
652
652
  // src/rules/no-reduce-accumulator-copy.ts
653
653
  var import_utils5 = require("@typescript-eslint/utils");
654
654
  var import_typescript3 = __toESM(require("typescript"), 1);
655
+
656
+ // src/rules/_for-each-ast-child.ts
657
+ function forEachAstChild(node, visitorKeys, visit) {
658
+ for (const key of visitorKeys[node.type] ?? []) {
659
+ const value = node[key];
660
+ if (Array.isArray(value)) {
661
+ const children = value;
662
+ for (const child of children) if (isNode(child) && visit(child) === true) return true;
663
+ } else if (isNode(value)) {
664
+ if (visit(value) === true) return true;
665
+ }
666
+ }
667
+ return false;
668
+ }
669
+ function isNode(value) {
670
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
671
+ }
672
+
673
+ // src/rules/no-reduce-accumulator-copy.ts
655
674
  var NO_REDUCE_ACCUMULATOR_COPY_DOCUMENTATION = {
656
675
  summary: "Review copies of the accumulated collection inside a built-in reduce callback.",
657
676
  rationale: "Copying a growing accumulator at every step can make collection quadratic instead of linear in its output size.",
@@ -722,12 +741,7 @@ var no_reduce_accumulator_copy_default = createRule({
722
741
  context.report({ node: current, messageId: "copy" });
723
742
  }
724
743
  if (current.type === import_utils5.AST_NODE_TYPES.CallExpression) inspectCall(current);
725
- for (const key of context.sourceCode.visitorKeys[current.type] ?? []) {
726
- const child = current[key];
727
- if (Array.isArray(child)) {
728
- for (const item of child) if (item !== null && typeof item === "object" && "type" in item) inspect(item);
729
- } else if (child !== null && typeof child === "object" && "type" in child) inspect(child);
730
- }
744
+ forEachAstChild(current, context.sourceCode.visitorKeys, inspect);
731
745
  };
732
746
  inspect(callback.body);
733
747
  }
@@ -3546,17 +3560,17 @@ function subtreeContainsStarLiteral(node) {
3546
3560
  const value = node[key];
3547
3561
  if (Array.isArray(value)) {
3548
3562
  for (const child of value) {
3549
- if (isNode(child) && subtreeContainsStarLiteral(child)) {
3563
+ if (isNode2(child) && subtreeContainsStarLiteral(child)) {
3550
3564
  return true;
3551
3565
  }
3552
3566
  }
3553
- } else if (isNode(value) && subtreeContainsStarLiteral(value)) {
3567
+ } else if (isNode2(value) && subtreeContainsStarLiteral(value)) {
3554
3568
  return true;
3555
3569
  }
3556
3570
  }
3557
3571
  return false;
3558
3572
  }
3559
- function isNode(value) {
3573
+ function isNode2(value) {
3560
3574
  return typeof value === "object" && value !== null && typeof value.type === "string";
3561
3575
  }
3562
3576
  function calleeName(node) {
@@ -4149,12 +4163,12 @@ function createSqlListener(handler) {
4149
4163
 
4150
4164
  // src/rules/no-dynamic-sql.ts
4151
4165
  var NO_DYNAMIC_SQL_DOCUMENTATION = {
4152
- summary: "Disallow runtime values embedded inside quoted SQL values passed to statement-execution methods.",
4153
- rationale: "Embedding runtime values in SQL bypasses driver parameterization and can introduce injection defects or unstable query plans.",
4154
- remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
4166
+ summary: "Disallow runtime values and fragments interpolated into SQL passed to statement-execution methods.",
4167
+ rationale: "Embedding runtime values or fragments in SQL bypasses driver parameterization or an explicit allowlist and can introduce injection defects or unstable query plans.",
4168
+ remediation: "Bind data values through SQL placeholders. Select non-bindable identifiers or clauses from fixed, reviewed fragments instead of interpolating runtime text.",
4155
4169
  category: "security",
4156
4170
  limitations: [
4157
- "Only single-quoted SQL values are inspected. Double-quoted identifiers, comments, dollar strings, and unquoted fragments are excluded; this is not a general SQL injection detector.",
4171
+ "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.",
4158
4172
  "Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
4159
4173
  "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."
4160
4174
  ],
@@ -4176,6 +4190,26 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
4176
4190
  focusPath: "src/users.ts",
4177
4191
  expectedCount: 1,
4178
4192
  public: true
4193
+ },
4194
+ {
4195
+ id: "bound-unquoted-value",
4196
+ scenarioId: "unquoted",
4197
+ title: "An unquoted runtime value is bound separately",
4198
+ outcome: "no-match",
4199
+ files: [{ path: "src/users.ts", source: "db.prepare('select id from users where id = ?').bind(userId);" }],
4200
+ focusPath: "src/users.ts",
4201
+ expectedCount: 0,
4202
+ public: true
4203
+ },
4204
+ {
4205
+ id: "unquoted-runtime-fragment",
4206
+ scenarioId: "unquoted",
4207
+ title: "A runtime fragment is interpolated into SQL",
4208
+ outcome: "match",
4209
+ files: [{ path: "src/users.ts", source: "db.prepare(`select id from users where id = ${userId}`);" }],
4210
+ focusPath: "src/users.ts",
4211
+ expectedCount: 1,
4212
+ public: true
4179
4213
  }
4180
4214
  ]
4181
4215
  };
@@ -4198,14 +4232,21 @@ function isStaticFragment(expression) {
4198
4232
  }
4199
4233
  function runtimeInterpolations(template) {
4200
4234
  const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
4201
- const ranges = sqlSingleQuotedRanges(parts.join(RUNTIME_MARKER));
4235
+ const statement = parts.join(RUNTIME_MARKER);
4236
+ const ranges = sqlSingleQuotedRanges(statement);
4237
+ const visible = stripSqlNoise(statement);
4202
4238
  let offset = 0;
4203
- return template.expressions.filter(
4239
+ return template.expressions.flatMap(
4204
4240
  (expression, index) => {
4205
4241
  offset += parts[index]?.length ?? 0;
4206
4242
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
4243
+ const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
4207
4244
  offset += RUNTIME_MARKER.length;
4208
- return inValue && !isStaticFragment(expression) && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "");
4245
+ if (isStaticFragment(expression)) return [];
4246
+ if (inValue && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "")) {
4247
+ return [{ expression, messageId: "dynamicSql" }];
4248
+ }
4249
+ return unquoted ? [{ expression, messageId: "dynamicFragment" }] : [];
4209
4250
  }
4210
4251
  );
4211
4252
  }
@@ -4236,16 +4277,21 @@ function runtimeConcatOperands(node) {
4236
4277
  return [];
4237
4278
  }
4238
4279
  const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
4239
- const ranges = sqlSingleQuotedRanges(parts.join(""));
4280
+ const statement = parts.join("");
4281
+ const ranges = sqlSingleQuotedRanges(statement);
4282
+ const visible = stripSqlNoise(statement);
4240
4283
  let offset = 0;
4241
- return operands.filter((operand, index) => {
4284
+ return operands.flatMap((operand, index) => {
4242
4285
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
4286
+ const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
4243
4287
  offset += parts[index]?.length ?? 0;
4244
- if (!inValue) return false;
4245
- if (isStaticFragment(operand)) return false;
4288
+ if (isStaticFragment(operand)) return [];
4246
4289
  const before = operands[index - 1];
4247
4290
  const after = operands[index + 1];
4248
- return before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "");
4291
+ if (inValue && before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "")) {
4292
+ return [{ expression: operand, messageId: "dynamicSql" }];
4293
+ }
4294
+ return unquoted ? [{ expression: operand, messageId: "dynamicFragment" }] : [];
4249
4295
  });
4250
4296
  }
4251
4297
  function concatOperands(node) {
@@ -4301,7 +4347,8 @@ var no_dynamic_sql_default = createRule({
4301
4347
  }
4302
4348
  ],
4303
4349
  messages: {
4304
- dynamicSql: "Runtime value embedded inside a quoted SQL value passed to `{{method}}()`. Replace the quoted interpolation with a placeholder and bind the value separately."
4350
+ dynamicSql: "Runtime value embedded inside a quoted SQL value passed to `{{method}}()`. Replace the quoted interpolation with a placeholder and bind the value separately.",
4351
+ dynamicFragment: "Runtime fragment interpolated into SQL passed to `{{method}}()`. Bind data values; select non-bindable SQL fragments from fixed, reviewed alternatives."
4305
4352
  }
4306
4353
  },
4307
4354
  defaultOptions: [{}],
@@ -4320,8 +4367,8 @@ var no_dynamic_sql_default = createRule({
4320
4367
  const offenders = statement.type === import_utils27.AST_NODE_TYPES.TemplateLiteral ? runtimeInterpolations(statement) : runtimeConcatOperands(statement);
4321
4368
  for (const offender of offenders) {
4322
4369
  context.report({
4323
- node: offender,
4324
- messageId: "dynamicSql",
4370
+ node: offender.expression,
4371
+ messageId: offender.messageId,
4325
4372
  data: { method }
4326
4373
  });
4327
4374
  }
@@ -4537,15 +4584,7 @@ function functionComplexity(fn, visitorKeys) {
4537
4584
  visitChildren(node, nesting);
4538
4585
  }
4539
4586
  function visitChildren(node, nesting) {
4540
- for (const key of visitorKeys[node.type] ?? []) {
4541
- const value = node[key];
4542
- const candidates = Array.isArray(value) ? value : [value];
4543
- for (const candidate2 of candidates) {
4544
- if (typeof candidate2 === "object" && candidate2 !== null && "type" in candidate2) {
4545
- visit(candidate2, nesting);
4546
- }
4547
- }
4548
- }
4587
+ forEachAstChild(node, visitorKeys, (child) => visit(child, nesting));
4549
4588
  }
4550
4589
  visit(fn.body, 0);
4551
4590
  return points;
@@ -4714,7 +4753,7 @@ var PURE_CONSTRUCTORS = /* @__PURE__ */ new Set([
4714
4753
  "Response",
4715
4754
  "AbortController"
4716
4755
  ]);
4717
- function isNode2(value) {
4756
+ function isNode3(value) {
4718
4757
  return typeof value === "object" && value !== null && typeof value.type === "string";
4719
4758
  }
4720
4759
  function isPureCall(node) {
@@ -4764,11 +4803,11 @@ function subtreeMatches(stmt, predicate, descendIntoFunctions = false) {
4764
4803
  const value = current[key];
4765
4804
  if (Array.isArray(value)) {
4766
4805
  for (const child of value) {
4767
- if (isNode2(child)) {
4806
+ if (isNode3(child)) {
4768
4807
  visit(child);
4769
4808
  }
4770
4809
  }
4771
- } else if (isNode2(value)) {
4810
+ } else if (isNode3(value)) {
4772
4811
  visit(value);
4773
4812
  }
4774
4813
  if (found) {
@@ -9927,7 +9966,7 @@ function isSentinelArgument(arg) {
9927
9966
  function isFunctionNode(node) {
9928
9967
  return node.type === import_utils61.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils61.AST_NODE_TYPES.FunctionExpression || node.type === import_utils61.AST_NODE_TYPES.ArrowFunctionExpression;
9929
9968
  }
9930
- function isNode3(value) {
9969
+ function isNode4(value) {
9931
9970
  return typeof value === "object" && value !== null && typeof value.type === "string";
9932
9971
  }
9933
9972
  function walkWithinScope(node, visit) {
@@ -9950,11 +9989,11 @@ function walkWithinScope(node, visit) {
9950
9989
  const value = current[key];
9951
9990
  if (Array.isArray(value)) {
9952
9991
  for (const child of value) {
9953
- if (isNode3(child)) {
9992
+ if (isNode4(child)) {
9954
9993
  recurse(child);
9955
9994
  }
9956
9995
  }
9957
- } else if (isNode3(value)) {
9996
+ } else if (isNode4(value)) {
9958
9997
  recurse(value);
9959
9998
  }
9960
9999
  }
@@ -10012,11 +10051,11 @@ function subtreeReadsName(node, name) {
10012
10051
  const value = current[key];
10013
10052
  if (Array.isArray(value)) {
10014
10053
  for (const child of value) {
10015
- if (isNode3(child)) {
10054
+ if (isNode4(child)) {
10016
10055
  recurse(child);
10017
10056
  }
10018
10057
  }
10019
- } else if (isNode3(value)) {
10058
+ } else if (isNode4(value)) {
10020
10059
  recurse(value);
10021
10060
  }
10022
10061
  }
@@ -10077,7 +10116,7 @@ function enclosingFunctionName(node) {
10077
10116
  let current = node.parent;
10078
10117
  while (current !== void 0 && current !== null) {
10079
10118
  if (isFunctionNode(current)) {
10080
- if ("id" in current && isNode3(current.id) && current.id.type === import_utils61.AST_NODE_TYPES.Identifier) {
10119
+ if ("id" in current && isNode4(current.id) && current.id.type === import_utils61.AST_NODE_TYPES.Identifier) {
10081
10120
  return current.id.name;
10082
10121
  }
10083
10122
  const parent = current.parent;
@@ -10143,7 +10182,7 @@ function tryReturnsSafeParse(catchNode) {
10143
10182
  const value = current[key];
10144
10183
  const children = Array.isArray(value) ? value : [value];
10145
10184
  for (const child of children) {
10146
- if (isNode3(child)) recurse(child);
10185
+ if (isNode4(child)) recurse(child);
10147
10186
  }
10148
10187
  }
10149
10188
  }
@@ -10182,7 +10221,7 @@ function functionReturnsSameSentinelKindElsewhere(catchNode, kind) {
10182
10221
  function enclosingFunctionBody(node) {
10183
10222
  let current = node.parent;
10184
10223
  while (current !== void 0 && current !== null) {
10185
- if (isFunctionNode(current) && "body" in current && isNode3(current.body) && current.body.type === import_utils61.AST_NODE_TYPES.BlockStatement) {
10224
+ if (isFunctionNode(current) && "body" in current && isNode4(current.body) && current.body.type === import_utils61.AST_NODE_TYPES.BlockStatement) {
10186
10225
  return current.body;
10187
10226
  }
10188
10227
  current = current.parent;
@@ -16282,13 +16321,7 @@ function isEmptyGuard(node, access) {
16282
16321
  }
16283
16322
  function contains(node, visitorKeys, predicate) {
16284
16323
  if (predicate(node)) return true;
16285
- for (const key of visitorKeys[node.type] ?? []) {
16286
- const child = node[key];
16287
- for (const value of Array.isArray(child) ? child : [child]) {
16288
- if (value !== null && typeof value === "object" && "type" in value && contains(value, visitorKeys, predicate)) return true;
16289
- }
16290
- }
16291
- return false;
16324
+ return forEachAstChild(node, visitorKeys, (child) => contains(child, visitorKeys, predicate));
16292
16325
  }
16293
16326
  function belongsToFunction(node, fn) {
16294
16327
  let current = node;
@@ -22052,14 +22085,7 @@ function referencedPropertyName(node) {
22052
22085
  function walk(node, visitorKeys, visit, nestedFunction = false) {
22053
22086
  visit(node, nestedFunction);
22054
22087
  const nested = nestedFunction || isFunction(node);
22055
- for (const key of visitorKeys[node.type] ?? []) {
22056
- const child = node[key];
22057
- if (Array.isArray(child)) {
22058
- for (const item of child) if (typeof item === "object" && item !== null && "type" in item) walk(item, visitorKeys, visit, nested);
22059
- } else if (typeof child === "object" && child !== null && "type" in child) {
22060
- walk(child, visitorKeys, visit, nested);
22061
- }
22062
- }
22088
+ forEachAstChild(node, visitorKeys, (child) => walk(child, visitorKeys, visit, nested));
22063
22089
  }
22064
22090
  function classScope(context, node, computedReferenceNames) {
22065
22091
  const methods = node.body.body.filter(
@@ -23252,7 +23278,7 @@ var RULES = {
23252
23278
  };
23253
23279
  var meta = {
23254
23280
  name: "@sarj/eslint-plugin",
23255
- version: "15.26.1"
23281
+ version: "15.26.3"
23256
23282
  };
23257
23283
  var APPLICATION_ONLY_RULES = [];
23258
23284
  var LIBRARY_IMPORT_POLICY = ["error", {
package/dist/index.d.cts CHANGED
@@ -175,11 +175,6 @@ interface RuleOptions$1 {
175
175
  readonly max?: number;
176
176
  }
177
177
 
178
- /**
179
- * @fileoverview no-dynamic-sql — bind runtime values separately from SQL string literals.
180
- *
181
- * Examples: https://github.com/sarj-ai/code-standards/blob/main/packages/typescript/tests/rules/no-dynamic-sql.test.ts
182
- */
183
178
  interface RuleOptions {
184
179
  readonly methods?: readonly string[];
185
180
  }
@@ -247,7 +242,7 @@ declare const RULES: {
247
242
  readonly "no-cors-wildcard-with-credentials": DocumentedRule<readonly [], "corsWildcardWithCredentials">;
248
243
  readonly "no-duplicate-lifecycle-refresh-listeners": DocumentedRule<readonly [], "duplicateLifecycleRefresh">;
249
244
  readonly "no-dangerously-allow-svg": DocumentedRule<readonly [], "noDangerouslyAllowSvg">;
250
- readonly "no-dynamic-sql": DocumentedRule<readonly [RuleOptions?], "dynamicSql">;
245
+ readonly "no-dynamic-sql": DocumentedRule<readonly [RuleOptions?], "dynamicSql" | "dynamicFragment">;
251
246
  readonly "no-enum": DocumentedRule<readonly [{
252
247
  ignoreFiles?: readonly string[];
253
248
  }?], "noEnum">;
@@ -1001,7 +996,7 @@ type FlatPreset = {
1001
996
  declare const PLUGIN: {
1002
997
  readonly meta: {
1003
998
  readonly name: "@sarj/eslint-plugin";
1004
- readonly version: "15.26.1";
999
+ readonly version: "15.26.3";
1005
1000
  };
1006
1001
  readonly rules: {
1007
1002
  readonly "no-conditional-empty-object-spread": DocumentedRule<[], "avoid">;
@@ -1037,7 +1032,7 @@ declare const PLUGIN: {
1037
1032
  readonly "no-cors-wildcard-with-credentials": DocumentedRule<readonly [], "corsWildcardWithCredentials">;
1038
1033
  readonly "no-duplicate-lifecycle-refresh-listeners": DocumentedRule<readonly [], "duplicateLifecycleRefresh">;
1039
1034
  readonly "no-dangerously-allow-svg": DocumentedRule<readonly [], "noDangerouslyAllowSvg">;
1040
- readonly "no-dynamic-sql": DocumentedRule<readonly [RuleOptions?], "dynamicSql">;
1035
+ readonly "no-dynamic-sql": DocumentedRule<readonly [RuleOptions?], "dynamicSql" | "dynamicFragment">;
1041
1036
  readonly "no-enum": DocumentedRule<readonly [{
1042
1037
  ignoreFiles?: readonly string[];
1043
1038
  }?], "noEnum">;
package/dist/index.d.ts CHANGED
@@ -175,11 +175,6 @@ interface RuleOptions$1 {
175
175
  readonly max?: number;
176
176
  }
177
177
 
178
- /**
179
- * @fileoverview no-dynamic-sql — bind runtime values separately from SQL string literals.
180
- *
181
- * Examples: https://github.com/sarj-ai/code-standards/blob/main/packages/typescript/tests/rules/no-dynamic-sql.test.ts
182
- */
183
178
  interface RuleOptions {
184
179
  readonly methods?: readonly string[];
185
180
  }
@@ -247,7 +242,7 @@ declare const RULES: {
247
242
  readonly "no-cors-wildcard-with-credentials": DocumentedRule<readonly [], "corsWildcardWithCredentials">;
248
243
  readonly "no-duplicate-lifecycle-refresh-listeners": DocumentedRule<readonly [], "duplicateLifecycleRefresh">;
249
244
  readonly "no-dangerously-allow-svg": DocumentedRule<readonly [], "noDangerouslyAllowSvg">;
250
- readonly "no-dynamic-sql": DocumentedRule<readonly [RuleOptions?], "dynamicSql">;
245
+ readonly "no-dynamic-sql": DocumentedRule<readonly [RuleOptions?], "dynamicSql" | "dynamicFragment">;
251
246
  readonly "no-enum": DocumentedRule<readonly [{
252
247
  ignoreFiles?: readonly string[];
253
248
  }?], "noEnum">;
@@ -1001,7 +996,7 @@ type FlatPreset = {
1001
996
  declare const PLUGIN: {
1002
997
  readonly meta: {
1003
998
  readonly name: "@sarj/eslint-plugin";
1004
- readonly version: "15.26.1";
999
+ readonly version: "15.26.3";
1005
1000
  };
1006
1001
  readonly rules: {
1007
1002
  readonly "no-conditional-empty-object-spread": DocumentedRule<[], "avoid">;
@@ -1037,7 +1032,7 @@ declare const PLUGIN: {
1037
1032
  readonly "no-cors-wildcard-with-credentials": DocumentedRule<readonly [], "corsWildcardWithCredentials">;
1038
1033
  readonly "no-duplicate-lifecycle-refresh-listeners": DocumentedRule<readonly [], "duplicateLifecycleRefresh">;
1039
1034
  readonly "no-dangerously-allow-svg": DocumentedRule<readonly [], "noDangerouslyAllowSvg">;
1040
- readonly "no-dynamic-sql": DocumentedRule<readonly [RuleOptions?], "dynamicSql">;
1035
+ readonly "no-dynamic-sql": DocumentedRule<readonly [RuleOptions?], "dynamicSql" | "dynamicFragment">;
1041
1036
  readonly "no-enum": DocumentedRule<readonly [{
1042
1037
  ignoreFiles?: readonly string[];
1043
1038
  }?], "noEnum">;
package/dist/index.js CHANGED
@@ -608,6 +608,25 @@ var no_broad_return_type_default = createRule({
608
608
  // src/rules/no-reduce-accumulator-copy.ts
609
609
  import { AST_NODE_TYPES as AST_NODE_TYPES4, ASTUtils as ASTUtils2, ESLintUtils as ESLintUtils4 } from "@typescript-eslint/utils";
610
610
  import ts3 from "typescript";
611
+
612
+ // src/rules/_for-each-ast-child.ts
613
+ function forEachAstChild(node, visitorKeys, visit) {
614
+ for (const key of visitorKeys[node.type] ?? []) {
615
+ const value = node[key];
616
+ if (Array.isArray(value)) {
617
+ const children = value;
618
+ for (const child of children) if (isNode(child) && visit(child) === true) return true;
619
+ } else if (isNode(value)) {
620
+ if (visit(value) === true) return true;
621
+ }
622
+ }
623
+ return false;
624
+ }
625
+ function isNode(value) {
626
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
627
+ }
628
+
629
+ // src/rules/no-reduce-accumulator-copy.ts
611
630
  var NO_REDUCE_ACCUMULATOR_COPY_DOCUMENTATION = {
612
631
  summary: "Review copies of the accumulated collection inside a built-in reduce callback.",
613
632
  rationale: "Copying a growing accumulator at every step can make collection quadratic instead of linear in its output size.",
@@ -678,12 +697,7 @@ var no_reduce_accumulator_copy_default = createRule({
678
697
  context.report({ node: current, messageId: "copy" });
679
698
  }
680
699
  if (current.type === AST_NODE_TYPES4.CallExpression) inspectCall(current);
681
- for (const key of context.sourceCode.visitorKeys[current.type] ?? []) {
682
- const child = current[key];
683
- if (Array.isArray(child)) {
684
- for (const item of child) if (item !== null && typeof item === "object" && "type" in item) inspect(item);
685
- } else if (child !== null && typeof child === "object" && "type" in child) inspect(child);
686
- }
700
+ forEachAstChild(current, context.sourceCode.visitorKeys, inspect);
687
701
  };
688
702
  inspect(callback.body);
689
703
  }
@@ -3510,17 +3524,17 @@ function subtreeContainsStarLiteral(node) {
3510
3524
  const value = node[key];
3511
3525
  if (Array.isArray(value)) {
3512
3526
  for (const child of value) {
3513
- if (isNode(child) && subtreeContainsStarLiteral(child)) {
3527
+ if (isNode2(child) && subtreeContainsStarLiteral(child)) {
3514
3528
  return true;
3515
3529
  }
3516
3530
  }
3517
- } else if (isNode(value) && subtreeContainsStarLiteral(value)) {
3531
+ } else if (isNode2(value) && subtreeContainsStarLiteral(value)) {
3518
3532
  return true;
3519
3533
  }
3520
3534
  }
3521
3535
  return false;
3522
3536
  }
3523
- function isNode(value) {
3537
+ function isNode2(value) {
3524
3538
  return typeof value === "object" && value !== null && typeof value.type === "string";
3525
3539
  }
3526
3540
  function calleeName(node) {
@@ -4113,12 +4127,12 @@ function createSqlListener(handler) {
4113
4127
 
4114
4128
  // src/rules/no-dynamic-sql.ts
4115
4129
  var NO_DYNAMIC_SQL_DOCUMENTATION = {
4116
- summary: "Disallow runtime values embedded inside quoted SQL values passed to statement-execution methods.",
4117
- rationale: "Embedding runtime values in SQL bypasses driver parameterization and can introduce injection defects or unstable query plans.",
4118
- remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
4130
+ summary: "Disallow runtime values and fragments interpolated into SQL passed to statement-execution methods.",
4131
+ rationale: "Embedding runtime values or fragments in SQL bypasses driver parameterization or an explicit allowlist and can introduce injection defects or unstable query plans.",
4132
+ remediation: "Bind data values through SQL placeholders. Select non-bindable identifiers or clauses from fixed, reviewed fragments instead of interpolating runtime text.",
4119
4133
  category: "security",
4120
4134
  limitations: [
4121
- "Only single-quoted SQL values are inspected. Double-quoted identifiers, comments, dollar strings, and unquoted fragments are excluded; this is not a general SQL injection detector.",
4135
+ "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.",
4122
4136
  "Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
4123
4137
  "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."
4124
4138
  ],
@@ -4140,6 +4154,26 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
4140
4154
  focusPath: "src/users.ts",
4141
4155
  expectedCount: 1,
4142
4156
  public: true
4157
+ },
4158
+ {
4159
+ id: "bound-unquoted-value",
4160
+ scenarioId: "unquoted",
4161
+ title: "An unquoted runtime value is bound separately",
4162
+ outcome: "no-match",
4163
+ files: [{ path: "src/users.ts", source: "db.prepare('select id from users where id = ?').bind(userId);" }],
4164
+ focusPath: "src/users.ts",
4165
+ expectedCount: 0,
4166
+ public: true
4167
+ },
4168
+ {
4169
+ id: "unquoted-runtime-fragment",
4170
+ scenarioId: "unquoted",
4171
+ title: "A runtime fragment is interpolated into SQL",
4172
+ outcome: "match",
4173
+ files: [{ path: "src/users.ts", source: "db.prepare(`select id from users where id = ${userId}`);" }],
4174
+ focusPath: "src/users.ts",
4175
+ expectedCount: 1,
4176
+ public: true
4143
4177
  }
4144
4178
  ]
4145
4179
  };
@@ -4162,14 +4196,21 @@ function isStaticFragment(expression) {
4162
4196
  }
4163
4197
  function runtimeInterpolations(template) {
4164
4198
  const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
4165
- const ranges = sqlSingleQuotedRanges(parts.join(RUNTIME_MARKER));
4199
+ const statement = parts.join(RUNTIME_MARKER);
4200
+ const ranges = sqlSingleQuotedRanges(statement);
4201
+ const visible = stripSqlNoise(statement);
4166
4202
  let offset = 0;
4167
- return template.expressions.filter(
4203
+ return template.expressions.flatMap(
4168
4204
  (expression, index) => {
4169
4205
  offset += parts[index]?.length ?? 0;
4170
4206
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
4207
+ const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
4171
4208
  offset += RUNTIME_MARKER.length;
4172
- return inValue && !isStaticFragment(expression) && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "");
4209
+ if (isStaticFragment(expression)) return [];
4210
+ if (inValue && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "")) {
4211
+ return [{ expression, messageId: "dynamicSql" }];
4212
+ }
4213
+ return unquoted ? [{ expression, messageId: "dynamicFragment" }] : [];
4173
4214
  }
4174
4215
  );
4175
4216
  }
@@ -4200,16 +4241,21 @@ function runtimeConcatOperands(node) {
4200
4241
  return [];
4201
4242
  }
4202
4243
  const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
4203
- const ranges = sqlSingleQuotedRanges(parts.join(""));
4244
+ const statement = parts.join("");
4245
+ const ranges = sqlSingleQuotedRanges(statement);
4246
+ const visible = stripSqlNoise(statement);
4204
4247
  let offset = 0;
4205
- return operands.filter((operand, index) => {
4248
+ return operands.flatMap((operand, index) => {
4206
4249
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
4250
+ const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
4207
4251
  offset += parts[index]?.length ?? 0;
4208
- if (!inValue) return false;
4209
- if (isStaticFragment(operand)) return false;
4252
+ if (isStaticFragment(operand)) return [];
4210
4253
  const before = operands[index - 1];
4211
4254
  const after = operands[index + 1];
4212
- return before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "");
4255
+ if (inValue && before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "")) {
4256
+ return [{ expression: operand, messageId: "dynamicSql" }];
4257
+ }
4258
+ return unquoted ? [{ expression: operand, messageId: "dynamicFragment" }] : [];
4213
4259
  });
4214
4260
  }
4215
4261
  function concatOperands(node) {
@@ -4265,7 +4311,8 @@ var no_dynamic_sql_default = createRule({
4265
4311
  }
4266
4312
  ],
4267
4313
  messages: {
4268
- dynamicSql: "Runtime value embedded inside a quoted SQL value passed to `{{method}}()`. Replace the quoted interpolation with a placeholder and bind the value separately."
4314
+ dynamicSql: "Runtime value embedded inside a quoted SQL value passed to `{{method}}()`. Replace the quoted interpolation with a placeholder and bind the value separately.",
4315
+ dynamicFragment: "Runtime fragment interpolated into SQL passed to `{{method}}()`. Bind data values; select non-bindable SQL fragments from fixed, reviewed alternatives."
4269
4316
  }
4270
4317
  },
4271
4318
  defaultOptions: [{}],
@@ -4284,8 +4331,8 @@ var no_dynamic_sql_default = createRule({
4284
4331
  const offenders = statement.type === AST_NODE_TYPES25.TemplateLiteral ? runtimeInterpolations(statement) : runtimeConcatOperands(statement);
4285
4332
  for (const offender of offenders) {
4286
4333
  context.report({
4287
- node: offender,
4288
- messageId: "dynamicSql",
4334
+ node: offender.expression,
4335
+ messageId: offender.messageId,
4289
4336
  data: { method }
4290
4337
  });
4291
4338
  }
@@ -4501,15 +4548,7 @@ function functionComplexity(fn, visitorKeys) {
4501
4548
  visitChildren(node, nesting);
4502
4549
  }
4503
4550
  function visitChildren(node, nesting) {
4504
- for (const key of visitorKeys[node.type] ?? []) {
4505
- const value = node[key];
4506
- const candidates = Array.isArray(value) ? value : [value];
4507
- for (const candidate2 of candidates) {
4508
- if (typeof candidate2 === "object" && candidate2 !== null && "type" in candidate2) {
4509
- visit(candidate2, nesting);
4510
- }
4511
- }
4512
- }
4551
+ forEachAstChild(node, visitorKeys, (child) => visit(child, nesting));
4513
4552
  }
4514
4553
  visit(fn.body, 0);
4515
4554
  return points;
@@ -4678,7 +4717,7 @@ var PURE_CONSTRUCTORS = /* @__PURE__ */ new Set([
4678
4717
  "Response",
4679
4718
  "AbortController"
4680
4719
  ]);
4681
- function isNode2(value) {
4720
+ function isNode3(value) {
4682
4721
  return typeof value === "object" && value !== null && typeof value.type === "string";
4683
4722
  }
4684
4723
  function isPureCall(node) {
@@ -4728,11 +4767,11 @@ function subtreeMatches(stmt, predicate, descendIntoFunctions = false) {
4728
4767
  const value = current[key];
4729
4768
  if (Array.isArray(value)) {
4730
4769
  for (const child of value) {
4731
- if (isNode2(child)) {
4770
+ if (isNode3(child)) {
4732
4771
  visit(child);
4733
4772
  }
4734
4773
  }
4735
- } else if (isNode2(value)) {
4774
+ } else if (isNode3(value)) {
4736
4775
  visit(value);
4737
4776
  }
4738
4777
  if (found) {
@@ -9909,7 +9948,7 @@ function isSentinelArgument(arg) {
9909
9948
  function isFunctionNode(node) {
9910
9949
  return node.type === AST_NODE_TYPES48.FunctionDeclaration || node.type === AST_NODE_TYPES48.FunctionExpression || node.type === AST_NODE_TYPES48.ArrowFunctionExpression;
9911
9950
  }
9912
- function isNode3(value) {
9951
+ function isNode4(value) {
9913
9952
  return typeof value === "object" && value !== null && typeof value.type === "string";
9914
9953
  }
9915
9954
  function walkWithinScope(node, visit) {
@@ -9932,11 +9971,11 @@ function walkWithinScope(node, visit) {
9932
9971
  const value = current[key];
9933
9972
  if (Array.isArray(value)) {
9934
9973
  for (const child of value) {
9935
- if (isNode3(child)) {
9974
+ if (isNode4(child)) {
9936
9975
  recurse(child);
9937
9976
  }
9938
9977
  }
9939
- } else if (isNode3(value)) {
9978
+ } else if (isNode4(value)) {
9940
9979
  recurse(value);
9941
9980
  }
9942
9981
  }
@@ -9994,11 +10033,11 @@ function subtreeReadsName(node, name) {
9994
10033
  const value = current[key];
9995
10034
  if (Array.isArray(value)) {
9996
10035
  for (const child of value) {
9997
- if (isNode3(child)) {
10036
+ if (isNode4(child)) {
9998
10037
  recurse(child);
9999
10038
  }
10000
10039
  }
10001
- } else if (isNode3(value)) {
10040
+ } else if (isNode4(value)) {
10002
10041
  recurse(value);
10003
10042
  }
10004
10043
  }
@@ -10059,7 +10098,7 @@ function enclosingFunctionName(node) {
10059
10098
  let current = node.parent;
10060
10099
  while (current !== void 0 && current !== null) {
10061
10100
  if (isFunctionNode(current)) {
10062
- if ("id" in current && isNode3(current.id) && current.id.type === AST_NODE_TYPES48.Identifier) {
10101
+ if ("id" in current && isNode4(current.id) && current.id.type === AST_NODE_TYPES48.Identifier) {
10063
10102
  return current.id.name;
10064
10103
  }
10065
10104
  const parent = current.parent;
@@ -10125,7 +10164,7 @@ function tryReturnsSafeParse(catchNode) {
10125
10164
  const value = current[key];
10126
10165
  const children = Array.isArray(value) ? value : [value];
10127
10166
  for (const child of children) {
10128
- if (isNode3(child)) recurse(child);
10167
+ if (isNode4(child)) recurse(child);
10129
10168
  }
10130
10169
  }
10131
10170
  }
@@ -10164,7 +10203,7 @@ function functionReturnsSameSentinelKindElsewhere(catchNode, kind) {
10164
10203
  function enclosingFunctionBody(node) {
10165
10204
  let current = node.parent;
10166
10205
  while (current !== void 0 && current !== null) {
10167
- if (isFunctionNode(current) && "body" in current && isNode3(current.body) && current.body.type === AST_NODE_TYPES48.BlockStatement) {
10206
+ if (isFunctionNode(current) && "body" in current && isNode4(current.body) && current.body.type === AST_NODE_TYPES48.BlockStatement) {
10168
10207
  return current.body;
10169
10208
  }
10170
10209
  current = current.parent;
@@ -16283,13 +16322,7 @@ function isEmptyGuard(node, access) {
16283
16322
  }
16284
16323
  function contains(node, visitorKeys, predicate) {
16285
16324
  if (predicate(node)) return true;
16286
- for (const key of visitorKeys[node.type] ?? []) {
16287
- const child = node[key];
16288
- for (const value of Array.isArray(child) ? child : [child]) {
16289
- if (value !== null && typeof value === "object" && "type" in value && contains(value, visitorKeys, predicate)) return true;
16290
- }
16291
- }
16292
- return false;
16325
+ return forEachAstChild(node, visitorKeys, (child) => contains(child, visitorKeys, predicate));
16293
16326
  }
16294
16327
  function belongsToFunction(node, fn) {
16295
16328
  let current = node;
@@ -22070,14 +22103,7 @@ function referencedPropertyName(node) {
22070
22103
  function walk(node, visitorKeys, visit, nestedFunction = false) {
22071
22104
  visit(node, nestedFunction);
22072
22105
  const nested = nestedFunction || isFunction(node);
22073
- for (const key of visitorKeys[node.type] ?? []) {
22074
- const child = node[key];
22075
- if (Array.isArray(child)) {
22076
- for (const item of child) if (typeof item === "object" && item !== null && "type" in item) walk(item, visitorKeys, visit, nested);
22077
- } else if (typeof child === "object" && child !== null && "type" in child) {
22078
- walk(child, visitorKeys, visit, nested);
22079
- }
22080
- }
22106
+ forEachAstChild(node, visitorKeys, (child) => walk(child, visitorKeys, visit, nested));
22081
22107
  }
22082
22108
  function classScope(context, node, computedReferenceNames) {
22083
22109
  const methods = node.body.body.filter(
@@ -23273,7 +23299,7 @@ var RULES = {
23273
23299
  };
23274
23300
  var meta = {
23275
23301
  name: "@sarj/eslint-plugin",
23276
- version: "15.26.1"
23302
+ version: "15.26.3"
23277
23303
  };
23278
23304
  var APPLICATION_ONLY_RULES = [];
23279
23305
  var LIBRARY_IMPORT_POLICY = ["error", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.26.1",
3
+ "version": "15.26.3",
4
4
  "packageManager": "npm@12.0.2",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",