@danieljvdm/dev-kit 0.17.0 → 1.0.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 (68) hide show
  1. package/README.md +148 -648
  2. package/package.json +19 -62
  3. package/skills/dev-kit/SKILL.md +42 -213
  4. package/skills/dev-kit/agents/openai.yaml +2 -2
  5. package/skills/dev-kit/references/cloudflare-worker-api.md +37 -0
  6. package/skills/dev-kit/references/default-typescript-repository.md +43 -0
  7. package/skills/dev-kit/references/legacy-eject.md +46 -0
  8. package/skills/dev-kit/references/repository-setup.md +53 -0
  9. package/skills/dev-kit/references/skills.md +35 -0
  10. package/src/bin/dev-kit.ts +153 -134
  11. package/src/catalog-manager.ts +23 -18
  12. package/src/catalog.ts +8 -5
  13. package/src/cli-ui.ts +2 -2
  14. package/src/effect-tsgo.ts +10 -6
  15. package/src/eject.ts +715 -0
  16. package/src/gitignore.ts +6 -3
  17. package/src/legacy-project.ts +67 -0
  18. package/src/oxfmt.ts +1 -4
  19. package/src/oxlint-plugin-anti-slop/LICENSE +21 -0
  20. package/src/oxlint-plugin-anti-slop/index.ts +43 -0
  21. package/src/oxlint-plugin-anti-slop/rules/no-chained-type-assertions.ts +79 -0
  22. package/src/oxlint-plugin-anti-slop/rules/no-conditional-empty-object-spread.ts +51 -0
  23. package/src/oxlint-plugin-anti-slop/rules/no-known-value-widening.ts +267 -0
  24. package/src/oxlint-plugin-anti-slop/rules/no-module-mocking.ts +105 -0
  25. package/src/oxlint-plugin-anti-slop/rules/no-object-parameters.ts +141 -0
  26. package/src/oxlint-plugin-anti-slop/rules/no-reflect-apply.ts +28 -0
  27. package/src/oxlint-plugin-anti-slop/rules/no-reflect-get.ts +28 -0
  28. package/src/oxlint-plugin-anti-slop/rules/no-runtime-typeof.ts +25 -0
  29. package/src/oxlint-plugin-anti-slop/rules/no-shape-in-symbol-names.ts +38 -0
  30. package/src/oxlint-plugin-anti-slop/rules/no-unknown-parameters.ts +86 -0
  31. package/src/oxlint-plugin-anti-slop/rules/no-unknown-returns.ts +125 -0
  32. package/src/oxlint-plugin-anti-slop/rules/no-unknown-type-aliases.ts +73 -0
  33. package/src/oxlint-plugin-anti-slop/rules/no-unsafe-dictionary-type.ts +139 -0
  34. package/src/oxlint-plugin-anti-slop/rules/no-widen-then-assert.ts +396 -0
  35. package/src/oxlint-plugin-anti-slop/rules/require-safety-comment-for-type-assertion.ts +61 -0
  36. package/src/oxlint-plugin-anti-slop/runtime.js +1209 -0
  37. package/src/oxlint-plugin-anti-slop/shared/dictionary-types.ts +555 -0
  38. package/src/oxlint-plugin-anti-slop/shared/reflect-method.ts +40 -0
  39. package/src/oxlint.ts +26 -14
  40. package/src/package-skill-source.ts +17 -25
  41. package/src/path-digest.ts +62 -1
  42. package/src/project-package.ts +14 -12
  43. package/src/project-skills.ts +722 -0
  44. package/src/tool-metadata.ts +0 -2
  45. package/src/vendor.ts +35 -21
  46. package/src/vite-plus.ts +1 -3
  47. package/dev-kit.example.jsonc +0 -22
  48. package/schema/dev-kit.schema.json +0 -218
  49. package/schema/skill-sources.schema.json +0 -83
  50. package/src/index.ts +0 -125
  51. package/src/manifest.ts +0 -216
  52. package/src/oxfmt.js +0 -23
  53. package/src/oxlint-plugin-effect.d.ts +0 -17
  54. package/src/oxlint-plugin-style.d.ts +0 -8
  55. package/src/oxlint.js +0 -94
  56. package/src/project-state.ts +0 -122
  57. package/src/scaffold.ts +0 -79
  58. package/src/skill-manager.ts +0 -515
  59. package/src/sync.ts +0 -1919
  60. package/src/tool-ignore-patterns.js +0 -9
  61. package/src/vite-plus-dependency.ts +0 -69
  62. package/src/vite-plus-hooks.ts +0 -175
  63. package/src/vite-plus-workflow.ts +0 -82
  64. package/src/vite-plus.js +0 -88
  65. package/src/worktrunk-config.ts +0 -88
  66. package/templates/AGENTS.md +0 -11
  67. package/templates/vite-plus/github-actions-check.yml +0 -51
  68. package/templates/worktrunk/wt.toml +0 -27
