eslint-plugin-no-mistakes 0.75.0 → 0.76.1
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 +1 -1
- package/src/rules/postgres-cursor-call-contract.js +12 -0
- package/src/rules/postgres-cursor-options.js +30 -1
- package/src/rules/postgres-cursor-query.js +34 -7
- package/src/rules/postgres-executor.js +15 -2
- package/src/rules/postgres-no-manual-transaction.js +1 -1
- package/src/rules/postgres-no-unbounded-query-fanout.js +1 -1
- package/src/rules/postgres-scoped-executors.js +96 -4
package/package.json
CHANGED
|
@@ -94,6 +94,18 @@ module.exports = rule(
|
|
|
94
94
|
includeFiles: { type: "array", items: { type: "string" } },
|
|
95
95
|
annotation: { type: "string" },
|
|
96
96
|
sqlTagModules: { type: "array", items: { type: "string" } },
|
|
97
|
+
trustedSqlTags: {
|
|
98
|
+
type: "array",
|
|
99
|
+
items: {
|
|
100
|
+
type: "object",
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
required: ["module", "name"],
|
|
103
|
+
properties: {
|
|
104
|
+
module: { type: "string" },
|
|
105
|
+
name: { type: "string" },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
97
109
|
},
|
|
98
110
|
},
|
|
99
111
|
],
|
|
@@ -34,6 +34,27 @@ function optionalStringArray(value, present) {
|
|
|
34
34
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function trustedSqlTagEntry(entry) {
|
|
38
|
+
return (
|
|
39
|
+
entry !== null &&
|
|
40
|
+
typeof entry === "object" &&
|
|
41
|
+
!Array.isArray(entry) &&
|
|
42
|
+
typeof entry.module === "string" &&
|
|
43
|
+
typeof entry.name === "string"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseTrustedSqlTags(value, present) {
|
|
48
|
+
if (!present) return undefined;
|
|
49
|
+
if (!Array.isArray(value)) return null;
|
|
50
|
+
const tags = [];
|
|
51
|
+
for (const entry of value) {
|
|
52
|
+
if (!trustedSqlTagEntry(entry)) return null;
|
|
53
|
+
tags.push({ module: entry.module, name: entry.name });
|
|
54
|
+
}
|
|
55
|
+
return tags;
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
function resolveCursorContractOptions(raw) {
|
|
38
59
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
39
60
|
const modules = stringArray(raw.modules);
|
|
@@ -44,7 +65,14 @@ function resolveCursorContractOptions(raw) {
|
|
|
44
65
|
const exclude = optionalStringArray(raw.exclude, raw.exclude !== undefined);
|
|
45
66
|
const includeFiles = optionalStringArray(raw.includeFiles, raw.includeFiles !== undefined);
|
|
46
67
|
const sqlTagModules = optionalStringArray(raw.sqlTagModules, raw.sqlTagModules !== undefined);
|
|
47
|
-
|
|
68
|
+
const trustedSqlTags = parseTrustedSqlTags(raw.trustedSqlTags, raw.trustedSqlTags !== undefined);
|
|
69
|
+
if (
|
|
70
|
+
include === null ||
|
|
71
|
+
exclude === null ||
|
|
72
|
+
includeFiles === null ||
|
|
73
|
+
sqlTagModules === null ||
|
|
74
|
+
trustedSqlTags === null
|
|
75
|
+
) {
|
|
48
76
|
return null;
|
|
49
77
|
}
|
|
50
78
|
let annotation;
|
|
@@ -64,6 +92,7 @@ function resolveCursorContractOptions(raw) {
|
|
|
64
92
|
exclude: exclude ?? [],
|
|
65
93
|
includeFiles: (includeFiles ?? []).map((file) => file.replace(/^(?:\.\/)+/, "")),
|
|
66
94
|
sqlTagModules: new Set(sqlTagModules ?? DEFAULT_SQL_TAG_MODULES),
|
|
95
|
+
trustedSqlTags: trustedSqlTags ?? [],
|
|
67
96
|
annotation,
|
|
68
97
|
};
|
|
69
98
|
}
|
|
@@ -38,6 +38,37 @@ function firstQuasiText(template, allowRaw) {
|
|
|
38
38
|
return quasi.value?.cooked ?? (allowRaw ? (quasi.value?.raw ?? null) : null);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function importedBindingName(specifier) {
|
|
42
|
+
const imported = specifier?.imported;
|
|
43
|
+
if (!imported) return null;
|
|
44
|
+
return imported.type === "Literal" ? String(imported.value) : imported.name;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Named opt-in only. An empty module must not match every import.
|
|
48
|
+
function matchesTrustedSqlTag(source, imported, tag) {
|
|
49
|
+
if (!tag.module || !tag.name || tag.name !== imported) return false;
|
|
50
|
+
return source === tag.module || String(source).startsWith(`${tag.module}/`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function trustedNamedSqlImport(specifier, declaration, source, config) {
|
|
54
|
+
if (specifier?.type !== "ImportSpecifier") return false;
|
|
55
|
+
if (specifier.importKind === "type" || declaration?.importKind === "type") return false;
|
|
56
|
+
if (declaration?.type !== "ImportDeclaration" || typeof source !== "string") return false;
|
|
57
|
+
const imported = importedBindingName(specifier);
|
|
58
|
+
return (config.trustedSqlTags ?? []).some((tag) => matchesTrustedSqlTag(source, imported, tag));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultSqlTagImport(specifier, declaration, source, config) {
|
|
62
|
+
return (
|
|
63
|
+
specifier.type === "ImportDefaultSpecifier" &&
|
|
64
|
+
specifier.importKind !== "type" &&
|
|
65
|
+
declaration?.type === "ImportDeclaration" &&
|
|
66
|
+
declaration.importKind !== "type" &&
|
|
67
|
+
typeof source === "string" &&
|
|
68
|
+
config.sqlTagModules.has(source)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
41
72
|
function exactSqlTag(context, tag, helpers, config) {
|
|
42
73
|
const identifier = helpers.unwrap(tag);
|
|
43
74
|
if (identifier?.type !== "Identifier") return false;
|
|
@@ -46,14 +77,10 @@ function exactSqlTag(context, tag, helpers, config) {
|
|
|
46
77
|
const specifier = candidate.node;
|
|
47
78
|
const declaration = candidate.parent || specifier.parent;
|
|
48
79
|
const source = declaration?.source?.value;
|
|
80
|
+
if (candidate.type !== "ImportBinding") return false;
|
|
49
81
|
return (
|
|
50
|
-
|
|
51
|
-
specifier
|
|
52
|
-
specifier.importKind !== "type" &&
|
|
53
|
-
declaration?.type === "ImportDeclaration" &&
|
|
54
|
-
declaration.importKind !== "type" &&
|
|
55
|
-
typeof source === "string" &&
|
|
56
|
-
config.sqlTagModules.has(source)
|
|
82
|
+
trustedNamedSqlImport(specifier, declaration, source, config) ||
|
|
83
|
+
defaultSqlTagImport(specifier, declaration, source, config)
|
|
57
84
|
);
|
|
58
85
|
});
|
|
59
86
|
return Boolean(
|
|
@@ -21,6 +21,7 @@ function executorOptionDefaults(options = {}) {
|
|
|
21
21
|
executorNames: options.executorNames ?? (options.importSpecifier ? DEFAULT_EXECUTOR_NAMES : []),
|
|
22
22
|
executorFactoryNames: options.executorFactoryNames ?? [],
|
|
23
23
|
executorTypeNames: options.executorTypeNames ?? [],
|
|
24
|
+
trustedSqlTags: options.trustedSqlTags ?? [],
|
|
24
25
|
owners: options.owners ?? [],
|
|
25
26
|
chunkFunctionNames: options.chunkFunctionNames ?? DEFAULT_CHUNK_FUNCTION_NAMES,
|
|
26
27
|
};
|
|
@@ -34,6 +35,18 @@ function executorOptionSchema(extraProperties = {}) {
|
|
|
34
35
|
executorNames: { type: "array", items: { type: "string" } },
|
|
35
36
|
executorFactoryNames: { type: "array", items: { type: "string" } },
|
|
36
37
|
executorTypeNames: { type: "array", items: { type: "string" } },
|
|
38
|
+
trustedSqlTags: {
|
|
39
|
+
type: "array",
|
|
40
|
+
items: {
|
|
41
|
+
type: "object",
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
required: ["module", "name"],
|
|
44
|
+
properties: {
|
|
45
|
+
module: { type: "string" },
|
|
46
|
+
name: { type: "string" },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
37
50
|
...extraProperties,
|
|
38
51
|
},
|
|
39
52
|
additionalProperties: false,
|
|
@@ -46,11 +59,11 @@ function importedName(specifier) {
|
|
|
46
59
|
return imported.type === "Literal" ? String(imported.value) : imported.name;
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
function executorBindings(program, options = {}) {
|
|
62
|
+
function executorBindings(program, options = {}, filename) {
|
|
50
63
|
const bindings = new Set();
|
|
51
64
|
const defaults = executorOptionDefaults(options);
|
|
52
65
|
const { importSpecifier, executorNames } = defaults;
|
|
53
|
-
bindings.scoped = collectScopedExecutors(program, defaults);
|
|
66
|
+
bindings.scoped = collectScopedExecutors(program, defaults, filename);
|
|
54
67
|
bindings.queryMembers = Boolean(importSpecifier) || executorNames.includes(QUERY_PROPERTY);
|
|
55
68
|
for (const statement of program?.body ?? []) {
|
|
56
69
|
if (statement.type !== "ImportDeclaration") continue;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
3
5
|
const { childNodes, unwrapTs } = require("./postgres-query-text");
|
|
4
6
|
|
|
7
|
+
const RELATIVE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".jsx"];
|
|
8
|
+
|
|
5
9
|
const FUNCTION_TYPES = new Set([
|
|
6
10
|
"FunctionDeclaration",
|
|
7
11
|
"FunctionExpression",
|
|
@@ -25,14 +29,102 @@ function fromConfiguredModule(source, specifier) {
|
|
|
25
29
|
return !specifier || source === specifier || String(source).startsWith(`${specifier}/`);
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
function packageName(specifier) {
|
|
33
|
+
if (
|
|
34
|
+
typeof specifier !== "string" ||
|
|
35
|
+
specifier === "" ||
|
|
36
|
+
specifier.startsWith(".") ||
|
|
37
|
+
specifier.startsWith("/")
|
|
38
|
+
) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (specifier.startsWith("@")) {
|
|
42
|
+
const slash = specifier.indexOf("/");
|
|
43
|
+
if (slash <= 1) return null;
|
|
44
|
+
const rest = specifier.slice(slash + 1);
|
|
45
|
+
const name = rest.split("/")[0];
|
|
46
|
+
if (!name) return null;
|
|
47
|
+
return specifier.slice(0, slash + 1 + name.length);
|
|
48
|
+
}
|
|
49
|
+
const name = specifier.split("/")[0];
|
|
50
|
+
return name || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isRelativeSpecifier(source) {
|
|
54
|
+
return (
|
|
55
|
+
source === "." ||
|
|
56
|
+
source === ".." ||
|
|
57
|
+
(typeof source === "string" && (source.startsWith("./") || source.startsWith("../")))
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readPackageName(dir) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
64
|
+
return typeof parsed.name === "string" ? parsed.name : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Walk up from the linted file. A missing or unreadable root fails closed.
|
|
71
|
+
function packageRoot(filename, specifier) {
|
|
72
|
+
const name = packageName(specifier);
|
|
73
|
+
if (!name || typeof filename !== "string" || filename === "") return null;
|
|
74
|
+
let dir = path.dirname(path.resolve(filename));
|
|
75
|
+
const stop = path.parse(dir).root;
|
|
76
|
+
while (dir) {
|
|
77
|
+
if (readPackageName(dir) === name) return dir;
|
|
78
|
+
const nested = path.join(dir, "node_modules", ...name.split("/"));
|
|
79
|
+
if (readPackageName(nested) === name) return nested;
|
|
80
|
+
if (dir === stop) return null;
|
|
81
|
+
const parent = path.dirname(dir);
|
|
82
|
+
if (parent === dir) return null;
|
|
83
|
+
dir = parent;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveRelativeFile(filename, specifier) {
|
|
89
|
+
if (typeof filename !== "string" || typeof specifier !== "string") return null;
|
|
90
|
+
const base = path.resolve(path.dirname(path.resolve(filename)), specifier);
|
|
91
|
+
const candidates = [base];
|
|
92
|
+
for (const extension of RELATIVE_EXTENSIONS) candidates.push(`${base}${extension}`);
|
|
93
|
+
for (const extension of RELATIVE_EXTENSIONS) {
|
|
94
|
+
candidates.push(path.join(base, `index${extension}`));
|
|
95
|
+
}
|
|
96
|
+
for (const candidate of candidates) {
|
|
97
|
+
try {
|
|
98
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
99
|
+
} catch {
|
|
100
|
+
// Missing paths and unreadable entries fail closed.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fileInsidePackage(file, root) {
|
|
107
|
+
const relative = path.relative(root, file);
|
|
108
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function matchesConfiguredModule(source, specifier, filename) {
|
|
112
|
+
if (fromConfiguredModule(source, specifier)) return true;
|
|
113
|
+
if (!specifier || !isRelativeSpecifier(source)) return false;
|
|
114
|
+
const root = packageRoot(filename, specifier);
|
|
115
|
+
if (!root) return false;
|
|
116
|
+
const resolved = resolveRelativeFile(filename, source);
|
|
117
|
+
return resolved !== null && fileInsidePackage(resolved, root);
|
|
118
|
+
}
|
|
119
|
+
|
|
28
120
|
// Local names of configured factory (value) and type imports, by module.
|
|
29
|
-
function scopedImports(program, options) {
|
|
121
|
+
function scopedImports(program, options, filename) {
|
|
30
122
|
const factories = new Set();
|
|
31
123
|
const types = new Set();
|
|
32
124
|
const { importSpecifier, executorFactoryNames, executorTypeNames } = options;
|
|
33
125
|
for (const statement of program?.body ?? []) {
|
|
34
126
|
if (statement.type !== "ImportDeclaration") continue;
|
|
35
|
-
if (!
|
|
127
|
+
if (!matchesConfiguredModule(statement.source?.value, importSpecifier, filename)) continue;
|
|
36
128
|
for (const specifier of statement.specifiers ?? []) {
|
|
37
129
|
if (specifier.type !== "ImportSpecifier") continue;
|
|
38
130
|
const imported = importedName(specifier);
|
|
@@ -94,9 +186,9 @@ function parameterNames(param, types) {
|
|
|
94
186
|
* configured factory call (the declaring block) and parameters typed with a
|
|
95
187
|
* configured type (the declaring function). Returns `{ name, range }` entries.
|
|
96
188
|
*/
|
|
97
|
-
function collectScopedExecutors(program, options) {
|
|
189
|
+
function collectScopedExecutors(program, options, filename) {
|
|
98
190
|
const found = [];
|
|
99
|
-
const { factories, types } = scopedImports(program, options);
|
|
191
|
+
const { factories, types } = scopedImports(program, options, filename);
|
|
100
192
|
if (factories.size === 0 && types.size === 0) return found;
|
|
101
193
|
const walk = (node, scope) => {
|
|
102
194
|
if (SCOPE_TYPES.has(node.type)) scope = node;
|