@geonosis/oxlint-plugin-biological-architecture 3.2.0 → 3.3.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/CHANGELOG.md +13 -0
- package/README.md +27 -1
- package/RULES.md +7 -7
- package/corpus/medusa/queries/bad-silent-cap.ts +11 -0
- package/corpus/medusa/queries/good-paged-through.ts +19 -0
- package/corpus/medusa/queries/integration-tests/any-row.ts +10 -0
- package/dist/index.js +3 -1
- package/dist/presets.js +7 -2
- package/dist/rules/lib/direction-rule.d.ts +1 -0
- package/dist/rules/lib/direction-rule.js +13 -1
- package/dist/rules/lib/doors.js +4 -2
- package/dist/rules/lib/fresh.d.ts +5 -0
- package/dist/rules/lib/fresh.js +15 -0
- package/dist/rules/lib/options.d.ts +1 -0
- package/dist/rules/lib/options.js +5 -0
- package/dist/rules/lib/run-files.d.ts +18 -0
- package/dist/rules/lib/run-files.js +144 -0
- package/dist/rules/lib/token-layer.d.ts +19 -0
- package/dist/rules/lib/token-layer.js +170 -0
- package/dist/rules/lib/workspace-members.js +17 -19
- package/dist/rules/list-take-without-order.d.ts +24 -0
- package/dist/rules/list-take-without-order.js +293 -10
- package/dist/rules/no-orm-outside-db.js +1 -0
- package/dist/rules/subpath-over-barrel.d.ts +1 -0
- package/dist/rules/subpath-over-barrel.js +18 -12
- package/dist/rules/test-no-clock-polling-for-promise-flips.d.ts +5 -1
- package/dist/rules/test-no-clock-polling-for-promise-flips.js +3 -0
- package/dist/rules/tissue-must-compose.d.ts +1 -1
- package/dist/rules/tissue-no-data-props.d.ts +2 -2
- package/dist/rules/tokens-only-colors.d.ts +8 -11
- package/dist/rules/tokens-only-colors.js +36 -7
- package/dist/rules/worker-handles-are-scoped.d.ts +1 -1
- package/dist/timing.d.ts +18 -0
- package/dist/timing.js +75 -0
- package/package.json +2 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// src/rules/lib/token-layer.ts
|
|
2
|
+
import { isTypeOnly, sourceOf } from "./imports.js";
|
|
3
|
+
import { propertyNameOf } from "./nodes.js";
|
|
4
|
+
import { withoutTypes } from "./objects.js";
|
|
5
|
+
var THEMEKIT = "@geonosis/themekit";
|
|
6
|
+
var PALETTE_TAKERS = [
|
|
7
|
+
"compile",
|
|
8
|
+
"defineTheme",
|
|
9
|
+
"emitVariables",
|
|
10
|
+
"toDtcg",
|
|
11
|
+
"valueAt"
|
|
12
|
+
];
|
|
13
|
+
var PALETTE_TYPES = [
|
|
14
|
+
"StatusPair",
|
|
15
|
+
"Theme",
|
|
16
|
+
"ThemeColors",
|
|
17
|
+
"ThemeFonts",
|
|
18
|
+
"ThemeMode",
|
|
19
|
+
"ThemeModes",
|
|
20
|
+
"ThemeRadii",
|
|
21
|
+
"ThemeShadows",
|
|
22
|
+
"ThemeStatusColors",
|
|
23
|
+
"TokenValue"
|
|
24
|
+
];
|
|
25
|
+
var isNode = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
var childOf = (node, key) => {
|
|
27
|
+
const child = node?.[key];
|
|
28
|
+
return isNode(child) ? child : undefined;
|
|
29
|
+
};
|
|
30
|
+
var childrenOf = (node, key) => {
|
|
31
|
+
const children = node[key];
|
|
32
|
+
return Array.isArray(children) ? children.filter(isNode) : [];
|
|
33
|
+
};
|
|
34
|
+
var nameOf = (node) => typeof node?.name === "string" ? node.name : undefined;
|
|
35
|
+
var tokenLayerOf = () => {
|
|
36
|
+
const takers = new Set;
|
|
37
|
+
const namespaces = new Set;
|
|
38
|
+
const typeLocals = new Set;
|
|
39
|
+
const typeNamespaces = new Set;
|
|
40
|
+
const typed = [];
|
|
41
|
+
const objects = new Set;
|
|
42
|
+
const passed = new Set;
|
|
43
|
+
const bindings = new Map;
|
|
44
|
+
const open = [];
|
|
45
|
+
const single = (name) => {
|
|
46
|
+
const binding = bindings.get(name);
|
|
47
|
+
return binding?.declarations === 1 ? binding.object : undefined;
|
|
48
|
+
};
|
|
49
|
+
const isPaletteType = (reference) => {
|
|
50
|
+
if (reference?.type !== "TSTypeReference")
|
|
51
|
+
return false;
|
|
52
|
+
const name = childOf(reference, "typeName");
|
|
53
|
+
if (name?.type === "Identifier")
|
|
54
|
+
return typeLocals.has(nameOf(name) ?? "");
|
|
55
|
+
if (name?.type !== "TSQualifiedName")
|
|
56
|
+
return false;
|
|
57
|
+
const namespace = nameOf(childOf(name, "left"));
|
|
58
|
+
const member = nameOf(childOf(name, "right"));
|
|
59
|
+
return namespace !== undefined && member !== undefined && typeNamespaces.has(namespace) && PALETTE_TYPES.includes(member);
|
|
60
|
+
};
|
|
61
|
+
const typedObject = (expression, reference) => {
|
|
62
|
+
const object = withoutTypes(expression);
|
|
63
|
+
if (object?.type === "ObjectExpression")
|
|
64
|
+
typed.push({ object, reference });
|
|
65
|
+
};
|
|
66
|
+
const takesPalette = (callee) => {
|
|
67
|
+
if (callee?.type === "Identifier")
|
|
68
|
+
return takers.has(nameOf(callee) ?? "");
|
|
69
|
+
if (callee?.type !== "MemberExpression")
|
|
70
|
+
return false;
|
|
71
|
+
const owner = nameOf(childOf(callee, "object"));
|
|
72
|
+
const name = propertyNameOf(callee);
|
|
73
|
+
return owner !== undefined && namespaces.has(owner) && name !== null && PALETTE_TAKERS.includes(name);
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
covers: (chain) => chain.some((one) => objects.has(one)),
|
|
77
|
+
enclosing: () => [...open],
|
|
78
|
+
resolve: () => {
|
|
79
|
+
for (const one of typed) {
|
|
80
|
+
if (isPaletteType(one.reference))
|
|
81
|
+
objects.add(one.object);
|
|
82
|
+
}
|
|
83
|
+
const queue = [...objects];
|
|
84
|
+
for (const name of passed) {
|
|
85
|
+
const held = single(name);
|
|
86
|
+
if (held !== undefined && !objects.has(held)) {
|
|
87
|
+
objects.add(held);
|
|
88
|
+
queue.push(held);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const object of queue) {
|
|
92
|
+
for (const member of childrenOf(object, "properties")) {
|
|
93
|
+
const value = withoutTypes(member.type === "SpreadElement" ? member.argument : member.value);
|
|
94
|
+
const held = value?.type === "Identifier" ? single(nameOf(value) ?? "") : undefined;
|
|
95
|
+
if (held !== undefined && !objects.has(held)) {
|
|
96
|
+
objects.add(held);
|
|
97
|
+
queue.push(held);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
visitor: {
|
|
103
|
+
CallExpression(node) {
|
|
104
|
+
if (!takesPalette(childOf(node, "callee")))
|
|
105
|
+
return;
|
|
106
|
+
const argument = withoutTypes(childrenOf(node, "arguments")[0]);
|
|
107
|
+
if (argument?.type === "ObjectExpression")
|
|
108
|
+
objects.add(argument);
|
|
109
|
+
else if (argument?.type === "Identifier")
|
|
110
|
+
passed.add(nameOf(argument) ?? "");
|
|
111
|
+
},
|
|
112
|
+
ImportDeclaration(node) {
|
|
113
|
+
if (sourceOf(node) !== THEMEKIT)
|
|
114
|
+
return;
|
|
115
|
+
for (const specifier of node.specifiers ?? []) {
|
|
116
|
+
const local = specifier.local?.name;
|
|
117
|
+
if (local === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
const typeOnly = isTypeOnly(node) || specifier.importKind === "type";
|
|
120
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
121
|
+
typeNamespaces.add(local);
|
|
122
|
+
if (!typeOnly)
|
|
123
|
+
namespaces.add(local);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const imported = specifier.imported?.name;
|
|
127
|
+
if (imported === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
if (PALETTE_TYPES.includes(imported))
|
|
130
|
+
typeLocals.add(local);
|
|
131
|
+
if (!typeOnly && PALETTE_TAKERS.includes(imported))
|
|
132
|
+
takers.add(local);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
TSAsExpression(node) {
|
|
136
|
+
typedObject(node.expression, childOf(node, "typeAnnotation"));
|
|
137
|
+
},
|
|
138
|
+
TSSatisfiesExpression(node) {
|
|
139
|
+
typedObject(node.expression, childOf(node, "typeAnnotation"));
|
|
140
|
+
},
|
|
141
|
+
ObjectExpression(node) {
|
|
142
|
+
open.push(node);
|
|
143
|
+
},
|
|
144
|
+
"ObjectExpression:exit"() {
|
|
145
|
+
open.pop();
|
|
146
|
+
},
|
|
147
|
+
VariableDeclaration(node) {
|
|
148
|
+
for (const declarator of childrenOf(node, "declarations")) {
|
|
149
|
+
const id = childOf(declarator, "id");
|
|
150
|
+
const name = nameOf(id);
|
|
151
|
+
if (id?.type !== "Identifier" || name === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
typedObject(declarator.init, childOf(childOf(id, "typeAnnotation"), "typeAnnotation"));
|
|
154
|
+
const init = withoutTypes(declarator.init);
|
|
155
|
+
const held = node.kind === "const" && init?.type === "ObjectExpression" ? init : undefined;
|
|
156
|
+
bindings.set(name, {
|
|
157
|
+
declarations: (bindings.get(name)?.declarations ?? 0) + 1,
|
|
158
|
+
...held === undefined ? {} : { object: held }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
export {
|
|
166
|
+
PALETTE_TAKERS,
|
|
167
|
+
PALETTE_TYPES,
|
|
168
|
+
THEMEKIT,
|
|
169
|
+
tokenLayerOf
|
|
170
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/rules/lib/workspace-members.ts
|
|
2
|
-
import { existsSync, readdirSync, readFileSync
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { remembered } from "./fresh.js";
|
|
4
5
|
import { readManifest } from "./module-text.js";
|
|
5
6
|
var MARKERS = ["pnpm-workspace.yaml", "package.json"];
|
|
6
7
|
var SKIPPED = new Set(["node_modules", ".git"]);
|
|
@@ -62,8 +63,9 @@ var nameOf = (manifest) => {
|
|
|
62
63
|
const name = readManifest(manifest)?.name;
|
|
63
64
|
return typeof name === "string" ? name : undefined;
|
|
64
65
|
};
|
|
65
|
-
var
|
|
66
|
-
var
|
|
66
|
+
var rooted = new Map;
|
|
67
|
+
var members = new Map;
|
|
68
|
+
var rootAbove = (from) => remembered(rooted, dirname(from), () => {
|
|
67
69
|
let dir = dirname(from);
|
|
68
70
|
for (;; ) {
|
|
69
71
|
for (const marker of MARKERS) {
|
|
@@ -85,27 +87,23 @@ var rootAbove = (from) => {
|
|
|
85
87
|
return;
|
|
86
88
|
dir = parent;
|
|
87
89
|
}
|
|
88
|
-
};
|
|
90
|
+
});
|
|
89
91
|
var membersFrom = (from) => {
|
|
90
92
|
const found = rootAbove(from);
|
|
91
93
|
if (found === undefined)
|
|
92
94
|
return;
|
|
93
95
|
const marker = join(found.root, found.marker);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
const members = { names, root: found.root };
|
|
107
|
-
cache.set(marker, { members, stamp });
|
|
108
|
-
return members;
|
|
96
|
+
return remembered(members, marker, () => {
|
|
97
|
+
const globs = globsOf(found.marker, readFileSync(marker, "utf8")) ?? [];
|
|
98
|
+
const names = new Map;
|
|
99
|
+
for (const dir of memberDirs(found.root, globs)) {
|
|
100
|
+
const manifest = join(dir, "package.json");
|
|
101
|
+
const name = existsSync(manifest) ? nameOf(manifest) : undefined;
|
|
102
|
+
if (name !== undefined && !names.has(name))
|
|
103
|
+
names.set(name, manifest);
|
|
104
|
+
}
|
|
105
|
+
return { names, root: found.root };
|
|
106
|
+
});
|
|
109
107
|
};
|
|
110
108
|
var workspaceMemberNamed = (from, name) => membersFrom(from)?.names.get(name);
|
|
111
109
|
var hasWorkspaceAbove = (from) => rootAbove(from) !== undefined;
|
|
@@ -2,6 +2,8 @@ import { type RuleContextWithRawOptions } from './lib/context.js';
|
|
|
2
2
|
import { type AnyNode } from './lib/nodes.js';
|
|
3
3
|
declare const listTakeWithoutOrder: {
|
|
4
4
|
create(context: RuleContextWithRawOptions): {
|
|
5
|
+
CallExpression?: undefined;
|
|
6
|
+
} | {
|
|
5
7
|
CallExpression(node: AnyNode): void;
|
|
6
8
|
};
|
|
7
9
|
fixShape: string;
|
|
@@ -10,7 +12,12 @@ declare const listTakeWithoutOrder: {
|
|
|
10
12
|
description: string;
|
|
11
13
|
};
|
|
12
14
|
messages: {
|
|
15
|
+
truncated: string;
|
|
13
16
|
unordered: string;
|
|
17
|
+
unorderedFind: string;
|
|
18
|
+
unorderedFirst: string;
|
|
19
|
+
unorderedLimit: string;
|
|
20
|
+
unorderedSql: string;
|
|
14
21
|
};
|
|
15
22
|
schema: {
|
|
16
23
|
additionalProperties: boolean;
|
|
@@ -21,12 +28,29 @@ declare const listTakeWithoutOrder: {
|
|
|
21
28
|
};
|
|
22
29
|
type: 'array';
|
|
23
30
|
};
|
|
31
|
+
exempt: {
|
|
32
|
+
anyOf: ({
|
|
33
|
+
type: 'string';
|
|
34
|
+
items?: undefined;
|
|
35
|
+
} | {
|
|
36
|
+
items: {
|
|
37
|
+
type: 'string';
|
|
38
|
+
};
|
|
39
|
+
type: 'array';
|
|
40
|
+
})[];
|
|
41
|
+
};
|
|
24
42
|
orderKeys: {
|
|
25
43
|
items: {
|
|
26
44
|
type: 'string';
|
|
27
45
|
};
|
|
28
46
|
type: 'array';
|
|
29
47
|
};
|
|
48
|
+
uniqueColumns: {
|
|
49
|
+
items: {
|
|
50
|
+
type: 'string';
|
|
51
|
+
};
|
|
52
|
+
type: 'array';
|
|
53
|
+
};
|
|
30
54
|
};
|
|
31
55
|
type: 'object';
|
|
32
56
|
}[];
|
|
@@ -1,10 +1,130 @@
|
|
|
1
1
|
// src/rules/list-take-without-order.ts
|
|
2
|
-
import { propertyNameOf, walkNode } from "./lib/nodes.js";
|
|
2
|
+
import { FUNCTION_BOUNDARY_TYPES, propertyNameOf, walkNode } from "./lib/nodes.js";
|
|
3
3
|
import { keyNameOf } from "./lib/objects.js";
|
|
4
|
+
import { DEFAULT_TEST_EXEMPT, PATTERNS, patternsOf } from "./lib/options.js";
|
|
5
|
+
import { anyOf, normalise } from "./lib/paths.js";
|
|
4
6
|
import { bagOf } from "./lib/probe.js";
|
|
5
7
|
import { optionsBag, STRINGS } from "./lib/schema.js";
|
|
8
|
+
var UNIQUE_COLUMNS = ["id"];
|
|
9
|
+
var RELATIONAL = new Set(["findFirst", "findMany"]);
|
|
10
|
+
var AGGREGATE = /^\s*(?:count|sum|min|max|avg|total|group_concat)\s*\(/i;
|
|
6
11
|
var CALLEES = [String.raw`^(?:\w+\.)?list[A-Z]`, String.raw`^(?:\w+\.)?graph$`];
|
|
7
12
|
var ORDER_KEYS = ["order", "orderBy"];
|
|
13
|
+
var TEST_PATHS = [...DEFAULT_TEST_EXEMPT, "integration-tests/"];
|
|
14
|
+
var PAGING_KEYS = ["take", "skip", "offset"];
|
|
15
|
+
var WHOLE_MEMBERS = new Set([
|
|
16
|
+
"entries",
|
|
17
|
+
"every",
|
|
18
|
+
"filter",
|
|
19
|
+
"flat",
|
|
20
|
+
"flatMap",
|
|
21
|
+
"forEach",
|
|
22
|
+
"join",
|
|
23
|
+
"keys",
|
|
24
|
+
"length",
|
|
25
|
+
"map",
|
|
26
|
+
"reduce",
|
|
27
|
+
"reduceRight",
|
|
28
|
+
"some",
|
|
29
|
+
"values"
|
|
30
|
+
]);
|
|
31
|
+
var ROW_MEMBERS = new Set([
|
|
32
|
+
"at",
|
|
33
|
+
"find",
|
|
34
|
+
"findIndex",
|
|
35
|
+
"findLast",
|
|
36
|
+
"findLastIndex",
|
|
37
|
+
"pop",
|
|
38
|
+
"shift",
|
|
39
|
+
"slice",
|
|
40
|
+
"sort",
|
|
41
|
+
"toSorted"
|
|
42
|
+
]);
|
|
43
|
+
var WRAPPERS = new Set([
|
|
44
|
+
"AwaitExpression",
|
|
45
|
+
"ParenthesizedExpression",
|
|
46
|
+
"TSAsExpression",
|
|
47
|
+
"TSNonNullExpression",
|
|
48
|
+
"TSSatisfiesExpression"
|
|
49
|
+
]);
|
|
50
|
+
var isNode = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
var parentOf = (node) => isNode(node.parent) ? node.parent : undefined;
|
|
52
|
+
var valueOf = (call) => {
|
|
53
|
+
let at = call;
|
|
54
|
+
for (let up = parentOf(at);up !== undefined && WRAPPERS.has(up.type ?? ""); up = parentOf(at)) {
|
|
55
|
+
at = up;
|
|
56
|
+
}
|
|
57
|
+
return at;
|
|
58
|
+
};
|
|
59
|
+
var useOf = (value) => {
|
|
60
|
+
const up = parentOf(value);
|
|
61
|
+
if (up === undefined)
|
|
62
|
+
return "other";
|
|
63
|
+
if (up.type === "MemberExpression" && up.object === value) {
|
|
64
|
+
if (up.computed === true)
|
|
65
|
+
return "row";
|
|
66
|
+
const member = propertyNameOf(up);
|
|
67
|
+
if (member !== null && ROW_MEMBERS.has(member))
|
|
68
|
+
return "row";
|
|
69
|
+
return member !== null && WHOLE_MEMBERS.has(member) ? "whole" : "other";
|
|
70
|
+
}
|
|
71
|
+
if (up.type === "ForOfStatement" && up.right === value)
|
|
72
|
+
return "whole";
|
|
73
|
+
if (up.type === "SpreadElement")
|
|
74
|
+
return "whole";
|
|
75
|
+
if (up.type === "CallExpression" || up.type === "NewExpression") {
|
|
76
|
+
return Array.isArray(up.arguments) && up.arguments.includes(value) ? "whole" : "other";
|
|
77
|
+
}
|
|
78
|
+
return "other";
|
|
79
|
+
};
|
|
80
|
+
var nameOf = (node) => isNode(node) && node.type === "Identifier" && typeof node.name === "string" ? [node.name] : [];
|
|
81
|
+
var boundNames = (id) => {
|
|
82
|
+
if (!isNode(id))
|
|
83
|
+
return [];
|
|
84
|
+
if (id.type === "Identifier")
|
|
85
|
+
return nameOf(id);
|
|
86
|
+
if (id.type === "ArrayPattern")
|
|
87
|
+
return "row";
|
|
88
|
+
if (id.type !== "ObjectPattern" || !Array.isArray(id.properties))
|
|
89
|
+
return [];
|
|
90
|
+
return id.properties.flatMap((one) => {
|
|
91
|
+
if (!isNode(one) || one.type !== "Property")
|
|
92
|
+
return [];
|
|
93
|
+
return isNode(one.value) && one.value.type === "AssignmentPattern" ? nameOf(one.value.left) : nameOf(one.value);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
var scopeOf = (node) => {
|
|
97
|
+
let at = node;
|
|
98
|
+
for (let up = parentOf(at);up !== undefined; up = parentOf(at)) {
|
|
99
|
+
at = up;
|
|
100
|
+
if (up.type === "Program" || FUNCTION_BOUNDARY_TYPES.has(up.type ?? ""))
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
return at;
|
|
104
|
+
};
|
|
105
|
+
var usesOf = (call) => {
|
|
106
|
+
const value = valueOf(call);
|
|
107
|
+
const up = parentOf(value);
|
|
108
|
+
if (up?.type !== "VariableDeclarator" || up.init !== value)
|
|
109
|
+
return [useOf(value)];
|
|
110
|
+
const names = boundNames(up.id);
|
|
111
|
+
if (names === "row")
|
|
112
|
+
return ["row"];
|
|
113
|
+
const uses = [];
|
|
114
|
+
walkNode(scopeOf(up), (inner) => {
|
|
115
|
+
if (inner.type === "Identifier" && names.includes(String(inner.name)))
|
|
116
|
+
uses.push(useOf(inner));
|
|
117
|
+
});
|
|
118
|
+
return uses;
|
|
119
|
+
};
|
|
120
|
+
var capOf = (take, skip) => {
|
|
121
|
+
if (!isNode(take) || take.type !== "Literal")
|
|
122
|
+
return null;
|
|
123
|
+
if (typeof take.value !== "number" || take.value <= 1)
|
|
124
|
+
return null;
|
|
125
|
+
const fromTheStart = skip === undefined || isNode(skip) && skip.type === "Literal" && skip.value === 0;
|
|
126
|
+
return fromTheStart ? take.value : null;
|
|
127
|
+
};
|
|
8
128
|
var spellingOf = (callee) => {
|
|
9
129
|
if (callee === undefined)
|
|
10
130
|
return null;
|
|
@@ -16,17 +136,139 @@ var spellingOf = (callee) => {
|
|
|
16
136
|
const object = callee.object;
|
|
17
137
|
return object?.type === "Identifier" ? `${String(object.name)}.${property}` : property;
|
|
18
138
|
};
|
|
139
|
+
var linksOf = (top) => {
|
|
140
|
+
const links = [];
|
|
141
|
+
let at = top;
|
|
142
|
+
while (at.type === "CallExpression" && isNode(at.callee)) {
|
|
143
|
+
const callee = at.callee;
|
|
144
|
+
const name = callee.type === "MemberExpression" ? propertyNameOf(callee) : null;
|
|
145
|
+
if (name === null || callee.computed === true || !isNode(callee.object))
|
|
146
|
+
break;
|
|
147
|
+
links.unshift({ args: at.arguments ?? [], call: at, name });
|
|
148
|
+
at = callee.object;
|
|
149
|
+
}
|
|
150
|
+
return { links, root: at };
|
|
151
|
+
};
|
|
152
|
+
var isChainTop = (node) => {
|
|
153
|
+
const up = parentOf(node);
|
|
154
|
+
return !(up?.type === "MemberExpression" && up.object === node);
|
|
155
|
+
};
|
|
156
|
+
var pins = (node, unique) => {
|
|
157
|
+
if (!isNode(node))
|
|
158
|
+
return false;
|
|
159
|
+
if (node.type === "ArrowFunctionExpression")
|
|
160
|
+
return pins(node.body, unique);
|
|
161
|
+
if (node.type !== "CallExpression" || !isNode(node.callee))
|
|
162
|
+
return false;
|
|
163
|
+
const name = node.callee.type === "Identifier" ? node.callee.name : propertyNameOf(node.callee);
|
|
164
|
+
const [left] = node.arguments ?? [];
|
|
165
|
+
const column = isNode(left) && left.type === "MemberExpression" ? propertyNameOf(left) : null;
|
|
166
|
+
return name === "eq" && column !== null && unique.includes(column);
|
|
167
|
+
};
|
|
168
|
+
var sqlOf = (node) => {
|
|
169
|
+
if (!isNode(node))
|
|
170
|
+
return null;
|
|
171
|
+
if (node.type === "Literal")
|
|
172
|
+
return typeof node.value === "string" ? node.value : null;
|
|
173
|
+
if (node.type !== "TemplateLiteral" || !Array.isArray(node.quasis))
|
|
174
|
+
return null;
|
|
175
|
+
return node.quasis.map((quasi) => isNode(quasi) && isNode(quasi.value) && typeof quasi.value.cooked === "string" ? quasi.value.cooked : "").join(" ? ");
|
|
176
|
+
};
|
|
177
|
+
var preparedSql = (links) => {
|
|
178
|
+
const prepare = links.find((one) => one.name === "prepare");
|
|
179
|
+
const sql = prepare === undefined ? null : sqlOf(prepare.args[0]);
|
|
180
|
+
return prepare === undefined || sql === null ? null : { at: prepare.call, sql };
|
|
181
|
+
};
|
|
182
|
+
var valueInside = (node) => WRAPPERS.has(node.type ?? "") && isNode(node.expression) ? valueInside(node.expression) : node;
|
|
183
|
+
var declaredSql = (root) => {
|
|
184
|
+
if (root.type !== "Identifier")
|
|
185
|
+
return null;
|
|
186
|
+
let found = null;
|
|
187
|
+
walkNode(scopeOf(root), (inner) => {
|
|
188
|
+
if (found !== null || inner.type !== "VariableDeclarator")
|
|
189
|
+
return;
|
|
190
|
+
const id = inner.id;
|
|
191
|
+
if (id?.type !== "Identifier" || id.name !== root.name || !isNode(inner.init))
|
|
192
|
+
return;
|
|
193
|
+
found = preparedSql(linksOf(valueInside(inner.init)).links)?.sql ?? null;
|
|
194
|
+
});
|
|
195
|
+
return found;
|
|
196
|
+
};
|
|
197
|
+
var readSql = (sql, unique) => {
|
|
198
|
+
const list = /^\s*select\s+([\s\S]*?)\s+from\b/i.exec(sql)?.[1];
|
|
199
|
+
const where = /\bwhere\b([\s\S]*)/i.exec(sql)?.[1] ?? "";
|
|
200
|
+
const columns = unique.map((one) => one.replace(/[^\w]/g, "")).join("|");
|
|
201
|
+
return {
|
|
202
|
+
limits: /\blimit\b/i.test(sql),
|
|
203
|
+
orders: /\border\s+by\b/i.test(sql),
|
|
204
|
+
pinned: columns !== "" && !/\bor\b/i.test(where) && new RegExp(String.raw`\b(?:\w+\.)?(?:${columns})\s*=\s*(?:\?\d*|[:@$]\w+|\d+|'[^']*')`, "i").test(where),
|
|
205
|
+
selects: /^\s*(?:with|select)\b/i.test(sql),
|
|
206
|
+
single: list !== undefined && !/\bgroup\s+by\b/i.test(sql) && list.split(",").every((part) => AGGREGATE.test(part))
|
|
207
|
+
};
|
|
208
|
+
};
|
|
19
209
|
var listTakeWithoutOrder = {
|
|
20
210
|
create(context) {
|
|
21
211
|
const options = bagOf(context.options);
|
|
22
212
|
const callees = (options.callees ?? CALLEES).map((pattern) => new RegExp(pattern));
|
|
23
213
|
const orderKeys = options.orderKeys ?? ORDER_KEYS;
|
|
214
|
+
const exempt = options.exempt === undefined ? TEST_PATHS : patternsOf(options.exempt);
|
|
215
|
+
const unique = options.uniqueColumns ?? UNIQUE_COLUMNS;
|
|
216
|
+
if (anyOf(exempt, normalise(context.filename)))
|
|
217
|
+
return {};
|
|
218
|
+
const judgeChain = (node) => {
|
|
219
|
+
if (!isChainTop(node))
|
|
220
|
+
return;
|
|
221
|
+
const { links, root } = linksOf(node);
|
|
222
|
+
const names = links.map((one) => one.name);
|
|
223
|
+
const last = links.at(-1);
|
|
224
|
+
if (names.includes("from") && names.includes("limit") && !names.includes("orderBy")) {
|
|
225
|
+
const where = links.find((one) => one.name === "where");
|
|
226
|
+
const limit = links.find((one) => one.name === "limit");
|
|
227
|
+
if (limit !== undefined && !pins(where?.args[0], unique)) {
|
|
228
|
+
context.report({ data: {}, messageId: "unorderedLimit", node: limit.call });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (last !== undefined && RELATIONAL.has(last.name) && root.type === "MemberExpression" && isNode(root.object) && root.object.type === "MemberExpression" && propertyNameOf(root.object) === "query") {
|
|
232
|
+
const config = new Map;
|
|
233
|
+
const [first] = last.args;
|
|
234
|
+
if (isNode(first) && first.type === "ObjectExpression" && Array.isArray(first.properties)) {
|
|
235
|
+
for (const one of first.properties) {
|
|
236
|
+
const key = one.type === "Property" ? keyNameOf(one) : null;
|
|
237
|
+
if (key !== null)
|
|
238
|
+
config.set(key, one.value);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const paged = last.name === "findFirst" || config.has("limit");
|
|
242
|
+
if (paged && !config.has("orderBy") && !(last.name === "findFirst" && pins(config.get("where"), unique))) {
|
|
243
|
+
context.report({
|
|
244
|
+
data: { callee: last.name },
|
|
245
|
+
messageId: "unorderedFind",
|
|
246
|
+
node: last.call
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const inline = preparedSql(links);
|
|
251
|
+
if (inline !== null) {
|
|
252
|
+
const read = readSql(inline.sql, unique);
|
|
253
|
+
if (read.selects && read.limits && !read.orders && !read.pinned) {
|
|
254
|
+
context.report({ data: {}, messageId: "unorderedSql", node: inline.at });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (last?.name === "first") {
|
|
258
|
+
const sql = inline?.sql ?? declaredSql(root);
|
|
259
|
+
const read = sql === null ? null : readSql(sql, unique);
|
|
260
|
+
if (read !== null && read.selects && !read.orders && !read.limits && !read.pinned && !read.single) {
|
|
261
|
+
context.report({ data: {}, messageId: "unorderedFirst", node: last.call });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
24
265
|
return {
|
|
25
266
|
CallExpression(node) {
|
|
267
|
+
judgeChain(node);
|
|
26
268
|
const callee = spellingOf(node.callee);
|
|
27
269
|
if (callee === null || !callees.some((pattern) => pattern.test(callee)))
|
|
28
270
|
return;
|
|
29
|
-
|
|
271
|
+
const paging = new Map;
|
|
30
272
|
let orders = false;
|
|
31
273
|
let unreadable = false;
|
|
32
274
|
for (const argument of node.arguments ?? []) {
|
|
@@ -36,13 +278,24 @@ var listTakeWithoutOrder = {
|
|
|
36
278
|
if (inner.type !== "Property")
|
|
37
279
|
return;
|
|
38
280
|
const key = keyNameOf(inner);
|
|
39
|
-
if (key
|
|
40
|
-
|
|
281
|
+
if (key !== null && PAGING_KEYS.includes(key) && !paging.has(key)) {
|
|
282
|
+
paging.set(key, inner.value);
|
|
283
|
+
}
|
|
41
284
|
if (key !== null && orderKeys.includes(key))
|
|
42
285
|
orders = true;
|
|
43
286
|
});
|
|
44
287
|
}
|
|
45
|
-
if (!
|
|
288
|
+
if (!paging.has("take") || unreadable)
|
|
289
|
+
return;
|
|
290
|
+
const cap = capOf(paging.get("take"), paging.get("skip") ?? paging.get("offset"));
|
|
291
|
+
if (cap !== null) {
|
|
292
|
+
const uses = usesOf(node);
|
|
293
|
+
if (uses.includes("whole") && !uses.includes("row")) {
|
|
294
|
+
context.report({ data: { callee, take: String(cap) }, messageId: "truncated", node });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (orders)
|
|
46
299
|
return;
|
|
47
300
|
context.report({
|
|
48
301
|
data: { callee, order: orderKeys[0] ?? "" },
|
|
@@ -56,18 +309,48 @@ var listTakeWithoutOrder = {
|
|
|
56
309
|
or \`query.graph\` call carrying \`take\` names an \`order\` beside it — the column the page is a page OF —
|
|
57
310
|
so the first row is the first row every time, and the row a previous stage just wrote is where the
|
|
58
311
|
reader expects it. Without one the database returns whatever is cheapest, which is stable in
|
|
59
|
-
development and different under load. A spread the rule cannot read is left alone.
|
|
60
|
-
|
|
312
|
+
development and different under load. A spread the rule cannot read is left alone.
|
|
313
|
+
|
|
314
|
+
A \`take\` that is a cap, not a page, is the other finding, and naming an \`order\` does not clear it:
|
|
315
|
+
a literal above 1, from the first row (no \`skip\` or \`offset\`, or a literal 0), whose result is bound
|
|
316
|
+
to a name and then iterated, counted, mapped, filtered or handed whole to another call, with no
|
|
317
|
+
first-row read (\`[n]\`, \`.at()\`, \`.find()\`, \`.slice()\`, \`.sort()\`) beside it. Ordering makes the cut
|
|
318
|
+
deterministic: the row after the cap is still never read. Remove the cap, or read pages with \`skip\`
|
|
319
|
+
until one comes back short, or count first. A \`take\` the caller supplies, a \`skip\` the caller
|
|
320
|
+
supplies, a \`take\` of 1 and a result nothing here consumes stay the first finding, which an
|
|
321
|
+
\`order\` clears.
|
|
322
|
+
|
|
323
|
+
On D1 the same question is asked three more ways, and each is answered by an order or by a unique
|
|
324
|
+
column. A Drizzle select chain with \`.limit(n)\` and no \`.orderBy(…)\` anywhere in it; a relational
|
|
325
|
+
\`findMany({ limit })\` with no \`orderBy\`, or a \`findFirst\`, which is \`LIMIT 1\`, with no \`orderBy\`; and
|
|
326
|
+
on the raw binding a \`prepare()\` SELECT with LIMIT and no ORDER BY, or a \`.first()\` over a SELECT with
|
|
327
|
+
no ORDER BY — D1's \`.first()\` does not alter the statement, it returns the first row it produced. A
|
|
328
|
+
\`where\` that is \`eq(t.<unique>, …)\`, or a SQL WHERE \`<unique> = ?\` with no OR, pins the row and is not
|
|
329
|
+
the bug; the unique columns are \`uniqueColumns\` (default \`id\`). A \`.first()\` over an aggregate
|
|
330
|
+
SELECT with no GROUP BY is one row and is not judged, and only SQL written as a string or a template
|
|
331
|
+
literal is read.
|
|
332
|
+
|
|
333
|
+
Which calls are list calls is the \`callees\` option and which key declares the ordering is
|
|
334
|
+
\`orderKeys\`. Test files are not judged (\`__tests__/\`, \`.test.\`, \`.spec.\`, \`integration-tests/\`):
|
|
335
|
+
the row a fixture takes does not matter. \`exempt\` replaces that list; \`exempt: []\` judges test
|
|
336
|
+
files too.`,
|
|
61
337
|
meta: {
|
|
62
338
|
docs: {
|
|
63
|
-
description: "A list or graph call that takes a page without declaring an order. The rows come back in whatever order is cheapest, so `[0]`, `.find()` and `.sort()` over the result answer a question about the page rather than about the table."
|
|
339
|
+
description: "A list or graph call that takes a page without declaring an order. The rows come back in whatever order is cheapest, so `[0]`, `.find()` and `.sort()` over the result answer a question about the page rather than about the table. A literal `take` whose result is consumed whole is a cap that drops rows silently, ordered or not, and has its own message. Test files are not judged unless `exempt` is emptied."
|
|
64
340
|
},
|
|
65
341
|
messages: {
|
|
66
|
-
|
|
342
|
+
truncated: "`{{callee}}` reads at most {{take}} rows and the code below treats them as all of them: the rows past the {{take}}th are never seen, in any order. Remove the cap, page through with `skip` until a page comes back short, or count first.",
|
|
343
|
+
unordered: "`{{callee}}` takes a page with no `{{order}}`: the rows come back in whatever order the database finds cheapest, so the first row is a different row under load. Name the column the page is a page of.",
|
|
344
|
+
unorderedFind: "`{{callee}}` reads a page with no `orderBy` (`findFirst` is `LIMIT 1`): SQLite returns the rows in whatever order is cheapest, so the row it answers with changes under load. Add `orderBy`, or pin the row with `eq` on a unique column.",
|
|
345
|
+
unorderedFirst: "`.first()` returns the first row the statement produced, and this SELECT has no ORDER BY — D1 adds neither a LIMIT nor an order — so when more than one row matches, which one comes back is SQLite's choice. Add ORDER BY, or pin the row by a unique column in the WHERE.",
|
|
346
|
+
unorderedLimit: "`.limit()` in a select with no `.orderBy()`: SQLite returns the rows in whatever order is cheapest, so the page is a different page under load. Add `.orderBy(…)` naming the column the page is a page of.",
|
|
347
|
+
unorderedSql: "This SELECT has LIMIT and no ORDER BY: SQLite returns the rows in whatever order is cheapest, so the page is a different page under load. Add ORDER BY naming the column the page is a page of."
|
|
67
348
|
},
|
|
68
349
|
schema: optionsBag({
|
|
69
350
|
callees: STRINGS,
|
|
70
|
-
|
|
351
|
+
exempt: PATTERNS,
|
|
352
|
+
orderKeys: STRINGS,
|
|
353
|
+
uniqueColumns: STRINGS
|
|
71
354
|
}),
|
|
72
355
|
type: "problem"
|
|
73
356
|
}
|
|
@@ -14,6 +14,7 @@ Everywhere else, reach the data through a query the data layer exports. The ORM
|
|
|
14
14
|
below the seam, and a caller that names it has crossed it. Which paths are the data layer is the
|
|
15
15
|
\`dataLayer\` option (default \`/packages/db/\`).`,
|
|
16
16
|
forbidden: (target, importer, options) => isOrmModule(target.source) && !anyOf(options.dataLayer, importer.path),
|
|
17
|
+
mayForbid: isOrmModule,
|
|
17
18
|
messageId: "ormOutsideDb",
|
|
18
19
|
messages: {
|
|
19
20
|
ormOutsideDb: 'This file imports "{{source}}". drizzle-orm lives only in the data-layer package — reach the data through a query that package exports, or add the query there.'
|