@configg/bases 0.1.5 → 0.1.7

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 (36) hide show
  1. package/README.md +14 -0
  2. package/package.json +2 -1
  3. package/packages/oxc/src/anti-slop/LICENSE +21 -0
  4. package/packages/oxc/src/anti-slop/UPSTREAM.md +8 -0
  5. package/packages/oxc/src/anti-slop/index.ts +47 -0
  6. package/packages/oxc/src/anti-slop/rules/no-array-filter-map.ts +28 -0
  7. package/packages/oxc/src/anti-slop/rules/no-chained-type-assertions.ts +77 -0
  8. package/packages/oxc/src/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
  9. package/packages/oxc/src/anti-slop/rules/no-known-value-widening.ts +415 -0
  10. package/packages/oxc/src/anti-slop/rules/no-module-mocking.ts +80 -0
  11. package/packages/oxc/src/anti-slop/rules/no-object-parameters.ts +83 -0
  12. package/packages/oxc/src/anti-slop/rules/no-reduce-accumulator-copy.ts +109 -0
  13. package/packages/oxc/src/anti-slop/rules/no-reflect-apply.ts +28 -0
  14. package/packages/oxc/src/anti-slop/rules/no-reflect-get.ts +28 -0
  15. package/packages/oxc/src/anti-slop/rules/no-runtime-typeof.ts +77 -0
  16. package/packages/oxc/src/anti-slop/rules/no-shape-in-symbol-names.ts +46 -0
  17. package/packages/oxc/src/anti-slop/rules/no-unknown-parameters.ts +69 -0
  18. package/packages/oxc/src/anti-slop/rules/no-unknown-returns.ts +82 -0
  19. package/packages/oxc/src/anti-slop/rules/no-unknown-type-aliases.ts +54 -0
  20. package/packages/oxc/src/anti-slop/rules/no-unsafe-dictionary-type.ts +154 -0
  21. package/packages/oxc/src/anti-slop/rules/no-widen-then-assert.ts +366 -0
  22. package/packages/oxc/src/anti-slop/rules/require-readable-spacing.ts +47 -0
  23. package/packages/oxc/src/anti-slop/rules/require-safety-comment-for-type-assertion.ts +131 -0
  24. package/packages/oxc/src/anti-slop/shared/array-method.ts +94 -0
  25. package/packages/oxc/src/anti-slop/shared/dictionary-types.ts +515 -0
  26. package/packages/oxc/src/anti-slop/shared/function-parameters.ts +49 -0
  27. package/packages/oxc/src/anti-slop/shared/lexical-type-parameters.ts +61 -0
  28. package/packages/oxc/src/anti-slop/shared/reflect-method.ts +24 -0
  29. package/packages/oxc/src/anti-slop/shared/scope.ts +15 -0
  30. package/packages/oxc/src/anti-slop/shared/type-alias-resolution.ts +250 -0
  31. package/packages/oxc/src/anti-slop/vendor/eslint-stylistic/LICENSE +22 -0
  32. package/packages/oxc/src/anti-slop/vendor/eslint-stylistic/UPSTREAM.md +28 -0
  33. package/packages/oxc/src/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
  34. package/packages/oxc/src/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
  35. package/packages/oxc/src/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts +87 -0
  36. package/packages/oxc/src/lint.ts +31 -2
package/README.md CHANGED
@@ -62,6 +62,20 @@ Installing a single package works the same way, for example `@configg/oxc/fmt` a
62
62
  - `oxlint` exports `configs`: `client` (browser env, React, and a `**/*.spec.{ts,tsx}` override with Vitest and Testing Library rules) and `scripts` (Node env).
63
63
  - `tsconfig/bun` (`strictest` + Bun), `tsconfig/vite` (`strictest` + Vite/React) and `tsconfig/client` (`vite` with ambient `types` cleared, so a package only picks up the `@types/*` it asks for).
64
64
 
