@danieljvdm/dev-kit 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.
Files changed (43) hide show
  1. package/README.md +18 -7
  2. package/package.json +20 -9
  3. package/scripts/sync-anti-slop-runtime.mjs +19 -0
  4. package/skill-sources.jsonc +17 -0
  5. package/skill-sources.lock.json +48 -0
  6. package/skills/dev-kit/SKILL.md +4 -5
  7. package/src/bin/dev-kit.ts +11 -7
  8. package/src/catalog-manager.ts +23 -18
  9. package/src/catalog.ts +8 -5
  10. package/src/cli-ui.ts +2 -2
  11. package/src/effect-tsgo.ts +10 -6
  12. package/src/gitignore.ts +6 -3
  13. package/src/manifest.ts +10 -2
  14. package/src/oxlint-plugin-anti-slop/LICENSE +21 -0
  15. package/src/oxlint-plugin-anti-slop/index.ts +43 -0
  16. package/src/oxlint-plugin-anti-slop/rules/no-chained-type-assertions.ts +79 -0
  17. package/src/oxlint-plugin-anti-slop/rules/no-conditional-empty-object-spread.ts +51 -0
  18. package/src/oxlint-plugin-anti-slop/rules/no-known-value-widening.ts +267 -0
  19. package/src/oxlint-plugin-anti-slop/rules/no-module-mocking.ts +105 -0
  20. package/src/oxlint-plugin-anti-slop/rules/no-object-parameters.ts +141 -0
  21. package/src/oxlint-plugin-anti-slop/rules/no-reflect-apply.ts +28 -0
  22. package/src/oxlint-plugin-anti-slop/rules/no-reflect-get.ts +28 -0
  23. package/src/oxlint-plugin-anti-slop/rules/no-runtime-typeof.ts +25 -0
  24. package/src/oxlint-plugin-anti-slop/rules/no-shape-in-symbol-names.ts +38 -0
  25. package/src/oxlint-plugin-anti-slop/rules/no-unknown-parameters.ts +86 -0
  26. package/src/oxlint-plugin-anti-slop/rules/no-unknown-returns.ts +125 -0
  27. package/src/oxlint-plugin-anti-slop/rules/no-unknown-type-aliases.ts +73 -0
  28. package/src/oxlint-plugin-anti-slop/rules/no-unsafe-dictionary-type.ts +139 -0
  29. package/src/oxlint-plugin-anti-slop/rules/no-widen-then-assert.ts +396 -0
  30. package/src/oxlint-plugin-anti-slop/rules/require-safety-comment-for-type-assertion.ts +61 -0
  31. package/src/oxlint-plugin-anti-slop/runtime.d.ts +22 -0
  32. package/src/oxlint-plugin-anti-slop/runtime.js +1209 -0
  33. package/src/oxlint-plugin-anti-slop/shared/dictionary-types.ts +555 -0
  34. package/src/oxlint-plugin-anti-slop/shared/reflect-method.ts +40 -0
  35. package/src/oxlint-plugin-effect.d.ts +4 -2
  36. package/src/oxlint.js +19 -0
  37. package/src/oxlint.ts +22 -5
  38. package/src/package-skill-source.ts +17 -25
  39. package/src/path-digest.ts +2 -1
  40. package/src/project-package.ts +14 -12
  41. package/src/skill-manager.ts +25 -13
  42. package/src/sync.ts +65 -49
  43. package/src/vendor.ts +35 -16
