@avi2dg/checks 0.16.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 +10 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +2 -1
- package/dependency-cruiser.config.js +8 -0
- package/dist/feature-rules.js +5 -4
- 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/typescript-rules.md +36 -0
- package/docs/design.md +3 -1
- package/docs/gates/checks-comment-gate.md +2 -0
- package/docs/gates/checks-size-budget.md +4 -4
- package/oxlintrc.json +1 -0
- package/package.json +1 -1
- package/quality.schema.json +2 -2
- package/scripts/comment-matchers.ts +19 -1
- package/scripts/size-budget.ts +13 -9
- package/scripts/size-rules.ts +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.17.0
|
|
6
|
+
|
|
7
|
+
Released 2026-09-25.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **effect-channel:** add a cognitive complexity rule and make it the size budget's (#50)
|
|
12
|
+
- **scripts:** export the refused directive names from comment-matchers (#49)
|
|
13
|
+
- refuse undeclared package imports and deprecated symbol use (#48)
|
|
14
|
+
|
|
5
15
|
## 0.16.0
|
|
6
16
|
|
|
7
17
|
Released 2026-09-25.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -65,7 +65,7 @@ To place a change:
|
|
|
65
65
|
| Path | What it holds |
|
|
66
66
|
| --- | --- |
|
|
67
67
|
| `scripts/` | every bin, and the modules they share |
|
|
68
|
-
| `effect-channel/` | the Effect error-channel
|
|
68
|
+
| `effect-channel/` | the oxlint plugin with the Effect error-channel and cognitive complexity rules |
|
|
69
69
|
| `dist/` | the committed bundles of the plugin and of `featureRules` |
|
|
70
70
|
| `presets/` | the Effect presets `checks-quality` builds its fragments from |
|
|
71
71
|
| `templates/` | one template per kind of doc file, which `bun run build` renders |
|
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
|
170
170
|
| `oxlintrc.json` | the oxlint base config `.oxlintrc.json` extends |
|
|
171
171
|
| `stryker.preset.js` | the Stryker mutation-testing preset |
|
|
172
172
|
| `tsconfig.effect.json` | the tsconfig fragment with the Effect language-service block |
|
|
173
|
-
| `dist/` | the compiled Effect error-channel
|
|
173
|
+
| `dist/` | the compiled oxlint plugin with the Effect error-channel and cognitive complexity rules, and `featureRules` |
|
|
174
174
|
|
|
175
175
|
<!-- end generated shipped -->
|
|
176
176
|
|
|
@@ -178,6 +178,7 @@ Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
|
178
178
|
|
|
179
179
|
- [The quality file](docs/configs/quality-file.md)
|
|
180
180
|
- [The Effect rules](docs/configs/effect-rules.md)
|
|
181
|
+
- [The TypeScript rules](docs/configs/typescript-rules.md)
|
|
181
182
|
- [The dependency rules](docs/configs/dependency-rules.md)
|
|
182
183
|
- [The commit message lint](docs/configs/commit-messages.md)
|
|
183
184
|
- [Why it is shaped this way](docs/design.md)
|
|
@@ -41,6 +41,14 @@ export default {
|
|
|
41
41
|
dependencyTypesNot: ["type-only", "npm-peer"],
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
name: "no-non-package-json",
|
|
46
|
+
severity: "error",
|
|
47
|
+
comment:
|
|
48
|
+
"The import resolves to an installed package the nearest package.json does not declare, so it holds only while something else keeps it hoisted. Declare it in dependencies, devDependencies or peerDependencies.",
|
|
49
|
+
from: {},
|
|
50
|
+
to: { dependencyTypes: ["npm-no-pkg", "npm-unknown"] },
|
|
51
|
+
},
|
|
44
52
|
{
|
|
45
53
|
name: "not-to-unresolvable",
|
|
46
54
|
severity: "error",
|
package/dist/feature-rules.js
CHANGED
|
@@ -59,10 +59,11 @@ var STATEMENTS = {
|
|
|
59
59
|
};
|
|
60
60
|
var COMPLEXITY = {
|
|
61
61
|
key: "complexity",
|
|
62
|
-
rule: "complexity",
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
rule: "cognitive-complexity",
|
|
63
|
+
plugin: "effect-channel",
|
|
64
|
+
options: {},
|
|
65
|
+
measured: /has a cognitive complexity of (\d+)/,
|
|
66
|
+
limits: "The highest cognitive complexity a function may reach, a switch counted once"
|
|
66
67
|
};
|
|
67
68
|
var DEPTH = {
|
|
68
69
|
key: "depth",
|
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)
|
|
@@ -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,9 @@ 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.
|
|
17
19
|
- `checks-size-budget` ratchets against the base of the range rather than a committed baseline such as `oxlint-suppressions.json`.
|
|
18
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.
|
|
19
21
|
The gate sums how far each site runs over its limit instead, which grows with the file.
|
|
@@ -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
|
|
@@ -13,7 +13,7 @@ It holds production and test files to the size budget, and lists every other fil
|
|
|
13
13
|
|
|
14
14
|
A production file is one under `sources.production`, and a test file is a tracked `.ts` or `.tsx` file under `tests/`.
|
|
15
15
|
A test file keeps to the tests budget, and every other file keeps to the production budget.
|
|
16
|
-
It runs oxlint with a configuration of five rules and
|
|
16
|
+
It runs oxlint with a configuration of five rules, one for each limit, and the kit's own plugin bundle, which holds the complexity rule.
|
|
17
17
|
The kit sets each limit:
|
|
18
18
|
|
|
19
19
|
<!-- generated size-limits: bun run build writes it from SIZE_RULES and SIZE_DEFAULTS in scripts/size-rules.ts and scripts/doc-blocks.ts -->
|
|
@@ -23,7 +23,7 @@ The kit sets each limit:
|
|
|
23
23
|
| `fileLines` | The most lines a file may hold, blank and comment lines counted | `max-lines` | 400 | 600 |
|
|
24
24
|
| `functionLines` | The most lines a function may span, blank and comment lines counted | `max-lines-per-function` | 100 | none |
|
|
25
25
|
| `statements` | The most statements a function may hold | `max-statements` | 30 | 50 |
|
|
26
|
-
| `complexity` | The highest
|
|
26
|
+
| `complexity` | The highest cognitive complexity a function may reach, a switch counted once | `effect-channel/cognitive-complexity` | 15 | 15 |
|
|
27
27
|
| `depth` | The deepest a block may nest inside a function | `max-depth` | 4 | 4 |
|
|
28
28
|
|
|
29
29
|
<!-- end generated size-limits -->
|
|
@@ -88,8 +88,8 @@ With one it is that commit against its parent, or against the empty tree for a r
|
|
|
88
88
|
size-budget: 2 overrun(s) grew past the base in the production and test files the range adds or changes:
|
|
89
89
|
src/billing/invoice.ts: max-lines over by 31 in total, up from 19
|
|
90
90
|
src/billing/invoice.ts: File has too many lines (431). Maximum allowed is 400.
|
|
91
|
-
src/billing/ledger.ts: complexity over by 3 in total, up from 0
|
|
92
|
-
src/billing/ledger.ts:12: function `settle` has a complexity of 18. Maximum allowed is 15.
|
|
91
|
+
src/billing/ledger.ts: cognitive-complexity over by 3 in total, up from 0
|
|
92
|
+
src/billing/ledger.ts:12: function `settle` has a cognitive complexity of 18. Maximum allowed is 15.
|
|
93
93
|
size-budget: advisory, 1 overrun(s) where the budget does not hold yet:
|
|
94
94
|
tests/e2e/billing.test.ts: File has too many lines (612). Maximum allowed is 600.
|
|
95
95
|
```
|
package/oxlintrc.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "ignoreRestSiblings": true }],
|
|
17
17
|
"typescript/no-unsafe-type-assertion": "error",
|
|
18
18
|
"typescript/no-non-null-assertion": "error",
|
|
19
|
+
"typescript/no-deprecated": "error",
|
|
19
20
|
"typescript/consistent-return": "off"
|
|
20
21
|
}
|
|
21
22
|
}
|
package/package.json
CHANGED
package/quality.schema.json
CHANGED
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
"complexity": {
|
|
179
179
|
"type": "integer",
|
|
180
180
|
"exclusiveMinimum": 0,
|
|
181
|
-
"description": "The highest
|
|
181
|
+
"description": "The highest cognitive complexity a function may reach, a switch counted once; 15 when absent"
|
|
182
182
|
},
|
|
183
183
|
"depth": {
|
|
184
184
|
"type": "integer",
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
"complexity": {
|
|
206
206
|
"type": "integer",
|
|
207
207
|
"exclusiveMinimum": 0,
|
|
208
|
-
"description": "The highest
|
|
208
|
+
"description": "The highest cognitive complexity a function may reach, a switch counted once; 15 when absent"
|
|
209
209
|
},
|
|
210
210
|
"depth": {
|
|
211
211
|
"type": "integer",
|
|
@@ -244,6 +244,24 @@ function openingBlock(found: readonly Comment[], source: string): Comment[] {
|
|
|
244
244
|
return block;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
export const REFUSED_DIRECTIVES = [
|
|
248
|
+
"@ts-expect-error",
|
|
249
|
+
"@ts-ignore",
|
|
250
|
+
"biome-ignore",
|
|
251
|
+
"eslint-disable",
|
|
252
|
+
"prettier-ignore",
|
|
253
|
+
] as const satisfies readonly string[];
|
|
254
|
+
|
|
255
|
+
const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
256
|
+
|
|
257
|
+
const directiveSource = (name: string): string => {
|
|
258
|
+
const escaped = escapeRegExp(name);
|
|
259
|
+
if (name === "eslint-disable") return `\\b${escaped}[\\w-]*`;
|
|
260
|
+
return /^[\w]/.test(name) ? `\\b${escaped}\\b` : escaped;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const DIRECTIVE = new RegExp(REFUSED_DIRECTIVES.map(directiveSource).join("|"));
|
|
264
|
+
|
|
247
265
|
type Check = {
|
|
248
266
|
readonly find: RegExp;
|
|
249
267
|
readonly refusal: (match: string) => string;
|
|
@@ -251,7 +269,7 @@ type Check = {
|
|
|
251
269
|
|
|
252
270
|
const CHECKS: readonly Check[] = [
|
|
253
271
|
{
|
|
254
|
-
find:
|
|
272
|
+
find: DIRECTIVE,
|
|
255
273
|
refusal: (match) =>
|
|
256
274
|
`carries the machine-read directive \`${match}\`. Fix what the tool is reporting, or stop running the tool on this file`,
|
|
257
275
|
},
|
package/scripts/size-budget.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { readQuality, renderJson } from "./quality-file.ts";
|
|
|
6
6
|
import {
|
|
7
7
|
budgetOf,
|
|
8
8
|
budgetsOf,
|
|
9
|
+
diagnosticCode,
|
|
10
|
+
qualifiedName,
|
|
9
11
|
SIZE_DEFAULTS,
|
|
10
12
|
SIZE_RULES,
|
|
11
13
|
TESTS_DIRECTORY,
|
|
@@ -68,16 +70,17 @@ const decodeReport = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Str
|
|
|
68
70
|
|
|
69
71
|
function rulesOf(budget: Budget): Record<string, unknown> {
|
|
70
72
|
return Object.fromEntries(
|
|
71
|
-
SIZE_RULES.map((
|
|
72
|
-
const max = budget[key];
|
|
73
|
-
return [
|
|
73
|
+
SIZE_RULES.map((entry) => {
|
|
74
|
+
const max = budget[entry.key];
|
|
75
|
+
return [qualifiedName(entry), max === undefined ? "off" : ["error", { max, ...entry.options }]];
|
|
74
76
|
}),
|
|
75
77
|
);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
function sizeConfig({ production, tests }: Budgets): unknown {
|
|
80
|
+
function sizeConfig({ production, tests }: Budgets, plugin: string): unknown {
|
|
79
81
|
return {
|
|
80
82
|
plugins: [],
|
|
83
|
+
jsPlugins: [plugin],
|
|
81
84
|
categories: { correctness: "off" },
|
|
82
85
|
rules: rulesOf(production),
|
|
83
86
|
overrides: [{ files: [`${TESTS_DIRECTORY}/**`], rules: rulesOf(tests) }],
|
|
@@ -116,7 +119,7 @@ const materializeBase = Effect.fn("materializeBase")(function* (root: string, ba
|
|
|
116
119
|
|
|
117
120
|
const siteOf = (budgets: Budgets) =>
|
|
118
121
|
Effect.fn("siteOf")(function* ({ code, message, filename, labels }: typeof Diagnostic.Type) {
|
|
119
|
-
const rule = SIZE_RULES.find((candidate) => code ===
|
|
122
|
+
const rule = SIZE_RULES.find((candidate) => code === diagnosticCode(candidate));
|
|
120
123
|
if (rule === undefined) return [];
|
|
121
124
|
const measured = rule.measured.exec(message)?.[1];
|
|
122
125
|
const max = budgetOf(budgets, filename)[rule.key];
|
|
@@ -135,11 +138,11 @@ const siteOf = (budgets: Budgets) =>
|
|
|
135
138
|
];
|
|
136
139
|
});
|
|
137
140
|
|
|
138
|
-
const measure = Effect.fn("measure")(function* (tree: string, budgets: Budgets) {
|
|
141
|
+
const measure = Effect.fn("measure")(function* (tree: string, budgets: Budgets, plugin: string) {
|
|
139
142
|
const fs = yield* FileSystem.FileSystem;
|
|
140
143
|
if (!(yield* fs.exists(tree))) return [];
|
|
141
144
|
// oxlint reads an override's glob from the directory of the config that holds it, so the config sits in the tree.
|
|
142
|
-
yield* fs.writeFileString((yield* Path.Path).join(tree, CONFIG), renderJson(sizeConfig(budgets)));
|
|
145
|
+
yield* fs.writeFileString((yield* Path.Path).join(tree, CONFIG), renderJson(sizeConfig(budgets, plugin)));
|
|
143
146
|
|
|
144
147
|
const { stdout, stderr, exitCode } = yield* collect("oxlint", ["-c", CONFIG, "-f", "json", "."], tree).pipe(
|
|
145
148
|
Effect.mapError((cause) => new OxlintUnreadable({ message: `cannot run oxlint: ${cause.message}` })),
|
|
@@ -190,7 +193,8 @@ const runBudget = Effect.fn("runBudget")(
|
|
|
190
193
|
|
|
191
194
|
const headTree = path.join(scratch, "head");
|
|
192
195
|
if (held.length + others.length > 0) yield* materializeHead(root, head, [...holds, ...others], headTree);
|
|
193
|
-
const
|
|
196
|
+
const plugin = path.join(import.meta.dir, "..", "dist", "index.js");
|
|
197
|
+
const sites = yield* measure(headTree, budgets, plugin);
|
|
194
198
|
const heldSites = sites.filter((site) => holds.has(site.file));
|
|
195
199
|
if (applies !== "ratchet") {
|
|
196
200
|
return { applies, held: held.length, overruns: heldSites, advisory: sites.filter((site) => !holds.has(site.file)) } satisfies Verdict;
|
|
@@ -198,7 +202,7 @@ const runBudget = Effect.fn("runBudget")(
|
|
|
198
202
|
|
|
199
203
|
const baseTree = path.join(scratch, "base");
|
|
200
204
|
if (held.length > 0) yield* materializeBase(root, base, held, baseTree);
|
|
201
|
-
const growths = growthsOf(heldSites, yield* measure(baseTree, budgets));
|
|
205
|
+
const growths = growthsOf(heldSites, yield* measure(baseTree, budgets, plugin));
|
|
202
206
|
const failing = new Set(growths.flatMap((growth) => growth.sites));
|
|
203
207
|
return { applies, held: held.length, growths, advisory: sites.filter((site) => !failing.has(site)) } satisfies Verdict;
|
|
204
208
|
},
|
package/scripts/size-rules.ts
CHANGED
|
@@ -30,10 +30,11 @@ const STATEMENTS = {
|
|
|
30
30
|
|
|
31
31
|
const COMPLEXITY = {
|
|
32
32
|
key: "complexity",
|
|
33
|
-
rule: "complexity",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
rule: "cognitive-complexity",
|
|
34
|
+
plugin: "effect-channel",
|
|
35
|
+
options: {},
|
|
36
|
+
measured: /has a cognitive complexity of (\d+)/,
|
|
37
|
+
limits: "The highest cognitive complexity a function may reach, a switch counted once",
|
|
37
38
|
} as const;
|
|
38
39
|
|
|
39
40
|
const DEPTH = {
|
|
@@ -49,6 +50,14 @@ export const SIZE_RULES = [FILE_LINES, FUNCTION_LINES, STATEMENTS, COMPLEXITY, D
|
|
|
49
50
|
export type SizeRule = (typeof SIZE_RULES)[number];
|
|
50
51
|
export type LimitKey = SizeRule["key"];
|
|
51
52
|
|
|
53
|
+
export function qualifiedName(rule: SizeRule): string {
|
|
54
|
+
return "plugin" in rule ? `${rule.plugin}/${rule.rule}` : rule.rule;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function diagnosticCode(rule: SizeRule): string {
|
|
58
|
+
return "plugin" in rule ? `${rule.plugin}(${rule.rule})` : `eslint(${rule.rule})`;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
// An absent limit turns its rule off.
|
|
53
62
|
export type Budget = { readonly [K in LimitKey]?: number };
|
|
54
63
|
|