eslint-plugin-no-mistakes 0.48.0 → 0.48.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.48.0",
3
+ "version": "0.48.2",
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,7 @@ 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-cursor-call-contract": require("./rules/postgres-cursor-call-contract"),
32
33
  "postgres-no-manual-transaction": require("./rules/postgres-no-manual-transaction"),
33
34
  "postgres-no-unbounded-query-fanout": require("./rules/postgres-no-unbounded-query-fanout"),
34
35
  "react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ function appendCall(node, helpers, transparentParent) {
4
+ const object = transparentParent(node);
5
+ const member = object.parent;
6
+ if (member?.type !== "MemberExpression" || member.object !== object) return null;
7
+ if (member.computed || helpers.propertyName(member) !== "append") return null;
8
+ const callee = transparentParent(member);
9
+ const call = callee.parent;
10
+ return call?.type === "CallExpression" && call.callee === callee ? call : null;
11
+ }
12
+
13
+ function isDiscardedSqlStatementAppend(identifier, helpers, transparentParent) {
14
+ let call = appendCall(identifier, helpers, transparentParent);
15
+ while (call) {
16
+ const result = transparentParent(call);
17
+ if (result.parent?.type === "ExpressionStatement") return true;
18
+ call = appendCall(call, helpers, transparentParent);
19
+ }
20
+ return false;
21
+ }
22
+
23
+ module.exports = { isDiscardedSqlStatementAppend };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ const WRAPPERS = new Set([
4
+ "ChainExpression",
5
+ "TSAsExpression",
6
+ "TSSatisfiesExpression",
7
+ "TSTypeAssertion",
8
+ "TSNonNullExpression",
9
+ ]);
10
+
11
+ function unwrap(node) {
12
+ let current = node;
13
+ while (current && WRAPPERS.has(current.type)) {
14
+ current = current.expression;
15
+ }
16
+ return current;
17
+ }
18
+
19
+ function findVariable(context, identifier) {
20
+ if (typeof identifier?.name !== "string") return null;
21
+ let scope = context.sourceCode.getScope(identifier);
22
+ while (scope) {
23
+ const variable =
24
+ (typeof scope.set?.get === "function" && scope.set.get(identifier.name)) ||
25
+ scope.variables?.find((candidate) => candidate.name === identifier.name);
26
+ if (variable) return variable;
27
+ scope = scope.upper;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function literalName(value) {
33
+ if (
34
+ typeof value === "string" ||
35
+ typeof value === "number" ||
36
+ typeof value === "boolean" ||
37
+ typeof value === "bigint"
38
+ ) {
39
+ return value;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function staticPropertyName(node) {
45
+ const value = unwrap(node);
46
+ if (value?.type === "Literal") return literalName(value.value);
47
+ if (value?.type === "TemplateLiteral" && (value.expressions ?? []).length === 0) {
48
+ const quasi = (value.quasis ?? [])[0];
49
+ return quasi?.value?.cooked ?? quasi?.value?.raw ?? null;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function propertyName(member) {
55
+ if (member?.type !== "MemberExpression") return null;
56
+ const property = member.property;
57
+ if (!member.computed && property?.type === "Identifier" && typeof property.name === "string") {
58
+ return property.name;
59
+ }
60
+ return member.computed ? staticPropertyName(property) : null;
61
+ }
62
+
63
+ function normalizeFilename(context) {
64
+ const filename = String(context.filename ?? "").replaceAll("\\", "/");
65
+ const cwd = context.cwd?.replaceAll("\\", "/").replace(/\/$/, "");
66
+ return cwd && filename.startsWith(`${cwd}/`) ? filename.slice(cwd.length + 1) : filename;
67
+ }
68
+
69
+ module.exports = {
70
+ findVariable,
71
+ normalizeFilename,
72
+ propertyName,
73
+ unwrap,
74
+ };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+
3
+ const { rule } = require("../helpers");
4
+ const { findVariable, propertyName, unwrap } = require("./postgres-cursor-ast");
5
+ const { matchesCursorFile, resolveCursorContractOptions } = require("./postgres-cursor-options");
6
+ const {
7
+ isCursorExecutor,
8
+ isCursorModule,
9
+ namedCursorImport,
10
+ namespaceCursorImport,
11
+ namespaceCursorMember,
12
+ namespaceImportMember,
13
+ } = require("./postgres-cursor-imports");
14
+ const {
15
+ directCallParent,
16
+ isTypeQuery,
17
+ queryHead,
18
+ transparentParent,
19
+ } = require("./postgres-cursor-query");
20
+
21
+ const helpers = { findVariable, propertyName, unwrap };
22
+
23
+ function isTypeOnlyExport(identifier) {
24
+ const specifier = identifier.parent;
25
+ return (
26
+ specifier?.type === "ExportSpecifier" &&
27
+ (specifier.exportKind === "type" || specifier.parent?.exportKind === "type")
28
+ );
29
+ }
30
+
31
+ function reExportsCursor(node, config) {
32
+ if (!isCursorModule(node.source?.value, config) || node.exportKind === "type") return false;
33
+ if (node.type === "ExportAllDeclaration") return true;
34
+ return (
35
+ node.specifiers?.some((specifier) => {
36
+ const name = specifier.local?.name ?? specifier.local?.value;
37
+ return specifier.exportKind !== "type" && isCursorExecutor(name, config);
38
+ }) === true
39
+ );
40
+ }
41
+
42
+ function visitCursorIdentifier(context, node, options, reportedDirectUses) {
43
+ const namedImport = namedCursorImport(context, node, helpers, options);
44
+ const namespaceImport = namespaceCursorImport(context, node, helpers, options);
45
+ if (!namedImport && !namespaceImport) return;
46
+ if (
47
+ node.parent?.type === "ImportSpecifier" ||
48
+ node.parent?.type === "ImportNamespaceSpecifier" ||
49
+ isTypeQuery(node) ||
50
+ isTypeOnlyExport(node)
51
+ ) {
52
+ return;
53
+ }
54
+ const variable = helpers.findVariable(context, node);
55
+ if (!variable?.references.some((reference) => reference.identifier === node)) return;
56
+ const expression = transparentParent(node);
57
+ if (namespaceImport && namespaceImportMember(context, expression.parent, helpers, options)) {
58
+ return;
59
+ }
60
+ if (!directCallParent(node) && !reportedDirectUses.has(node)) {
61
+ reportedDirectUses.add(node);
62
+ context.report({ messageId: "directUse", node });
63
+ }
64
+ }
65
+
66
+ function visitNamespaceMember(context, node, options) {
67
+ const namespaceMember = namespaceImportMember(context, node, helpers, options);
68
+ if (!namespaceMember || isTypeQuery(node)) return;
69
+ if (namespaceMember.computed && helpers.propertyName(namespaceMember) == null) {
70
+ context.report({ messageId: "staticNamespaceMember", node });
71
+ return;
72
+ }
73
+ if (namespaceCursorMember(context, node, helpers, options) && !directCallParent(node)) {
74
+ context.report({ messageId: "directUse", node });
75
+ }
76
+ }
77
+
78
+ module.exports = rule(
79
+ {
80
+ type: "problem",
81
+ docs: {
82
+ description: "require direct cursor calls with statically annotated SQL",
83
+ recommended: false,
84
+ },
85
+ schema: [
86
+ {
87
+ type: "object",
88
+ additionalProperties: false,
89
+ properties: {
90
+ modules: { type: "array", items: { type: "string" } },
91
+ executors: { type: "array", items: { type: "string" } },
92
+ include: { type: "array", items: { type: "string" } },
93
+ exclude: { type: "array", items: { type: "string" } },
94
+ includeFiles: { type: "array", items: { type: "string" } },
95
+ annotation: { type: "string" },
96
+ sqlTagModules: { type: "array", items: { type: "string" } },
97
+ },
98
+ },
99
+ ],
100
+ messages: {
101
+ annotation: "PostgreSQL cursor SQL must start with a static /* name */ annotation.",
102
+ directUse: "PostgreSQL cursor helpers must be called directly so their SQL can be verified.",
103
+ staticQuery:
104
+ "PostgreSQL cursor SQL must be visible at the callsite or in one immutable local binding.",
105
+ staticNamespaceMember:
106
+ "PostgreSQL namespace members must use a static property name so cursor use can be verified.",
107
+ },
108
+ },
109
+ (context) => {
110
+ const options = resolveCursorContractOptions(context.options[0]);
111
+ if (!options || !matchesCursorFile(context, options)) return {};
112
+ const reportedDirectUses = new WeakSet();
113
+ return {
114
+ CallExpression(node) {
115
+ const callee = helpers.unwrap(node.callee);
116
+ const executor =
117
+ namedCursorImport(context, callee, helpers, options) ||
118
+ namespaceCursorMember(context, callee, helpers, options);
119
+ if (!executor) return;
120
+ const argument = node.arguments?.[0];
121
+ const reportNode = argument ?? node;
122
+ const head = argument ? queryHead(context, argument, helpers, options) : null;
123
+ if (head === null) {
124
+ context.report({ messageId: "staticQuery", node: reportNode });
125
+ return;
126
+ }
127
+ if (!options.annotation.test(head)) {
128
+ context.report({ messageId: "annotation", node: reportNode });
129
+ }
130
+ },
131
+ Identifier(node) {
132
+ visitCursorIdentifier(context, node, options, reportedDirectUses);
133
+ },
134
+ MemberExpression(node) {
135
+ visitNamespaceMember(context, node, options);
136
+ },
137
+ ExportAllDeclaration(node) {
138
+ if (reExportsCursor(node, options)) context.report({ messageId: "directUse", node });
139
+ },
140
+ ExportNamedDeclaration(node) {
141
+ if (reExportsCursor(node, options)) context.report({ messageId: "directUse", node });
142
+ },
143
+ };
144
+ },
145
+ );
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ function isCursorModule(name, config) {
4
+ return typeof name === "string" && config.modules.has(name);
5
+ }
6
+
7
+ function isCursorExecutor(name, config) {
8
+ return typeof name === "string" && config.executors.has(name);
9
+ }
10
+
11
+ function importDefinition(variable, specifierType, config) {
12
+ return variable?.defs.find((definition) => {
13
+ const specifier = definition.node;
14
+ const declaration = definition.parent || specifier.parent;
15
+ return (
16
+ definition.type === "ImportBinding" &&
17
+ specifier.type === specifierType &&
18
+ specifier.importKind !== "type" &&
19
+ declaration?.type === "ImportDeclaration" &&
20
+ declaration.importKind !== "type" &&
21
+ isCursorModule(declaration.source?.value, config)
22
+ );
23
+ });
24
+ }
25
+
26
+ function namedCursorImport(context, identifier, helpers, config) {
27
+ if (identifier?.type !== "Identifier") return null;
28
+ const imported = importDefinition(
29
+ helpers.findVariable(context, identifier),
30
+ "ImportSpecifier",
31
+ config,
32
+ )?.node.imported;
33
+ const name = imported?.name ?? imported?.value;
34
+ return isCursorExecutor(name, config) ? String(name) : null;
35
+ }
36
+
37
+ function namespaceCursorImport(context, identifier, helpers, config) {
38
+ return (
39
+ identifier?.type === "Identifier" &&
40
+ Boolean(
41
+ importDefinition(
42
+ helpers.findVariable(context, identifier),
43
+ "ImportNamespaceSpecifier",
44
+ config,
45
+ ),
46
+ )
47
+ );
48
+ }
49
+
50
+ function namespaceImportMember(context, node, helpers, config) {
51
+ const member = helpers.unwrap(node);
52
+ if (member?.type !== "MemberExpression") return null;
53
+ const object = helpers.unwrap(member.object);
54
+ if (object?.type !== "Identifier") return null;
55
+ return namespaceCursorImport(context, object, helpers, config) ? member : null;
56
+ }
57
+
58
+ function namespaceCursorMember(context, node, helpers, config) {
59
+ const member = namespaceImportMember(context, node, helpers, config);
60
+ const name = member && helpers.propertyName(member);
61
+ return isCursorExecutor(name, config) ? String(name) : null;
62
+ }
63
+
64
+ module.exports = {
65
+ isCursorExecutor,
66
+ isCursorModule,
67
+ namedCursorImport,
68
+ namespaceCursorImport,
69
+ namespaceCursorMember,
70
+ namespaceImportMember,
71
+ };
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ const { stringMatches } = require("./module-mock-helpers");
4
+ const { normalizeFilename } = require("./postgres-cursor-ast");
5
+
6
+ const DEFAULT_CURSOR_INCLUDE = ["**/*.{ts,mts,tsx,js,mjs}"];
7
+ const DEFAULT_CURSOR_ANNOTATION = "^\\s*/\\*\\s*\\S[^]*?\\*/";
8
+ const DEFAULT_SQL_TAG_MODULES = ["sql-template-strings"];
9
+
10
+ function expandBraces(pattern) {
11
+ const start = pattern.indexOf("{");
12
+ const end = pattern.indexOf("}", start + 1);
13
+ if (start === -1 || end === -1) return [pattern];
14
+ const prefix = pattern.slice(0, start);
15
+ const suffix = pattern.slice(end + 1);
16
+ return pattern
17
+ .slice(start + 1, end)
18
+ .split(",")
19
+ .flatMap((alt) => expandBraces(`${prefix}${alt}${suffix}`));
20
+ }
21
+
22
+ function globListMatches(filename, patterns) {
23
+ return patterns.some((pattern) => stringMatches(filename, expandBraces(pattern)));
24
+ }
25
+
26
+ function stringArray(value) {
27
+ if (value === undefined) return undefined;
28
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) return undefined;
29
+ return value;
30
+ }
31
+
32
+ function optionalStringArray(value, present) {
33
+ if (!present) return undefined;
34
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : null;
35
+ }
36
+
37
+ function resolveCursorContractOptions(raw) {
38
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
39
+ const modules = stringArray(raw.modules);
40
+ const executors = stringArray(raw.executors);
41
+ if (!modules?.length || !executors?.length) return null;
42
+ if (raw.annotation !== undefined && typeof raw.annotation !== "string") return null;
43
+ const include = optionalStringArray(raw.include, raw.include !== undefined);
44
+ const exclude = optionalStringArray(raw.exclude, raw.exclude !== undefined);
45
+ const includeFiles = optionalStringArray(raw.includeFiles, raw.includeFiles !== undefined);
46
+ const sqlTagModules = optionalStringArray(raw.sqlTagModules, raw.sqlTagModules !== undefined);
47
+ if (include === null || exclude === null || includeFiles === null || sqlTagModules === null) {
48
+ return null;
49
+ }
50
+ let annotation;
51
+ try {
52
+ annotation = new RegExp(
53
+ typeof raw.annotation === "string" ? raw.annotation : DEFAULT_CURSOR_ANNOTATION,
54
+ );
55
+ } catch {
56
+ throw new Error(
57
+ `postgres-cursor-call-contract annotation is not a valid regular expression: ${String(raw.annotation)}`,
58
+ );
59
+ }
60
+ return {
61
+ modules: new Set(modules),
62
+ executors: new Set(executors),
63
+ include: include ?? DEFAULT_CURSOR_INCLUDE,
64
+ exclude: exclude ?? [],
65
+ includeFiles: (includeFiles ?? []).map((file) => file.replace(/^(?:\.\/)+/, "")),
66
+ sqlTagModules: new Set(sqlTagModules ?? DEFAULT_SQL_TAG_MODULES),
67
+ annotation,
68
+ };
69
+ }
70
+
71
+ function matchesCursorFile(context, options) {
72
+ const filename = normalizeFilename(context).replace(/^(?:\.\/)+/, "");
73
+ if (options.includeFiles.includes(filename)) return true;
74
+ if (options.exclude.length > 0 && globListMatches(filename, options.exclude)) return false;
75
+ return globListMatches(filename, options.include);
76
+ }
77
+
78
+ module.exports = {
79
+ DEFAULT_CURSOR_INCLUDE,
80
+ DEFAULT_SQL_TAG_MODULES,
81
+ matchesCursorFile,
82
+ resolveCursorContractOptions,
83
+ };
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+
3
+ const { isDiscardedSqlStatementAppend } = require("./postgres-cursor-append");
4
+ const { namedCursorImport, namespaceCursorMember } = require("./postgres-cursor-imports");
5
+
6
+ function transparentParent(node) {
7
+ let current = node;
8
+ while (
9
+ current.parent &&
10
+ (current.parent.type === "ChainExpression" ||
11
+ current.parent.type === "TSAsExpression" ||
12
+ current.parent.type === "TSSatisfiesExpression" ||
13
+ current.parent.type === "TSTypeAssertion" ||
14
+ current.parent.type === "TSNonNullExpression") &&
15
+ current.parent.expression === current
16
+ ) {
17
+ current = current.parent;
18
+ }
19
+ return current;
20
+ }
21
+
22
+ function directCallParent(node) {
23
+ const current = transparentParent(node);
24
+ return current.parent?.type === "CallExpression" && current.parent.callee === current
25
+ ? current.parent
26
+ : null;
27
+ }
28
+
29
+ function isTypeQuery(node) {
30
+ let current = transparentParent(node);
31
+ while (current.parent?.type === "TSQualifiedName") current = current.parent;
32
+ return current.parent?.type === "TSTypeQuery";
33
+ }
34
+
35
+ function firstQuasiText(template, allowRaw) {
36
+ const quasi = template?.quasis?.[0];
37
+ if (!quasi) return null;
38
+ return quasi.value?.cooked ?? (allowRaw ? (quasi.value?.raw ?? null) : null);
39
+ }
40
+
41
+ function exactSqlTag(context, tag, helpers, config) {
42
+ const identifier = helpers.unwrap(tag);
43
+ if (identifier?.type !== "Identifier") return false;
44
+ const variable = helpers.findVariable(context, identifier);
45
+ const definition = variable?.defs.find((candidate) => {
46
+ const specifier = candidate.node;
47
+ const declaration = candidate.parent || specifier.parent;
48
+ const source = declaration?.source?.value;
49
+ return (
50
+ candidate.type === "ImportBinding" &&
51
+ specifier.type === "ImportDefaultSpecifier" &&
52
+ specifier.importKind !== "type" &&
53
+ declaration?.type === "ImportDeclaration" &&
54
+ declaration.importKind !== "type" &&
55
+ typeof source === "string" &&
56
+ config.sqlTagModules.has(source)
57
+ );
58
+ });
59
+ return Boolean(
60
+ definition && variable && !variable.references.some((reference) => reference.isWrite()),
61
+ );
62
+ }
63
+
64
+ function directQueryHead(context, node, helpers, config) {
65
+ const value = helpers.unwrap(node);
66
+ if (value?.type === "Literal") return typeof value.value === "string" ? value.value : null;
67
+ if (value?.type === "TemplateLiteral") return firstQuasiText(value, true);
68
+ if (
69
+ value?.type === "TaggedTemplateExpression" &&
70
+ exactSqlTag(context, value.tag, helpers, config)
71
+ ) {
72
+ return firstQuasiText(value.quasi, false);
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function isCursorQueryArgument(context, identifier, helpers, config) {
78
+ const argument = transparentParent(identifier);
79
+ const call = argument.parent;
80
+ if (call?.type !== "CallExpression" || call.arguments?.[0] !== argument) return false;
81
+ const callee = helpers.unwrap(call.callee);
82
+ return Boolean(
83
+ namedCursorImport(context, callee, helpers, config) ||
84
+ namespaceCursorMember(context, callee, helpers, config),
85
+ );
86
+ }
87
+
88
+ function queryHead(context, node, helpers, config) {
89
+ const direct = directQueryHead(context, node, helpers, config);
90
+ if (direct !== null) return direct;
91
+ const value = helpers.unwrap(node);
92
+ if (value?.type !== "Identifier") return null;
93
+ const variable = helpers.findVariable(context, value);
94
+ const definitions = variable?.defs.filter((definition) => definition.type === "Variable") ?? [];
95
+ if (definitions.length !== 1) return null;
96
+ const declaration = definitions[0]?.node;
97
+ if (
98
+ declaration?.type !== "VariableDeclarator" ||
99
+ declaration.parent?.type !== "VariableDeclaration" ||
100
+ declaration.parent.kind !== "const" ||
101
+ variable?.references.some((reference) => {
102
+ const identifier = reference.identifier;
103
+ return (
104
+ identifier !== declaration.id &&
105
+ !isTypeQuery(identifier) &&
106
+ !isCursorQueryArgument(context, identifier, helpers, config) &&
107
+ !isDiscardedSqlStatementAppend(identifier, helpers, transparentParent)
108
+ );
109
+ })
110
+ ) {
111
+ return null;
112
+ }
113
+ return directQueryHead(context, declaration.init, helpers, config);
114
+ }
115
+
116
+ module.exports = {
117
+ directCallParent,
118
+ firstQuasiText,
119
+ isTypeQuery,
120
+ queryHead,
121
+ transparentParent,
122
+ };