@avi2dg/checks 0.22.0 → 0.23.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 +8 -0
- package/CONTRIBUTING.md +6 -5
- package/README.md +1 -1
- package/dist/effect-channel/index.js +122 -0
- package/dist/{index.js → readability/index.js} +8 -120
- package/docs/configs/native-settings.md +2 -2
- package/docs/design.md +2 -2
- package/docs/gates/checks-repetition.md +1 -0
- package/oxlintrc.json +1 -1
- package/package.json +6 -4
- package/scripts/repetition.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.23.0
|
|
6
|
+
|
|
7
|
+
Released 2026-09-26.
|
|
8
|
+
|
|
9
|
+
### Breaking changes
|
|
10
|
+
|
|
11
|
+
- move cognitive-complexity into its own readability oxlint plugin [#74](https://github.com/avi2d/checks/pull/74)
|
|
12
|
+
|
|
5
13
|
## 0.22.0
|
|
6
14
|
|
|
7
15
|
Released 2026-09-26.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -26,8 +26,8 @@ Each generated file is committed, and lint, the suite or CI's diff after the bui
|
|
|
26
26
|
|
|
27
27
|
To regenerate after an edit:
|
|
28
28
|
|
|
29
|
-
1. After editing `effect-channel/` or the templates in `scripts/doc-templates.ts`, run `bun run build`.
|
|
30
|
-
It rewrites `dist
|
|
29
|
+
1. After editing `effect-channel/`, `readability/` or the templates in `scripts/doc-templates.ts`, run `bun run build`.
|
|
30
|
+
It rewrites `dist/` and `templates/`.
|
|
31
31
|
1. After editing anything a generated block names as its source in its opening marker, run `bun run build`, which rewrites every generated block.
|
|
32
32
|
1. Commit what the command rewrote in the same commit as the edit.
|
|
33
33
|
|
|
@@ -68,8 +68,9 @@ To place a change:
|
|
|
68
68
|
| Path | What it holds |
|
|
69
69
|
| --- | --- |
|
|
70
70
|
| `scripts/` | every bin, and the modules they share |
|
|
71
|
-
| `effect-channel/` | the oxlint plugin with the Effect error-channel
|
|
72
|
-
| `
|
|
71
|
+
| `effect-channel/` | the oxlint plugin with the Effect error-channel rules |
|
|
72
|
+
| `readability/` | the oxlint plugin with the readability rules |
|
|
73
|
+
| `dist/` | the committed oxlint plugin bundles |
|
|
73
74
|
| `presets/` | the Effect rule blocks consumers copy into native configs |
|
|
74
75
|
| `templates/` | one template per kind of doc file, which `bun run build` renders |
|
|
75
76
|
| `CHANGELOG.md` | every release, which `bun run build` writes from the conventional commits |
|
|
@@ -83,7 +84,7 @@ To place a change:
|
|
|
83
84
|
A new bin gets its page under `docs/gates/`, and the suite fails until it has one.
|
|
84
85
|
|
|
85
86
|
This repository holds itself to the kit, with two exceptions of its own.
|
|
86
|
-
Its `.dependency-cruiser.cjs` redeclares `no-orphans` with
|
|
87
|
+
Its `.dependency-cruiser.cjs` redeclares `no-orphans` with each plugin entry added to its `pathNot`.
|
|
87
88
|
Its `.oxlintrc.json` lifts `effect-channel/no-throw` from `scripts/comment-matchers.ts`, whose synchronous `refused()` a host loads without `node_modules`.
|
|
88
89
|
|
|
89
90
|
## Related topics
|
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
|
164
164
|
| `oxlintrc.json` | the oxlint base config `.oxlintrc.json` extends |
|
|
165
165
|
| `stryker.preset.js` | the Stryker mutation-testing preset |
|
|
166
166
|
| `tsconfig.effect.json` | the tsconfig fragment with the Effect language-service block |
|
|
167
|
-
| `dist/` | the compiled oxlint
|
|
167
|
+
| `dist/` | the compiled oxlint plugins, one per purpose |
|
|
168
168
|
|
|
169
169
|
<!-- end generated shipped -->
|
|
170
170
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// effect-channel/no-error-channel-escape.ts
|
|
2
|
+
var EFFECT_SOURCES = new Set(["effect", "effect/Effect"]);
|
|
3
|
+
var INSTEAD = {
|
|
4
|
+
drops: "it drops the failure and the success together, so no caller can tell one from the other. Handle the error by tag, or keep it as a value with Effect.result or Effect.exit",
|
|
5
|
+
swallows: "a Cause is the typed error plus defects plus interruption, so this swallows the bugs and the cancellations along with it. Catch the errors you named, with Effect.catchTag or Effect.catchTags",
|
|
6
|
+
blind: "the handler cannot see what it is recovering from, so every error in the channel collapses into one fallback. Name the errors with Effect.catchTag, or take the error and use it"
|
|
7
|
+
};
|
|
8
|
+
var REFUSED = new Map([
|
|
9
|
+
["ignore", INSTEAD.drops],
|
|
10
|
+
["ignoreCause", INSTEAD.drops],
|
|
11
|
+
["catchCause", INSTEAD.swallows],
|
|
12
|
+
["catchCauseIf", INSTEAD.swallows],
|
|
13
|
+
["catchCauseFilter", INSTEAD.swallows]
|
|
14
|
+
]);
|
|
15
|
+
var blindToTheError = (handler) => {
|
|
16
|
+
if (!handler)
|
|
17
|
+
return false;
|
|
18
|
+
if (handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression")
|
|
19
|
+
return false;
|
|
20
|
+
return handler.params.every((param) => param.type === "Identifier" && /^_+$/.test(param.name));
|
|
21
|
+
};
|
|
22
|
+
var rule = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: { description: "Disallow the combinators that erase Effect's error channel" }
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
const effect = new Set;
|
|
29
|
+
const named = (node) => {
|
|
30
|
+
if (!node || node.type !== "MemberExpression" || node.computed)
|
|
31
|
+
return null;
|
|
32
|
+
if (node.object.type !== "Identifier" || !effect.has(node.object.name))
|
|
33
|
+
return null;
|
|
34
|
+
return node.property.type === "Identifier" ? node.property.name : null;
|
|
35
|
+
};
|
|
36
|
+
const refuse = (node, combinator, instead) => {
|
|
37
|
+
context.report({ node, message: `Effect.${combinator} erases the error channel: ${instead}` });
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
ImportDeclaration(node) {
|
|
41
|
+
if (!EFFECT_SOURCES.has(node.source.value))
|
|
42
|
+
return;
|
|
43
|
+
const module = node.source.value === "effect/Effect";
|
|
44
|
+
for (const specifier of node.specifiers) {
|
|
45
|
+
if (specifier.type === "ImportSpecifier") {
|
|
46
|
+
if (specifier.imported.type === "Identifier" && specifier.imported.name === "Effect")
|
|
47
|
+
effect.add(specifier.local.name);
|
|
48
|
+
} else if (module)
|
|
49
|
+
effect.add(specifier.local.name);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
MemberExpression(node) {
|
|
53
|
+
const combinator = named(node);
|
|
54
|
+
if (combinator === null)
|
|
55
|
+
return;
|
|
56
|
+
const instead = REFUSED.get(combinator);
|
|
57
|
+
if (instead !== undefined)
|
|
58
|
+
refuse(node, combinator, instead);
|
|
59
|
+
},
|
|
60
|
+
CallExpression(node) {
|
|
61
|
+
if (named(node.callee) !== "catch")
|
|
62
|
+
return;
|
|
63
|
+
if (!blindToTheError(node.arguments[node.arguments.length - 1]))
|
|
64
|
+
return;
|
|
65
|
+
refuse(node, "catch", INSTEAD.blind);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var no_error_channel_escape_default = rule;
|
|
71
|
+
|
|
72
|
+
// effect-channel/no-throw.ts
|
|
73
|
+
var rule2 = {
|
|
74
|
+
meta: {
|
|
75
|
+
type: "problem",
|
|
76
|
+
docs: { description: "Disallow throw, which fails outside Effect's error channel" }
|
|
77
|
+
},
|
|
78
|
+
create(context) {
|
|
79
|
+
return {
|
|
80
|
+
ThrowStatement(node) {
|
|
81
|
+
context.report({
|
|
82
|
+
node,
|
|
83
|
+
message: "throw escapes the error channel: no type records the failure, so no caller has to answer for it. Define the failure with Schema.TaggedError and fail with it through Effect.fail, so it stays in E for Effect.catchTag to handle"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var no_throw_default = rule2;
|
|
90
|
+
|
|
91
|
+
// effect-channel/no-try-catch.ts
|
|
92
|
+
var rule3 = {
|
|
93
|
+
meta: {
|
|
94
|
+
type: "problem",
|
|
95
|
+
docs: { description: "Disallow a try statement with a catch clause, which recovers outside Effect's error channel" }
|
|
96
|
+
},
|
|
97
|
+
create(context) {
|
|
98
|
+
return {
|
|
99
|
+
CatchClause(node) {
|
|
100
|
+
context.report({
|
|
101
|
+
node,
|
|
102
|
+
message: "catch recovers outside the error channel: it takes whatever was thrown as unknown, bugs included. Wrap the throwing call in Effect.try or Effect.tryPromise, whose catch maps the cause to a Schema.TaggedError, and recover by tag with Effect.catchTag"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var no_try_catch_default = rule3;
|
|
109
|
+
|
|
110
|
+
// effect-channel/index.ts
|
|
111
|
+
var plugin = {
|
|
112
|
+
meta: { name: "effect-channel" },
|
|
113
|
+
rules: {
|
|
114
|
+
"no-error-channel-escape": no_error_channel_escape_default,
|
|
115
|
+
"no-throw": no_throw_default,
|
|
116
|
+
"no-try-catch": no_try_catch_default
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var effect_channel_default = plugin;
|
|
120
|
+
export {
|
|
121
|
+
effect_channel_default as default
|
|
122
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// readability/cognitive-nodes.ts
|
|
2
2
|
var CONTROL_TYPES = ["IfStatement", "ConditionalExpression", "SwitchStatement", "SwitchCase", "TryStatement", "CatchClause"];
|
|
3
3
|
var LOOP_TYPES = ["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement", "LabeledStatement"];
|
|
4
4
|
var CALL_TYPES = ["LogicalExpression", "BreakStatement", "ContinueStatement", "CallExpression", "NewExpression", "ImportExpression"];
|
|
@@ -7,7 +7,7 @@ var PLAIN_A_TYPES = ["BlockStatement", "ExpressionStatement", "ReturnStatement",
|
|
|
7
7
|
var PLAIN_B_TYPES = ["ClassDeclaration", "ClassExpression", "ClassBody", "MethodDefinition", "TSAbstractMethodDefinition", "PropertyDefinition", "TSAbstractPropertyDefinition", "AccessorProperty", "TSAbstractAccessorProperty", "ObjectExpression", "ArrayExpression"];
|
|
8
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
9
|
|
|
10
|
-
//
|
|
10
|
+
// readability/cognitive-plain.ts
|
|
11
11
|
function isControl(node) {
|
|
12
12
|
return CONTROL_TYPES.includes(node.type);
|
|
13
13
|
}
|
|
@@ -142,7 +142,7 @@ function plainChildrenC(node) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
//
|
|
145
|
+
// readability/cognitive.ts
|
|
146
146
|
function unreachable2(_value) {}
|
|
147
147
|
function scoreList(state, nodes, nesting, parent) {
|
|
148
148
|
for (const node of nodes) {
|
|
@@ -334,7 +334,7 @@ function cognitiveComplexity(root, names) {
|
|
|
334
334
|
return state.recursive ? state.total + 1 : state.total;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
//
|
|
337
|
+
// readability/cognitive-complexity.ts
|
|
338
338
|
var DEFAULT_MAX = 15;
|
|
339
339
|
function maxOf(options) {
|
|
340
340
|
const [first] = options;
|
|
@@ -416,126 +416,14 @@ var rule = {
|
|
|
416
416
|
};
|
|
417
417
|
var cognitive_complexity_default = rule;
|
|
418
418
|
|
|
419
|
-
//
|
|
420
|
-
var EFFECT_SOURCES = new Set(["effect", "effect/Effect"]);
|
|
421
|
-
var INSTEAD = {
|
|
422
|
-
drops: "it drops the failure and the success together, so no caller can tell one from the other. Handle the error by tag, or keep it as a value with Effect.result or Effect.exit",
|
|
423
|
-
swallows: "a Cause is the typed error plus defects plus interruption, so this swallows the bugs and the cancellations along with it. Catch the errors you named, with Effect.catchTag or Effect.catchTags",
|
|
424
|
-
blind: "the handler cannot see what it is recovering from, so every error in the channel collapses into one fallback. Name the errors with Effect.catchTag, or take the error and use it"
|
|
425
|
-
};
|
|
426
|
-
var REFUSED = new Map([
|
|
427
|
-
["ignore", INSTEAD.drops],
|
|
428
|
-
["ignoreCause", INSTEAD.drops],
|
|
429
|
-
["catchCause", INSTEAD.swallows],
|
|
430
|
-
["catchCauseIf", INSTEAD.swallows],
|
|
431
|
-
["catchCauseFilter", INSTEAD.swallows]
|
|
432
|
-
]);
|
|
433
|
-
var blindToTheError = (handler) => {
|
|
434
|
-
if (!handler)
|
|
435
|
-
return false;
|
|
436
|
-
if (handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression")
|
|
437
|
-
return false;
|
|
438
|
-
return handler.params.every((param) => param.type === "Identifier" && /^_+$/.test(param.name));
|
|
439
|
-
};
|
|
440
|
-
var rule2 = {
|
|
441
|
-
meta: {
|
|
442
|
-
type: "problem",
|
|
443
|
-
docs: { description: "Disallow the combinators that erase Effect's error channel" }
|
|
444
|
-
},
|
|
445
|
-
create(context) {
|
|
446
|
-
const effect = new Set;
|
|
447
|
-
const named = (node) => {
|
|
448
|
-
if (!node || node.type !== "MemberExpression" || node.computed)
|
|
449
|
-
return null;
|
|
450
|
-
if (node.object.type !== "Identifier" || !effect.has(node.object.name))
|
|
451
|
-
return null;
|
|
452
|
-
return node.property.type === "Identifier" ? node.property.name : null;
|
|
453
|
-
};
|
|
454
|
-
const refuse = (node, combinator, instead) => {
|
|
455
|
-
context.report({ node, message: `Effect.${combinator} erases the error channel: ${instead}` });
|
|
456
|
-
};
|
|
457
|
-
return {
|
|
458
|
-
ImportDeclaration(node) {
|
|
459
|
-
if (!EFFECT_SOURCES.has(node.source.value))
|
|
460
|
-
return;
|
|
461
|
-
const module = node.source.value === "effect/Effect";
|
|
462
|
-
for (const specifier of node.specifiers) {
|
|
463
|
-
if (specifier.type === "ImportSpecifier") {
|
|
464
|
-
if (specifier.imported.type === "Identifier" && specifier.imported.name === "Effect")
|
|
465
|
-
effect.add(specifier.local.name);
|
|
466
|
-
} else if (module)
|
|
467
|
-
effect.add(specifier.local.name);
|
|
468
|
-
}
|
|
469
|
-
},
|
|
470
|
-
MemberExpression(node) {
|
|
471
|
-
const combinator = named(node);
|
|
472
|
-
if (combinator === null)
|
|
473
|
-
return;
|
|
474
|
-
const instead = REFUSED.get(combinator);
|
|
475
|
-
if (instead !== undefined)
|
|
476
|
-
refuse(node, combinator, instead);
|
|
477
|
-
},
|
|
478
|
-
CallExpression(node) {
|
|
479
|
-
if (named(node.callee) !== "catch")
|
|
480
|
-
return;
|
|
481
|
-
if (!blindToTheError(node.arguments[node.arguments.length - 1]))
|
|
482
|
-
return;
|
|
483
|
-
refuse(node, "catch", INSTEAD.blind);
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
var no_error_channel_escape_default = rule2;
|
|
489
|
-
|
|
490
|
-
// effect-channel/no-throw.ts
|
|
491
|
-
var rule3 = {
|
|
492
|
-
meta: {
|
|
493
|
-
type: "problem",
|
|
494
|
-
docs: { description: "Disallow throw, which fails outside Effect's error channel" }
|
|
495
|
-
},
|
|
496
|
-
create(context) {
|
|
497
|
-
return {
|
|
498
|
-
ThrowStatement(node) {
|
|
499
|
-
context.report({
|
|
500
|
-
node,
|
|
501
|
-
message: "throw escapes the error channel: no type records the failure, so no caller has to answer for it. Define the failure with Schema.TaggedError and fail with it through Effect.fail, so it stays in E for Effect.catchTag to handle"
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
var no_throw_default = rule3;
|
|
508
|
-
|
|
509
|
-
// effect-channel/no-try-catch.ts
|
|
510
|
-
var rule4 = {
|
|
511
|
-
meta: {
|
|
512
|
-
type: "problem",
|
|
513
|
-
docs: { description: "Disallow a try statement with a catch clause, which recovers outside Effect's error channel" }
|
|
514
|
-
},
|
|
515
|
-
create(context) {
|
|
516
|
-
return {
|
|
517
|
-
CatchClause(node) {
|
|
518
|
-
context.report({
|
|
519
|
-
node,
|
|
520
|
-
message: "catch recovers outside the error channel: it takes whatever was thrown as unknown, bugs included. Wrap the throwing call in Effect.try or Effect.tryPromise, whose catch maps the cause to a Schema.TaggedError, and recover by tag with Effect.catchTag"
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
var no_try_catch_default = rule4;
|
|
527
|
-
|
|
528
|
-
// effect-channel/index.ts
|
|
419
|
+
// readability/index.ts
|
|
529
420
|
var plugin = {
|
|
530
|
-
meta: { name: "
|
|
421
|
+
meta: { name: "readability" },
|
|
531
422
|
rules: {
|
|
532
|
-
"no-error-channel-escape": no_error_channel_escape_default,
|
|
533
|
-
"no-throw": no_throw_default,
|
|
534
|
-
"no-try-catch": no_try_catch_default,
|
|
535
423
|
"cognitive-complexity": cognitive_complexity_default
|
|
536
424
|
}
|
|
537
425
|
};
|
|
538
|
-
var
|
|
426
|
+
var readability_default = plugin;
|
|
539
427
|
export {
|
|
540
|
-
|
|
428
|
+
readability_default as default
|
|
541
429
|
};
|
|
@@ -57,12 +57,12 @@ The kit's own `.oxlintrc.json` sets these limits for each size override, and a r
|
|
|
57
57
|
|
|
58
58
|
<!-- generated size-limits: bun run build writes it from .oxlintrc.json, SIZE_RULES in scripts/size-rules.ts and scripts/doc-blocks.ts -->
|
|
59
59
|
|
|
60
|
-
| Limits | oxlint rule | `effect-channel/**/*.ts`, `scripts/**/*.ts` | `tests/**/*.ts` |
|
|
60
|
+
| Limits | oxlint rule | `effect-channel/**/*.ts`, `readability/**/*.ts`, `scripts/**/*.ts` | `tests/**/*.ts` |
|
|
61
61
|
| --- | --- | --- | --- |
|
|
62
62
|
| The most lines a file may hold, blank and comment lines counted | `max-lines` | 400 | 600 |
|
|
63
63
|
| The most lines a function may span, blank and comment lines counted | `max-lines-per-function` | 100 | off |
|
|
64
64
|
| The most statements a function may hold | `max-statements` | 30 | 50 |
|
|
65
|
-
| The highest cognitive complexity a function may reach, a switch counted once | `
|
|
65
|
+
| The highest cognitive complexity a function may reach, a switch counted once | `readability/cognitive-complexity` | 15 | 15 |
|
|
66
66
|
| The deepest a block may nest inside a function | `max-depth` | 4 | 4 |
|
|
67
67
|
|
|
68
68
|
<!-- end generated size-limits -->
|
package/docs/design.md
CHANGED
|
@@ -12,7 +12,7 @@ Each entry below is a choice in the kit's shape and the constraint that forced i
|
|
|
12
12
|
- `files` in package.json is the published surface: `tests/`, `AGENTS.md` and the `.ts` plugin source never reach an install.
|
|
13
13
|
npm adds `package.json`, `README` and `LICENSE` to the tarball whatever `files` says.
|
|
14
14
|
`bun pm pack` builds the same tarball the registry serves, which is what the packed-tarball consumer e2e test installs.
|
|
15
|
-
-
|
|
15
|
+
- Each plugin ships compiled under `dist/`, built with `bun build <name>/index.ts --outdir dist/<name> --target node --format esm`.
|
|
16
16
|
Node refuses to type-strip a `.ts` plugin under `node_modules`, so the `.ts` source would fail to load from an installed package.
|
|
17
17
|
- The size rules are plain oxlint rules at `error` in `.oxlintrc.json`, and `bun run lint` enforces them on the whole tree.
|
|
18
18
|
A repository records its existing violations with `oxlint --suppress-all`, and `checks-suppressions-ratchet` refuses any count that rises.
|
|
@@ -51,7 +51,7 @@ Each entry below is a choice in the kit's shape and the constraint that forced i
|
|
|
51
51
|
`@effect/platform-node-shared` is a direct dependency at the same exact version only to pin it: `@effect/platform-bun` asks for it with a `^` range, and a newer rc peers on a newer `effect` than consumers install, so all three move together.
|
|
52
52
|
- Each runnable script ships a `checks-` bin entry, so consumer `package.json` scripts call the short name, which the package manager puts on `PATH` only there.
|
|
53
53
|
A shell runs it through `bun run`, which never falls back to the registry the way `bunx` does.
|
|
54
|
-
The `.ts` checks keep a `bun` shebang, which needs no build step and no `dist/` entry, unlike the oxlint
|
|
54
|
+
The `.ts` checks keep a `bun` shebang, which needs no build step and no `dist/` entry, unlike the oxlint plugins that node loads.
|
|
55
55
|
- `checks-lint` runs each gate as its own bin in a child process rather than importing it, so a gate behaves the same called alone or through the entry point, and `lint-coverage.sh` stays a shell script.
|
|
56
56
|
The gates run one at a time with their output passed straight through, so each report reads whole and in the table's order.
|
|
57
57
|
- `checks-lint` determines applicable gates from tracked files instead of accepting a repository selection.
|
|
@@ -16,6 +16,7 @@ A file that repeats lines without a rise is advisory.
|
|
|
16
16
|
## What it reads
|
|
17
17
|
|
|
18
18
|
jscpd reads the head's `.jscpd.json` at both ends, so its `path` and `ignore` globs select the files to measure.
|
|
19
|
+
A `path` entry the base revision lacks reads as empty there, so a newly measured folder never fails the scan.
|
|
19
20
|
|
|
20
21
|
```json
|
|
21
22
|
{ "path": ["src"], "format": ["typescript"], "ignore": ["**/*.d.ts"] }
|
package/oxlintrc.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"plugins": ["typescript", "oxc", "eslint", "import"],
|
|
3
|
-
"jsPlugins": ["./dist/index.js"],
|
|
3
|
+
"jsPlugins": ["./dist/effect-channel/index.js", "./dist/readability/index.js"],
|
|
4
4
|
"categories": { "correctness": "error", "suspicious": "error" },
|
|
5
5
|
"rules": {
|
|
6
6
|
"effect-channel/no-error-channel-escape": "error",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Deterministic checks shared across the captain's TypeScript repos",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"oxlintrc.json",
|
|
59
59
|
"stryker.preset.js",
|
|
60
60
|
"tsconfig.effect.json",
|
|
61
|
-
"dist/index.js"
|
|
61
|
+
"dist/effect-channel/index.js",
|
|
62
|
+
"dist/readability/index.js"
|
|
62
63
|
],
|
|
63
64
|
"exports": {
|
|
64
65
|
"./bunfig.toml": "./bunfig.toml",
|
|
@@ -98,7 +99,8 @@
|
|
|
98
99
|
"./oxlintrc.json": "./oxlintrc.json",
|
|
99
100
|
"./stryker.preset.js": "./stryker.preset.js",
|
|
100
101
|
"./tsconfig.effect.json": "./tsconfig.effect.json",
|
|
101
|
-
"./dist/index.js": "./dist/index.js"
|
|
102
|
+
"./dist/effect-channel/index.js": "./dist/effect-channel/index.js",
|
|
103
|
+
"./dist/readability/index.js": "./dist/readability/index.js"
|
|
102
104
|
},
|
|
103
105
|
"bin": {
|
|
104
106
|
"checks-lint": "scripts/lint.ts",
|
|
@@ -120,7 +122,7 @@
|
|
|
120
122
|
},
|
|
121
123
|
"scripts": {
|
|
122
124
|
"prepare": "bun scripts/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
|
|
123
|
-
"build": "bun build effect-channel/index.ts --outdir dist --target node --format esm && bun scripts/doc-templates-write.ts && bun scripts/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
125
|
+
"build": "bun build effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build readability/index.ts --outdir dist/readability --target node --format esm && bun scripts/doc-templates-write.ts && bun scripts/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
124
126
|
"lint": "oxlint --type-aware && bun scripts/lint.ts && depcruise --config .dependency-cruiser.cjs .",
|
|
125
127
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
126
128
|
"test": "bun scripts/test.ts"
|
package/scripts/repetition.ts
CHANGED
|
@@ -48,6 +48,10 @@ const decodeReport = Schema.decodeUnknownEffect(
|
|
|
48
48
|
),
|
|
49
49
|
);
|
|
50
50
|
|
|
51
|
+
const decodeConfigPaths = Schema.decodeUnknownEffect(
|
|
52
|
+
Schema.fromJsonString(Schema.Struct({ path: Schema.optional(Schema.Array(Schema.String)) })),
|
|
53
|
+
);
|
|
54
|
+
|
|
51
55
|
type Scan = { readonly sources: number; readonly clones: readonly Clone[] };
|
|
52
56
|
|
|
53
57
|
function fragmentOf(file: (name: string) => string, { name, start, end }: typeof Location.Type): Fragment {
|
|
@@ -66,8 +70,9 @@ export function repeatedLines(clones: readonly Clone[]): ReadonlyMap<string, num
|
|
|
66
70
|
|
|
67
71
|
// jscpd reads files from disk, and the working tree need not hold the revision: a merge checkout or an uncommitted edit.
|
|
68
72
|
// Both ends scan under the head's config, so a changed file set never reads as a change in repetition.
|
|
73
|
+
// jscpd refuses a missing path, so a path in `emptied` the revision lacks is created empty.
|
|
69
74
|
const scan = Effect.fn("scan")(
|
|
70
|
-
function* (root: string, rev: string, config: string) {
|
|
75
|
+
function* (root: string, rev: string, config: string, emptied: readonly string[]) {
|
|
71
76
|
const files = yield* pathsAt(rev, [], root);
|
|
72
77
|
if (files.length === 0) return { sources: 0, clones: [] } satisfies Scan;
|
|
73
78
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -75,6 +80,9 @@ const scan = Effect.fn("scan")(
|
|
|
75
80
|
const scratch = yield* fs.makeTempDirectoryScoped({ prefix: "checks-repetition-" });
|
|
76
81
|
const tree = yield* checkoutFiles(rev, files, scratch, root);
|
|
77
82
|
yield* fs.writeFileString(path.join(tree, CONFIG), config);
|
|
83
|
+
for (const absent of emptied.map((entry) => path.join(tree, entry))) {
|
|
84
|
+
if (!(yield* fs.exists(absent))) yield* fs.makeDirectory(absent, { recursive: true });
|
|
85
|
+
}
|
|
78
86
|
const output = path.join(scratch, "report");
|
|
79
87
|
const flags = ["--silent", "--no-tips", "--no-gitignore", "--absolute", "--reporters", "json", "--output", output];
|
|
80
88
|
const thresholds = ["--min-tokens", `${MIN_TOKENS}`, "--min-lines", `${MIN_LINES}`];
|
|
@@ -137,8 +145,11 @@ const runHold = Effect.fn("runHold")(function* (root: string, config: string, ba
|
|
|
137
145
|
const formerPath = new Map(
|
|
138
146
|
(yield* changedPaths(base, head, [], root)).flatMap((change) => (change.kind === "renamed" ? [[change.path, change.from]] : [])),
|
|
139
147
|
);
|
|
140
|
-
const
|
|
141
|
-
|
|
148
|
+
const { path: configPaths = [] } = yield* decodeConfigPaths(config).pipe(
|
|
149
|
+
Effect.mapError((cause) => new JscpdUnreadable({ message: `cannot read ${CONFIG}: ${cause.message}` })),
|
|
150
|
+
);
|
|
151
|
+
const before = repeatedLines((yield* scan(root, base, config, configPaths)).clones);
|
|
152
|
+
const { sources, clones } = yield* scan(root, head, config, []);
|
|
142
153
|
const after = repeatedLines(clones);
|
|
143
154
|
const rises = risesOf([...after.keys()], before, after, formerPath, clones);
|
|
144
155
|
return heldOf(sources, rises, after) satisfies Held;
|