package/src/gitignore.ts CHANGED
@@ -50,15 +50,18 @@ type PlannedGitignorePatch = GitignorePatch & {
50
50
  readonly previousContents: string;
51
51
  };
52
52
 
53
+ type GitignoreContentsPatch = {
54
+ readonly contents: string;
55
+ readonly added: ReadonlyArray<string>;
56
+ };
57
+
53
58
  const publicPatch = (patch: PlannedGitignorePatch): GitignorePatch => ({
54
59
  path: patch.path,
55
60
  changed: patch.changed,
56
61
  added: patch.added,
57
62
  });
58
63
 
59
- export const patchGitignoreContents = (
60
- current: string,
61
- ): { readonly contents: string; readonly added: ReadonlyArray<string> } => {
64
+ export const patchGitignoreContents = (current: string): GitignoreContentsPatch => {
62
65
  const lines = current.split(/\r?\n/);
63
66
  const added = DEV_KIT_GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
64
67
 
@@ -0,0 +1,67 @@
1
+ import { Schema } from "effect";
2
+
3
+ import { DigestSchema } from "./path-digest.ts";
4
+
5
+ const EnabledSetupSchema = Schema.Struct({
6
+ enabled: Schema.optional(Schema.Boolean),
7
+ });
8
+
9
+ export const LegacyDevKitManifestSchema = Schema.Struct({
10
+ setup: Schema.optional(
11
+ Schema.Struct({
12
+ effectSource: Schema.optional(EnabledSetupSchema),
13
+ effectTsgo: Schema.optional(EnabledSetupSchema),
14
+ vitePlus: Schema.optional(
15
+ Schema.Struct({
16
+ hooks: Schema.optional(EnabledSetupSchema),
17
+ }),
18
+ ),
19
+ }),
20
+ ),
21
+ });
22
+
23
+ export type LegacyDevKitManifest = typeof LegacyDevKitManifestSchema.Type;
24
+
25
+ export const legacySetupFlags = (manifest: LegacyDevKitManifest) => ({
26
+ effectSource: manifest.setup?.effectSource?.enabled ?? false,
27
+ effectTsgo: manifest.setup?.effectTsgo?.enabled ?? false,
28
+ vitePlusHooks: manifest.setup?.vitePlus?.hooks?.enabled ?? false,
29
+ });
30
+
31
+ const LegacyCatalogProvenanceSchema = Schema.Union([
32
+ Schema.Struct({
33
+ source: Schema.String,
34
+ repository: Schema.String,
35
+ resolved: Schema.String,
36
+ }),
37
+ Schema.Struct({
38
+ package: Schema.String,
39
+ version: Schema.String,
40
+ skill: Schema.String,
41
+ digest: DigestSchema,
42
+ }),
43
+ ]);
44
+
45
+ export const LegacyManagedSkillOutputSchema = Schema.Struct({
46
+ resourceId: Schema.String,
47
+ path: Schema.String,
48
+ skill: Schema.String,
49
+ target: Schema.Literals(["agents", "claude", "opencode"]),
50
+ mode: Schema.Literals(["copy", "symlink"]),
51
+ kind: Schema.Literals(["directory", "symlink"]),
52
+ digest: DigestSchema,
53
+ catalog: Schema.optional(LegacyCatalogProvenanceSchema),
54
+ });
55
+
56
+ export type LegacyManagedSkillOutput = typeof LegacyManagedSkillOutputSchema.Type;
57
+
58
+ const LegacyOtherOutputSchema = Schema.Struct({
59
+ resourceId: Schema.String,
60
+ path: Schema.String,
61
+ });
62
+
63
+ export const LegacyDevKitLockSchema = Schema.Struct({
64
+ version: Schema.Literal(1),
65
+ toolVersion: Schema.String,
66
+ outputs: Schema.Array(Schema.Union([LegacyManagedSkillOutputSchema, LegacyOtherOutputSchema])),
67
+ });
package/src/oxfmt.ts CHANGED
@@ -5,10 +5,7 @@ import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
5
5
  export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
6
6
 
7
7
  /**
8
- * Canonical formatting defaults for standalone Oxfmt and Vite+ projects.
9
- *
10
- * Oxfmt does not support config inheritance. Spread this object into a
11
- * standalone Oxfmt config or Vite+'s `fmt` block before local overrides.
8
+ * Formatting defaults for this repository's Vite+ config.
12
9
  */
13
10
  export const recommendedOxfmtConfig = {
14
11
  arrowParens: "always",
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dillon Mulroy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,43 @@
1
+ // Vendored from dmmulroy/anti-slop commit 9b80d9a5c317d3af94d88a577bdbde4d9a45f7be.
2
+ // The upstream MIT notice is retained in LICENSE.
3
+ import { definePlugin } from "@oxlint/plugins";
4
+
5
+ import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts";
6
+ import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts";
7
+ import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts";
8
+ import { noModuleMockingRule } from "./rules/no-module-mocking.ts";
9
+ import { noObjectParametersRule } from "./rules/no-object-parameters.ts";
10
+ import { noReflectApplyRule } from "./rules/no-reflect-apply.ts";
11
+ import { noReflectGetRule } from "./rules/no-reflect-get.ts";
12
+ import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.ts";
13
+ import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.ts";
14
+ import { noUnknownParametersRule } from "./rules/no-unknown-parameters.ts";
15
+ import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts";
16
+ import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts";
17
+ import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts";
18
+ import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts";
19
+ import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts";
20
+
21
+ /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
22
+ const antiSlopPlugin = definePlugin({
23
+ meta: { name: "anti-slop" },
24
+ rules: {
25
+ "no-chained-type-assertions": noChainedTypeAssertionsRule,
26
+ "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
27
+ "no-known-value-widening": noKnownValueWideningRule,
28
+ "no-module-mocking": noModuleMockingRule,
29
+ "no-object-parameters": noObjectParametersRule,
30
+ "no-reflect-apply": noReflectApplyRule,
31
+ "no-reflect-get": noReflectGetRule,
32
+ "no-runtime-typeof": noRuntimeTypeofRule,
33
+ "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
34
+ "no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
35
+ "no-unknown-parameters": noUnknownParametersRule,
36
+ "no-unknown-returns": noUnknownReturnsRule,
37
+ "no-unknown-type-aliases": noUnknownTypeAliasesRule,
38
+ "no-widen-then-assert": noWidenThenAssertRule,
39
+ "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
40
+ },
41
+ });
42
+
43
+ export default antiSlopPlugin;
@@ -0,0 +1,79 @@
1
+ import { defineRule, type ESTree } from "@oxlint/plugins";
2
+
3
+ type TypeAssertionExpression = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
4
+
5
+ function isTypeAssertionExpression(node: ESTree.Node): node is TypeAssertionExpression {
6
+ return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
7
+ }
8
+
9
+ function unwrapParenthesizedExpression(expression: ESTree.Expression): ESTree.Expression {
10
+ let current = expression;
11
+
12
+ while (current.type === "ParenthesizedExpression") {
13
+ current = current.expression;
14
+ }
15
+
16
+ return current;
17
+ }
18
+
19
+ function isConstAssertion(node: TypeAssertionExpression): boolean {
20
+ const { typeAnnotation } = node;
21
+
22
+ return (
23
+ typeAnnotation.type === "TSTypeReference" &&
24
+ typeAnnotation.typeName.type === "Identifier" &&
25
+ typeAnnotation.typeName.name === "const"
26
+ );
27
+ }
28
+
29
+ function isOutermostAssertionInChain(node: TypeAssertionExpression): boolean {
30
+ let current: ESTree.Expression = node;
31
+ let parent = node.parent;
32
+
33
+ while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
34
+ current = parent;
35
+ parent = parent.parent;
36
+ }
37
+
38
+ return !isTypeAssertionExpression(parent) || parent.expression !== current;
39
+ }
40
+
41
+ function isForbiddenAssertionChain(node: TypeAssertionExpression): boolean {
42
+ let assertionCount = 0;
43
+ let hasNonConstAssertion = false;
44
+ let current: ESTree.Expression = node;
45
+
46
+ while (isTypeAssertionExpression(current)) {
47
+ assertionCount += 1;
48
+ hasNonConstAssertion ||= !isConstAssertion(current);
49
+ current = unwrapParenthesizedExpression(current.expression);
50
+ }
51
+
52
+ return assertionCount > 1 && hasNonConstAssertion;
53
+ }
54
+
55
+ /** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
56
+ export const noChainedTypeAssertionsRule = defineRule({
57
+ meta: {
58
+ type: "problem",
59
+ docs: {
60
+ description:
61
+ "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.",
62
+ },
63
+ messages: {
64
+ chained:
65
+ "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
66
+ },
67
+ },
68
+ create(context) {
69
+ const checkTypeAssertion = (node: TypeAssertionExpression) => {
70
+ if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
71
+ context.report({ node, messageId: "chained" });
72
+ };
73
+
74
+ return {
75
+ TSAsExpression: checkTypeAssertion,
76
+ TSTypeAssertion: checkTypeAssertion,
77
+ };
78
+ },
79
+ });
@@ -0,0 +1,51 @@
1
+ import { defineRule, type ESTree } from "@oxlint/plugins";
2
+
3
+ function unwrapParentheses(node: ESTree.Expression): ESTree.Expression {
4
+ let current = node;
5
+
6
+ while (current.type === "ParenthesizedExpression") {
7
+ current = current.expression;
8
+ }
9
+
10
+ return current;
11
+ }
12
+
13
+ function isEmptyObjectExpression(node: ESTree.Expression): boolean {
14
+ return node.type === "ObjectExpression" && node.properties.length === 0;
15
+ }
16
+
17
+ function isConditionalEmptyObjectSpread(node: ESTree.Expression): boolean {
18
+ const conditional = unwrapParentheses(node);
19
+
20
+ return (
21
+ conditional.type === "ConditionalExpression" &&
22
+ (isEmptyObjectExpression(conditional.consequent) ||
23
+ isEmptyObjectExpression(conditional.alternate))
24
+ );
25
+ }
26
+
27
+ /** Ban conditional empty-object spreads without changing their omission semantics. */
28
+ export const noConditionalEmptyObjectSpreadRule = defineRule({
29
+ meta: {
30
+ type: "suggestion",
31
+ docs: {
32
+ description:
33
+ "Disallow object spreads that conditionally spread an empty object to omit fields.",
34
+ },
35
+ messages: {
36
+ avoid:
37
+ "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
38
+ },
39
+ },
40
+ create(context) {
41
+ return {
42
+ SpreadElement(node) {
43
+ if (node.parent.type !== "ObjectExpression") return;
44
+
45
+ if (isConditionalEmptyObjectSpread(node.argument)) {
46
+ context.report({ node, messageId: "avoid" });
47
+ }
48
+ },
49
+ };
50
+ },
51
+ });
@@ -0,0 +1,267 @@
1
+ import {
2
+ defineRule,
3
+ type ESTree,
4
+ type Scope,
5
+ type SourceCode,
6
+ type Variable,
7
+ } from "@oxlint/plugins";
8
+
9
+ import {
10
+ classifyWideningTarget,
11
+ createTypeEnvironment,
12
+ isKnownEvidenceExpression,
13
+ type TypeEnvironment,
14
+ type WideningTarget,
15
+ } from "../shared/dictionary-types.ts";
16
+
17
+ type FunctionExpression = ESTree.ArrowFunctionExpression | ESTree.Function;
18
+
19
+ function unwrapExpression(expression: ESTree.Expression): ESTree.Expression {
20
+ let current = expression;
21
+
22
+ while (
23
+ current.type === "ParenthesizedExpression" ||
24
+ current.type === "TSAsExpression" ||
25
+ current.type === "TSSatisfiesExpression" ||
26
+ current.type === "TSTypeAssertion" ||
27
+ current.type === "TSNonNullExpression"
28
+ ) {
29
+ current = current.expression;
30
+ }
31
+
32
+ return current;
33
+ }
34
+
35
+ function resolveVariable(
36
+ sourceCode: SourceCode,
37
+ identifier: ESTree.IdentifierReference,
38
+ ): Variable | null {
39
+ let scope: Scope | null = sourceCode.getScope(identifier);
40
+
41
+ while (scope !== null) {
42
+ const variable = scope.set.get(identifier.name);
43
+
44
+ if (variable !== undefined) return variable;
45
+ scope = scope.upper;
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
52
+ if (variable.defs.length !== 1) return null;
53
+ const [definition] = variable.defs;
54
+
55
+ return definition?.type === "Variable" && definition.node.type === "VariableDeclarator"
56
+ ? definition.node
57
+ : null;
58
+ }
59
+
60
+ function isStableConstVariable(variable: Variable, declarator: ESTree.VariableDeclarator): boolean {
61
+ return (
62
+ declarator.parent.type === "VariableDeclaration" &&
63
+ declarator.parent.kind === "const" &&
64
+ variable.references.every((reference) => reference.init || !reference.isWrite())
65
+ );
66
+ }
67
+
68
+ function hasKnownEvidence(
69
+ sourceCode: SourceCode,
70
+ expression: ESTree.Expression,
71
+ visitedVariables = new Set<Variable>(),
72
+ ): boolean {
73
+ if (isKnownEvidenceExpression(expression)) return true;
74
+ const unwrapped = unwrapExpression(expression);
75
+
76
+ if (unwrapped.type !== "Identifier") return false;
77
+ const variable = resolveVariable(sourceCode, unwrapped);
78
+
79
+ if (variable === null || visitedVariables.has(variable)) return false;
80
+ const declarator = variableDeclarator(variable);
81
+
82
+ if (
83
+ declarator === null ||
84
+ declarator.init === null ||
85
+ !isStableConstVariable(variable, declarator)
86
+ ) {
87
+ return false;
88
+ }
89
+ visitedVariables.add(variable);
90
+
91
+ return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
92
+ }
93
+
94
+ function annotationTarget(
95
+ annotation: ESTree.TSTypeAnnotation | null | undefined,
96
+ environment: TypeEnvironment,
97
+ ): WideningTarget | null {
98
+ return annotation === null || annotation === undefined
99
+ ? null
100
+ : classifyWideningTarget(annotation.typeAnnotation, environment);
101
+ }
102
+
103
+ function enclosingFunction(node: ESTree.Node): FunctionExpression | null {
104
+ let current: ESTree.Node | null = node.parent;
105
+
106
+ while (current !== null && current.type !== "Program") {
107
+ if (
108
+ current.type === "ArrowFunctionExpression" ||
109
+ current.type === "FunctionDeclaration" ||
110
+ current.type === "FunctionExpression"
111
+ ) {
112
+ return current;
113
+ }
114
+ current = current.parent;
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ function sourceKeyName(sourceCode: SourceCode, key: ESTree.PropertyKey): string {
121
+ if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
122
+ if (key.type === "Literal") return String(key.value);
123
+
124
+ return sourceCode.getText(key);
125
+ }
126
+
127
+ function functionName(sourceCode: SourceCode, owner: FunctionExpression | null): string {
128
+ if (owner === null) return "anonymous function";
129
+ if (owner.id !== null) return owner.id.name;
130
+ const parent = owner.parent;
131
+
132
+ if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier")
133
+ return parent.id.name;
134
+ if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
135
+
136
+ return "anonymous function";
137
+ }
138
+
139
+ function isEmptyObjectExpression(expression: ESTree.Expression): boolean {
140
+ const unwrapped = unwrapExpression(expression);
141
+
142
+ return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0;
143
+ }
144
+
145
+ function isDictionaryAccumulatorTarget(destination: WideningTarget): boolean {
146
+ return destination.kind === "open dictionary" || destination.kind === "generic container";
147
+ }
148
+
149
+ function hasParentAssertion(node: ESTree.Node): boolean {
150
+ return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
151
+ }
152
+
153
+ /** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
154
+ export const noKnownValueWideningRule = defineRule({
155
+ meta: {
156
+ type: "problem",
157
+ docs: {
158
+ description:
159
+ "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.",
160
+ },
161
+ messages: {
162
+ widening:
163
+ "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract.",
164
+ },
165
+ },
166
+ createOnce(context) {
167
+ let environment: TypeEnvironment | null = null;
168
+
169
+ const reportFlow = (
170
+ expression: ESTree.Expression,
171
+ destination: WideningTarget | null,
172
+ subject: string,
173
+ ) => {
174
+ if (destination === null) return;
175
+ if (isDictionaryAccumulatorTarget(destination) && isEmptyObjectExpression(expression)) {
176
+ return;
177
+ }
178
+ if (!hasKnownEvidence(context.sourceCode, expression)) return;
179
+ context.report({
180
+ node: expression,
181
+ messageId: "widening",
182
+ data: { subject, target: destination.kind },
183
+ });
184
+ };
185
+
186
+ const targetFromAnnotation = (annotation: ESTree.TSTypeAnnotation | null | undefined) =>
187
+ environment === null ? null : annotationTarget(annotation, environment);
188
+
189
+ return {
190
+ Program(node) {
191
+ environment = createTypeEnvironment(node);
192
+ },
193
+ VariableDeclarator(node) {
194
+ if (node.init === null || node.id.type !== "Identifier") return;
195
+ reportFlow(
196
+ node.init,
197
+ targetFromAnnotation(node.id.typeAnnotation),
198
+ `binding \`${node.id.name}\``,
199
+ );
200
+ },
201
+ PropertyDefinition(node) {
202
+ if (node.value === null) return;
203
+ reportFlow(
204
+ node.value,
205
+ targetFromAnnotation(node.typeAnnotation),
206
+ `property \`${sourceKeyName(context.sourceCode, node.key)}\``,
207
+ );
208
+ },
209
+ AccessorProperty(node) {
210
+ if (node.value === null) return;
211
+ reportFlow(
212
+ node.value,
213
+ targetFromAnnotation(node.typeAnnotation),
214
+ `property \`${sourceKeyName(context.sourceCode, node.key)}\``,
215
+ );
216
+ },
217
+ AssignmentExpression(node) {
218
+ if (node.operator !== "=" || node.left.type !== "Identifier") return;
219
+ const variable = resolveVariable(context.sourceCode, node.left);
220
+
221
+ if (variable === null) return;
222
+ const declarator = variableDeclarator(variable);
223
+
224
+ if (declarator === null || declarator.id.type !== "Identifier") return;
225
+ reportFlow(
226
+ node.right,
227
+ targetFromAnnotation(declarator.id.typeAnnotation),
228
+ `binding \`${declarator.id.name}\``,
229
+ );
230
+ },
231
+ ReturnStatement(node) {
232
+ if (node.argument === null) return;
233
+ const owner = enclosingFunction(node);
234
+
235
+ reportFlow(
236
+ node.argument,
237
+ targetFromAnnotation(owner?.returnType),
238
+ `return value of \`${functionName(context.sourceCode, owner)}\``,
239
+ );
240
+ },
241
+ ArrowFunctionExpression(node) {
242
+ if (node.body.type === "BlockStatement") return;
243
+ reportFlow(
244
+ node.body,
245
+ targetFromAnnotation(node.returnType),
246
+ `return value of \`${functionName(context.sourceCode, node)}\``,
247
+ );
248
+ },
249
+ TSAsExpression(node) {
250
+ if (environment === null || hasParentAssertion(node)) return;
251
+ reportFlow(
252
+ node.expression,
253
+ classifyWideningTarget(node.typeAnnotation, environment),
254
+ "assertion",
255
+ );
256
+ },
257
+ TSTypeAssertion(node) {
258
+ if (environment === null || hasParentAssertion(node)) return;
259
+ reportFlow(
260
+ node.expression,
261
+ classifyWideningTarget(node.typeAnnotation, environment),
262
+ "assertion",
263
+ );
264
+ },
265
+ };
266
+ },
267
+ });
@@ -0,0 +1,105 @@
1
+ import {
2
+ defineRule,
3
+ type ESTree,
4
+ type Scope,
5
+ type SourceCode,
6
+ type Variable,
7
+ } from "@oxlint/plugins";
8
+
9
+ const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
10
+
11
+ function resolveVariable(
12
+ sourceCode: SourceCode,
13
+ identifier: ESTree.IdentifierReference,
14
+ ): Variable | null {
15
+ let scope: Scope | null = sourceCode.getScope(identifier);
16
+
17
+ while (scope !== null) {
18
+ const variable = scope.set.get(identifier.name);
19
+
20
+ if (variable !== undefined) return variable;
21
+ scope = scope.upper;
22
+ }
23
+
24
+ return null;
25
+ }
26
+
27
+ function importedName(node: ESTree.Node): string | null {
28
+ if (node.type !== "ImportSpecifier") return null;
29
+
30
+ return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
31
+ }
32
+
33
+ function isTestFrameworkObject(
34
+ sourceCode: SourceCode,
35
+ expression: ESTree.Expression,
36
+ ): expression is ESTree.IdentifierReference {
37
+ if (expression.type !== "Identifier") return false;
38
+ if (
39
+ (expression.name === "vi" || expression.name === "jest") &&
40
+ sourceCode.isGlobalReference(expression)
41
+ ) {
42
+ return true;
43
+ }
44
+
45
+ const variable = resolveVariable(sourceCode, expression);
46
+
47
+ if (variable === null || variable.defs.length === 0) {
48
+ return expression.name === "vi" || expression.name === "jest";
49
+ }
50
+
51
+ return variable.defs.some((definition) => {
52
+ if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") {
53
+ return false;
54
+ }
55
+ const source = definition.parent.source.value;
56
+ const name = importedName(definition.node);
57
+
58
+ return (
59
+ (source === "vitest" && name === "vi") || (source === "@jest/globals" && name === "jest")
60
+ );
61
+ });
62
+ }
63
+
64
+ function moduleMockCall(sourceCode: SourceCode, callee: ESTree.Expression): boolean {
65
+ if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
66
+ if (!isTestFrameworkObject(sourceCode, callee.object)) return false;
67
+ const property = callee.property;
68
+ const method = callee.computed
69
+ ? property.type === "Literal" &&
70
+ (property.value === "doMock" ||
71
+ property.value === "mock" ||
72
+ property.value === "unstable_mockModule")
73
+ ? property.value
74
+ : null
75
+ : property.type === "Identifier"
76
+ ? property.name
77
+ : null;
78
+
79
+ return method !== null && moduleMockMethods.has(method);
80
+ }
81
+
82
+ /** Ban test framework module mocking in favor of real dependency seams. */
83
+ export const noModuleMockingRule = defineRule({
84
+ meta: {
85
+ type: "problem",
86
+ docs: {
87
+ description:
88
+ "Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces.",
89
+ },
90
+ messages: {
91
+ moduleMock:
92
+ "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.",
93
+ },
94
+ },
95
+ create(context) {
96
+ return {
97
+ CallExpression(node) {
98
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
99
+ if (moduleMockCall(context.sourceCode, node.callee)) {
100
+ context.report({ node, messageId: "moduleMock" });
101
+ }
102
+ },
103
+ };
104
+ },
105
+ });