@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 +86 -60
- package/dist/index.d.cts +3 -8
- package/dist/index.d.ts +3 -8
- package/dist/index.js +86 -60
- package/package.json +1 -1
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
|
-
|
|
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 (
|
|
3563
|
+
if (isNode2(child) && subtreeContainsStarLiteral(child)) {
|
|
3550
3564
|
return true;
|
|
3551
3565
|
}
|
|
3552
3566
|
}
|
|
3553
|
-
} else if (
|
|
3567
|
+
} else if (isNode2(value) && subtreeContainsStarLiteral(value)) {
|
|
3554
3568
|
return true;
|
|
3555
3569
|
}
|
|
3556
3570
|
}
|
|
3557
3571
|
return false;
|
|
3558
3572
|
}
|
|
3559
|
-
function
|
|
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
|
|
4153
|
-
rationale: "Embedding runtime values in SQL bypasses driver parameterization and can introduce injection defects or unstable query plans.",
|
|
4154
|
-
remediation: "
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
4280
|
+
const statement = parts.join("");
|
|
4281
|
+
const ranges = sqlSingleQuotedRanges(statement);
|
|
4282
|
+
const visible = stripSqlNoise(statement);
|
|
4240
4283
|
let offset = 0;
|
|
4241
|
-
return operands.
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 (
|
|
4806
|
+
if (isNode3(child)) {
|
|
4768
4807
|
visit(child);
|
|
4769
4808
|
}
|
|
4770
4809
|
}
|
|
4771
|
-
} else if (
|
|
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
|
|
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 (
|
|
9992
|
+
if (isNode4(child)) {
|
|
9954
9993
|
recurse(child);
|
|
9955
9994
|
}
|
|
9956
9995
|
}
|
|
9957
|
-
} else if (
|
|
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 (
|
|
10054
|
+
if (isNode4(child)) {
|
|
10016
10055
|
recurse(child);
|
|
10017
10056
|
}
|
|
10018
10057
|
}
|
|
10019
|
-
} else if (
|
|
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 &&
|
|
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 (
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
3527
|
+
if (isNode2(child) && subtreeContainsStarLiteral(child)) {
|
|
3514
3528
|
return true;
|
|
3515
3529
|
}
|
|
3516
3530
|
}
|
|
3517
|
-
} else if (
|
|
3531
|
+
} else if (isNode2(value) && subtreeContainsStarLiteral(value)) {
|
|
3518
3532
|
return true;
|
|
3519
3533
|
}
|
|
3520
3534
|
}
|
|
3521
3535
|
return false;
|
|
3522
3536
|
}
|
|
3523
|
-
function
|
|
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
|
|
4117
|
-
rationale: "Embedding runtime values in SQL bypasses driver parameterization and can introduce injection defects or unstable query plans.",
|
|
4118
|
-
remediation: "
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
4244
|
+
const statement = parts.join("");
|
|
4245
|
+
const ranges = sqlSingleQuotedRanges(statement);
|
|
4246
|
+
const visible = stripSqlNoise(statement);
|
|
4204
4247
|
let offset = 0;
|
|
4205
|
-
return operands.
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 (
|
|
4770
|
+
if (isNode3(child)) {
|
|
4732
4771
|
visit(child);
|
|
4733
4772
|
}
|
|
4734
4773
|
}
|
|
4735
|
-
} else if (
|
|
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
|
|
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 (
|
|
9974
|
+
if (isNode4(child)) {
|
|
9936
9975
|
recurse(child);
|
|
9937
9976
|
}
|
|
9938
9977
|
}
|
|
9939
|
-
} else if (
|
|
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 (
|
|
10036
|
+
if (isNode4(child)) {
|
|
9998
10037
|
recurse(child);
|
|
9999
10038
|
}
|
|
10000
10039
|
}
|
|
10001
|
-
} else if (
|
|
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 &&
|
|
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 (
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
23302
|
+
version: "15.26.3"
|
|
23277
23303
|
};
|
|
23278
23304
|
var APPLICATION_ONLY_RULES = [];
|
|
23279
23305
|
var LIBRARY_IMPORT_POLICY = ["error", {
|