65
+ The Oxlint presets don't enable anti-slop. `antiSlop` from the oxlint entry is an opt-in set with the plugin and all 18 rules as errors; `unicorn/prefer-reflect-apply` is off. See [UPSTREAM.md](./packages/oxc/src/anti-slop/UPSTREAM.md) for its source and update policy.
66
+
67
+ ```ts
68
+ import { antiSlop, configs } from '@configg/bases/oxlint'
69
+
70
+ export default defineConfig({
71
+ ...configs.client,
72
+ jsPlugins: [...antiSlop.jsPlugins],
73
+ rules: { ...configs.client.rules, ...antiSlop.rules },
74
+ })
75
+ ```
76
+
77
+ If your config has its own `jsPlugins`, keep the anti-slop entry in that array. The plugin ships as TypeScript, so run oxlint under Bun (`bunx --bun oxlint`): Node refuses to strip types from files in `node_modules`.
78
+
65
79
  `eslint-plugin-testing-library` is only needed for `configs.client` in the oxlint preset.
66
80
 
67
81
  ## Development
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configg/bases",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "All @configg presets in one package: oxc and tsconfig",
5
5
  "keywords": [
6
6
  "config",
@@ -51,6 +51,7 @@
51
51
  "version:set": "bun scripts/versions.ts set"
52
52
  },
53
53
  "dependencies": {
54
+ "@oxlint/plugins": "^1.83.0",
54
55
  "@tsconfig/bases": "^1.0.28"
55
56
  },
