@sarj/eslint-plugin 15.26.1 → 15.26.2

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
@@ -4149,12 +4149,12 @@ function createSqlListener(handler) {
4149
4149
 
4150
4150
  // src/rules/no-dynamic-sql.ts
4151
4151
  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.",
4152
+ summary: "Disallow runtime values and fragments interpolated into SQL passed to statement-execution methods.",
4153
+ rationale: "Embedding runtime values or fragments in SQL bypasses driver parameterization or an explicit allowlist and can introduce injection defects or unstable query plans.",
4154
+ remediation: "Bind data values through SQL placeholders. Select non-bindable identifiers or clauses from fixed, reviewed fragments instead of interpolating runtime text.",
4155
4155
  category: "security",
4156
4156
  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.",
4157
+ "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
4158
  "Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
4159
4159
  "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
4160
  ],
@@ -4176,6 +4176,26 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
4176
4176
  focusPath: "src/users.ts",
4177
4177
  expectedCount: 1,
4178
4178
  public: true
4179
+ },
4180
+ {
4181
+ id: "bound-unquoted-value",
4182
+ scenarioId: "unquoted",
4183
+ title: "An unquoted runtime value is bound separately",
4184
+ outcome: "no-match",
4185
+ files: [{ path: "src/users.ts", source: "db.prepare('select id from users where id = ?').bind(userId);" }],
4186
+ focusPath: "src/users.ts",
4187
+ expectedCount: 0,
4188
+ public: true
4189
+ },
4190
+ {
4191
+ id: "unquoted-runtime-fragment",
4192
+ scenarioId: "unquoted",
4193
+ title: "A runtime fragment is interpolated into SQL",
4194
+ outcome: "match",
4195
+ files: [{ path: "src/users.ts", source: "db.prepare(`select id from users where id = ${userId}`);" }],
4196
+ focusPath: "src/users.ts",
4197
+ expectedCount: 1,
4198
+ public: true
4179
4199
  }
4180
4200
  ]
4181
4201
  };
@@ -4198,14 +4218,21 @@ function isStaticFragment(expression) {
4198
4218
  }
4199
4219
  function runtimeInterpolations(template) {
4200
4220
  const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
4201
- const ranges = sqlSingleQuotedRanges(parts.join(RUNTIME_MARKER));
4221
+ const statement = parts.join(RUNTIME_MARKER);
4222
+ const ranges = sqlSingleQuotedRanges(statement);
4223
+ const visible = stripSqlNoise(statement);
4202
4224
  let offset = 0;
4203
- return template.expressions.filter(
4225
+ return template.expressions.flatMap(
4204
4226
  (expression, index) => {
4205
4227
  offset += parts[index]?.length ?? 0;
4206
4228
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
4229
+ const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
4207
4230
  offset += RUNTIME_MARKER.length;
4208
- return inValue && !isStaticFragment(expression) && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "");
4231
+ if (isStaticFragment(expression)) return [];
4232
+ if (inValue && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "")) {
4233
+ return [{ expression, messageId: "dynamicSql" }];
4234
+ }
4235
+ return unquoted ? [{ expression, messageId: "dynamicFragment" }] : [];
4209
4236
  }
4210
4237
  );
4211
4238
  }
@@ -4236,16 +4263,21 @@ function runtimeConcatOperands(node) {
4236
4263
  return [];
4237
4264
  }
4238
4265
  const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
4239
- const ranges = sqlSingleQuotedRanges(parts.join(""));
4266
+ const statement = parts.join("");
4267
+ const ranges = sqlSingleQuotedRanges(statement);
4268
+ const visible = stripSqlNoise(statement);
4240
4269
  let offset = 0;
