@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +27 -1
  3. package/RULES.md +7 -7
  4. package/corpus/medusa/queries/bad-silent-cap.ts +11 -0
  5. package/corpus/medusa/queries/good-paged-through.ts +19 -0
  6. package/corpus/medusa/queries/integration-tests/any-row.ts +10 -0
  7. package/dist/index.js +3 -1
  8. package/dist/presets.js +7 -2
  9. package/dist/rules/lib/direction-rule.d.ts +1 -0
  10. package/dist/rules/lib/direction-rule.js +13 -1
  11. package/dist/rules/lib/doors.js +4 -2
  12. package/dist/rules/lib/fresh.d.ts +5 -0
  13. package/dist/rules/lib/fresh.js +15 -0
  14. package/dist/rules/lib/options.d.ts +1 -0
  15. package/dist/rules/lib/options.js +5 -0
  16. package/dist/rules/lib/run-files.d.ts +18 -0
  17. package/dist/rules/lib/run-files.js +144 -0
  18. package/dist/rules/lib/token-layer.d.ts +19 -0
  19. package/dist/rules/lib/token-layer.js +170 -0
  20. package/dist/rules/lib/workspace-members.js +17 -19
  21. package/dist/rules/list-take-without-order.d.ts +24 -0
  22. package/dist/rules/list-take-without-order.js +293 -10
  23. package/dist/rules/no-orm-outside-db.js +1 -0
  24. package/dist/rules/subpath-over-barrel.d.ts +1 -0
  25. package/dist/rules/subpath-over-barrel.js +18 -12
  26. package/dist/rules/test-no-clock-polling-for-promise-flips.d.ts +5 -1
  27. package/dist/rules/test-no-clock-polling-for-promise-flips.js +3 -0
  28. package/dist/rules/tissue-must-compose.d.ts +1 -1
  29. package/dist/rules/tissue-no-data-props.d.ts +2 -2
  30. package/dist/rules/tokens-only-colors.d.ts +8 -11
  31. package/dist/rules/tokens-only-colors.js +36 -7
  32. package/dist/rules/worker-handles-are-scoped.d.ts +1 -1
  33. package/dist/timing.d.ts +18 -0
  34. package/dist/timing.js +75 -0
  35. 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, statSync } from "node:fs";
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 cache = new Map;
66
- var rootAbove = (from) => {
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
- const stamp = statSync(marker).mtimeMs;
95
- const held = cache.get(marker);
96
- if (held?.stamp === stamp)
97
- return held.members;
98
- const globs = globsOf(found.marker, readFileSync(marker, "utf8")) ?? [];
99
- const names = new Map;
100
- for (const dir of memberDirs(found.root, globs)) {
101
- const manifest = join(dir, "package.json");
102
- const name = existsSync(manifest) ? nameOf(manifest) : undefined;
103
- if (name !== undefined && !names.has(name))
104
- names.set(name, manifest);
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
- let takes = false;
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 === "take")
40
- takes = true;
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 (!takes || orders || unreadable)
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. Which calls are
60
- list calls is the \`callees\` option and which key declares the ordering is \`orderKeys\`.`,
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
- 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."
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
- orderKeys: STRINGS
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.'
@@ -12,6 +12,7 @@ declare const subpathOverBarrel: {
12
12
  create(context: RuleContextWithOptions<Options>): {
13
13
  ExportNamedDeclaration: (node: AnyNode) => void;
14
14
  ImportDeclaration: (node: AnyNode) => void;
15
+ Program(node: AnyNode): void;
15
16
  };
16
17
  fixShape: string;
17
18
  meta: {