56
57
  "devDependencies": {
@@ -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,8 @@
1
+ # Upstream
2
+
3
+ - Source: https://github.com/dmmulroy/anti-slop
4
+ - Commit: c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b
5
+ - Date: 2026-09-10
6
+ - Left out: tests and `effect/`.
7
+
8
+ Files are kept byte-identical to upstream so updates can be three-way merged; local policy (which rules are on) lives in `../lint.ts`.
@@ -0,0 +1,47 @@
1
+ import { eslintCompatPlugin } from "@oxlint/plugins";
2
+
3
+ import { noArrayFilterMapRule } from "./rules/no-array-filter-map.ts";
4
+ import { noReduceAccumulatorCopyRule } from "./rules/no-reduce-accumulator-copy.ts";
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 { requireReadableSpacingRule } from "./rules/require-readable-spacing.ts";
20
+ import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts";
21
+
22
+ /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
23
+ const antiSlopPlugin = eslintCompatPlugin({
24
+ meta: { name: "anti-slop" },
25
+ rules: {
26
+ "no-array-filter-map": noArrayFilterMapRule,
27
+ "no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
28
+ "no-chained-type-assertions": noChainedTypeAssertionsRule,
29
+ "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
30
+ "no-known-value-widening": noKnownValueWideningRule,
31
+ "no-module-mocking": noModuleMockingRule,
32
+ "no-object-parameters": noObjectParametersRule,
33
+ "no-reflect-apply": noReflectApplyRule,
34
+ "no-reflect-get": noReflectGetRule,
35
+ "no-runtime-typeof": noRuntimeTypeofRule,
36
+ "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
37
+ "no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
38
+ "no-unknown-parameters": noUnknownParametersRule,
39
+ "no-unknown-returns": noUnknownReturnsRule,
40
+ "no-unknown-type-aliases": noUnknownTypeAliasesRule,
41
+ "no-widen-then-assert": noWidenThenAssertRule,
42
+ "require-readable-spacing": requireReadableSpacingRule,
43
+ "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
44
+ },
45
+ });
46
+
47
+ export default antiSlopPlugin;
@@ -0,0 +1,28 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+
3
+ import { arrayMethodTarget, isKnownArrayExpression, unwrapArrayExpression } from "../shared/array-method.ts";
4
+
5
+ /** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */
6
+ export const noArrayFilterMapRule = defineRule({
7
+ meta: {
8
+ type: "suggestion",
9
+ docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
10
+ messages: {
11
+ arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.",
12
+ },
13
+ },
14
+ createOnce(context) {
15
+ return {
16
+ CallExpression(node) {
17
+ const outer = arrayMethodTarget(node.callee);
18
+ if (outer === null || (outer.name !== "map" && outer.name !== "filter")) return;
19
+ const innerCall = unwrapArrayExpression(outer.object);
20
+ if (innerCall.type !== "CallExpression") return;
21
+ const inner = arrayMethodTarget(innerCall.callee);
22
+ if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map")) return;
23
+ if (!isKnownArrayExpression(context.sourceCode, inner.object)) return;
24
+ context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } });
25
+ },
26
+ };
27
+ },
28
+ });
@@ -0,0 +1,77 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import type { ESTree } from "@oxlint/plugins";
3
+
4
+ type TypeAssertionExpression = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
5
+
6
+ function isTypeAssertionExpression(node: ESTree.Node): node is TypeAssertionExpression {
7
+ return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
8
+ }
9
+
10
+ function unwrapParenthesizedExpression(expression: ESTree.Expression): ESTree.Expression {
11
+ let current = expression;
12
+ while (current.type === "ParenthesizedExpression") {
13
+ current = current.expression;
14
+ }
15
+ return current;
16
+ }
17
+
18
+ function isConstAssertion(node: TypeAssertionExpression): boolean {
19
+ const { typeAnnotation } = node;
20
+ return (
21
+ typeAnnotation.type === "TSTypeReference" &&
22
+ typeAnnotation.typeName.type === "Identifier" &&
23
+ typeAnnotation.typeName.name === "const"
24
+ );
25
+ }
26
+
27
+ function isOutermostAssertionInChain(node: TypeAssertionExpression): boolean {
28
+ let current: ESTree.Expression = node;
29
+ let parent = node.parent;
30
+
31
+ while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
32
+ current = parent;
33
+ parent = parent.parent;
34
+ }
35
+
36
+ return !isTypeAssertionExpression(parent) || parent.expression !== current;
37
+ }
38
+
39
+ function isForbiddenAssertionChain(node: TypeAssertionExpression): boolean {
40
+ let assertionCount = 0;
41
+ let hasNonConstAssertion = false;
42
+ let current: ESTree.Expression = node;
43
+
44
+ while (isTypeAssertionExpression(current)) {
45
+ assertionCount += 1;
46
+ hasNonConstAssertion ||= !isConstAssertion(current);
47
+ current = unwrapParenthesizedExpression(current.expression);
48
+ }
49
+
50
+ return assertionCount > 1 && hasNonConstAssertion;
51
+ }
52
+
53
+ /** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
54
+ export const noChainedTypeAssertionsRule = defineRule({
55
+ meta: {
56
+ type: "problem",
57
+ docs: {
58
+ description:
59
+ "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.",
60
+ },
61
+ messages: {
62
+ chained:
63
+ "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
64
+ },
65
+ },
66
+ createOnce(context) {
67
+ const checkTypeAssertion = (node: TypeAssertionExpression) => {
68
+ if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
69
+ context.report({ node, messageId: "chained" });
70
+ };
71
+
72
+ return {
73
+ TSAsExpression: checkTypeAssertion,
74
+ TSTypeAssertion: checkTypeAssertion,
75
+ };
76
+ },
77
+ });
@@ -0,0 +1,49 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import type { ESTree } from "@oxlint/plugins";
3
+
4
+ function unwrapParentheses(node: ESTree.Expression): ESTree.Expression {
5
+ let current = node;
6
+ while (current.type === "ParenthesizedExpression") {
7
+ current = current.expression;
8
+ }
9
+ return current;
10
+ }
11
+
12
+ function isEmptyObjectExpression(node: ESTree.Expression): boolean {
13
+ return node.type === "ObjectExpression" && node.properties.length === 0;
14
+ }
15
+
16
+ function isConditionalEmptyObjectSpread(node: ESTree.Expression): boolean {
17
+ const conditional = unwrapParentheses(node);
18
+ return (
19
+ conditional.type === "ConditionalExpression" &&
20
+ (isEmptyObjectExpression(conditional.consequent) ||
21
+ isEmptyObjectExpression(conditional.alternate))
22
+ );
23
+ }
24
+
25
+ /** Ban conditional empty-object spreads without changing their omission semantics. */
26
+ export const noConditionalEmptyObjectSpreadRule = defineRule({
27
+ meta: {
28
+ type: "suggestion",
29
+ docs: {
30
+ description:
31
+ "Disallow object spreads that conditionally spread an empty object to omit fields.",
32
+ },
33
+ messages: {
34
+ avoid:
35
+ "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
36
+ },
37
+ },
38
+ createOnce(context) {
39
+ return {
40
+ SpreadElement(node) {
41
+ if (node.parent.type !== "ObjectExpression") return;
42
+
43
+ if (isConditionalEmptyObjectSpread(node.argument)) {
44
+ context.report({ node, messageId: "avoid" });
45
+ }
46
+ },
47
+ };
48
+ },
49
+ });
@@ -0,0 +1,415 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+
3
+ import {
4
+ classifyUnsafeDictionaryValue,
5
+ classifyWideningTarget,
6
+ createTypeEnvironment,
7
+ isKnownEvidenceExpression,
8
+ type TypeEnvironment,
9
+ type WideningTarget,
10
+ } from "../shared/dictionary-types.ts";
11
+ import {
12
+ containsUnknownType,
13
+ functionParameterBindingName,
14
+ functionParameterTypeAnnotation,
15
+ } from "../shared/function-parameters.ts";
16
+ import { resolveVariable } from "../shared/scope.ts";
17
+
18
+ import type { ESTree, SourceCode, Variable } from "@oxlint/plugins";
19
+
20
+ type FunctionExpression = ESTree.ArrowFunctionExpression | ESTree.Function;
21
+
22
+ function unwrapExpression(expression: ESTree.Expression): ESTree.Expression {
23
+ let current = expression;
24
+ while (
25
+ current.type === "ParenthesizedExpression" ||
26
+ current.type === "TSAsExpression" ||
27
+ current.type === "TSSatisfiesExpression" ||
28
+ current.type === "TSTypeAssertion" ||
29
+ current.type === "TSNonNullExpression"
30
+ ) {
31
+ current = current.expression;
32
+ }
33
+ return current;
34
+ }
35
+
36
+ function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
37
+ if (variable.defs.length !== 1) return null;
38
+ const [definition] = variable.defs;
39
+ return definition?.type === "Variable" && definition.node.type === "VariableDeclarator"
40
+ ? definition.node
41
+ : null;
42
+ }
43
+
44
+ function isStableConstVariable(variable: Variable, declarator: ESTree.VariableDeclarator): boolean {
45
+ return (
46
+ declarator.parent.type === "VariableDeclaration" &&
47
+ declarator.parent.kind === "const" &&
48
+ variable.references.every((reference) => reference.init || !reference.isWrite())
49
+ );
50
+ }
51
+
52
+ function hasKnownEvidence(
53
+ sourceCode: SourceCode,
54
+ expression: ESTree.Expression,
55
+ visitedVariables = new Set<Variable>(),
56
+ ): boolean {
57
+ if (isKnownEvidenceExpression(expression)) return true;
58
+ const unwrapped = unwrapExpression(expression);
59
+ if (unwrapped.type !== "Identifier") return false;
60
+ const variable = resolveVariable(sourceCode, unwrapped);
61
+ if (variable === null || visitedVariables.has(variable)) return false;
62
+ const declarator = variableDeclarator(variable);
63
+ if (
64
+ declarator === null ||
65
+ declarator.init === null ||
66
+ !isStableConstVariable(variable, declarator)
67
+ ) {
68
+ return false;
69
+ }
70
+ visitedVariables.add(variable);
71
+ return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
72
+ }
73
+
74
+ function isFunctionExpression(node: ESTree.Node): node is FunctionExpression {
75
+ return (
76
+ node.type === "ArrowFunctionExpression" ||
77
+ node.type === "FunctionDeclaration" ||
78
+ node.type === "FunctionExpression" ||
79
+ node.type === "TSDeclareFunction" ||
80
+ node.type === "TSEmptyBodyFunctionExpression"
81
+ );
82
+ }
83
+
84
+ function localFunctionForCall(
85
+ sourceCode: SourceCode,
86
+ callee: ESTree.Expression,
87
+ ): FunctionExpression | null {
88
+ const unwrapped = unwrapExpression(callee);
89
+ if (isFunctionExpression(unwrapped)) return unwrapped;
90
+ if (unwrapped.type !== "Identifier") return null;
91
+ const variable = resolveVariable(sourceCode, unwrapped);
92
+ if (variable === null || variable.defs.length !== 1) return null;
93
+ const [definition] = variable.defs;
94
+ if (definition === undefined) return null;
95
+ if (definition.type === "FunctionName" && isFunctionExpression(definition.node)) {
96
+ return definition.node;
97
+ }
98
+ if (definition.type !== "Variable" || definition.node.type !== "VariableDeclarator") {
99
+ return null;
100
+ }
101
+ const initializer = definition.node.init;
102
+ if (initializer === null) return null;
103
+ const unwrappedInitializer = unwrapExpression(initializer);
104
+ return isFunctionExpression(unwrappedInitializer) ? unwrappedInitializer : null;
105
+ }
106
+
107
+ function variableTypeAnnotation(
108
+ sourceCode: SourceCode,
109
+ variable: Variable,
110
+ ): ESTree.TSTypeAnnotation | null {
111
+ if (variable.defs.length !== 1) return null;
112
+ const [definition] = variable.defs;
113
+ if (definition === undefined) return null;
114
+ if (
115
+ definition.type === "Variable" &&
116
+ definition.node.type === "VariableDeclarator" &&
117
+ definition.node.id.type === "Identifier"
118
+ ) {
119
+ return definition.node.id.typeAnnotation ?? null;
120
+ }
121
+ if (definition.type !== "Parameter" || !isFunctionExpression(definition.node)) {
122
+ return null;
123
+ }
124
+ const parameter = definition.node.params.find(
125
+ (candidate) =>
126
+ functionParameterBindingName(candidate, sourceCode) === variable.name,
127
+ );
128
+ return parameter === undefined ? null : (functionParameterTypeAnnotation(parameter) ?? null);
129
+ }
130
+
131
+ function hasInformativeType(
132
+ type: ESTree.TSType,
133
+ environment: TypeEnvironment,
134
+ ): boolean {
135
+ return classifyUnsafeDictionaryValue(type, environment) === null;
136
+ }
137
+
138
+ function hasKnownCallArgumentEvidence(
139
+ sourceCode: SourceCode,
140
+ expression: ESTree.Expression,
141
+ environment: TypeEnvironment,
142
+ visitedVariables = new Set<Variable>(),
143
+ ): boolean {
144
+ if (expression.type === "ParenthesizedExpression" || expression.type === "TSNonNullExpression") {
145
+ return hasKnownCallArgumentEvidence(
146
+ sourceCode,
147
+ expression.expression,
148
+ environment,
149
+ visitedVariables,
150
+ );
151
+ }
152
+ if (expression.type === "TSAsExpression" || expression.type === "TSTypeAssertion") {
153
+ return hasInformativeType(expression.typeAnnotation, environment);
154
+ }
155
+ if (expression.type === "TSSatisfiesExpression") {
156
+ return hasKnownCallArgumentEvidence(
157
+ sourceCode,
158
+ expression.expression,
159
+ environment,
160
+ visitedVariables,
161
+ );
162
+ }
163
+ if (expression.type === "CallExpression") {
164
+ const owner = localFunctionForCall(sourceCode, expression.callee);
165
+ const returnType = owner?.returnType?.typeAnnotation;
166
+ return returnType !== undefined && hasInformativeType(returnType, environment);
167
+ }
168
+ if (expression.type !== "Identifier") return isKnownEvidenceExpression(expression);
169
+ const variable = resolveVariable(sourceCode, expression);
170
+ if (variable === null || visitedVariables.has(variable)) return false;
171
+ const annotation = variableTypeAnnotation(sourceCode, variable);
172
+ if (annotation !== null) {
173
+ return hasInformativeType(annotation.typeAnnotation, environment);
174
+ }
175
+ const declarator = variableDeclarator(variable);
176
+ if (
177
+ declarator === null ||
178
+ declarator.init === null ||
179
+ !isStableConstVariable(variable, declarator)
180
+ ) {
181
+ return false;
182
+ }
183
+ visitedVariables.add(variable);
184
+ return hasKnownCallArgumentEvidence(
185
+ sourceCode,
186
+ declarator.init,
187
+ environment,
188
+ visitedVariables,
189
+ );
190
+ }
191
+
192
+ function typePredicateSubjectIndex(
193
+ sourceCode: SourceCode,
194
+ owner: FunctionExpression,
195
+ ): number | null {
196
+ const predicate = owner.returnType?.typeAnnotation;
197
+ if (predicate?.type !== "TSTypePredicate" || predicate.parameterName.type !== "Identifier") {
198
+ return null;
199
+ }
200
+ const predicateParameterName = predicate.parameterName.name;
201
+ const index = owner.params.findIndex(
202
+ (parameter) =>
203
+ functionParameterBindingName(parameter, sourceCode) === predicateParameterName,
204
+ );
205
+ return index === -1 ? null : index;
206
+ }
207
+
208
+ function annotationTarget(
209
+ annotation: ESTree.TSTypeAnnotation | null | undefined,
210
+ environment: TypeEnvironment,
211
+ ): WideningTarget | null {
212
+ return annotation === null || annotation === undefined
213
+ ? null
214
+ : classifyWideningTarget(annotation.typeAnnotation, environment);
215
+ }
216
+
217
+ function enclosingFunction(node: ESTree.Node): FunctionExpression | null {
218
+ let current: ESTree.Node | null = node.parent;
219
+ while (current !== null && current.type !== "Program") {
220
+ if (
221
+ current.type === "ArrowFunctionExpression" ||
222
+ current.type === "FunctionDeclaration" ||
223
+ current.type === "FunctionExpression"
224
+ ) {
225
+ return current;
226
+ }
227
+ current = current.parent;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ function sourceKeyName(sourceCode: SourceCode, key: ESTree.PropertyKey): string {
233
+ if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
234
+ if (key.type === "Literal") return String(key.value);
235
+ return sourceCode.getText(key);
236
+ }
237
+
238
+ function functionName(sourceCode: SourceCode, owner: FunctionExpression | null): string {
239
+ if (owner === null) return "anonymous function";
240
+ if (owner.id !== null) return owner.id.name;
241
+ const parent = owner.parent;
242
+ if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier")
243
+ return parent.id.name;
244
+ if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
245
+ return "anonymous function";
246
+ }
247
+
248
+ function isEmptyObjectExpression(expression: ESTree.Expression): boolean {
249
+ const unwrapped = unwrapExpression(expression);
250
+ return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0;
251
+ }
252
+
253
+ function isDictionaryAccumulatorTarget(destination: WideningTarget): boolean {
254
+ return destination.kind === "open dictionary" || destination.kind === "generic container";
255
+ }
256
+
257
+ function hasParentAssertion(node: ESTree.Node): boolean {
258
+ return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
259
+ }
260
+
261
+ /** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
262
+ export const noKnownValueWideningRule = defineRule({
263
+ meta: {
264
+ type: "problem",
265
+ docs: {
266
+ description:
267
+ "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.",
268
+ },
269
+ messages: {
270
+ widening:
271
+ "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract.",
272
+ },
273
+ },
274
+ createOnce(context) {
275
+ let environment: TypeEnvironment | null = null;
276
+
277
+ const reportFlow = (
278
+ expression: ESTree.Expression,
279
+ destination: WideningTarget | null,
280
+ subject: string,
281
+ ) => {
282
+ if (destination === null) return;
283
+ if (
284
+ isDictionaryAccumulatorTarget(destination) &&
285
+ isEmptyObjectExpression(expression)
286
+ ) {
287
+ return;
288
+ }
289
+ if (!hasKnownEvidence(context.sourceCode, expression)) return;
290
+ context.report({
291
+ node: expression,
292
+ messageId: "widening",
293
+ data: { subject, target: destination.kind },
294
+ });
295
+ };
296
+
297
+ const targetFromAnnotation = (annotation: ESTree.TSTypeAnnotation | null | undefined) =>
298
+ environment === null ? null : annotationTarget(annotation, environment);
299
+
300
+ return {
301
+ Program(node) {
302
+ environment = createTypeEnvironment(
303
+ node,
304
+ context.sourceCode.visitorKeys,
305
+ );
306
+ },
307
+ VariableDeclarator(node) {
308
+ if (node.init === null || node.id.type !== "Identifier") return;
309
+ reportFlow(
310
+ node.init,
311
+ targetFromAnnotation(node.id.typeAnnotation),
312
+ `binding \`${node.id.name}\``,
313
+ );
314
+ },
315
+ PropertyDefinition(node) {
316
+ if (node.value === null) return;
317
+ reportFlow(
318
+ node.value,
319
+ targetFromAnnotation(node.typeAnnotation),
320
+ `property \`${sourceKeyName(context.sourceCode, node.key)}\``,
321
+ );
322
+ },
323
+ AccessorProperty(node) {
324
+ if (node.value === null) return;
325
+ reportFlow(
326
+ node.value,
327
+ targetFromAnnotation(node.typeAnnotation),
328
+ `property \`${sourceKeyName(context.sourceCode, node.key)}\``,
329
+ );
330
+ },
331
+ AssignmentExpression(node) {
332
+ if (node.operator !== "=" || node.left.type !== "Identifier") return;
333
+ const variable = resolveVariable(context.sourceCode, node.left);
334
+ if (variable === null) return;
335
+ const declarator = variableDeclarator(variable);
336
+ if (declarator === null || declarator.id.type !== "Identifier") return;
337
+ reportFlow(
338
+ node.right,
339
+ targetFromAnnotation(declarator.id.typeAnnotation),
340
+ `binding \`${declarator.id.name}\``,
341
+ );
342
+ },
343
+ CallExpression(node) {
344
+ if (environment === null) return;
345
+ const owner = localFunctionForCall(context.sourceCode, node.callee);
346
+ if (owner === null) return;
347
+ const parameterIndex = typePredicateSubjectIndex(context.sourceCode, owner);
348
+ if (parameterIndex === null) return;
349
+ const parameter = owner.params[parameterIndex];
350
+ const argument = node.arguments[parameterIndex];
351
+ if (parameter === undefined || argument === undefined || argument.type === "SpreadElement") {
352
+ return;
353
+ }
354
+ const parameterAnnotation = functionParameterTypeAnnotation(parameter);
355
+ if (
356
+ parameterAnnotation === null ||
357
+ parameterAnnotation === undefined ||
358
+ !containsUnknownType(parameterAnnotation.typeAnnotation)
359
+ ) {
360
+ return;
361
+ }
362
+ if (
363
+ !hasKnownCallArgumentEvidence(
364
+ context.sourceCode,
365
+ argument,
366
+ environment,
367
+ )
368
+ ) {
369
+ return;
370
+ }
371
+ context.report({
372
+ node: argument,
373
+ messageId: "widening",
374
+ data: {
375
+ subject: `argument for parameter \`${functionParameterBindingName(parameter, context.sourceCode)}\` of \`${functionName(context.sourceCode, owner)}\``,
376
+ target: "unknown",
377
+ },
378
+ });
379
+ },
380
+ ReturnStatement(node) {
381
+ if (node.argument === null) return;
382
+ const owner = enclosingFunction(node);
383
+ reportFlow(
384
+ node.argument,
385
+ targetFromAnnotation(owner?.returnType),
386
+ `return value of \`${functionName(context.sourceCode, owner)}\``,
387
+ );
388
+ },
389
+ ArrowFunctionExpression(node) {
390
+ if (node.body.type === "BlockStatement") return;
391
+ reportFlow(
392
+ node.body,
393
+ targetFromAnnotation(node.returnType),
394
+ `return value of \`${functionName(context.sourceCode, node)}\``,
395
+ );
396
+ },
397
+ TSAsExpression(node) {
398
+ if (environment === null || hasParentAssertion(node)) return;
399
+ reportFlow(
400
+ node.expression,
401
+ classifyWideningTarget(node.typeAnnotation, environment),
402
+ "assertion",
403
+ );
404
+ },
405
+ TSTypeAssertion(node) {
406
+ if (environment === null || hasParentAssertion(node)) return;
407
+ reportFlow(
408
+ node.expression,
409
+ classifyWideningTarget(node.typeAnnotation, environment),
410
+ "assertion",
411
+ );
412
+ },
413
+ };
414
+ },
415
+ });