4241
- return operands.filter((operand, index) => {
4270
+ return operands.flatMap((operand, index) => {
4242
4271
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
4272
+ const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
4243
4273
  offset += parts[index]?.length ?? 0;
4244
- if (!inValue) return false;
4245
- if (isStaticFragment(operand)) return false;
4274
+ if (isStaticFragment(operand)) return [];
4246
4275
  const before = operands[index - 1];
4247
4276
  const after = operands[index + 1];
4248
- return before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "");
4277
+ if (inValue && before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "")) {
4278
+ return [{ expression: operand, messageId: "dynamicSql" }];
4279
+ }
4280
+ return unquoted ? [{ expression: operand, messageId: "dynamicFragment" }] : [];
4249
4281
  });
4250
4282
  }
4251
4283
  function concatOperands(node) {
@@ -4301,7 +4333,8 @@ var no_dynamic_sql_default = createRule({
4301
4333
  }
4302
4334
  ],
4303
4335
  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."
4336
+ dynamicSql: "Runtime value embedded inside a quoted SQL value passed to `{{method}}()`. Replace the quoted interpolation with a placeholder and bind the value separately.",
4337
+ dynamicFragment: "Runtime fragment interpolated into SQL passed to `{{method}}()`. Bind data values; select non-bindable SQL fragments from fixed, reviewed alternatives."
4305
4338
  }
4306
4339
  },
4307
4340
  defaultOptions: [{}],
@@ -4320,8 +4353,8 @@ var no_dynamic_sql_default = createRule({
4320
4353
  const offenders = statement.type === import_utils27.AST_NODE_TYPES.TemplateLiteral ? runtimeInterpolations(statement) : runtimeConcatOperands(statement);
4321
4354
  for (const offender of offenders) {
4322
4355
  context.report({
4323
- node: offender,
4324
- messageId: "dynamicSql",
4356
+ node: offender.expression,
4357
+ messageId: offender.messageId,
4325
4358
  data: { method }
4326
4359
  });
4327
4360
  }
@@ -23252,7 +23285,7 @@ var RULES = {
23252
23285
  };
23253
23286
  var meta = {
23254
23287
  name: "@sarj/eslint-plugin",
23255
- version: "15.26.1"
23288
+ version: "15.26.2"
23256
23289
  };
23257
23290
  var APPLICATION_ONLY_RULES = [];
23258
23291
  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.2";
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.2";
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
@@ -4113,12 +4113,12 @@ function createSqlListener(handler) {
4113
4113
 
4114
4114
  // src/rules/no-dynamic-sql.ts
4115
4115
  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.",
4116
+ summary: "Disallow runtime values and fragments interpolated into SQL passed to statement-execution methods.",
4117
+ rationale: "Embedding runtime values or fragments in SQL bypasses driver parameterization or an explicit allowlist and can introduce injection defects or unstable query plans.",
4118
+ remediation: "Bind data values through SQL placeholders. Select non-bindable identifiers or clauses from fixed, reviewed fragments instead of interpolating runtime text.",
4119
4119
  category: "security",
4120
4120
  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.",
4121
+ "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
4122
  "Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
4123
4123
  "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
4124
  ],
@@ -4140,6 +4140,26 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
4140
4140
  focusPath: "src/users.ts",
4141
4141
  expectedCount: 1,
4142
4142
  public: true
4143
+ },
4144
+ {
4145
+ id: "bound-unquoted-value",
4146
+ scenarioId: "unquoted",
4147
+ title: "An unquoted runtime value is bound separately",
4148
+ outcome: "no-match",
4149
+ files: [{ path: "src/users.ts", source: "db.prepare('select id from users where id = ?').bind(userId);" }],
4150
+ focusPath: "src/users.ts",
4151
+ expectedCount: 0,
4152
+ public: true
4153
+ },
4154
+ {
4155
+ id: "unquoted-runtime-fragment",
4156
+ scenarioId: "unquoted",
4157
+ title: "A runtime fragment is interpolated into SQL",
4158
+ outcome: "match",
4159
+ files: [{ path: "src/users.ts", source: "db.prepare(`select id from users where id = ${userId}`);" }],
4160
+ focusPath: "src/users.ts",
4161
+ expectedCount: 1,
4162
+ public: true
4143
4163
  }
4144
4164
  ]
4145
4165
  };
@@ -4162,14 +4182,21 @@ function isStaticFragment(expression) {
4162
4182
  }