@@ -0,0 +1,396 @@
1
+ import { defineRule, type ESTree, type Variable } from "@oxlint/plugins";
2
+
3
+ type BroadTypeKind = "top" | "object" | "record";
4
+
5
+ type KnownValueEvidence = {
6
+ readonly type: ESTree.TSType | null;
7
+ };
8
+
9
+ const functionBoundaryTypes = new Set([
10
+ "ArrowFunctionExpression",
11
+ "FunctionDeclaration",
12
+ "FunctionExpression",
13
+ "TSDeclareFunction",
14
+ "TSEmptyBodyFunctionExpression",
15
+ ]);
16
+
17
+ function unwrapExpressionParentheses(expression: ESTree.Expression): ESTree.Expression {
18
+ let current = expression;
19
+
20
+ while (current.type === "ParenthesizedExpression") current = current.expression;
21
+
22
+ return current;
23
+ }
24
+
25
+ function unwrapTypeParentheses(type: ESTree.TSType): ESTree.TSType {
26
+ let current = type;
27
+
28
+ while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
29
+
30
+ return current;
31
+ }
32
+
33
+ function typeReferenceName(type: ESTree.TSTypeReference): string | null {
34
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
35
+ }
36
+
37
+ function isUnknownOrAnyType(type: ESTree.TSType): boolean {
38
+ const unwrapped = unwrapTypeParentheses(type);
39
+
40
+ return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
41
+ }
42
+
43
+ function isBroadRecordKeyType(type: ESTree.TSType): boolean {
44
+ const unwrapped = unwrapTypeParentheses(type);
45
+
46
+ if (
47
+ unwrapped.type === "TSStringKeyword" ||
48
+ unwrapped.type === "TSNumberKeyword" ||
49
+ unwrapped.type === "TSSymbolKeyword"
50
+ ) {
51
+ return true;
52
+ }
53
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
54
+
55
+ return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
56
+ }
57
+
58
+ function isBroadRecordType(type: ESTree.TSType): boolean {
59
+ const unwrapped = unwrapTypeParentheses(type);
60
+
61
+ if (unwrapped.type === "TSTypeReference") {
62
+ if (typeReferenceName(unwrapped) === "Readonly") {
63
+ const [inner] = unwrapped.typeArguments?.params ?? [];
64
+
65
+ return inner !== undefined && isBroadRecordType(inner);
66
+ }
67
+
68
+ if (typeReferenceName(unwrapped) !== "Record") return false;
69
+ const parameters = unwrapped.typeArguments?.params ?? [];
70
+
71
+ return (
72
+ parameters.length === 2 &&
73
+ parameters[0] !== undefined &&
74
+ parameters[1] !== undefined &&
75
+ isBroadRecordKeyType(parameters[0]) &&
76
+ isUnknownOrAnyType(parameters[1])
77
+ );
78
+ }
79
+
80
+ if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
81
+ const [member] = unwrapped.members;
82
+ const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
83
+
84
+ return (
85
+ member?.type === "TSIndexSignature" &&
86
+ member.parameters.length === 1 &&
87
+ parameter !== undefined &&
88
+ isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
89
+ isUnknownOrAnyType(member.typeAnnotation.typeAnnotation)
90
+ );
91
+ }
92
+
93
+ function broadTypeKind(type: ESTree.TSType): BroadTypeKind | null {
94
+ const unwrapped = unwrapTypeParentheses(type);
95
+
96
+ if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
97
+ if (unwrapped.type === "TSObjectKeyword") return "object";
98
+
99
+ return isBroadRecordType(unwrapped) ? "record" : null;
100
+ }
101
+
102
+ function assertedExpression(
103
+ node: ESTree.TSAsExpression | ESTree.TSTypeAssertion,
104
+ ): ESTree.Expression {
105
+ return unwrapExpressionParentheses(node.expression);
106
+ }
107
+
108
+ function assertionFromExpression(
109
+ expression: ESTree.Expression,
110
+ ): ESTree.TSAsExpression | ESTree.TSTypeAssertion | null {
111
+ const unwrapped = unwrapExpressionParentheses(expression);
112
+
113
+ return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
114
+ ? unwrapped
115
+ : null;
116
+ }
117
+
118
+ function normalizedTypeText(sourceText: string, type: ESTree.TSType): string {
119
+ return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
120
+ }
121
+
122
+ function typesHaveSameSyntax(
123
+ sourceText: string,
124
+ left: ESTree.TSType | null,
125
+ right: ESTree.TSType,
126
+ ): boolean {
127
+ return (
128
+ left !== null &&
129
+ normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
130
+ normalizedTypeText(sourceText, unwrapTypeParentheses(right))
131
+ );
132
+ }
133
+
134
+ function isDefinitelyObjectType(type: ESTree.TSType): boolean {
135
+ const unwrapped = unwrapTypeParentheses(type);
136
+
137
+ switch (unwrapped.type) {
138
+ case "TSArrayType":
139
+ case "TSConstructorType":
140
+ case "TSFunctionType":
141
+ case "TSMappedType":
142
+ case "TSObjectKeyword":
143
+ case "TSTupleType":
144
+ return true;
145
+ case "TSTypeLiteral":
146
+ return unwrapped.members.length > 0;
147
+ case "TSIntersectionType":
148
+ return unwrapped.types.every(isDefinitelyObjectType);
149
+ case "TSTypeOperator":
150
+ return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
151
+ default:
152
+ return false;
153
+ }
154
+ }
155
+
156
+ function isDefinitelyNarrowerRecordType(type: ESTree.TSType): boolean {
157
+ const unwrapped = unwrapTypeParentheses(type);
158
+
159
+ if (unwrapped.type === "TSTypeLiteral") {
160
+ return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
161
+ }
162
+
163
+ if (unwrapped.type !== "TSTypeReference") return false;
164
+ if (typeReferenceName(unwrapped) === "Readonly") {
165
+ const [inner] = unwrapped.typeArguments?.params ?? [];
166
+
167
+ return inner !== undefined && isDefinitelyNarrowerRecordType(inner);
168
+ }
169
+ if (typeReferenceName(unwrapped) !== "Record") return false;
170
+
171
+ const parameters = unwrapped.typeArguments?.params ?? [];
172
+
173
+ return (
174
+ parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1])
175
+ );
176
+ }
177
+
178
+ function functionBoundary(node: ESTree.Node): ESTree.Node | null {
179
+ let current = node.parent;
180
+
181
+ while (current !== null && current.type !== "Program") {
182
+ if (functionBoundaryTypes.has(current.type)) return current;
183
+ current = current.parent;
184
+ }
185
+
186
+ return null;
187
+ }
188
+
189
+ function resolvedVariableForIdentifier(
190
+ scopes: readonly {
191
+ readonly references: readonly {
192
+ readonly identifier: ESTree.Node;
193
+ readonly resolved: Variable | null;
194
+ }[];
195
+ }[],
196
+ identifier: ESTree.IdentifierReference,
197
+ ): Variable | null {
198
+ for (const scope of scopes) {
199
+ const reference = scope.references.find(
200
+ (candidate) =>
201
+ candidate.identifier.start === identifier.start &&
202
+ candidate.identifier.end === identifier.end,
203
+ );
204
+
205
+ if (reference !== undefined) return reference.resolved;
206
+ }
207
+
208
+ return null;
209
+ }
210
+
211
+ function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
212
+ for (const definition of variable.defs) {
213
+ if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") {
214
+ return definition.node;
215
+ }
216
+ }
217
+
218
+ return null;
219
+ }
220
+
221
+ function knownValueEvidence(
222
+ expression: ESTree.Expression,
223
+ scopes: Parameters<typeof resolvedVariableForIdentifier>[0],
224
+ boundary: ESTree.Node | null,
225
+ visitedVariables: ReadonlySet<Variable>,
226
+ ): KnownValueEvidence | null {
227
+ const unwrapped = unwrapExpressionParentheses(expression);
228
+
229
+ if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
230
+ if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
231
+
232
+ return { type: unwrapped.typeAnnotation };
233
+ }
234
+
235
+ if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
236
+ return { type: null };
237
+ }
238
+
239
+ if (
240
+ unwrapped.type === "ArrayExpression" ||
241
+ unwrapped.type === "ArrowFunctionExpression" ||
242
+ unwrapped.type === "ClassExpression" ||
243
+ unwrapped.type === "FunctionExpression" ||
244
+ unwrapped.type === "NewExpression" ||
245
+ unwrapped.type === "ObjectExpression"
246
+ ) {
247
+ return { type: null };
248
+ }
249
+
250
+ if (unwrapped.type !== "Identifier") return null;
251
+ const variable = resolvedVariableForIdentifier(scopes, unwrapped);
252
+
253
+ if (variable === null || visitedVariables.has(variable)) return null;
254
+
255
+ const annotatedIdentifier = variable.identifiers.find(
256
+ (identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined,
257
+ );
258
+ const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
259
+
260
+ if (annotation !== undefined && annotatedIdentifier !== undefined) {
261
+ if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
262
+ return null;
263
+ }
264
+
265
+ return { type: annotation };
266
+ }
267
+
268
+ const declarator = variableDeclarator(variable);
269
+
270
+ if (
271
+ declarator === null ||
272
+ declarator.parent.type !== "VariableDeclaration" ||
273
+ declarator.parent.kind !== "const" ||
274
+ declarator.init === null ||
275
+ variable.references.some((reference) => reference.isWrite() && !reference.init) ||
276
+ functionBoundary(declarator) !== boundary
277
+ ) {
278
+ return null;
279
+ }
280
+
281
+ return knownValueEvidence(
282
+ declarator.init,
283
+ scopes,
284
+ boundary,
285
+ new Set([...visitedVariables, variable]),
286
+ );
287
+ }
288
+
289
+ function widenedBinding(
290
+ variable: Variable,
291
+ scopes: Parameters<typeof resolvedVariableForIdentifier>[0],
292
+ ): {
293
+ readonly broadKind: BroadTypeKind;
294
+ readonly evidence: KnownValueEvidence;
295
+ readonly declaredAt: number;
296
+ readonly boundary: ESTree.Node | null;
297
+ } | null {
298
+ const declarator = variableDeclarator(variable);
299
+
300
+ if (
301
+ declarator === null ||
302
+ declarator.parent.type !== "VariableDeclaration" ||
303
+ declarator.parent.kind !== "const" ||
304
+ declarator.id.type !== "Identifier" ||
305
+ declarator.init === null ||
306
+ variable.references.some((reference) => reference.isWrite() && !reference.init)
307
+ ) {
308
+ return null;
309
+ }
310
+
311
+ const boundary = functionBoundary(declarator);
312
+ const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
313
+ const initializerAssertion = assertionFromExpression(declarator.init);
314
+ const initializerBroadKind =
315
+ initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
316
+ const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType);
317
+ const broadKind = declaredBroadKind ?? initializerBroadKind;
318
+
319
+ if (broadKind === null) return null;
320
+
321
+ const originalExpression =
322
+ initializerAssertion !== null && initializerBroadKind !== null
323
+ ? assertedExpression(initializerAssertion)
324
+ : declarator.init;
325
+ const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable]));
326
+
327
+ return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
328
+ }
329
+
330
+ function assertionIsNarrower(
331
+ sourceText: string,
332
+ broadKind: BroadTypeKind,
333
+ evidence: KnownValueEvidence,
334
+ assertedType: ESTree.TSType,
335
+ ): boolean {
336
+ if (broadTypeKind(assertedType) !== null) return false;
337
+ if (broadKind === "top") return true;
338
+ if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
339
+ if (broadKind === "object") return isDefinitelyObjectType(assertedType);
340
+
341
+ return isDefinitelyNarrowerRecordType(assertedType);
342
+ }
343
+
344
+ /** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
345
+ export const noWidenThenAssertRule = defineRule({
346
+ meta: {
347
+ type: "problem",
348
+ docs: {
349
+ description:
350
+ "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
351
+ },
352
+ messages: {
353
+ widenThenAssert:
354
+ 'Binding "{{name}}" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once.',
355
+ },
356
+ },
357
+ create(context) {
358
+ const scopes = context.sourceCode.scopeManager.scopes;
359
+
360
+ const checkAssertion = (node: ESTree.TSAsExpression | ESTree.TSTypeAssertion) => {
361
+ const expression = assertedExpression(node);
362
+
363
+ if (expression.type !== "Identifier") return;
364
+
365
+ const variable = resolvedVariableForIdentifier(scopes, expression);
366
+
367
+ if (variable === null) return;
368
+ const widened = widenedBinding(variable, scopes);
369
+
370
+ if (
371
+ widened === null ||
372
+ node.start <= widened.declaredAt ||
373
+ functionBoundary(node) !== widened.boundary ||
374
+ !assertionIsNarrower(
375
+ context.sourceCode.text,
376
+ widened.broadKind,
377
+ widened.evidence,
378
+ node.typeAnnotation,
379
+ )
380
+ ) {
381
+ return;
382
+ }
383
+
384
+ context.report({
385
+ node,
386
+ messageId: "widenThenAssert",
387
+ data: { name: expression.name },
388
+ });
389
+ };
390
+
391
+ return {
392
+ TSAsExpression: checkAssertion,
393
+ TSTypeAssertion: checkAssertion,
394
+ };
395
+ },
396
+ });
@@ -0,0 +1,61 @@
1
+ import { defineRule, type ESTree, type SourceCode } from "@oxlint/plugins";
2
+
3
+ type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
4
+
5
+ const commentOwnerKinds = new Set([
6
+ "ExpressionStatement",
7
+ "PropertyDefinition",
8
+ "ReturnStatement",
9
+ "ThrowStatement",
10
+ "VariableDeclaration",
11
+ ]);
12
+
13
+ function isConstAssertion(node: TypeAssertion): boolean {
14
+ return (
15
+ node.typeAnnotation.type === "TSTypeReference" &&
16
+ node.typeAnnotation.typeName.type === "Identifier" &&
17
+ node.typeAnnotation.typeName.name === "const"
18
+ );
19
+ }
20
+
21
+ function hasSafetyComment(sourceCode: SourceCode, node: TypeAssertion): boolean {
22
+ let current: ESTree.Node = node;
23
+
24
+ while (true) {
25
+ if (
26
+ sourceCode
27
+ .getCommentsBefore(current)
28
+ .some((comment) => comment.end <= node.start && /\bSAFETY\s*:/u.test(comment.value))
29
+ ) {
30
+ return true;
31
+ }
32
+ if (commentOwnerKinds.has(current.type) || current.parent.type === "Program") return false;
33
+ current = current.parent;
34
+ }
35
+ }
36
+
37
+ /** Require every non-const type assertion to state the invariant TypeScript cannot express. */
38
+ export const requireSafetyCommentForTypeAssertionRule = defineRule({
39
+ meta: {
40
+ type: "problem",
41
+ docs: {
42
+ description:
43
+ "Require a nearby SAFETY comment for every TypeScript type assertion except const assertions.",
44
+ },
45
+ messages: {
46
+ missingSafetyComment:
47
+ "This type assertion has no `SAFETY:` justification. State the checked invariant immediately before the assertion or its containing statement.",
48
+ },
49
+ },
50
+ create(context) {
51
+ const checkAssertion = (node: TypeAssertion) => {
52
+ if (isConstAssertion(node) || hasSafetyComment(context.sourceCode, node)) return;
53
+ context.report({ node, messageId: "missingSafetyComment" });
54
+ };
55
+
56
+ return {
57
+ TSAsExpression: checkAssertion,
58
+ TSTypeAssertion: checkAssertion,
59
+ };
60
+ },
61
+ });
@@ -0,0 +1,22 @@
1
+ declare const antiSlopPlugin: {
2
+ readonly meta: { readonly name: "anti-slop" };
3
+ readonly rules: {
4
+ readonly "no-chained-type-assertions": object;
5
+ readonly "no-conditional-empty-object-spread": object;
6
+ readonly "no-known-value-widening": object;
7
+ readonly "no-module-mocking": object;
8
+ readonly "no-object-parameters": object;
9
+ readonly "no-reflect-apply": object;
10
+ readonly "no-reflect-get": object;
11
+ readonly "no-runtime-typeof": object;
12
+ readonly "no-shape-in-symbol-names": object;
13
+ readonly "no-unknown-parameters": object;
14
+ readonly "no-unknown-returns": object;
15
+ readonly "no-unknown-type-aliases": object;
16
+ readonly "no-unsafe-dictionary-type": object;
17
+ readonly "no-widen-then-assert": object;
18
+ readonly "require-safety-comment-for-type-assertion": object;
19
+ };
20
+ };
21
+
22
+ export default antiSlopPlugin;