@avi2dg/checks 0.26.0 → 0.27.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 +17 -0
- package/README.md +6 -6
- package/dist/data-shape/index.js +428 -0
- package/dist/templates/reference.md +1 -1
- package/docs/configs/commit-messages.md +1 -1
- package/docs/configs/dependency-rules.md +7 -1
- package/docs/configs/effect-rules.md +10 -0
- package/docs/configs/native-settings.md +8 -30
- package/docs/configs/typescript-rules.md +43 -1
- package/docs/design.md +210 -104
- package/docs/gates/checks-changelog.md +8 -5
- package/docs/gates/checks-ci-wiring.md +2 -2
- package/docs/gates/checks-comment-gate.md +3 -4
- package/docs/gates/checks-commit-identity.md +3 -3
- package/docs/gates/checks-docs.md +56 -36
- package/docs/gates/checks-exports.md +4 -4
- package/docs/gates/checks-flake.md +3 -3
- package/docs/gates/checks-lint-coverage.md +17 -9
- package/docs/gates/checks-lint.md +8 -4
- package/docs/gates/checks-mutation-compare.md +3 -3
- package/docs/gates/checks-quarantine-clock.md +3 -5
- package/docs/gates/checks-release-notes.md +4 -4
- package/docs/gates/checks-release-report.md +3 -3
- package/docs/gates/checks-repetition.md +2 -1
- package/docs/gates/checks-subsumed-tests.md +3 -3
- package/docs/gates/checks-suppressions-ratchet.md +3 -4
- package/docs/gates/checks-test-layout.md +4 -4
- package/docs/gates/checks-test.md +3 -2
- package/docs/gates/checks-unused.md +4 -4
- package/docs/gates/checks-vendor.md +3 -3
- package/oxlintrc.json +22 -2
- package/package.json +10 -5
- package/src/complexity/exports.ts +8 -16
- package/src/complexity/knip.ts +1 -1
- package/src/delivery/ci-wiring.ts +1 -1
- package/src/dependencies/vendor.ts +4 -5
- package/src/docs/doc-names.ts +128 -0
- package/src/docs/doc-templates.ts +1 -1
- package/src/docs/docs.ts +17 -14
- package/src/docs/prose-matchers.ts +11 -1
- package/src/quality/lint-coverage.sh +35 -2
- package/src/quality/presets/effect.language-service.json +3 -1
- package/src/testing/mutation-compare.ts +51 -45
- package/src/testing/test-layout.ts +1 -1
- package/ts-reset.d.ts +2 -0
- package/tsconfig.effect.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.27.0
|
|
6
|
+
|
|
7
|
+
Released 2026-09-27.
|
|
8
|
+
|
|
9
|
+
### Breaking changes
|
|
10
|
+
|
|
11
|
+
- **quality:** add data-shape oxlint plugin for readonly params and schema twins [#96](https://github.com/avi2d/checks/pull/96)
|
|
12
|
+
- ship stricter type gates for any, optional keys, process.env and ts-reset [#94](https://github.com/avi2d/checks/pull/94)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **docs:** check agent files, vanished names and reports about the past [#95](https://github.com/avi2d/checks/pull/95)
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
- describe the kit without naming its owner in the package description [#91](https://github.com/avi2d/checks/pull/91)
|
|
21
|
+
|
|
5
22
|
## 0.26.0
|
|
6
23
|
|
|
7
24
|
Released 2026-09-27.
|
package/README.md
CHANGED
|
@@ -10,7 +10,6 @@ Each repository owns its workflows and native tool configs, as [Native settings]
|
|
|
10
10
|
|
|
11
11
|
- A git repository, whose history the range gates read.
|
|
12
12
|
- Bun 1.3.13, which runs every bin.
|
|
13
|
-
- TypeScript 7.0.2, whose `tsc` the `typecheck` script runs.
|
|
14
13
|
- The peer dependencies, at the exact versions the kit pins:
|
|
15
14
|
- `@effect/tsgo` 0.45.0
|
|
16
15
|
- `@swc/core` 1.16.2
|
|
@@ -19,6 +18,7 @@ Each repository owns its workflows and native tool configs, as [Native settings]
|
|
|
19
18
|
- `jscpd` 5.3.2
|
|
20
19
|
- `oxlint` 1.83.0
|
|
21
20
|
- `oxlint-tsgolint` 7.0.2002
|
|
21
|
+
- `typescript` 7.0.2
|
|
22
22
|
|
|
23
23
|
<!-- end generated prerequisites -->
|
|
24
24
|
|
|
@@ -108,9 +108,9 @@ Add a pull request title lint step in another workflow using `./node_modules/.bi
|
|
|
108
108
|
|
|
109
109
|
## What runs
|
|
110
110
|
|
|
111
|
-
`checks-lint` runs
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
`checks-lint` runs each of these gates that applies to the repository, and names every one that fails.
|
|
112
|
+
A gate reads either the working tree or the range `checks-lint` resolves.
|
|
113
|
+
The table groups the gates by vector, the part of a repository each one judges.
|
|
114
114
|
|
|
115
115
|
<!-- generated gates: bun run build writes it from KIT_GATES in src/core/gates.ts and scripts/doc-blocks.ts -->
|
|
116
116
|
|
|
@@ -155,7 +155,6 @@ To move a repository to a newer release of the kit:
|
|
|
155
155
|
|
|
156
156
|
The repository's lockfile pins the kit, so a repository moves only when it runs these steps.
|
|
157
157
|
[CHANGELOG.md](CHANGELOG.md), shipped in the package, lists what each release changed.
|
|
158
|
-
A repository that tracks `quality.json` moves its settings as [Native settings](docs/configs/native-settings.md#consumer-migration) maps.
|
|
159
158
|
|
|
160
159
|
## Where things are
|
|
161
160
|
|
|
@@ -175,7 +174,8 @@ Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
|
175
174
|
| `dist/` | the compiled oxlint plugins and the doc templates, one template per kind of doc file |
|
|
176
175
|
| `oxlintrc.json` | the oxlint base config `.oxlintrc.json` extends |
|
|
177
176
|
| `stryker.preset.js` | the Stryker mutation-testing preset |
|
|
178
|
-
| `tsconfig.effect.json` | the tsconfig fragment with the Effect language-service block |
|
|
177
|
+
| `tsconfig.effect.json` | the tsconfig fragment with the shared compiler options and the Effect language-service block |
|
|
178
|
+
| `ts-reset.d.ts` | the two ts-reset rules `tsconfig.effect.json` lists in `files` |
|
|
179
179
|
|
|
180
180
|
<!-- end generated shipped -->
|
|
181
181
|
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// src/quality/data-shape/readonly-collection-param.ts
|
|
2
|
+
var ARRAY_METHODS = new Set([
|
|
3
|
+
"push",
|
|
4
|
+
"pop",
|
|
5
|
+
"shift",
|
|
6
|
+
"unshift",
|
|
7
|
+
"splice",
|
|
8
|
+
"sort",
|
|
9
|
+
"reverse",
|
|
10
|
+
"fill",
|
|
11
|
+
"copyWithin"
|
|
12
|
+
]);
|
|
13
|
+
var COLLECTION_METHODS = new Set(["set", "delete", "clear", "add"]);
|
|
14
|
+
var HANDOFF_PARENTS = new Set([
|
|
15
|
+
"ArrayExpression",
|
|
16
|
+
"ReturnStatement",
|
|
17
|
+
"YieldExpression",
|
|
18
|
+
"JSXExpressionContainer",
|
|
19
|
+
"JSXSpreadAttribute"
|
|
20
|
+
]);
|
|
21
|
+
var READONLY_NAMES = new Set(["ReadonlyArray", "ReadonlyMap", "ReadonlySet"]);
|
|
22
|
+
function isArrayType(type) {
|
|
23
|
+
if (type === undefined)
|
|
24
|
+
return false;
|
|
25
|
+
if (type.type === "TSArrayType")
|
|
26
|
+
return true;
|
|
27
|
+
return type.type === "TSTypeReference" && type.typeName.type === "Identifier" && type.typeName.name === "Array";
|
|
28
|
+
}
|
|
29
|
+
function isMapOrSet(type) {
|
|
30
|
+
return type?.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Map" || type.typeName.name === "Set");
|
|
31
|
+
}
|
|
32
|
+
function namedOf(param) {
|
|
33
|
+
if (param.type === "Identifier")
|
|
34
|
+
return { name: param.name, type: param.typeAnnotation?.typeAnnotation };
|
|
35
|
+
if (param.type === "AssignmentPattern" && param.left.type === "Identifier") {
|
|
36
|
+
return { name: param.left.name, type: param.left.typeAnnotation?.typeAnnotation };
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
function isReadonlyType(type) {
|
|
41
|
+
if (type?.type === "TSTypeOperator")
|
|
42
|
+
return type.operator === "readonly";
|
|
43
|
+
return type?.type === "TSTypeReference" && type.typeName.type === "Identifier" && READONLY_NAMES.has(type.typeName.name);
|
|
44
|
+
}
|
|
45
|
+
function declaredFunctions(program) {
|
|
46
|
+
const found = new Map;
|
|
47
|
+
for (const statement of program.body) {
|
|
48
|
+
const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
49
|
+
if (declaration?.type === "FunctionDeclaration" && declaration.id !== null) {
|
|
50
|
+
found.set(declaration.id.name, declaration.params);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
function isPassThrough(parent, child) {
|
|
56
|
+
if (parent === null)
|
|
57
|
+
return false;
|
|
58
|
+
if (parent.type === "ConditionalExpression")
|
|
59
|
+
return parent.test !== child;
|
|
60
|
+
return parent.type === "LogicalExpression" || parent.type === "AwaitExpression" || parent.type === "TSAsExpression" || parent.type === "TSTypeAssertion" || parent.type === "TSSatisfiesExpression" || parent.type === "TSNonNullExpression";
|
|
61
|
+
}
|
|
62
|
+
function valuePosition(node) {
|
|
63
|
+
let current = node;
|
|
64
|
+
while (isPassThrough(current.parent, current))
|
|
65
|
+
current = current.parent;
|
|
66
|
+
return current;
|
|
67
|
+
}
|
|
68
|
+
function writtenObjects(target) {
|
|
69
|
+
if (target === null)
|
|
70
|
+
return [];
|
|
71
|
+
if (target.type === "MemberExpression")
|
|
72
|
+
return target.object.type === "Identifier" ? [target.object.name] : [];
|
|
73
|
+
if (target.type === "ArrayPattern")
|
|
74
|
+
return target.elements.flatMap((element) => writtenObjects(element));
|
|
75
|
+
if (target.type === "ObjectPattern") {
|
|
76
|
+
return target.properties.flatMap((one) => writtenObjects(one.type === "Property" ? one.value : one));
|
|
77
|
+
}
|
|
78
|
+
if (target.type === "AssignmentPattern")
|
|
79
|
+
return writtenObjects(target.left);
|
|
80
|
+
if (target.type === "RestElement")
|
|
81
|
+
return writtenObjects(target.argument);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
function calledMethod(callee) {
|
|
85
|
+
const { property } = callee;
|
|
86
|
+
if (!callee.computed && property.type === "Identifier")
|
|
87
|
+
return property.name;
|
|
88
|
+
if (property.type === "Literal" && typeof property.value === "string")
|
|
89
|
+
return property.value;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
function watch(params) {
|
|
93
|
+
const found = [];
|
|
94
|
+
for (const param of params) {
|
|
95
|
+
const named = namedOf(param);
|
|
96
|
+
if (named === undefined || named.type === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
if (isArrayType(named.type))
|
|
99
|
+
found.push({ name: named.name, type: named.type, methods: ARRAY_METHODS });
|
|
100
|
+
else if (isMapOrSet(named.type))
|
|
101
|
+
found.push({ name: named.name, type: named.type, methods: COLLECTION_METHODS });
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
function fixOf(type, text) {
|
|
106
|
+
return type.type === "TSArrayType" ? `readonly ${text}` : `Readonly${text}`;
|
|
107
|
+
}
|
|
108
|
+
var rule = {
|
|
109
|
+
meta: {
|
|
110
|
+
type: "problem",
|
|
111
|
+
docs: { description: "Type a collection parameter the function never mutates as readonly" }
|
|
112
|
+
},
|
|
113
|
+
create(context) {
|
|
114
|
+
const stack = [];
|
|
115
|
+
let functions = new Map;
|
|
116
|
+
const mark = (name, method) => {
|
|
117
|
+
for (let index = stack.length - 1;index >= 0; index--) {
|
|
118
|
+
const frame = stack[index];
|
|
119
|
+
if (frame === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
const param = frame.watched.find((one) => one.name === name);
|
|
122
|
+
if (param === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
if (method === undefined || param.methods.has(method))
|
|
125
|
+
frame.mutated.add(name);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const enter = (node) => {
|
|
130
|
+
stack.push({ watched: watch(node.body === null ? [] : node.params), mutated: new Set });
|
|
131
|
+
};
|
|
132
|
+
const leave = () => {
|
|
133
|
+
const frame = stack.pop();
|
|
134
|
+
if (frame === undefined)
|
|
135
|
+
return;
|
|
136
|
+
for (const param of frame.watched) {
|
|
137
|
+
if (frame.mutated.has(param.name))
|
|
138
|
+
continue;
|
|
139
|
+
const text = context.sourceCode.getText(param.type);
|
|
140
|
+
context.report({
|
|
141
|
+
node: param.type,
|
|
142
|
+
message: `parameter \`${param.name}\` is typed \`${text}\` but this function never mutates it. Type it \`${fixOf(param.type, text)}\` instead.`
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const call = (node) => {
|
|
147
|
+
const { callee } = node;
|
|
148
|
+
if (callee.type !== "MemberExpression")
|
|
149
|
+
return;
|
|
150
|
+
const { object } = callee;
|
|
151
|
+
if (object.type !== "Identifier")
|
|
152
|
+
return;
|
|
153
|
+
mark(object.name, calledMethod(callee));
|
|
154
|
+
};
|
|
155
|
+
const passedReadonly = (site, argument) => {
|
|
156
|
+
if (site.callee.type !== "Identifier")
|
|
157
|
+
return false;
|
|
158
|
+
const params = functions.get(site.callee.name);
|
|
159
|
+
const index = site.arguments.findIndex((one) => one === argument);
|
|
160
|
+
const param = params?.[index];
|
|
161
|
+
return param !== undefined && isReadonlyType(namedOf(param)?.type);
|
|
162
|
+
};
|
|
163
|
+
const escapes = (node) => {
|
|
164
|
+
const value = valuePosition(node);
|
|
165
|
+
const { parent } = value;
|
|
166
|
+
if (parent === null)
|
|
167
|
+
return false;
|
|
168
|
+
if (parent.type === "CallExpression" || parent.type === "NewExpression") {
|
|
169
|
+
return parent.callee !== value && !passedReadonly(parent, value);
|
|
170
|
+
}
|
|
171
|
+
if (parent.type === "VariableDeclarator")
|
|
172
|
+
return parent.init === value;
|
|
173
|
+
if (parent.type === "AssignmentExpression" || parent.type === "AssignmentPattern")
|
|
174
|
+
return parent.right === value;
|
|
175
|
+
if (parent.type === "Property" || parent.type === "PropertyDefinition")
|
|
176
|
+
return parent.value === value;
|
|
177
|
+
if (parent.type === "ArrowFunctionExpression")
|
|
178
|
+
return parent.body === value;
|
|
179
|
+
return HANDOFF_PARENTS.has(parent.type);
|
|
180
|
+
};
|
|
181
|
+
const written = (target) => {
|
|
182
|
+
for (const name of writtenObjects(target))
|
|
183
|
+
mark(name, undefined);
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
Program: (node) => {
|
|
187
|
+
functions = declaredFunctions(node);
|
|
188
|
+
},
|
|
189
|
+
Identifier: (node) => {
|
|
190
|
+
if (escapes(node))
|
|
191
|
+
mark(node.name, undefined);
|
|
192
|
+
},
|
|
193
|
+
FunctionDeclaration: enter,
|
|
194
|
+
FunctionExpression: enter,
|
|
195
|
+
ArrowFunctionExpression: enter,
|
|
196
|
+
"FunctionDeclaration:exit": leave,
|
|
197
|
+
"FunctionExpression:exit": leave,
|
|
198
|
+
"ArrowFunctionExpression:exit": leave,
|
|
199
|
+
CallExpression: call,
|
|
200
|
+
AssignmentExpression: (node) => {
|
|
201
|
+
written(node.left);
|
|
202
|
+
},
|
|
203
|
+
UpdateExpression: (node) => {
|
|
204
|
+
written(node.argument);
|
|
205
|
+
},
|
|
206
|
+
UnaryExpression: (node) => {
|
|
207
|
+
if (node.operator === "delete" && node.argument.type === "MemberExpression")
|
|
208
|
+
written(node.argument);
|
|
209
|
+
},
|
|
210
|
+
ForOfStatement: (node) => {
|
|
211
|
+
written(node.left);
|
|
212
|
+
},
|
|
213
|
+
ForInStatement: (node) => {
|
|
214
|
+
written(node.left);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var readonly_collection_param_default = rule;
|
|
220
|
+
|
|
221
|
+
// src/quality/data-shape/schema-twin.ts
|
|
222
|
+
var STRUCTS = new Set(["Struct", "TaggedStruct", "Class"]);
|
|
223
|
+
var REFINEMENTS = new Set(["check", "annotate"]);
|
|
224
|
+
var SCHEMA_KINDS = {
|
|
225
|
+
String: "string",
|
|
226
|
+
NonEmptyString: "string",
|
|
227
|
+
Trimmed: "string",
|
|
228
|
+
Literal: "literal",
|
|
229
|
+
Literals: "literal",
|
|
230
|
+
Number: "number",
|
|
231
|
+
Finite: "number",
|
|
232
|
+
Int: "number",
|
|
233
|
+
NonNegativeInt: "number",
|
|
234
|
+
Boolean: "boolean",
|
|
235
|
+
Array: "array",
|
|
236
|
+
NonEmptyArray: "array",
|
|
237
|
+
Struct: "object",
|
|
238
|
+
TaggedStruct: "object",
|
|
239
|
+
Record: "record",
|
|
240
|
+
Unknown: "unknown"
|
|
241
|
+
};
|
|
242
|
+
function keyName(key) {
|
|
243
|
+
if (key.type === "Identifier")
|
|
244
|
+
return key.name;
|
|
245
|
+
if (key.type === "Literal" && typeof key.value === "string")
|
|
246
|
+
return key.value;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
function typeKind(type) {
|
|
250
|
+
const value = type?.type === "TSTypeOperator" ? type.typeAnnotation : type;
|
|
251
|
+
if (value === undefined)
|
|
252
|
+
return "wild";
|
|
253
|
+
if (value.type === "TSStringKeyword")
|
|
254
|
+
return "string";
|
|
255
|
+
if (value.type === "TSNumberKeyword")
|
|
256
|
+
return "number";
|
|
257
|
+
if (value.type === "TSBooleanKeyword")
|
|
258
|
+
return "boolean";
|
|
259
|
+
if (value.type === "TSArrayType")
|
|
260
|
+
return "array";
|
|
261
|
+
if (value.type === "TSTypeLiteral")
|
|
262
|
+
return "object";
|
|
263
|
+
if (value.type === "TSLiteralType")
|
|
264
|
+
return "literal";
|
|
265
|
+
if (value.type === "TSUnknownKeyword")
|
|
266
|
+
return "unknown";
|
|
267
|
+
if (value.type === "TSTypeReference")
|
|
268
|
+
return referenceKind(value);
|
|
269
|
+
if (value.type === "TSUnionType" && value.types.every((one) => one.type === "TSLiteralType"))
|
|
270
|
+
return "literal";
|
|
271
|
+
return "wild";
|
|
272
|
+
}
|
|
273
|
+
function referenceKind(value) {
|
|
274
|
+
if (value.typeName.type !== "Identifier")
|
|
275
|
+
return "wild";
|
|
276
|
+
if (value.typeName.name === "Array" || value.typeName.name === "ReadonlyArray")
|
|
277
|
+
return "array";
|
|
278
|
+
if (value.typeName.name === "Record" || value.typeName.name === "ReadonlyRecord")
|
|
279
|
+
return "record";
|
|
280
|
+
return "wild";
|
|
281
|
+
}
|
|
282
|
+
function schemaCallName(node) {
|
|
283
|
+
if (node === undefined)
|
|
284
|
+
return;
|
|
285
|
+
if (node.type === "CallExpression")
|
|
286
|
+
return schemaCallName(node.callee);
|
|
287
|
+
if (node.type !== "MemberExpression")
|
|
288
|
+
return;
|
|
289
|
+
const { object, property } = node;
|
|
290
|
+
if (object.type !== "Identifier" || object.name !== "Schema" || property.type !== "Identifier")
|
|
291
|
+
return;
|
|
292
|
+
return property.name;
|
|
293
|
+
}
|
|
294
|
+
function kindOf(name) {
|
|
295
|
+
return name === undefined ? "wild" : SCHEMA_KINDS[name] ?? "wild";
|
|
296
|
+
}
|
|
297
|
+
function refinedBase(value) {
|
|
298
|
+
const { callee } = value;
|
|
299
|
+
if (callee.type !== "MemberExpression" || callee.object.type === "Super")
|
|
300
|
+
return;
|
|
301
|
+
if (callee.property.type !== "Identifier" || !REFINEMENTS.has(callee.property.name))
|
|
302
|
+
return;
|
|
303
|
+
return callee.object;
|
|
304
|
+
}
|
|
305
|
+
function schemaKind(value) {
|
|
306
|
+
if (value !== undefined && value.type === "CallExpression") {
|
|
307
|
+
const name = schemaCallName(value.callee);
|
|
308
|
+
if (name === "optionalKey" || name === "optional") {
|
|
309
|
+
const first = value.arguments[0];
|
|
310
|
+
const inner = first === undefined || first.type === "SpreadElement" ? undefined : first;
|
|
311
|
+
return { optional: true, kind: schemaKind(inner).kind };
|
|
312
|
+
}
|
|
313
|
+
const base = refinedBase(value);
|
|
314
|
+
if (base !== undefined)
|
|
315
|
+
return schemaKind(base);
|
|
316
|
+
return { optional: false, kind: kindOf(name) };
|
|
317
|
+
}
|
|
318
|
+
return { optional: false, kind: kindOf(schemaCallName(value)) };
|
|
319
|
+
}
|
|
320
|
+
function shapeFields(node) {
|
|
321
|
+
const members = node.type === "TSTypeLiteral" ? node.members : node.body;
|
|
322
|
+
const found = [];
|
|
323
|
+
for (const member of members) {
|
|
324
|
+
if (member.type !== "TSPropertySignature")
|
|
325
|
+
continue;
|
|
326
|
+
const name = keyName(member.key);
|
|
327
|
+
if (name === undefined)
|
|
328
|
+
continue;
|
|
329
|
+
found.push({ name, optional: member.optional, kind: typeKind(member.typeAnnotation?.typeAnnotation) });
|
|
330
|
+
}
|
|
331
|
+
return found;
|
|
332
|
+
}
|
|
333
|
+
function schemaFields(call) {
|
|
334
|
+
const { callee } = call;
|
|
335
|
+
const inner = callee.type === "CallExpression" ? callee.callee : callee;
|
|
336
|
+
if (inner.type !== "MemberExpression")
|
|
337
|
+
return;
|
|
338
|
+
const { object, property } = inner;
|
|
339
|
+
if (object.type !== "Identifier" || object.name !== "Schema" || property.type !== "Identifier" || !STRUCTS.has(property.name)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const fields = call.arguments.find((one) => one.type === "ObjectExpression");
|
|
343
|
+
if (fields === undefined)
|
|
344
|
+
return;
|
|
345
|
+
const found = property.name === "TaggedStruct" ? [{ name: "_tag", optional: false, kind: "literal" }] : [];
|
|
346
|
+
for (const prop of fields.properties) {
|
|
347
|
+
if (prop.type !== "Property" || prop.computed)
|
|
348
|
+
continue;
|
|
349
|
+
const name = keyName(prop.key);
|
|
350
|
+
if (name === undefined)
|
|
351
|
+
continue;
|
|
352
|
+
const { optional, kind } = schemaKind(prop.value);
|
|
353
|
+
found.push({ name, optional, kind });
|
|
354
|
+
}
|
|
355
|
+
return found.length >= 2 ? found : undefined;
|
|
356
|
+
}
|
|
357
|
+
function twins(typeFields, schema) {
|
|
358
|
+
return typeFields.length === schema.length && typeFields.every((field) => {
|
|
359
|
+
const match = schema.find((one) => one.name === field.name);
|
|
360
|
+
return match !== undefined && match.optional === field.optional && field.kind === match.kind;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function schemaName(call) {
|
|
364
|
+
const { parent } = call;
|
|
365
|
+
if (parent.type !== "VariableDeclarator" || parent.id.type !== "Identifier")
|
|
366
|
+
return;
|
|
367
|
+
return parent.id.name;
|
|
368
|
+
}
|
|
369
|
+
function shapeName(node) {
|
|
370
|
+
const { parent } = node;
|
|
371
|
+
if (parent.type !== "TSTypeAliasDeclaration" && parent.type !== "TSInterfaceDeclaration")
|
|
372
|
+
return;
|
|
373
|
+
return parent.id.name;
|
|
374
|
+
}
|
|
375
|
+
function describeTwin(type, schema) {
|
|
376
|
+
const subject = type === undefined ? "This object type" : `Type \`${type}\``;
|
|
377
|
+
if (schema === undefined)
|
|
378
|
+
return `${subject} repeats the fields of a Schema in this file. Derive it from the schema instead of writing both.`;
|
|
379
|
+
return `${subject} repeats the fields of schema \`${schema}\` in this file. Derive it as \`typeof ${schema}.Type\` instead of writing both.`;
|
|
380
|
+
}
|
|
381
|
+
var rule2 = {
|
|
382
|
+
meta: {
|
|
383
|
+
type: "problem",
|
|
384
|
+
docs: { description: "Derive an object type from the Schema it repeats instead of writing both" }
|
|
385
|
+
},
|
|
386
|
+
create(context) {
|
|
387
|
+
const shapes = [];
|
|
388
|
+
const schemas = [];
|
|
389
|
+
const collect = (node) => {
|
|
390
|
+
if (node.parent.type === "TSInterfaceDeclaration" && node.parent.extends.length > 0)
|
|
391
|
+
return;
|
|
392
|
+
const fields = shapeFields(node);
|
|
393
|
+
if (fields.length >= 2)
|
|
394
|
+
shapes.push({ node, fields });
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
TSTypeLiteral: collect,
|
|
398
|
+
TSInterfaceBody: collect,
|
|
399
|
+
CallExpression(node) {
|
|
400
|
+
const fields = schemaFields(node);
|
|
401
|
+
if (fields !== undefined)
|
|
402
|
+
schemas.push({ name: schemaName(node), fields });
|
|
403
|
+
},
|
|
404
|
+
"Program:exit"() {
|
|
405
|
+
for (const shape of shapes) {
|
|
406
|
+
const twin = schemas.find((schema) => twins(shape.fields, schema.fields));
|
|
407
|
+
if (twin === undefined)
|
|
408
|
+
continue;
|
|
409
|
+
context.report({ node: shape.node, message: describeTwin(shapeName(shape.node), twin.name) });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
var schema_twin_default = rule2;
|
|
416
|
+
|
|
417
|
+
// src/quality/data-shape/index.ts
|
|
418
|
+
var plugin = {
|
|
419
|
+
meta: { name: "data-shape" },
|
|
420
|
+
rules: {
|
|
421
|
+
"readonly-collection-param": readonly_collection_param_default,
|
|
422
|
+
"schema-twin": schema_twin_default
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var data_shape_default = plugin;
|
|
426
|
+
export {
|
|
427
|
+
data_shape_default as default
|
|
428
|
+
};
|
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# The commit message lint
|
|
6
6
|
|
|
7
|
-
The shared commitlint config holds each pull request title to
|
|
7
|
+
The shared commitlint config holds each pull request title to the conventional commit format.
|
|
8
8
|
|
|
9
9
|
## Config
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# The dependency rules
|
|
6
6
|
|
|
7
|
-
The shared dependency-cruiser base holds a repository's imports to a set of rules
|
|
7
|
+
The shared dependency-cruiser base holds a repository's imports to a set of rules, and a repository adds its own boundaries on top.
|
|
8
8
|
|
|
9
9
|
## Base rules
|
|
10
10
|
|
|
@@ -19,6 +19,12 @@ The shared dependency-cruiser base holds a repository's imports to a set of rule
|
|
|
19
19
|
|
|
20
20
|
The base parses with swc, so it needs `@swc/core` installed, and without it the cruise silently skips every `.ts` file.
|
|
21
21
|
|
|
22
|
+
`no-deep-imports` judges the import specifier, never the file it resolves to.
|
|
23
|
+
The base honours `exports` maps, so a subpath the map publishes resolves and passes, and a subpath it omits fails to resolve and is reported.
|
|
24
|
+
A package without an `exports` map publishes every file.
|
|
25
|
+
A bare import always passes, whatever file its entry lives in.
|
|
26
|
+
A repository that sets its own `options.enhancedResolveOptions` replaces the base's, and restates `exportsFields` and `conditionNames` in it.
|
|
27
|
+
|
|
22
28
|
## Boundaries
|
|
23
29
|
|
|
24
30
|
A repository appends a named rule per boundary it owns:
|
|
@@ -40,6 +40,16 @@ Each config in an oxlint `extends` chain sets `plugins` explicitly, because an o
|
|
|
40
40
|
The repository's `tsconfig.json` holds its Effect override under `compilerOptions.plugins`.
|
|
41
41
|
The override includes the same source paths and excludes the same exempt paths as oxlint.
|
|
42
42
|
The kit ships severity values in `src/quality/presets/effect.language-service.json` for the override's `options`.
|
|
43
|
+
The preset turns these diagnostics to errors:
|
|
44
|
+
|
|
45
|
+
- `nodeBuiltinImport` refuses an import of a Node built-in module that has an Effect counterpart.
|
|
46
|
+
- `asyncFunction` refuses an `async` function.
|
|
47
|
+
- `newPromise` refuses `new Promise`.
|
|
48
|
+
- `extendsNativeError` refuses a class that extends the native `Error` directly.
|
|
49
|
+
- `processEnv` refuses a read of `process.env` outside an Effect generator.
|
|
50
|
+
- `processEnvInEffect` refuses a read of `process.env` inside an Effect generator.
|
|
51
|
+
|
|
52
|
+
Effect's `Config` reads the environment in place of `process.env`.
|
|
43
53
|
The kit's `tsconfig.effect.json` keeps the shared language service diagnostics.
|
|
44
54
|
|
|
45
55
|
## Related topics
|
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# Native settings
|
|
6
6
|
|
|
7
|
-
A consuming repository puts each setting in the file its tool reads.
|
|
7
|
+
A consuming repository puts each setting in the file its tool reads, and no manifest of the kit gathers them.
|
|
8
8
|
|
|
9
9
|
## Settings by file
|
|
10
10
|
|
|
@@ -24,37 +24,14 @@ A consuming repository puts each setting in the file its tool reads.
|
|
|
24
24
|
| Each page under `docs/` | Diátaxis mode in `kind` front matter, and `audience: consumers` on a page that speaks to a consuming repository | `checks-docs` |
|
|
25
25
|
|
|
26
26
|
Every page under `docs/` names its mode in `kind` front matter, whatever directory holds it.
|
|
27
|
-
A page whose front matter sets `audience: consumers` names commands a consuming repository runs
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Consumer migration
|
|
31
|
-
|
|
32
|
-
The following table maps the former fields to their owners.
|
|
33
|
-
|
|
34
|
-
| Former field | New owner |
|
|
35
|
-
| --- | --- |
|
|
36
|
-
| `defaultBranch` | Git's `refs/remotes/origin/HEAD`, or the pull request base or event repository in CI |
|
|
37
|
-
| `runsOn`, `gates.ci`, `gates.scheduled` | `runs-on` and `run` steps in `.github/workflows/*.yml` |
|
|
38
|
-
| `gates.lint` | `checks-lint` runs every kit gate |
|
|
39
|
-
| `commitIdentity.authors` | `author` and `contributors` in `package.json` |
|
|
40
|
-
| `sources.production` | `path` and `ignore` in `.jscpd.json` |
|
|
41
|
-
| `size.production`, `size.tests` | File overrides and native size rules in `.oxlintrc.json` |
|
|
42
|
-
| `sources.effect.paths`, `sources.effect.exempt` | Overrides in `.oxlintrc.json` and `tsconfig.json` |
|
|
43
|
-
| `sources.libraries` | `checks-vendor` arguments in the `prepare` script of `package.json` |
|
|
44
|
-
| `docs.pages` | `kind` front matter on each page |
|
|
45
|
-
| `docs.forConsumers` | `audience: consumers` front matter on each page |
|
|
46
|
-
| `features`, `changeSignal`, `agentRules` | No active declarations used these fields |
|
|
47
|
-
|
|
48
|
-
The kit has no general configuration manifest or generated workflow.
|
|
49
|
-
`checks-ci-wiring` requires title lint on opened and synchronized pull requests.
|
|
50
|
-
It also requires lint, build, typecheck and test for each of those scripts that `package.json` defines, and a clean git diff after a build.
|
|
51
|
-
`checks-repetition` runs jscpd with the repository's own `.jscpd.json`, so its `path` and `ignore` globs decide which files are measured.
|
|
27
|
+
A page whose front matter sets `audience: consumers` names commands that a consuming repository runs.
|
|
28
|
+
`checks-docs` skips the `bun run` commands on such a page, and resolves those in every other living doc or agent file as [Paths, links and commands](../gates/checks-docs.md#paths-links-and-commands) says.
|
|
52
29
|
|
|
53
30
|
## Size limits
|
|
54
31
|
|
|
55
|
-
The size rules are
|
|
32
|
+
The size rules are oxlint's own rules at `error` in `.oxlintrc.json`, so `bun run lint` fails on any file over them.
|
|
56
33
|
A repository records its existing violations with `oxlint --suppress-all`, which writes them to `oxlint-suppressions.json`.
|
|
57
|
-
`checks-suppressions-ratchet` refuses any count in that file that rises
|
|
34
|
+
`checks-suppressions-ratchet` refuses any count in that file that rises.
|
|
58
35
|
The kit's own `.oxlintrc.json` sets these limits for each size override, and a repository may copy them:
|
|
59
36
|
|
|
60
37
|
<!-- generated size-limits: bun run build writes it from .oxlintrc.json, SIZE_RULES in src/complexity/size-rules.ts and scripts/doc-blocks.ts -->
|
|
@@ -72,5 +49,6 @@ The kit's own `.oxlintrc.json` sets these limits for each size override, and a r
|
|
|
72
49
|
## Related topics
|
|
73
50
|
|
|
74
51
|
- [The Effect rules](effect-rules.md)
|
|
75
|
-
- [
|
|
76
|
-
- [
|
|
52
|
+
- [checks-ci-wiring](../gates/checks-ci-wiring.md)
|
|
53
|
+
- [checks-suppressions-ratchet](../gates/checks-suppressions-ratchet.md)
|
|
54
|
+
- [checks-docs](../gates/checks-docs.md)
|
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# The TypeScript rules
|
|
6
6
|
|
|
7
|
-
The oxlint base config
|
|
7
|
+
The oxlint base config and the tsconfig fragment hold a repository's TypeScript to a set of rules, and some of them need type information.
|
|
8
8
|
|
|
9
9
|
## Syntax rules
|
|
10
10
|
|
|
@@ -33,6 +33,48 @@ Without the flag, oxlint skips them and reports nothing about them.
|
|
|
33
33
|
- `typescript/no-unsafe-type-assertion` refuses an `as` that narrows a value to a type the compiler cannot prove.
|
|
34
34
|
- `typescript/no-deprecated` refuses a use of a symbol whose declaration carries a `@deprecated` tag, in the repository's own code or in a package's types, and repeats the tag's text.
|
|
35
35
|
|
|
36
|
+
## Data-shape rules
|
|
37
|
+
|
|
38
|
+
The base loads the kit's `data-shape` plugin from `dist/` with one rule for every TypeScript file and one for production files.
|
|
39
|
+
|
|
40
|
+
- `data-shape/readonly-collection-param` refuses a parameter typed `T[]`, `Array<T>`, `Map` or `Set` that the function never mutates, stores, returns or passes on, and passing it to a readonly parameter of a function declared in the same file does not count as passing it on.
|
|
41
|
+
- Type such a parameter `readonly T[]`, `ReadonlyArray<T>`, `ReadonlyMap` or `ReadonlySet`.
|
|
42
|
+
- `data-shape/schema-twin` refuses an object type whose fields match a `Schema.Struct`, `TaggedStruct` or `Class` in the same file by name, count, optionality and kind.
|
|
43
|
+
- Derive such a type from the schema with `typeof Name.Type` instead of writing both.
|
|
44
|
+
- The twin rule runs on production files only, so a test that declares its own schema as an oracle stays green.
|
|
45
|
+
|
|
46
|
+
## Rules outside tests
|
|
47
|
+
|
|
48
|
+
An override in `oxlintrc.json` turns on these type-aware rules in each `.ts` and `.tsx` file outside `tests/`:
|
|
49
|
+
|
|
50
|
+
- `typescript/no-unsafe-assignment` refuses assigning an `any` value to a variable, a property or a destructured name.
|
|
51
|
+
- `typescript/no-unsafe-member-access` refuses reading a member of an `any` value.
|
|
52
|
+
- `typescript/no-unsafe-argument` refuses passing an `any` value to a parameter of another type.
|
|
53
|
+
- `typescript/no-unsafe-return` refuses returning an `any` value from a function, unless the function returns `unknown`.
|
|
54
|
+
- `typescript/no-unsafe-call` refuses calling an `any` value.
|
|
55
|
+
|
|
56
|
+
A consumer inherits the override through `extends`, and a file under `tests/` answers to none of the five.
|
|
57
|
+
|
|
58
|
+
## Compiler options
|
|
59
|
+
|
|
60
|
+
`tsconfig.effect.json` sets these compiler options in each repository whose `tsconfig.json` extends it:
|
|
61
|
+
|
|
62
|
+
- `erasableSyntaxOnly` refuses TypeScript syntax that does not erase to JavaScript, such as an `enum` or a parameter property.
|
|
63
|
+
- `exactOptionalPropertyTypes` refuses `undefined` as the value of an optional property whose type does not name `undefined`, so a type derived from a `Schema.optionalKey` field accepts only a missing key, as the schema does.
|
|
64
|
+
|
|
65
|
+
## ts-reset rules
|
|
66
|
+
|
|
67
|
+
`tsconfig.effect.json` lists the kit's `ts-reset.d.ts` in `files`, and that file loads two rules of `@total-typescript/ts-reset`, a dependency of the kit:
|
|
68
|
+
|
|
69
|
+
- `is-array` types the array `Array.isArray` narrows a value to as `unknown[]` rather than `any[]`.
|
|
70
|
+
- `json-parse` types the value `JSON.parse` returns as `unknown` rather than `any`.
|
|
71
|
+
|
|
72
|
+
tsc then refuses code that uses either value as a type it has not checked.
|
|
73
|
+
The fragment also sets `include` to every file under the directory of the repository's `tsconfig.json` and to `ts-reset.d.ts`.
|
|
74
|
+
A repository whose `tsconfig.json` sets `include` or `files` but not both keeps both rules.
|
|
75
|
+
A repository that sets only `files` also gets every file under that directory in its program.
|
|
76
|
+
A repository that sets both `files` and `include` drops both rules, and `checks-lint-coverage` fails it.
|
|
77
|
+
|
|
36
78
|
## Related topics
|
|
37
79
|
|
|
38
80
|
- [The Effect rules](effect-rules.md)
|