4163
4183
  function runtimeInterpolations(template) {
4164
4184
  const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
4165
- const ranges = sqlSingleQuotedRanges(parts.join(RUNTIME_MARKER));
4185
+ const statement = parts.join(RUNTIME_MARKER);
4186
+ const ranges = sqlSingleQuotedRanges(statement);
4187
+ const visible = stripSqlNoise(statement);
4166
4188
  let offset = 0;
4167
- return template.expressions.filter(
4189
+ return template.expressions.flatMap(
4168
4190
  (expression, index) => {
4169
4191
  offset += parts[index]?.length ?? 0;
4170
4192
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
4193
+ const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
4171
4194
  offset += RUNTIME_MARKER.length;
4172
- return inValue && !isStaticFragment(expression) && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "");
4195
+ if (isStaticFragment(expression)) return [];
4196
+ if (inValue && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "")) {
4197
+ return [{ expression, messageId: "dynamicSql" }];
4198
+ }
4199
+ return unquoted ? [{ expression, messageId: "dynamicFragment" }] : [];
4173
4200
  }
4174
4201
  );
4175
4202
  }
@@ -4200,16 +4227,21 @@ function runtimeConcatOperands(node) {
4200
4227
  return [];
4201
4228
  }
4202
4229
  const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
4203
- const ranges = sqlSingleQuotedRanges(parts.join(""));
4230
+ const statement = parts.join("");
4231
+ const ranges = sqlSingleQuotedRanges(statement);
4232
+ const visible = stripSqlNoise(statement);
4204
4233
  let offset = 0;
4205
- return operands.filter((operand, index) => {
4234
+ return operands.flatMap((operand, index) => {
4206
4235
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
4236
+ const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
4207
4237
  offset += parts[index]?.length ?? 0;
4208
- if (!inValue) return false;
4209
- if (isStaticFragment(operand)) return false;
4238
+ if (isStaticFragment(operand)) return [];
4210
4239
  const before = operands[index - 1];
4211
4240
  const after = operands[index + 1];
4212
- return before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "");
4241
+ if (inValue && before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "")) {
4242
+ return [{ expression: operand, messageId: "dynamicSql" }];
4243
+ }
4244
+ return unquoted ? [{ expression: operand, messageId: "dynamicFragment" }] : [];
4213
4245
  });
4214
4246
  }
4215
4247
  function concatOperands(node) {
@@ -4265,7 +4297,8 @@ var no_dynamic_sql_default = createRule({
4265
4297
  }
4266
4298
  ],
4267
4299
  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."
4300
+ dynamicSql: "Runtime value embedded inside a quoted SQL value passed to `{{method}}()`. Replace the quoted interpolation with a placeholder and bind the value separately.",
4301
+ dynamicFragment: "Runtime fragment interpolated into SQL passed to `{{method}}()`. Bind data values; select non-bindable SQL fragments from fixed, reviewed alternatives."
4269
4302
  }
4270
4303
  },
4271
4304
  defaultOptions: [{}],
@@ -4284,8 +4317,8 @@ var no_dynamic_sql_default = createRule({
4284
4317
  const offenders = statement.type === AST_NODE_TYPES25.TemplateLiteral ? runtimeInterpolations(statement) : runtimeConcatOperands(statement);
4285
4318
  for (const offender of offenders) {
4286
4319
  context.report({
4287
- node: offender,
4288
- messageId: "dynamicSql",
4320
+ node: offender.expression,
4321
+ messageId: offender.messageId,
4289
4322
  data: { method }
4290
4323
  });
4291
4324
  }
@@ -23273,7 +23306,7 @@ var RULES = {
23273
23306
  };
23274
23307
  var meta = {
23275
23308
  name: "@sarj/eslint-plugin",
23276
- version: "15.26.1"
23309
+ version: "15.26.2"
23277
23310
  };
23278
23311
  var APPLICATION_ONLY_RULES = [];
23279
23312
  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.2",
4
4
  "packageManager": "npm@12.0.2",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",