eslint-plugin-no-mistakes 0.44.0 → 0.45.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "ESLint and Oxlint rules for deterministic no-mistakes code analysis",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -29,6 +29,8 @@ const rules = {
29
29
  "playwright-selector-priority": require("./rules/playwright-selector-priority"),
30
30
  "react-no-iife-in-jsx": require("./rules/react-no-iife-in-jsx"),
31
31
  "playwright-unique": require("./rules/playwright-unique"),
32
+ "postgres-no-manual-transaction": require("./rules/postgres-no-manual-transaction"),
33
+ "postgres-no-unbounded-query-fanout": require("./rules/postgres-no-unbounded-query-fanout"),
32
34
  "react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
33
35
  "react-no-use-promise-resolve": require("./rules/react-no-use-promise-resolve"),
34
36
  "server-require-nullable-fetch-wrapper": require("./rules/server-require-nullable-fetch-wrapper"),
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ const { quasiText, sqlText, unwrapTs } = require("./postgres-query-text");
4
+
5
+ const DEFAULT_IMPORT_SPECIFIER = "@data-stores/psql";
6
+ const DEFAULT_EXECUTOR_NAMES = ["query", "read", "write"];
7
+ const DEFAULT_CHUNK_FUNCTION_NAMES = ["chunkArray"];
8
+ const TRANSACTION_IMPORTS = new Set(["withTransaction", "withTransactionOptions"]);
9
+ const QUERY_PROPERTY = "query";
10
+ const TRANSACTION_COMMAND = /^\s*(?:BEGIN|COMMIT|ROLLBACK)\b/i;
11
+
12
+ function executorOptionDefaults(options = {}) {
13
+ return {
14
+ importSpecifier: options.importSpecifier ?? DEFAULT_IMPORT_SPECIFIER,
15
+ executorNames: options.executorNames ?? DEFAULT_EXECUTOR_NAMES,
16
+ owners: options.owners ?? [],
17
+ chunkFunctionNames: options.chunkFunctionNames ?? DEFAULT_CHUNK_FUNCTION_NAMES,
18
+ };
19
+ }
20
+
21
+ function executorOptionSchema(extraProperties = {}) {
22
+ return {
23
+ type: "object",
24
+ properties: {
25
+ importSpecifier: { type: "string" },
26
+ executorNames: { type: "array", items: { type: "string" } },
27
+ ...extraProperties,
28
+ },
29
+ additionalProperties: false,
30
+ };
31
+ }
32
+
33
+ function importedName(specifier) {
34
+ const imported = specifier?.imported;
35
+ if (!imported) return null;
36
+ return imported.type === "Literal" ? String(imported.value) : imported.name;
37
+ }
38
+
39
+ function executorBindings(program, options = {}) {
40
+ const bindings = new Set();
41
+ const { importSpecifier, executorNames } = executorOptionDefaults(options);
42
+ for (const statement of program?.body ?? []) {
43
+ if (statement.type !== "ImportDeclaration") continue;
44
+ if (statement.importKind === "type") continue;
45
+ if (statement.source?.value !== importSpecifier) continue;
46
+ for (const specifier of statement.specifiers ?? []) {
47
+ if (specifier.type !== "ImportSpecifier") continue;
48
+ if (specifier.importKind === "type") continue;
49
+ const imported = importedName(specifier);
50
+ if (TRANSACTION_IMPORTS.has(imported)) bindings.add(QUERY_PROPERTY);
51
+ if (imported && executorNames.includes(imported)) bindings.add(specifier.local.name);
52
+ }
53
+ }
54
+ return bindings;
55
+ }
56
+
57
+ function staticQueryKey(node) {
58
+ node = unwrapTs(node);
59
+ if (!node) return false;
60
+ if (node.type === "Literal") return node.value === QUERY_PROPERTY;
61
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
62
+ return quasiText(node.quasis[0]) === QUERY_PROPERTY;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ function memberPropertyName(node) {
68
+ if (!node || node.type !== "MemberExpression") return null;
69
+ if (!node.computed) return node.property?.name ?? null;
70
+ if (staticQueryKey(node.property)) return QUERY_PROPERTY;
71
+ return sqlText(node.property);
72
+ }
73
+
74
+ function calleeName(call, bindings) {
75
+ const callee = unwrapTs(call?.callee);
76
+ if (!callee) return null;
77
+ if (callee.type === "Identifier" && bindings?.has(callee.name)) return callee.name;
78
+ if (callee.type === "MemberExpression" && memberPropertyName(callee) === QUERY_PROPERTY) {
79
+ return QUERY_PROPERTY;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function isDatabaseCall(call, bindings) {
85
+ return call?.type === "CallExpression" && calleeName(call, bindings) != null;
86
+ }
87
+
88
+ function firstCallArgument(call) {
89
+ const argument = call?.arguments?.[0];
90
+ if (!argument || argument.type === "SpreadElement") return null;
91
+ return argument;
92
+ }
93
+
94
+ function stripSqlComments(text) {
95
+ return text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n]*/g, " ");
96
+ }
97
+
98
+ function isManualTransactionText(text) {
99
+ return typeof text === "string" && TRANSACTION_COMMAND.test(stripSqlComments(text));
100
+ }
101
+
102
+ module.exports = {
103
+ DEFAULT_CHUNK_FUNCTION_NAMES,
104
+ DEFAULT_EXECUTOR_NAMES,
105
+ DEFAULT_IMPORT_SPECIFIER,
106
+ QUERY_PROPERTY,
107
+ TRANSACTION_COMMAND,
108
+ TRANSACTION_IMPORTS,
109
+ calleeName,
110
+ executorBindings,
111
+ executorOptionDefaults,
112
+ executorOptionSchema,
113
+ firstCallArgument,
114
+ isDatabaseCall,
115
+ isManualTransactionText,
116
+ memberPropertyName,
117
+ };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ const { rule } = require("../helpers");
4
+ const helpers = require("./postgres-runtime-helpers");
5
+
6
+ const {
7
+ executedQueryText,
8
+ executorBindings,
9
+ executorOptionDefaults,
10
+ executorOptionSchema,
11
+ firstCallArgument,
12
+ isDatabaseCall,
13
+ isManualTransactionText,
14
+ isOwnerFile,
15
+ sqlStatementBindings,
16
+ } = helpers;
17
+
18
+ module.exports = Object.assign(
19
+ rule(
20
+ {
21
+ type: "problem",
22
+ docs: {
23
+ description: "disallow manual BEGIN, COMMIT, and ROLLBACK executor calls",
24
+ recommended: false,
25
+ },
26
+ schema: [
27
+ executorOptionSchema({
28
+ owners: { type: "array", items: { type: "string" } },
29
+ }),
30
+ ],
31
+ messages: {
32
+ manualTransaction:
33
+ "Do not execute BEGIN, COMMIT, or ROLLBACK through a query executor. Use withTransaction / withTransactionOptions so the owner helper owns transaction lifecycle.",
34
+ },
35
+ },
36
+ (context) => {
37
+ const options = executorOptionDefaults(context.options?.[0] ?? {});
38
+ if (isOwnerFile(context.filename, options.owners)) return {};
39
+ let bindings = new Set();
40
+ let statements = new Map();
41
+
42
+ return {
43
+ Program(node) {
44
+ bindings = executorBindings(node, options);
45
+ statements = sqlStatementBindings(node);
46
+ },
47
+ CallExpression(node) {
48
+ if (!isDatabaseCall(node, bindings)) return;
49
+ const argument = firstCallArgument(node);
50
+ const text = executedQueryText(argument, statements, context);
51
+ if (!isManualTransactionText(text)) return;
52
+ context.report({ node, messageId: "manualTransaction" });
53
+ },
54
+ };
55
+ },
56
+ ),
57
+ { __test: helpers },
58
+ );
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ const { rule } = require("../helpers");
4
+ const helpers = require("./postgres-runtime-helpers");
5
+
6
+ const {
7
+ callbackContainsExecutor,
8
+ executorBindings,
9
+ executorOptionDefaults,
10
+ executorOptionSchema,
11
+ isPromiseAllCallee,
12
+ isStaticallyBounded,
13
+ mapCallArgument,
14
+ } = helpers;
15
+
16
+ module.exports = Object.assign(
17
+ rule(
18
+ {
19
+ type: "problem",
20
+ docs: {
21
+ description: "disallow unbounded Promise.all map fan-out of query executors",
22
+ recommended: false,
23
+ },
24
+ schema: [
25
+ executorOptionSchema({
26
+ chunkFunctionNames: { type: "array", items: { type: "string" } },
27
+ }),
28
+ ],
29
+ messages: {
30
+ unboundedFanout:
31
+ "Do not fan out unbounded mapped executor calls through Promise.all(). Use a static array, a SCREAMING_CASE constant, or a configured chunk helper first.",
32
+ },
33
+ },
34
+ (context) => {
35
+ const options = executorOptionDefaults(context.options?.[0] ?? {});
36
+ let bindings = new Set();
37
+
38
+ return {
39
+ Program(node) {
40
+ bindings = executorBindings(node, options);
41
+ },
42
+ CallExpression(node) {
43
+ if (!isPromiseAllCallee(node.callee)) return;
44
+ const mapped = mapCallArgument(node);
45
+ if (!mapped) return;
46
+ if (isStaticallyBounded(mapped.source, options.chunkFunctionNames, context)) return;
47
+ if (!callbackContainsExecutor(mapped.callback, bindings, context)) return;
48
+ context.report({ node, messageId: "unboundedFanout" });
49
+ },
50
+ };
51
+ },
52
+ ),
53
+ { __test: helpers },
54
+ );
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+
3
+ const TS_WRAPPERS = new Set([
4
+ "ChainExpression",
5
+ "TSAsExpression",
6
+ "TSInstantiationExpression",
7
+ "TSNonNullExpression",
8
+ "TSSatisfiesExpression",
9
+ "TSTypeAssertion",
10
+ ]);
11
+
12
+ function unwrapTs(node) {
13
+ while (node && TS_WRAPPERS.has(node.type)) {
14
+ node = node.expression;
15
+ }
16
+ return node;
17
+ }
18
+
19
+ function childNodes(node) {
20
+ const children = [];
21
+ if (!node || typeof node !== "object") return children;
22
+ for (const [key, value] of Object.entries(node)) {
23
+ if (key === "parent") continue;
24
+ if (Array.isArray(value)) {
25
+ for (const item of value) {
26
+ if (item?.type) children.push(item);
27
+ }
28
+ } else if (value?.type) {
29
+ children.push(value);
30
+ }
31
+ }
32
+ return children;
33
+ }
34
+
35
+ function quasiText(quasi) {
36
+ return quasi?.value?.cooked ?? quasi?.value?.raw ?? "";
37
+ }
38
+
39
+ function templateSqlText(template) {
40
+ let out = "";
41
+ const quasis = template?.quasis ?? [];
42
+ for (let index = 0; index < quasis.length; index += 1) {
43
+ if (index > 0) out += `sql_placeholder_${index}`;
44
+ out += quasiText(quasis[index]);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ function sqlText(node) {
50
+ node = unwrapTs(node);
51
+ if (!node) return null;
52
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
53
+ if (node.type === "TemplateLiteral") return templateSqlText(node);
54
+ if (node.type === "TaggedTemplateExpression") return templateSqlText(node.quasi);
55
+ return null;
56
+ }
57
+
58
+ function variableFromScope(scope, name) {
59
+ const get = scope?.set?.get;
60
+ if (typeof get === "function") return get.call(scope.set, name) ?? null;
61
+ return scope?.variables?.find((item) => item.name === name) ?? null;
62
+ }
63
+
64
+ function resolveVariable(node, context) {
65
+ if (node?.type !== "Identifier" || !context?.sourceCode?.getScope) return null;
66
+ let scope = context.sourceCode.getScope(node);
67
+ while (scope) {
68
+ const variable = variableFromScope(scope, node.name);
69
+ if (variable) return variable;
70
+ scope = scope.upper;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function queryTextFromScope(ident, context) {
76
+ const variable = resolveVariable(ident, context);
77
+ if (!variable) return null;
78
+ if (variable.defs.some((def) => def.type === "Parameter" || def.type === "CatchClause")) {
79
+ return null;
80
+ }
81
+ for (const def of variable.defs) {
82
+ if (def.type !== "Variable" || def.node?.id?.type !== "Identifier") continue;
83
+ const text = sqlText(def.node.init);
84
+ if (text != null) return text;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function executedQueryText(node, bindings, context) {
90
+ const text = sqlText(node);
91
+ if (text != null) return text;
92
+ node = unwrapTs(node);
93
+ if (node?.type !== "Identifier") return null;
94
+ if (context) {
95
+ const scoped = queryTextFromScope(node, context);
96
+ if (scoped != null) return scoped;
97
+ const variable = resolveVariable(node, context);
98
+ if (variable) return null;
99
+ }
100
+ return bindings instanceof Map ? (bindings.get(node.name) ?? null) : null;
101
+ }
102
+
103
+ function sqlStatementBindings(root) {
104
+ const bindings = new Map();
105
+ function visit(node) {
106
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
107
+ const text = sqlText(node.init);
108
+ if (text != null) bindings.set(node.id.name, text);
109
+ }
110
+ for (const child of childNodes(node)) visit(child);
111
+ }
112
+ if (root?.type) visit(root);
113
+ return bindings;
114
+ }
115
+
116
+ module.exports = {
117
+ childNodes,
118
+ executedQueryText,
119
+ quasiText,
120
+ resolveVariable,
121
+ sqlStatementBindings,
122
+ sqlText,
123
+ templateSqlText,
124
+ unwrapTs,
125
+ };
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+
3
+ const { repoRelativeFilename } = require("./module-mock-helpers");
4
+ const {
5
+ DEFAULT_CHUNK_FUNCTION_NAMES,
6
+ DEFAULT_EXECUTOR_NAMES,
7
+ DEFAULT_IMPORT_SPECIFIER,
8
+ QUERY_PROPERTY,
9
+ TRANSACTION_COMMAND,
10
+ TRANSACTION_IMPORTS,
11
+ calleeName,
12
+ executorBindings,
13
+ executorOptionDefaults,
14
+ executorOptionSchema,
15
+ firstCallArgument,
16
+ isDatabaseCall,
17
+ isManualTransactionText,
18
+ memberPropertyName,
19
+ } = require("./postgres-executor");
20
+ const {
21
+ childNodes,
22
+ executedQueryText,
23
+ resolveVariable,
24
+ sqlStatementBindings,
25
+ sqlText,
26
+ templateSqlText,
27
+ unwrapTs,
28
+ } = require("./postgres-query-text");
29
+
30
+ const SCREAMING_CASE = /^[A-Z][A-Z0-9_]*$/;
31
+
32
+ function isOwnerFile(filename, owners) {
33
+ if (!filename || !owners?.length) return false;
34
+ const normalized = String(filename).replace(/\\/g, "/");
35
+ const relative = repoRelativeFilename(filename);
36
+ return owners.some(
37
+ (owner) => pathMatchesOwner(normalized, owner) || pathMatchesOwner(relative, owner),
38
+ );
39
+ }
40
+
41
+ function pathMatchesOwner(path, owner) {
42
+ const needle = String(owner ?? "").replace(/\\/g, "/");
43
+ if (!needle) return false;
44
+ if (path === needle) return true;
45
+ const suffix = needle.replace(/^\/+/, "");
46
+ return Boolean(suffix) && path.endsWith(`/${suffix}`);
47
+ }
48
+
49
+ function callName(node) {
50
+ const callee = unwrapTs(node?.callee);
51
+ if (!callee) return null;
52
+ if (callee.type === "Identifier") return callee.name;
53
+ if (callee.type === "MemberExpression") return memberPropertyName(callee);
54
+ return null;
55
+ }
56
+
57
+ function isChunkCall(node, chunkFunctionNames) {
58
+ const name = callName(node);
59
+ return Boolean(name && chunkFunctionNames.includes(name));
60
+ }
61
+
62
+ function isStaticallyBounded(source, chunkFunctionNames, context) {
63
+ source = unwrapTs(source);
64
+ if (!source) return false;
65
+ if (source.type === "ArrayExpression") return true;
66
+ if (source.type === "Identifier" && SCREAMING_CASE.test(source.name)) return true;
67
+ if (source.type === "CallExpression" && isChunkCall(source, chunkFunctionNames)) return true;
68
+ if (source.type === "Identifier" && context) {
69
+ const variable = resolveVariable(source, context);
70
+ const init = variable?.defs?.find(
71
+ (def) => def.type === "Variable" && def.node?.id?.type === "Identifier",
72
+ )?.node?.init;
73
+ if (init && init !== source) return isStaticallyBounded(init, chunkFunctionNames, null);
74
+ }
75
+ return false;
76
+ }
77
+
78
+ function isPromiseAllCallee(node) {
79
+ const callee = unwrapTs(node);
80
+ if (callee?.type !== "MemberExpression" || callee.computed) return false;
81
+ if (callee.property?.name !== "all") return false;
82
+ const object = unwrapTs(callee.object);
83
+ return object?.type === "Identifier" && object.name === "Promise";
84
+ }
85
+
86
+ function mapCallArgument(node) {
87
+ const argument = unwrapTs(firstCallArgument(node));
88
+ if (argument?.type !== "CallExpression") return null;
89
+ const callee = unwrapTs(argument.callee);
90
+ if (callee?.type !== "MemberExpression") return null;
91
+ if (memberPropertyName(callee) !== "map") return null;
92
+ return {
93
+ source: unwrapTs(callee.object),
94
+ callback: argument.arguments[0],
95
+ mapCall: argument,
96
+ };
97
+ }
98
+
99
+ function containsDatabaseCall(node, bindings) {
100
+ if (!node || typeof node !== "object") return false;
101
+ if (isDatabaseCall(node, bindings)) return true;
102
+ for (const child of childNodes(node)) {
103
+ if (containsDatabaseCall(child, bindings)) return true;
104
+ }
105
+ return false;
106
+ }
107
+
108
+ function functionFromDefinition(def) {
109
+ const node = def?.node;
110
+ if (!node) return null;
111
+ if (
112
+ node.type === "FunctionDeclaration" ||
113
+ node.type === "FunctionExpression" ||
114
+ node.type === "ArrowFunctionExpression"
115
+ ) {
116
+ return node;
117
+ }
118
+ const init = node.init;
119
+ if (
120
+ init &&
121
+ (init.type === "FunctionExpression" ||
122
+ init.type === "ArrowFunctionExpression" ||
123
+ init.type === "FunctionDeclaration")
124
+ ) {
125
+ return init;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function callbackContainsExecutor(callback, bindings, context) {
131
+ callback = unwrapTs(callback);
132
+ if (!callback) return false;
133
+ if (
134
+ callback.type === "ArrowFunctionExpression" ||
135
+ callback.type === "FunctionExpression" ||
136
+ callback.type === "FunctionDeclaration"
137
+ ) {
138
+ return containsDatabaseCall(callback, bindings);
139
+ }
140
+ if (callback.type !== "Identifier") return false;
141
+ if (bindings?.has(callback.name)) return true;
142
+ if (!context) return false;
143
+ const variable = resolveVariable(callback, context);
144
+ for (const def of variable?.defs ?? []) {
145
+ const fn = functionFromDefinition(def);
146
+ if (fn && containsDatabaseCall(fn, bindings)) return true;
147
+ }
148
+ return false;
149
+ }
150
+
151
+ module.exports = {
152
+ DEFAULT_CHUNK_FUNCTION_NAMES,
153
+ DEFAULT_EXECUTOR_NAMES,
154
+ DEFAULT_IMPORT_SPECIFIER,
155
+ QUERY_PROPERTY,
156
+ TRANSACTION_COMMAND,
157
+ TRANSACTION_IMPORTS,
158
+ callbackContainsExecutor,
159
+ calleeName,
160
+ childNodes,
161
+ containsDatabaseCall,
162
+ executedQueryText,
163
+ executorBindings,
164
+ executorOptionDefaults,
165
+ executorOptionSchema,
166
+ firstCallArgument,
167
+ isDatabaseCall,
168
+ isManualTransactionText,
169
+ isOwnerFile,
170
+ isPromiseAllCallee,
171
+ isStaticallyBounded,
172
+ mapCallArgument,
173
+ resolveVariable,
174
+ sqlStatementBindings,
175
+ sqlText,
176
+ templateSqlText,
177
+ unwrapTs,
178
+ };