eslint-plugin-no-mistakes 0.72.0 → 0.73.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.72.0",
3
+ "version": "0.73.0",
4
4
  "description": "ESLint and Oxlint rules that keep code static enough for no-mistakes analyzers and coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const { quasiText, sqlText, unwrapTs } = require("./postgres-query-text");
4
+ const { collectScopedExecutors, isScopedExecutor } = require("./postgres-scoped-executors");
4
5
 
5
6
  const DEFAULT_IMPORT_SPECIFIER = "";
6
7
  const DEFAULT_EXECUTOR_NAMES = ["query", "read", "write"];
@@ -18,6 +19,8 @@ function executorOptionDefaults(options = {}) {
18
19
  return {
19
20
  importSpecifier: options.importSpecifier ?? DEFAULT_IMPORT_SPECIFIER,
20
21
  executorNames: options.executorNames ?? (options.importSpecifier ? DEFAULT_EXECUTOR_NAMES : []),
22
+ executorFactoryNames: options.executorFactoryNames ?? [],
23
+ executorTypeNames: options.executorTypeNames ?? [],
21
24
  owners: options.owners ?? [],
22
25
  chunkFunctionNames: options.chunkFunctionNames ?? DEFAULT_CHUNK_FUNCTION_NAMES,
23
26
  };
@@ -29,6 +32,8 @@ function executorOptionSchema(extraProperties = {}) {
29
32
  properties: {
30
33
  importSpecifier: { type: "string" },
31
34
  executorNames: { type: "array", items: { type: "string" } },
35
+ executorFactoryNames: { type: "array", items: { type: "string" } },
36
+ executorTypeNames: { type: "array", items: { type: "string" } },
32
37
  ...extraProperties,
33
38
  },
34
39
  additionalProperties: false,
@@ -43,7 +48,9 @@ function importedName(specifier) {
43
48
 
44
49
  function executorBindings(program, options = {}) {
45
50
  const bindings = new Set();
46
- const { importSpecifier, executorNames } = executorOptionDefaults(options);
51
+ const defaults = executorOptionDefaults(options);
52
+ const { importSpecifier, executorNames } = defaults;
53
+ bindings.scoped = collectScopedExecutors(program, defaults);
47
54
  bindings.queryMembers = Boolean(importSpecifier) || executorNames.includes(QUERY_PROPERTY);
48
55
  for (const statement of program?.body ?? []) {
49
56
  if (statement.type !== "ImportDeclaration") continue;
@@ -80,7 +87,12 @@ function memberPropertyName(node) {
80
87
  function calleeName(call, bindings) {
81
88
  const callee = unwrapTs(call?.callee);
82
89
  if (!callee) return null;
83
- if (callee.type === "Identifier" && bindings?.has(callee.name)) return callee.name;
90
+ if (
91
+ callee.type === "Identifier" &&
92
+ (bindings?.has(callee.name) || isScopedExecutor(bindings?.scoped, callee.name, call))
93
+ ) {
94
+ return callee.name;
95
+ }
84
96
  if (
85
97
  bindings?.queryMembers !== false &&
86
98
  callee.type === "MemberExpression" &&
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+
3
+ const { childNodes, unwrapTs } = require("./postgres-query-text");
4
+
5
+ const FUNCTION_TYPES = new Set([
6
+ "FunctionDeclaration",
7
+ "FunctionExpression",
8
+ "ArrowFunctionExpression",
9
+ ]);
10
+ const SCOPE_TYPES = new Set([
11
+ "Program",
12
+ "BlockStatement",
13
+ "StaticBlock",
14
+ "SwitchStatement",
15
+ "ForStatement",
16
+ ]);
17
+
18
+ function importedName(specifier) {
19
+ const imported = specifier?.imported;
20
+ return imported?.type === "Literal" ? String(imported.value) : imported?.name;
21
+ }
22
+
23
+ // Local names of configured factory (value) and type imports, by module.
24
+ function scopedImports(program, options) {
25
+ const factories = new Set();
26
+ const types = new Set();
27
+ const { importSpecifier, executorFactoryNames, executorTypeNames } = options;
28
+ for (const statement of program?.body ?? []) {
29
+ if (statement.type !== "ImportDeclaration") continue;
30
+ if (importSpecifier && statement.source?.value !== importSpecifier) continue;
31
+ for (const specifier of statement.specifiers ?? []) {
32
+ if (specifier.type !== "ImportSpecifier") continue;
33
+ const imported = importedName(specifier);
34
+ const typeOnly = statement.importKind === "type" || specifier.importKind === "type";
35
+ if (!typeOnly && executorFactoryNames.includes(imported)) factories.add(specifier.local.name);
36
+ if (executorTypeNames.includes(imported)) types.add(specifier.local.name);
37
+ }
38
+ }
39
+ return { factories, types };
40
+ }
41
+
42
+ function typeMatches(annotation, types) {
43
+ const node = annotation?.typeAnnotation ?? annotation;
44
+ if (node?.type === "TSTypeReference") {
45
+ return node.typeName?.type === "Identifier" && types.has(node.typeName.name);
46
+ }
47
+ if (node?.type === "TSUnionType") return node.types.some((member) => typeMatches(member, types));
48
+ return false;
49
+ }
50
+
51
+ function propertyKeyName(key) {
52
+ if (key?.type === "Identifier") return key.name;
53
+ return key?.type === "Literal" ? String(key.value) : null;
54
+ }
55
+
56
+ function isFactoryCall(init, factories) {
57
+ let node = unwrapTs(init);
58
+ if (node?.type === "AwaitExpression") node = unwrapTs(node.argument);
59
+ const callee = node?.type === "CallExpression" ? unwrapTs(node.callee) : null;
60
+ return callee?.type === "Identifier" && factories.has(callee.name);
61
+ }
62
+
63
+ function parameterNames(param, types) {
64
+ const target = param.type === "AssignmentPattern" ? param.left : param;
65
+ if (target.type === "Identifier") {
66
+ return typeMatches(target.typeAnnotation, types) ? [target.name] : [];
67
+ }
68
+ const literal = target.typeAnnotation?.typeAnnotation;
69
+ if (target.type !== "ObjectPattern" || literal?.type !== "TSTypeLiteral") return [];
70
+ const names = [];
71
+ for (const property of target.properties) {
72
+ const value =
73
+ property.value?.type === "AssignmentPattern" ? property.value.left : property.value;
74
+ if (property.type !== "Property" || value?.type !== "Identifier") continue;
75
+ const key = propertyKeyName(property.key);
76
+ const typed = literal.members.some(
77
+ (member) =>
78
+ member.type === "TSPropertySignature" &&
79
+ propertyKeyName(member.key) === key &&
80
+ typeMatches(member.typeAnnotation, types),
81
+ );
82
+ if (typed) names.push(value.name);
83
+ }
84
+ return names;
85
+ }
86
+
87
+ /**
88
+ * Executors that exist only inside a lexical scope: locals bound to a
89
+ * configured factory call (the declaring block) and parameters typed with a
90
+ * configured type (the declaring function). Returns `{ name, range }` entries.
91
+ */
92
+ function collectScopedExecutors(program, options) {
93
+ const found = [];
94
+ const { factories, types } = scopedImports(program, options);
95
+ if (factories.size === 0 && types.size === 0) return found;
96
+ const walk = (node, scope) => {
97
+ if (SCOPE_TYPES.has(node.type)) scope = node;
98
+ if (node.type === "VariableDeclaration" && node.kind !== "var") {
99
+ for (const declarator of node.declarations) {
100
+ if (declarator.id.type !== "Identifier" || !isFactoryCall(declarator.init, factories)) {
101
+ continue;
102
+ }
103
+ found.push({
104
+ name: declarator.id.name,
105
+ range: [declarator.range[0], scope.range[1]],
106
+ });
107
+ }
108
+ }
109
+ if (FUNCTION_TYPES.has(node.type)) {
110
+ for (const param of node.params) {
111
+ for (const name of parameterNames(param, types)) {
112
+ found.push({ name, range: node.range });
113
+ }
114
+ }
115
+ }
116
+ for (const child of childNodes(node)) walk(child, scope);
117
+ };
118
+ walk(program, program);
119
+ return found;
120
+ }
121
+
122
+ function isScopedExecutor(scoped, name, node) {
123
+ const position = node?.range?.[0];
124
+ return Boolean(
125
+ scoped?.some(
126
+ (entry) => entry.name === name && entry.range[0] <= position && position < entry.range[1],
127
+ ),
128
+ );
129
+ }
130
+
131
+ module.exports = { collectScopedExecutors, isScopedExecutor };