@avi2dg/checks 0.15.0 → 0.17.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 +19 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +5 -2
- package/dependency-cruiser.config.js +8 -0
- package/dist/feature-rules.js +139 -71
- package/dist/index.js +426 -7
- package/docs/configs/dependency-rules.md +1 -0
- package/docs/configs/effect-rules.md +1 -0
- package/docs/configs/quality-file.md +3 -3
- package/docs/configs/typescript-rules.md +36 -0
- package/docs/design.md +10 -1
- package/docs/gates/checks-comment-gate.md +2 -0
- package/docs/gates/checks-lint.md +4 -3
- package/docs/gates/checks-quality.md +1 -1
- package/docs/gates/checks-repetition.md +72 -0
- package/docs/gates/checks-size-budget.md +57 -14
- package/oxlintrc.json +1 -0
- package/package.json +7 -1
- package/quality.schema.json +63 -18
- package/scripts/comment-matchers.ts +19 -1
- package/scripts/gates.ts +1 -0
- package/scripts/git.ts +19 -1
- package/scripts/quality-file.ts +3 -17
- package/scripts/quality.ts +2 -2
- package/scripts/repetition.ts +161 -0
- package/scripts/size-budget.ts +191 -87
- package/scripts/size-rules.ts +126 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,421 @@
|
|
|
1
|
+
// effect-channel/cognitive-nodes.ts
|
|
2
|
+
var CONTROL_TYPES = ["IfStatement", "ConditionalExpression", "SwitchStatement", "SwitchCase", "TryStatement", "CatchClause"];
|
|
3
|
+
var LOOP_TYPES = ["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement", "LabeledStatement"];
|
|
4
|
+
var CALL_TYPES = ["LogicalExpression", "BreakStatement", "ContinueStatement", "CallExpression", "NewExpression", "ImportExpression"];
|
|
5
|
+
var FUNCTION_TYPES = ["FunctionDeclaration", "FunctionExpression", "TSDeclareFunction", "TSEmptyBodyFunctionExpression", "ArrowFunctionExpression", "StaticBlock"];
|
|
6
|
+
var PLAIN_A_TYPES = ["BlockStatement", "ExpressionStatement", "ReturnStatement", "ThrowStatement", "VariableDeclaration", "VariableDeclarator", "AssignmentPattern", "ObjectPattern", "ArrayPattern", "Property", "RestElement", "TSParameterProperty", "WithStatement", "TSEnumDeclaration", "TSEnumBody", "TSEnumMember"];
|
|
7
|
+
var PLAIN_B_TYPES = ["ClassDeclaration", "ClassExpression", "ClassBody", "MethodDefinition", "TSAbstractMethodDefinition", "PropertyDefinition", "TSAbstractPropertyDefinition", "AccessorProperty", "TSAbstractAccessorProperty", "ObjectExpression", "ArrayExpression"];
|
|
8
|
+
var PLAIN_C_TYPES = ["AwaitExpression", "UnaryExpression", "UpdateExpression", "SpreadElement", "YieldExpression", "BinaryExpression", "AssignmentExpression", "TSAsExpression", "TSSatisfiesExpression", "TSTypeAssertion", "TSNonNullExpression", "ChainExpression", "ParenthesizedExpression", "Decorator", "TSInstantiationExpression", "MemberExpression", "JSXMemberExpression", "TemplateLiteral", "TaggedTemplateExpression", "SequenceExpression", "JSXElement", "JSXFragment", "JSXOpeningElement", "JSXAttribute", "JSXExpressionContainer", "JSXSpreadChild", "JSXSpreadAttribute"];
|
|
9
|
+
|
|
10
|
+
// effect-channel/cognitive-plain.ts
|
|
11
|
+
function isControl(node) {
|
|
12
|
+
return CONTROL_TYPES.includes(node.type);
|
|
13
|
+
}
|
|
14
|
+
function isLoop(node) {
|
|
15
|
+
return LOOP_TYPES.includes(node.type);
|
|
16
|
+
}
|
|
17
|
+
function isCall(node) {
|
|
18
|
+
return CALL_TYPES.includes(node.type);
|
|
19
|
+
}
|
|
20
|
+
function isFunction(node) {
|
|
21
|
+
return FUNCTION_TYPES.includes(node.type);
|
|
22
|
+
}
|
|
23
|
+
function isPlainA(node) {
|
|
24
|
+
return PLAIN_A_TYPES.includes(node.type);
|
|
25
|
+
}
|
|
26
|
+
function isPlainB(node) {
|
|
27
|
+
return PLAIN_B_TYPES.includes(node.type);
|
|
28
|
+
}
|
|
29
|
+
function isPlainC(node) {
|
|
30
|
+
return PLAIN_C_TYPES.includes(node.type);
|
|
31
|
+
}
|
|
32
|
+
function unreachable(_value) {}
|
|
33
|
+
function plainChildrenA(node) {
|
|
34
|
+
switch (node.type) {
|
|
35
|
+
case "BlockStatement":
|
|
36
|
+
return node.body;
|
|
37
|
+
case "ExpressionStatement":
|
|
38
|
+
return [node.expression];
|
|
39
|
+
case "ReturnStatement":
|
|
40
|
+
case "ThrowStatement":
|
|
41
|
+
return [node.argument];
|
|
42
|
+
case "VariableDeclaration":
|
|
43
|
+
return node.declarations;
|
|
44
|
+
case "VariableDeclarator":
|
|
45
|
+
return [node.id, node.init];
|
|
46
|
+
case "AssignmentPattern":
|
|
47
|
+
return [node.left, node.right];
|
|
48
|
+
case "ObjectPattern":
|
|
49
|
+
return node.properties;
|
|
50
|
+
case "ArrayPattern":
|
|
51
|
+
return node.elements;
|
|
52
|
+
case "Property":
|
|
53
|
+
return [node.key, node.value];
|
|
54
|
+
case "RestElement":
|
|
55
|
+
return [node.argument];
|
|
56
|
+
case "TSParameterProperty":
|
|
57
|
+
return [node.parameter];
|
|
58
|
+
case "WithStatement":
|
|
59
|
+
return [node.object, node.body];
|
|
60
|
+
case "TSEnumDeclaration":
|
|
61
|
+
return [node.body];
|
|
62
|
+
case "TSEnumBody":
|
|
63
|
+
return node.members;
|
|
64
|
+
case "TSEnumMember":
|
|
65
|
+
return [node.initializer];
|
|
66
|
+
default:
|
|
67
|
+
unreachable(node);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function plainChildrenB(node) {
|
|
72
|
+
switch (node.type) {
|
|
73
|
+
case "ClassDeclaration":
|
|
74
|
+
case "ClassExpression":
|
|
75
|
+
return [...node.decorators, node.id, node.superClass, node.body];
|
|
76
|
+
case "ClassBody":
|
|
77
|
+
return node.body;
|
|
78
|
+
case "MethodDefinition":
|
|
79
|
+
case "TSAbstractMethodDefinition":
|
|
80
|
+
return [node.key, node.value];
|
|
81
|
+
case "PropertyDefinition":
|
|
82
|
+
case "TSAbstractPropertyDefinition":
|
|
83
|
+
case "AccessorProperty":
|
|
84
|
+
case "TSAbstractAccessorProperty":
|
|
85
|
+
return [node.key, node.value];
|
|
86
|
+
case "ObjectExpression":
|
|
87
|
+
return node.properties;
|
|
88
|
+
case "ArrayExpression":
|
|
89
|
+
return node.elements;
|
|
90
|
+
default:
|
|
91
|
+
unreachable(node);
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function plainChildrenC(node) {
|
|
96
|
+
switch (node.type) {
|
|
97
|
+
case "AwaitExpression":
|
|
98
|
+
case "UnaryExpression":
|
|
99
|
+
case "UpdateExpression":
|
|
100
|
+
case "SpreadElement":
|
|
101
|
+
return [node.argument];
|
|
102
|
+
case "YieldExpression":
|
|
103
|
+
return [node.argument];
|
|
104
|
+
case "BinaryExpression":
|
|
105
|
+
return [node.left, node.right];
|
|
106
|
+
case "AssignmentExpression":
|
|
107
|
+
return [node.left, node.right];
|
|
108
|
+
case "TSAsExpression":
|
|
109
|
+
case "TSSatisfiesExpression":
|
|
110
|
+
case "TSTypeAssertion":
|
|
111
|
+
case "TSNonNullExpression":
|
|
112
|
+
case "ChainExpression":
|
|
113
|
+
case "ParenthesizedExpression":
|
|
114
|
+
case "Decorator":
|
|
115
|
+
case "TSInstantiationExpression":
|
|
116
|
+
return [node.expression];
|
|
117
|
+
case "MemberExpression":
|
|
118
|
+
case "JSXMemberExpression":
|
|
119
|
+
return [node.object, node.property];
|
|
120
|
+
case "TemplateLiteral":
|
|
121
|
+
return node.expressions;
|
|
122
|
+
case "TaggedTemplateExpression":
|
|
123
|
+
return [node.tag, node.quasi];
|
|
124
|
+
case "SequenceExpression":
|
|
125
|
+
return node.expressions;
|
|
126
|
+
case "JSXElement":
|
|
127
|
+
return [node.openingElement, ...node.children];
|
|
128
|
+
case "JSXFragment":
|
|
129
|
+
return node.children;
|
|
130
|
+
case "JSXOpeningElement":
|
|
131
|
+
return node.attributes;
|
|
132
|
+
case "JSXAttribute":
|
|
133
|
+
return [node.value];
|
|
134
|
+
case "JSXExpressionContainer":
|
|
135
|
+
case "JSXSpreadChild":
|
|
136
|
+
return [node.expression];
|
|
137
|
+
case "JSXSpreadAttribute":
|
|
138
|
+
return [node.argument];
|
|
139
|
+
default:
|
|
140
|
+
unreachable(node);
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// effect-channel/cognitive.ts
|
|
146
|
+
function unreachable2(_value) {}
|
|
147
|
+
function scoreList(state, nodes, nesting, parent) {
|
|
148
|
+
for (const node of nodes) {
|
|
149
|
+
if (node !== null)
|
|
150
|
+
score(state, node, nesting, parent);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function score(state, node, nesting, parent) {
|
|
154
|
+
if (isControl(node))
|
|
155
|
+
return scoreControl(state, node, nesting);
|
|
156
|
+
if (isLoop(node))
|
|
157
|
+
return scoreLoop(state, node, nesting);
|
|
158
|
+
if (isCall(node))
|
|
159
|
+
return scoreCall(state, node, nesting, parent);
|
|
160
|
+
if (isFunction(node))
|
|
161
|
+
return;
|
|
162
|
+
if (isPlainA(node))
|
|
163
|
+
return scoreList(state, plainChildrenA(node), nesting, node);
|
|
164
|
+
if (isPlainB(node))
|
|
165
|
+
return scoreList(state, plainChildrenB(node), nesting, node);
|
|
166
|
+
if (isPlainC(node))
|
|
167
|
+
return scoreList(state, plainChildrenC(node), nesting, node);
|
|
168
|
+
}
|
|
169
|
+
function scoreBranch(state, node, nesting) {
|
|
170
|
+
score(state, node.test, nesting, node);
|
|
171
|
+
score(state, node.consequent, nesting + 1, node);
|
|
172
|
+
const alternate = node.alternate;
|
|
173
|
+
if (alternate === null)
|
|
174
|
+
return;
|
|
175
|
+
state.total += 1;
|
|
176
|
+
if (alternate.type === "IfStatement")
|
|
177
|
+
return scoreBranch(state, alternate, nesting);
|
|
178
|
+
score(state, alternate, nesting + 1, node);
|
|
179
|
+
}
|
|
180
|
+
function scoreIf(state, node, nesting) {
|
|
181
|
+
state.total += 1 + nesting;
|
|
182
|
+
scoreBranch(state, node, nesting);
|
|
183
|
+
}
|
|
184
|
+
function scoreControl(state, node, nesting) {
|
|
185
|
+
switch (node.type) {
|
|
186
|
+
case "IfStatement": {
|
|
187
|
+
return scoreIf(state, node, nesting);
|
|
188
|
+
}
|
|
189
|
+
case "ConditionalExpression": {
|
|
190
|
+
state.total += 1 + nesting;
|
|
191
|
+
score(state, node.test, nesting, node);
|
|
192
|
+
score(state, node.consequent, nesting + 1, node);
|
|
193
|
+
score(state, node.alternate, nesting + 1, node);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
case "SwitchStatement": {
|
|
197
|
+
state.total += 1 + nesting;
|
|
198
|
+
score(state, node.discriminant, nesting, node);
|
|
199
|
+
scoreList(state, node.cases, nesting + 1, node);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
case "SwitchCase": {
|
|
203
|
+
if (node.test !== null)
|
|
204
|
+
score(state, node.test, nesting, node);
|
|
205
|
+
scoreList(state, node.consequent, nesting, node);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case "TryStatement": {
|
|
209
|
+
score(state, node.block, nesting, node);
|
|
210
|
+
if (node.handler !== null)
|
|
211
|
+
score(state, node.handler, nesting, node);
|
|
212
|
+
if (node.finalizer !== null)
|
|
213
|
+
score(state, node.finalizer, nesting, node);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
case "CatchClause": {
|
|
217
|
+
state.total += 1 + nesting;
|
|
218
|
+
if (node.param !== null)
|
|
219
|
+
score(state, node.param, nesting, node);
|
|
220
|
+
score(state, node.body, nesting + 1, node);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
default: {
|
|
224
|
+
return unreachable2(node);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function scoreLoop(state, node, nesting) {
|
|
229
|
+
switch (node.type) {
|
|
230
|
+
case "ForStatement": {
|
|
231
|
+
state.total += 1 + nesting;
|
|
232
|
+
if (node.init !== null)
|
|
233
|
+
score(state, node.init, nesting, node);
|
|
234
|
+
if (node.test !== null)
|
|
235
|
+
score(state, node.test, nesting, node);
|
|
236
|
+
if (node.update !== null)
|
|
237
|
+
score(state, node.update, nesting, node);
|
|
238
|
+
score(state, node.body, nesting + 1, node);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
case "ForInStatement":
|
|
242
|
+
case "ForOfStatement": {
|
|
243
|
+
state.total += 1 + nesting;
|
|
244
|
+
score(state, node.left, nesting, node);
|
|
245
|
+
score(state, node.right, nesting, node);
|
|
246
|
+
score(state, node.body, nesting + 1, node);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
case "WhileStatement": {
|
|
250
|
+
state.total += 1 + nesting;
|
|
251
|
+
score(state, node.test, nesting, node);
|
|
252
|
+
score(state, node.body, nesting + 1, node);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
case "DoWhileStatement": {
|
|
256
|
+
state.total += 1 + nesting;
|
|
257
|
+
score(state, node.body, nesting + 1, node);
|
|
258
|
+
score(state, node.test, nesting, node);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
case "LabeledStatement": {
|
|
262
|
+
score(state, node.body, nesting, node);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
default: {
|
|
266
|
+
return unreachable2(node);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function insideRun(parent) {
|
|
271
|
+
return parent !== null && parent.type === "LogicalExpression" && (parent.operator === "&&" || parent.operator === "||");
|
|
272
|
+
}
|
|
273
|
+
function countRuns(node, parentOperator) {
|
|
274
|
+
if (node.type !== "LogicalExpression")
|
|
275
|
+
return 0;
|
|
276
|
+
if (node.operator !== "&&" && node.operator !== "||")
|
|
277
|
+
return 0;
|
|
278
|
+
const own = node.operator === parentOperator ? 0 : 1;
|
|
279
|
+
return own + countRuns(node.left, node.operator) + countRuns(node.right, node.operator);
|
|
280
|
+
}
|
|
281
|
+
function isSelfCall(state, callee) {
|
|
282
|
+
if (callee.type === "Identifier")
|
|
283
|
+
return state.names.identifiers.includes(callee.name);
|
|
284
|
+
if (callee.type === "MemberExpression" && callee.object.type === "ThisExpression" && callee.property.type === "Identifier") {
|
|
285
|
+
return state.names.members.includes(callee.property.name);
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
function scoreCall(state, node, nesting, parent) {
|
|
290
|
+
switch (node.type) {
|
|
291
|
+
case "LogicalExpression": {
|
|
292
|
+
if (!insideRun(parent) && (node.operator === "&&" || node.operator === "||"))
|
|
293
|
+
state.total += countRuns(node, null);
|
|
294
|
+
score(state, node.left, nesting, node);
|
|
295
|
+
score(state, node.right, nesting, node);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
case "BreakStatement":
|
|
299
|
+
case "ContinueStatement": {
|
|
300
|
+
if (node.label !== null)
|
|
301
|
+
state.total += 1;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
case "CallExpression":
|
|
305
|
+
case "NewExpression": {
|
|
306
|
+
if (isSelfCall(state, node.callee))
|
|
307
|
+
state.recursive = true;
|
|
308
|
+
score(state, node.callee, nesting, node);
|
|
309
|
+
scoreList(state, node.arguments, nesting, node);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
case "ImportExpression": {
|
|
313
|
+
score(state, node.source, nesting, node);
|
|
314
|
+
if (node.options !== null)
|
|
315
|
+
score(state, node.options, nesting, node);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
default: {
|
|
319
|
+
return unreachable2(node);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function cognitiveComplexity(root, names) {
|
|
324
|
+
const state = { total: 0, recursive: false, names };
|
|
325
|
+
if (root.type === "StaticBlock") {
|
|
326
|
+
scoreList(state, root.body, 0, root);
|
|
327
|
+
return state.total;
|
|
328
|
+
}
|
|
329
|
+
scoreList(state, root.params, 0, root);
|
|
330
|
+
if (root.type === "ArrowFunctionExpression")
|
|
331
|
+
score(state, root.body, 0, root);
|
|
332
|
+
else if (root.body !== null)
|
|
333
|
+
score(state, root.body, 0, root);
|
|
334
|
+
return state.recursive ? state.total + 1 : state.total;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// effect-channel/cognitive-complexity.ts
|
|
338
|
+
var DEFAULT_MAX = 15;
|
|
339
|
+
function maxOf(options) {
|
|
340
|
+
const [first] = options;
|
|
341
|
+
if (typeof first === "object" && first !== null && "max" in first && typeof first.max === "number" && first.max > 0) {
|
|
342
|
+
return Math.floor(first.max);
|
|
343
|
+
}
|
|
344
|
+
return DEFAULT_MAX;
|
|
345
|
+
}
|
|
346
|
+
function keyName(holder) {
|
|
347
|
+
if (holder.computed)
|
|
348
|
+
return;
|
|
349
|
+
if (holder.key.type === "Identifier")
|
|
350
|
+
return holder.key.name;
|
|
351
|
+
if (holder.key.type === "Literal" && typeof holder.key.value === "string")
|
|
352
|
+
return holder.key.value;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
function assignedBinding(target) {
|
|
356
|
+
if (target.type === "Identifier")
|
|
357
|
+
return { kind: "identifiers", name: target.name };
|
|
358
|
+
if (target.type === "MemberExpression" && target.object.type === "ThisExpression" && target.property.type === "Identifier") {
|
|
359
|
+
return { kind: "members", name: target.property.name };
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
function memberBinding(holder) {
|
|
364
|
+
const name = keyName(holder);
|
|
365
|
+
return name === undefined ? undefined : { kind: "members", name };
|
|
366
|
+
}
|
|
367
|
+
function binding(node) {
|
|
368
|
+
const parent = node.parent;
|
|
369
|
+
if (parent.type === "VariableDeclarator" && parent.init === node && parent.id.type === "Identifier") {
|
|
370
|
+
return { kind: "identifiers", name: parent.id.name };
|
|
371
|
+
}
|
|
372
|
+
if ((parent.type === "Property" || parent.type === "MethodDefinition" || parent.type === "PropertyDefinition" || parent.type === "AccessorProperty") && parent.value === node) {
|
|
373
|
+
return memberBinding(parent);
|
|
374
|
+
}
|
|
375
|
+
if (parent.type === "AssignmentExpression" && parent.right === node)
|
|
376
|
+
return assignedBinding(parent.left);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
function displayName(node) {
|
|
380
|
+
if (node.type !== "ArrowFunctionExpression" && node.id !== null)
|
|
381
|
+
return node.id.name;
|
|
382
|
+
return binding(node)?.name ?? "anonymous";
|
|
383
|
+
}
|
|
384
|
+
function selfNames(node) {
|
|
385
|
+
const own = node.type === "ArrowFunctionExpression" || node.id === null ? [] : [node.id.name];
|
|
386
|
+
const bound = binding(node);
|
|
387
|
+
return {
|
|
388
|
+
identifiers: bound?.kind === "identifiers" ? [...own, bound.name] : own,
|
|
389
|
+
members: bound?.kind === "members" ? [bound.name] : []
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
var NO_NAMES = { identifiers: [], members: [] };
|
|
393
|
+
var rule = {
|
|
394
|
+
meta: {
|
|
395
|
+
type: "problem",
|
|
396
|
+
docs: { description: "Hold each function to a cognitive complexity of 15" },
|
|
397
|
+
schema: [{ type: "object", properties: { max: { type: "number" } }, additionalProperties: false }],
|
|
398
|
+
defaultOptions: [{ max: DEFAULT_MAX }]
|
|
399
|
+
},
|
|
400
|
+
create(context) {
|
|
401
|
+
const max = maxOf(context.options);
|
|
402
|
+
const check = (node) => {
|
|
403
|
+
const score2 = node.type === "StaticBlock" ? cognitiveComplexity(node, NO_NAMES) : cognitiveComplexity(node, selfNames(node));
|
|
404
|
+
if (score2 <= max)
|
|
405
|
+
return;
|
|
406
|
+
const name = node.type === "StaticBlock" ? "static block" : `function \`${displayName(node)}\``;
|
|
407
|
+
context.report({ node, message: `${name} has a cognitive complexity of ${score2}. Maximum allowed is ${max}.` });
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
FunctionDeclaration: check,
|
|
411
|
+
FunctionExpression: check,
|
|
412
|
+
ArrowFunctionExpression: check,
|
|
413
|
+
StaticBlock: check
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
var cognitive_complexity_default = rule;
|
|
418
|
+
|
|
1
419
|
// effect-channel/no-error-channel-escape.ts
|
|
2
420
|
var EFFECT_SOURCES = new Set(["effect", "effect/Effect"]);
|
|
3
421
|
var INSTEAD = {
|
|
@@ -19,7 +437,7 @@ var blindToTheError = (handler) => {
|
|
|
19
437
|
return false;
|
|
20
438
|
return handler.params.every((param) => param.type === "Identifier" && /^_+$/.test(param.name));
|
|
21
439
|
};
|
|
22
|
-
var
|
|
440
|
+
var rule2 = {
|
|
23
441
|
meta: {
|
|
24
442
|
type: "problem",
|
|
25
443
|
docs: { description: "Disallow the combinators that erase Effect's error channel" }
|
|
@@ -67,10 +485,10 @@ var rule = {
|
|
|
67
485
|
};
|
|
68
486
|
}
|
|
69
487
|
};
|
|
70
|
-
var no_error_channel_escape_default =
|
|
488
|
+
var no_error_channel_escape_default = rule2;
|
|
71
489
|
|
|
72
490
|
// effect-channel/no-throw.ts
|
|
73
|
-
var
|
|
491
|
+
var rule3 = {
|
|
74
492
|
meta: {
|
|
75
493
|
type: "problem",
|
|
76
494
|
docs: { description: "Disallow throw, which fails outside Effect's error channel" }
|
|
@@ -86,10 +504,10 @@ var rule2 = {
|
|
|
86
504
|
};
|
|
87
505
|
}
|
|
88
506
|
};
|
|
89
|
-
var no_throw_default =
|
|
507
|
+
var no_throw_default = rule3;
|
|
90
508
|
|
|
91
509
|
// effect-channel/no-try-catch.ts
|
|
92
|
-
var
|
|
510
|
+
var rule4 = {
|
|
93
511
|
meta: {
|
|
94
512
|
type: "problem",
|
|
95
513
|
docs: { description: "Disallow a try statement with a catch clause, which recovers outside Effect's error channel" }
|
|
@@ -105,7 +523,7 @@ var rule3 = {
|
|
|
105
523
|
};
|
|
106
524
|
}
|
|
107
525
|
};
|
|
108
|
-
var no_try_catch_default =
|
|
526
|
+
var no_try_catch_default = rule4;
|
|
109
527
|
|
|
110
528
|
// effect-channel/index.ts
|
|
111
529
|
var plugin = {
|
|
@@ -113,7 +531,8 @@ var plugin = {
|
|
|
113
531
|
rules: {
|
|
114
532
|
"no-error-channel-escape": no_error_channel_escape_default,
|
|
115
533
|
"no-throw": no_throw_default,
|
|
116
|
-
"no-try-catch": no_try_catch_default
|
|
534
|
+
"no-try-catch": no_try_catch_default,
|
|
535
|
+
"cognitive-complexity": cognitive_complexity_default
|
|
117
536
|
}
|
|
118
537
|
};
|
|
119
538
|
var effect_channel_default = plugin;
|
|
@@ -9,6 +9,7 @@ The shared dependency-cruiser base holds a repository's imports to a set of rule
|
|
|
9
9
|
- `no-circular`
|
|
10
10
|
- `no-orphans`
|
|
11
11
|
- `not-to-dev-dep`, which refuses shipped source importing a dev-only package, and a package listed in `peerDependencies` too is not dev-only
|
|
12
|
+
- `no-non-package-json`, which refuses an import of an installed package that the nearest `package.json` does not declare
|
|
12
13
|
- `not-to-unresolvable`, which refuses a specifier nothing installed answers
|
|
13
14
|
- `no-deep-imports`, which refuses a subpath the package's exports map does not publish
|
|
14
15
|
|
|
@@ -43,6 +43,7 @@ effect-tsgo keeps the severities `tsconfig.effect.json` sets when a later config
|
|
|
43
43
|
|
|
44
44
|
## Related topics
|
|
45
45
|
|
|
46
|
+
- [The TypeScript rules](typescript-rules.md)
|
|
46
47
|
- [checks-quality](../gates/checks-quality.md)
|
|
47
48
|
- [The quality file](quality-file.md)
|
|
48
49
|
- [Why it is shaped this way](../design.md)
|
|
@@ -19,7 +19,7 @@ The kit's bins find the file at the git root and read it there:
|
|
|
19
19
|
"production": ["src/**/*.ts"],
|
|
20
20
|
"effect": { "paths": ["src/**/*.ts"], "exempt": ["src/host/*.ts"] }
|
|
21
21
|
},
|
|
22
|
-
"size": { "
|
|
22
|
+
"size": { "applies": "ratchet", "tests": { "fileLines": 800 } },
|
|
23
23
|
"features": [
|
|
24
24
|
{
|
|
25
25
|
"name": "billing",
|
|
@@ -44,9 +44,9 @@ The kit's bins find the file at the git root and read it there:
|
|
|
44
44
|
| `gates.scheduled` | `checks-ci-wiring` | the commands a schedule runs |
|
|
45
45
|
| `gates.lint` | `checks-lint`, `checks-ci-wiring` | the gates `checks-lint` runs when not all apply, as [Gate selection](../gates/checks-lint.md#gate-selection) says |
|
|
46
46
|
| `commitIdentity.authors` | `checks-commit-identity` | the identities allowed to author and commit, as [checks-commit-identity](../gates/checks-commit-identity.md) says |
|
|
47
|
-
| `sources.production` | `checks-size-budget`, `checks-quality` | the source the repository ships, as [checks-size-budget](../gates/checks-size-budget.md)
|
|
47
|
+
| `sources.production` | `checks-size-budget`, `checks-repetition`, `checks-quality` | the source the repository ships, as [checks-size-budget](../gates/checks-size-budget.md) and [checks-repetition](../gates/checks-repetition.md) say |
|
|
48
48
|
| `sources.effect` | `checks-quality` | the paths held to the Effect rules, and the files under them that are not, as [The Effect rules](effect-rules.md) says |
|
|
49
|
-
| `size` | `checks-size-budget` | the
|
|
49
|
+
| `size` | `checks-size-budget` | the size budget of production and test files, and how a change is held to it, as [checks-size-budget](../gates/checks-size-budget.md) says |
|
|
50
50
|
| `features` | `featureRules`, `checks-feature-owners` | each feature's root, entries, exempt importers and proof, as [checks-feature-owners](../gates/checks-feature-owners.md) says |
|
|
51
51
|
| `changeSignal` | `checks-feature-owners` | `advisory` to list the feature owners a change touches |
|
|
52
52
|
| `agentRules.on` | agent Rule selection, not the kit | catalogued Rules switched on for this repository |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# The TypeScript rules
|
|
2
|
+
|
|
3
|
+
The oxlint base config holds a repository's TypeScript to a set of rules, and a reader looks it up to learn what each rule refuses and which rules need type information.
|
|
4
|
+
|
|
5
|
+
## Syntax rules
|
|
6
|
+
|
|
7
|
+
`oxlintrc.json` turns on the `correctness` and `suspicious` categories as errors, and these rules on top of them:
|
|
8
|
+
|
|
9
|
+
- `typescript/no-explicit-any` refuses an `any` type written out.
|
|
10
|
+
- `typescript/ban-ts-comment` refuses `@ts-ignore`, `@ts-expect-error` and `@ts-nocheck`.
|
|
11
|
+
- `typescript/no-inferrable-types` refuses a type annotation on a variable or a parameter default whose literal initializer already gives the type.
|
|
12
|
+
- `typescript/explicit-module-boundary-types` refuses an exported function without a return type, and an exported function parameter typed `any`.
|
|
13
|
+
- `typescript/no-non-null-assertion` refuses the non-null assertion `!`.
|
|
14
|
+
- `eslint/no-unused-vars` refuses a variable, a parameter or an import nothing reads, and passes over a variable or a parameter whose name starts with `_` and the siblings of a rest property.
|
|
15
|
+
|
|
16
|
+
The base turns off `typescript/consistent-return`, which its categories would otherwise turn on.
|
|
17
|
+
It turns on `effect-channel/no-error-channel-escape` as well, and [The Effect rules](effect-rules.md) says what that rule refuses.
|
|
18
|
+
|
|
19
|
+
## Type-aware rules
|
|
20
|
+
|
|
21
|
+
These rules read the types, so they run only under `oxlint --type-aware` with `oxlint-tsgolint` installed.
|
|
22
|
+
Without the flag, oxlint skips them and reports nothing about them.
|
|
23
|
+
|
|
24
|
+
- `typescript/switch-exhaustiveness-check` refuses a `switch` over a union that leaves a member without a case.
|
|
25
|
+
- `typescript/prefer-readonly` refuses a private member that nothing reassigns and that is not `readonly`.
|
|
26
|
+
- `typescript/no-unnecessary-condition` refuses a condition whose type makes its result always the same, such as `??` on a value that cannot be null, and passes over a constant loop condition.
|
|
27
|
+
- `typescript/no-unnecessary-type-parameters` refuses a type parameter the signature uses only once.
|
|
28
|
+
- `typescript/use-unknown-in-catch-callback-variable` refuses a rejection callback whose parameter is not typed `unknown`.
|
|
29
|
+
- `typescript/no-unsafe-type-assertion` refuses an `as` that narrows a value to a type the compiler cannot prove.
|
|
30
|
+
- `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.
|
|
31
|
+
|
|
32
|
+
## Related topics
|
|
33
|
+
|
|
34
|
+
- [The Effect rules](effect-rules.md)
|
|
35
|
+
- [The dependency rules](dependency-rules.md)
|
|
36
|
+
- [Why it is shaped this way](../design.md)
|
package/docs/design.md
CHANGED
|
@@ -13,7 +13,16 @@ Each entry below is a choice in the kit's shape and the constraint that forced i
|
|
|
13
13
|
Node refuses to type-strip a `.ts` plugin under `node_modules`, so the `.ts` source would fail to load from an installed package.
|
|
14
14
|
- `featureRules` ships compiled as `dist/feature-rules.js` for the same reason, with `effect` left out of the bundle so it resolves the consumer's own copy.
|
|
15
15
|
dependency-cruiser uses a config's export as it is and never awaits it, so the declaration decodes synchronously, and `quality.json` exempts that one file from the Effect rules.
|
|
16
|
-
- `checks-size-budget` writes the head commit's files to a temporary directory and runs oxlint there
|
|
16
|
+
- `checks-size-budget` writes the head commit's files to a temporary directory and runs oxlint there.
|
|
17
|
+
Its configuration loads the kit's own plugin bundle for the complexity rule and turns every category off.
|
|
18
|
+
The consumer's own `.oxlintrc.json`, its ignore files and its other rules never reach the count.
|
|
19
|
+
- `checks-size-budget` ratchets against the base of the range rather than a committed baseline such as `oxlint-suppressions.json`.
|
|
20
|
+
A suppression file stores a count of sites per file and rule, and `max-lines` reports a file once however long it grows, so the count stays at one while the file doubles.
|
|
21
|
+
The gate sums how far each site runs over its limit instead, which grows with the file.
|
|
22
|
+
The base commit already holds that sum, so nothing is generated, committed or pruned.
|
|
23
|
+
- `checks-repetition` writes the production files of both ends of the range to temporary directories and runs jscpd in each, so the consumer's `.jscpd.json` and ignore files never reach the count.
|
|
24
|
+
It compares each file's count of repeated lines rather than using jscpd's `--baseline-from-ref`.
|
|
25
|
+
That flag reports a repeated block as new once its text changes, so a change that shortens a grandfathered block would fail.
|
|
17
26
|
- `dist/` is committed.
|
|
18
27
|
No `prepack` or `prepublishOnly` builds it, so a publish ships whatever bundle the publishing worktree holds.
|
|
19
28
|
Rebuild it after pulling with `bun run build`.
|
|
@@ -18,6 +18,8 @@ It passes over a file with any other extension.
|
|
|
18
18
|
`scripts/comment-matchers.ts` holds the scanner, the comment syntaxes and a synchronous `refused()`, and imports nothing.
|
|
19
19
|
A host such as a hook bundle can therefore copy it alone into a directory with no `node_modules` and import it as `@avi2dg/checks/scripts/comment-matchers.ts`.
|
|
20
20
|
The kit's own dependency cruise fails when that file gains an import.
|
|
21
|
+
`REFUSED_DIRECTIVES` in that file owns the refused directive names, and the checker builds its directive pattern from that list, so the two cannot disagree.
|
|
22
|
+
A consumer reads the same contract by importing `REFUSED_DIRECTIVES` from `@avi2dg/checks/scripts/comment-matchers.ts`.
|
|
21
23
|
`scripts/comments.ts` wraps the same matchers in Effect for the gate and for [checks-backtest](checks-backtest.md).
|
|
22
24
|
|
|
23
25
|
## Arguments
|
|
@@ -64,7 +64,7 @@ It prints the range, the declared selection if there is one, each gate's own rep
|
|
|
64
64
|
```
|
|
65
65
|
checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
66
66
|
...
|
|
67
|
-
checks-lint: 3 of
|
|
67
|
+
checks-lint: 3 of 11 gate(s) failed: checks-commit-identity, checks-comment-gate, checks-suppressions-ratchet
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
## Opting out
|
|
@@ -74,7 +74,7 @@ A repository that runs its gates without `checks-lint` calls each gate's bin in
|
|
|
74
74
|
|
|
75
75
|
## Gate selection
|
|
76
76
|
|
|
77
|
-
A repository with no TypeScript source gives `checks-lint-coverage`, `checks-test-layout`, `checks-size-budget` and `checks-feature-owners` nothing to check, and test-layout would still refuse its missing `bun test` script and `bunfig.toml`.
|
|
77
|
+
A repository with no TypeScript source gives `checks-lint-coverage`, `checks-test-layout`, `checks-size-budget`, `checks-repetition` and `checks-feature-owners` nothing to check, and test-layout would still refuse its missing `bun test` script and `bunfig.toml`.
|
|
78
78
|
It declares the gates `checks-lint` runs as `gates.lint`:
|
|
79
79
|
|
|
80
80
|
```json
|
|
@@ -100,10 +100,11 @@ Both `checks-lint` and `checks-ci-wiring` exit 2 on a `gates.lint` that names an
|
|
|
100
100
|
`checks-ci-wiring` exits 1 when the selection leaves out a gate the repository's tracked files make applicable, and names the gate and the files:
|
|
101
101
|
|
|
102
102
|
```
|
|
103
|
-
ci-wiring: quality.json gates.lint leaves out
|
|
103
|
+
ci-wiring: quality.json gates.lint leaves out 5 gate(s) this repository's contents make applicable:
|
|
104
104
|
checks-lint-coverage: the repository tracks TypeScript source (src/widget.ts)
|
|
105
105
|
checks-test-layout: the repository tracks TypeScript source (src/widget.ts)
|
|
106
106
|
checks-size-budget: the repository tracks TypeScript source (src/widget.ts)
|
|
107
|
+
checks-repetition: the repository tracks TypeScript source (src/widget.ts)
|
|
107
108
|
checks-feature-owners: the repository tracks TypeScript source (src/widget.ts)
|
|
108
109
|
```
|
|
109
110
|
|
|
@@ -34,7 +34,7 @@ A rule only this repository needs stays in its own `.oxlintrc.json`, whose overr
|
|
|
34
34
|
- A fragment is missing, or differs from what `generate` would write from `quality.json` and the installed kit's presets.
|
|
35
35
|
- A fragment is left over once `quality.json` stops declaring `sources.effect`.
|
|
36
36
|
- `.oxlintrc.json` or `tsconfig.json` does not list its fragment in `extends`, so the tool never reads it.
|
|
37
|
-
- A `sources.effect.paths`
|
|
37
|
+
- A `sources.effect.paths` or `sources.production` glob matches no tracked or untracked file, so it holds nothing.
|
|
38
38
|
|
|
39
39
|
Two details of the fragments are easy to get wrong, so the kit's tests pin both:
|
|
40
40
|
|