@avi2dg/checks 0.16.0 → 0.18.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 +22 -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/commit-messages.md +2 -18
- package/docs/configs/dependency-rules.md +1 -0
- package/docs/configs/effect-rules.md +1 -0
- package/docs/configs/quality-file.md +2 -2
- 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-quality.md +23 -8
- package/docs/gates/checks-size-budget.md +4 -4
- package/oxlintrc.json +1 -0
- package/package.json +3 -1
- package/quality.schema.json +2 -2
- package/scripts/backtest.ts +57 -41
- package/scripts/ci-wiring.ts +2 -63
- package/scripts/comment-gate.ts +3 -4
- package/scripts/comment-matchers.ts +73 -58
- package/scripts/commit-identity.ts +3 -4
- package/scripts/doc-outline.ts +34 -16
- package/scripts/doc-rules.ts +34 -21
- package/scripts/docs.ts +3 -4
- package/scripts/feature-owners.ts +5 -8
- package/scripts/git.ts +31 -26
- package/scripts/prose-matchers.ts +93 -83
- package/scripts/quality.ts +157 -10
- package/scripts/range-gate.ts +9 -0
- package/scripts/shell-command.ts +74 -0
- package/scripts/size-budget.ts +18 -17
- package/scripts/size-rules.ts +13 -4
- package/scripts/suppressions-ratchet.ts +22 -24
- package/scripts/test-layout.ts +37 -35
- package/scripts/test-report.ts +18 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.18.0
|
|
6
|
+
|
|
7
|
+
Released 2026-09-26.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **scripts:** generate commitlint and CI workflows with checks-quality (#54)
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- **scripts:** make checks-quality refuse a config that drops the kit's extends (#55)
|
|
16
|
+
|
|
17
|
+
## 0.17.0
|
|
18
|
+
|
|
19
|
+
Released 2026-09-25.
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
- **effect-channel:** add a cognitive complexity rule and make it the size budget's (#50)
|
|
24
|
+
- **scripts:** export the refused directive names from comment-matchers (#49)
|
|
25
|
+
- refuse undeclared package imports and deprecated symbol use (#48)
|
|
26
|
+
|
|
5
27
|
## 0.16.0
|
|
6
28
|
|
|
7
29
|
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;
|
|
@@ -10,24 +10,8 @@ It arrives with the kit, since `@commitlint/cli` and `@commitlint/config-convent
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
12
12
|
The lint runs in CI on pull requests, because `jj` never fires a git hook.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```yaml
|
|
16
|
-
on:
|
|
17
|
-
pull_request:
|
|
18
|
-
types: [opened, edited, synchronize, reopened]
|
|
19
|
-
jobs:
|
|
20
|
-
commitlint:
|
|
21
|
-
runs-on: ubuntu-latest
|
|
22
|
-
steps:
|
|
23
|
-
- uses: actions/checkout@v5
|
|
24
|
-
- uses: oven-sh/setup-bun@v2
|
|
25
|
-
- run: bun install --frozen-lockfile
|
|
26
|
-
- run: printf '%s' "$PR_TITLE (#0000)" > "$RUNNER_TEMP/pr-title"
|
|
27
|
-
env:
|
|
28
|
-
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
29
|
-
- run: ./node_modules/.bin/commitlint --config ./node_modules/@avi2dg/checks/commitlint.config.js --edit "$RUNNER_TEMP/pr-title"
|
|
30
|
-
```
|
|
13
|
+
`checks-quality generate` writes the workflow whole into `.github/workflows/commitlint.yml`, as [checks-quality](../gates/checks-quality.md) says.
|
|
14
|
+
The workflow lints with the installed kit's `commitlint.config.js`, so every repository holds titles to the same rules.
|
|
31
15
|
|
|
32
16
|
## What it lints
|
|
33
17
|
|
|
@@ -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)
|
|
@@ -39,8 +39,8 @@ The kit's bins find the file at the git root and read it there:
|
|
|
39
39
|
|
|
40
40
|
| Key | Read by | Holds |
|
|
41
41
|
| --- | --- | --- |
|
|
42
|
-
| `defaultBranch` | `checks-lint`, `checks-ci-wiring` | the branch pull requests merge into, `main` when absent |
|
|
43
|
-
| `gates.ci` | `checks-ci-wiring` | the commands CI runs on every pull request, as [checks-ci-wiring](../gates/checks-ci-wiring.md) says |
|
|
42
|
+
| `defaultBranch` | `checks-lint`, `checks-ci-wiring`, `checks-quality` | the branch pull requests merge into, `main` when absent |
|
|
43
|
+
| `gates.ci` | `checks-ci-wiring`, `checks-quality` | the commands CI runs on every pull request, as [checks-ci-wiring](../gates/checks-ci-wiring.md) says |
|
|
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 |
|
|
@@ -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.
|