@jameswyse/oxc-config 0.1.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/LICENSE +21 -0
- package/README.md +213 -0
- package/REFERENCE.md +134 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +97 -0
- package/dist/lint.d.ts +33 -0
- package/dist/lint.js +493 -0
- package/dist/rules/companion-has-owner.d.ts +1 -0
- package/dist/rules/companion-has-owner.js +74 -0
- package/dist/rules/electron-no-raw-ipc-renderer-exposure.d.ts +1 -0
- package/dist/rules/electron-no-raw-ipc-renderer-exposure.js +127 -0
- package/dist/rules/electron-no-remote-module.d.ts +1 -0
- package/dist/rules/electron-no-remote-module.js +112 -0
- package/dist/rules/electron-secure-web-preferences.d.ts +1 -0
- package/dist/rules/electron-secure-web-preferences.js +108 -0
- package/dist/rules/explicit-api-states.d.ts +1 -0
- package/dist/rules/explicit-api-states.js +143 -0
- package/dist/rules/import-boundaries.d.ts +1 -0
- package/dist/rules/import-boundaries.js +99 -0
- package/dist/rules/no-ambient-clock.d.ts +1 -0
- package/dist/rules/no-ambient-clock.js +35 -0
- package/dist/rules/no-ambient-random.d.ts +1 -0
- package/dist/rules/no-ambient-random.js +71 -0
- package/dist/rules/no-comments.d.ts +1 -0
- package/dist/rules/no-comments.js +64 -0
- package/dist/rules/no-deep-ternary.d.ts +1 -0
- package/dist/rules/no-deep-ternary.js +32 -0
- package/dist/rules/no-edit-proof-assertions.d.ts +1 -0
- package/dist/rules/no-edit-proof-assertions.js +120 -0
- package/dist/rules/no-env-access.d.ts +1 -0
- package/dist/rules/no-env-access.js +35 -0
- package/dist/rules/no-fixed-test-delay.d.ts +1 -0
- package/dist/rules/no-fixed-test-delay.js +130 -0
- package/dist/rules/no-forwarding-module.d.ts +1 -0
- package/dist/rules/no-forwarding-module.js +158 -0
- package/dist/rules/no-generic-file-suffix.d.ts +1 -0
- package/dist/rules/no-generic-file-suffix.js +74 -0
- package/dist/rules/no-pass-through-function.d.ts +1 -0
- package/dist/rules/no-pass-through-function.js +106 -0
- package/dist/rules/no-swallowed-errors.d.ts +1 -0
- package/dist/rules/no-swallowed-errors.js +168 -0
- package/dist/rules/no-test-timeout-overrides.d.ts +1 -0
- package/dist/rules/no-test-timeout-overrides.js +105 -0
- package/dist/rules/no-vacuous-test-assertions.d.ts +1 -0
- package/dist/rules/no-vacuous-test-assertions.js +71 -0
- package/dist/rules/server-action-guard.d.ts +1 -0
- package/dist/rules/server-action-guard.js +109 -0
- package/dist/rules/statement-spacing.d.ts +1 -0
- package/dist/rules/statement-spacing.js +29 -0
- package/dist/rules/type-guard-name.d.ts +1 -0
- package/dist/rules/type-guard-name.js +59 -0
- package/dist/shared/index.d.ts +26 -0
- package/dist/shared/index.js +260 -0
- package/dist/shared/rule-options.d.ts +2 -0
- package/dist/shared/rule-options.js +8 -0
- package/dist/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.d.ts +1 -0
- package/dist/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.js +35 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.d.ts +1 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +48 -0
- package/dist/vendor/anti-slop/effect/shared/tagged-values.d.ts +9 -0
- package/dist/vendor/anti-slop/effect/shared/tagged-values.js +65 -0
- package/dist/vendor/anti-slop/rules/no-array-filter-map.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-array-filter-map.js +40 -0
- package/dist/vendor/anti-slop/rules/no-chained-type-assertions.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-chained-type-assertions.js +58 -0
- package/dist/vendor/anti-slop/rules/no-conditional-empty-object-spread.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-conditional-empty-object-spread.js +41 -0
- package/dist/vendor/anti-slop/rules/no-known-value-widening.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-known-value-widening.js +333 -0
- package/dist/vendor/anti-slop/rules/no-module-mocking.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-module-mocking.js +74 -0
- package/dist/vendor/anti-slop/rules/no-object-parameters.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-object-parameters.js +59 -0
- package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.js +124 -0
- package/dist/vendor/anti-slop/rules/no-reflect-apply.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reflect-apply.js +26 -0
- package/dist/vendor/anti-slop/rules/no-reflect-get.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reflect-get.js +26 -0
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +63 -0
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +37 -0
- package/dist/vendor/anti-slop/rules/no-unknown-returns.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unknown-returns.js +61 -0
- package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.js +38 -0
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +143 -0
- package/dist/vendor/anti-slop/rules/no-widen-then-assert.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-widen-then-assert.js +288 -0
- package/dist/vendor/anti-slop/shared/array-method.d.ts +10 -0
- package/dist/vendor/anti-slop/shared/array-method.js +95 -0
- package/dist/vendor/anti-slop/shared/dictionary-types.d.ts +23 -0
- package/dist/vendor/anti-slop/shared/dictionary-types.js +408 -0
- package/dist/vendor/anti-slop/shared/function-parameters.d.ts +6 -0
- package/dist/vendor/anti-slop/shared/function-parameters.js +41 -0
- package/dist/vendor/anti-slop/shared/lexical-type-parameters.d.ts +4 -0
- package/dist/vendor/anti-slop/shared/lexical-type-parameters.js +32 -0
- package/dist/vendor/anti-slop/shared/reflect-method.d.ts +3 -0
- package/dist/vendor/anti-slop/shared/reflect-method.js +24 -0
- package/dist/vendor/anti-slop/shared/scope.d.ts +3 -0
- package/dist/vendor/anti-slop/shared/scope.js +12 -0
- package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +21 -0
- package/dist/vendor/anti-slop/shared/type-alias-resolution.js +147 -0
- package/dist/vendor/eslint-stylistic/padding-line-ast.d.ts +14 -0
- package/dist/vendor/eslint-stylistic/padding-line-ast.js +19 -0
- package/dist/vendor/eslint-stylistic/padding-line-between-statements.d.ts +91 -0
- package/dist/vendor/eslint-stylistic/padding-line-between-statements.js +493 -0
- package/package.json +89 -0
- package/src/vendor/anti-slop/LICENSE +21 -0
- package/src/vendor/eslint-stylistic/LICENSE +22 -0
- package/tsconfig.base.json +10 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Adapted from dmmulroy/anti-slop
|
|
2
|
+
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
function unwrapParentheses(node) {
|
|
4
|
+
let current = node;
|
|
5
|
+
while (current.type === "ParenthesizedExpression") {
|
|
6
|
+
current = current.expression;
|
|
7
|
+
}
|
|
8
|
+
return current;
|
|
9
|
+
}
|
|
10
|
+
function isEmptyObjectExpression(node) {
|
|
11
|
+
return node.type === "ObjectExpression" && node.properties.length === 0;
|
|
12
|
+
}
|
|
13
|
+
function isConditionalEmptyObjectSpread(node) {
|
|
14
|
+
const conditional = unwrapParentheses(node);
|
|
15
|
+
return (conditional.type === "ConditionalExpression" &&
|
|
16
|
+
(isEmptyObjectExpression(conditional.consequent) ||
|
|
17
|
+
isEmptyObjectExpression(conditional.alternate)));
|
|
18
|
+
}
|
|
19
|
+
export const noConditionalEmptyObjectSpread = defineRule({
|
|
20
|
+
meta: {
|
|
21
|
+
type: "suggestion",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "Disallow object spreads that conditionally spread an empty object to omit fields.",
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
avoid: "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
createOnce(context) {
|
|
30
|
+
return {
|
|
31
|
+
SpreadElement(node) {
|
|
32
|
+
if (node.parent.type !== "ObjectExpression") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (isConditionalEmptyObjectSpread(node.argument)) {
|
|
36
|
+
context.report({ node, messageId: "avoid" });
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noKnownValueWidening: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// Adapted from dmmulroy/anti-slop
|
|
2
|
+
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { classifyUnsafeDictionaryValue, classifyWideningTarget, createTypeEnvironment, isKnownEvidenceExpression, } from "../shared/dictionary-types.js";
|
|
4
|
+
import { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation, } from "../shared/function-parameters.js";
|
|
5
|
+
import { resolveVariable } from "../shared/scope.js";
|
|
6
|
+
function unwrapExpression(expression) {
|
|
7
|
+
let current = expression;
|
|
8
|
+
while (current.type === "ParenthesizedExpression" ||
|
|
9
|
+
current.type === "TSAsExpression" ||
|
|
10
|
+
current.type === "TSSatisfiesExpression" ||
|
|
11
|
+
current.type === "TSTypeAssertion" ||
|
|
12
|
+
current.type === "TSNonNullExpression") {
|
|
13
|
+
current = current.expression;
|
|
14
|
+
}
|
|
15
|
+
return current;
|
|
16
|
+
}
|
|
17
|
+
function variableDeclarator(variable) {
|
|
18
|
+
if (variable.defs.length !== 1) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const [definition] = variable.defs;
|
|
22
|
+
return definition?.type === "Variable" && definition.node.type === "VariableDeclarator"
|
|
23
|
+
? definition.node
|
|
24
|
+
: null;
|
|
25
|
+
}
|
|
26
|
+
function isStableConstVariable(variable, declarator) {
|
|
27
|
+
return (declarator.parent.type === "VariableDeclaration" &&
|
|
28
|
+
declarator.parent.kind === "const" &&
|
|
29
|
+
variable.references.every((reference) => reference.init || !reference.isWrite()));
|
|
30
|
+
}
|
|
31
|
+
function hasKnownEvidence(sourceCode, expression, visitedVariables = new Set()) {
|
|
32
|
+
if (isKnownEvidenceExpression(expression)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const unwrapped = unwrapExpression(expression);
|
|
36
|
+
if (unwrapped.type !== "Identifier") {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const variable = resolveVariable(sourceCode, unwrapped);
|
|
40
|
+
if (variable === null || visitedVariables.has(variable)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const declarator = variableDeclarator(variable);
|
|
44
|
+
if (declarator === null ||
|
|
45
|
+
declarator.init === null ||
|
|
46
|
+
!isStableConstVariable(variable, declarator)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
visitedVariables.add(variable);
|
|
50
|
+
return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
|
|
51
|
+
}
|
|
52
|
+
function isFunctionExpression(node) {
|
|
53
|
+
return (node.type === "ArrowFunctionExpression" ||
|
|
54
|
+
node.type === "FunctionDeclaration" ||
|
|
55
|
+
node.type === "FunctionExpression" ||
|
|
56
|
+
node.type === "TSDeclareFunction" ||
|
|
57
|
+
node.type === "TSEmptyBodyFunctionExpression");
|
|
58
|
+
}
|
|
59
|
+
function localFunctionForCall(sourceCode, callee) {
|
|
60
|
+
const unwrapped = unwrapExpression(callee);
|
|
61
|
+
if (isFunctionExpression(unwrapped)) {
|
|
62
|
+
return unwrapped;
|
|
63
|
+
}
|
|
64
|
+
if (unwrapped.type !== "Identifier") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const variable = resolveVariable(sourceCode, unwrapped);
|
|
68
|
+
if (variable === null || variable.defs.length !== 1) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const [definition] = variable.defs;
|
|
72
|
+
if (definition === void 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (definition.type === "FunctionName" && isFunctionExpression(definition.node)) {
|
|
76
|
+
return definition.node;
|
|
77
|
+
}
|
|
78
|
+
if (definition.type !== "Variable" || definition.node.type !== "VariableDeclarator") {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const initializer = definition.node.init;
|
|
82
|
+
if (initializer === null) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const unwrappedInitializer = unwrapExpression(initializer);
|
|
86
|
+
return isFunctionExpression(unwrappedInitializer) ? unwrappedInitializer : null;
|
|
87
|
+
}
|
|
88
|
+
function variableTypeAnnotation(sourceCode, variable) {
|
|
89
|
+
if (variable.defs.length !== 1) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const [definition] = variable.defs;
|
|
93
|
+
if (definition === void 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (definition.type === "Variable" &&
|
|
97
|
+
definition.node.type === "VariableDeclarator" &&
|
|
98
|
+
definition.node.id.type === "Identifier") {
|
|
99
|
+
return definition.node.id.typeAnnotation ?? null;
|
|
100
|
+
}
|
|
101
|
+
if (definition.type !== "Parameter" || !isFunctionExpression(definition.node)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const parameter = definition.node.params.find((candidate) => functionParameterBindingName(candidate, sourceCode) === variable.name);
|
|
105
|
+
return parameter === void 0 ? null : (functionParameterTypeAnnotation(parameter) ?? null);
|
|
106
|
+
}
|
|
107
|
+
function hasInformativeType(type, environment) {
|
|
108
|
+
return classifyUnsafeDictionaryValue(type, environment) === null;
|
|
109
|
+
}
|
|
110
|
+
function hasKnownCallArgumentEvidence(sourceCode, expression, environment, visitedVariables = new Set()) {
|
|
111
|
+
if (expression.type === "ParenthesizedExpression" || expression.type === "TSNonNullExpression") {
|
|
112
|
+
return hasKnownCallArgumentEvidence(sourceCode, expression.expression, environment, visitedVariables);
|
|
113
|
+
}
|
|
114
|
+
if (expression.type === "TSAsExpression" || expression.type === "TSTypeAssertion") {
|
|
115
|
+
return hasInformativeType(expression.typeAnnotation, environment);
|
|
116
|
+
}
|
|
117
|
+
if (expression.type === "TSSatisfiesExpression") {
|
|
118
|
+
return hasKnownCallArgumentEvidence(sourceCode, expression.expression, environment, visitedVariables);
|
|
119
|
+
}
|
|
120
|
+
if (expression.type === "CallExpression") {
|
|
121
|
+
const owner = localFunctionForCall(sourceCode, expression.callee);
|
|
122
|
+
const returnType = owner?.returnType?.typeAnnotation;
|
|
123
|
+
return returnType !== void 0 && hasInformativeType(returnType, environment);
|
|
124
|
+
}
|
|
125
|
+
if (expression.type !== "Identifier") {
|
|
126
|
+
return isKnownEvidenceExpression(expression);
|
|
127
|
+
}
|
|
128
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
129
|
+
if (variable === null || visitedVariables.has(variable)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const annotation = variableTypeAnnotation(sourceCode, variable);
|
|
133
|
+
if (annotation !== null) {
|
|
134
|
+
return hasInformativeType(annotation.typeAnnotation, environment);
|
|
135
|
+
}
|
|
136
|
+
const declarator = variableDeclarator(variable);
|
|
137
|
+
if (declarator === null ||
|
|
138
|
+
declarator.init === null ||
|
|
139
|
+
!isStableConstVariable(variable, declarator)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
visitedVariables.add(variable);
|
|
143
|
+
return hasKnownCallArgumentEvidence(sourceCode, declarator.init, environment, visitedVariables);
|
|
144
|
+
}
|
|
145
|
+
function typePredicateSubjectIndex(sourceCode, owner) {
|
|
146
|
+
const predicate = owner.returnType?.typeAnnotation;
|
|
147
|
+
if (predicate?.type !== "TSTypePredicate" || predicate.parameterName.type !== "Identifier") {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const predicateParameterName = predicate.parameterName.name;
|
|
151
|
+
const index = owner.params.findIndex((parameter) => functionParameterBindingName(parameter, sourceCode) === predicateParameterName);
|
|
152
|
+
return index === -1 ? null : index;
|
|
153
|
+
}
|
|
154
|
+
function annotationTarget(annotation, environment) {
|
|
155
|
+
return annotation === null || annotation === void 0
|
|
156
|
+
? null
|
|
157
|
+
: classifyWideningTarget(annotation.typeAnnotation, environment);
|
|
158
|
+
}
|
|
159
|
+
function enclosingFunction(node) {
|
|
160
|
+
let current = node.parent;
|
|
161
|
+
while (current !== null && current.type !== "Program") {
|
|
162
|
+
if (current.type === "ArrowFunctionExpression" ||
|
|
163
|
+
current.type === "FunctionDeclaration" ||
|
|
164
|
+
current.type === "FunctionExpression") {
|
|
165
|
+
return current;
|
|
166
|
+
}
|
|
167
|
+
current = current.parent;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function sourceKeyName(sourceCode, key) {
|
|
172
|
+
if (key.type === "Identifier" || key.type === "PrivateIdentifier") {
|
|
173
|
+
return key.name;
|
|
174
|
+
}
|
|
175
|
+
if (key.type === "Literal") {
|
|
176
|
+
return String(key.value);
|
|
177
|
+
}
|
|
178
|
+
return sourceCode.getText(key);
|
|
179
|
+
}
|
|
180
|
+
function functionName(sourceCode, owner) {
|
|
181
|
+
if (owner === null) {
|
|
182
|
+
return "anonymous function";
|
|
183
|
+
}
|
|
184
|
+
if (owner.id !== null) {
|
|
185
|
+
return owner.id.name;
|
|
186
|
+
}
|
|
187
|
+
const parent = owner.parent;
|
|
188
|
+
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") {
|
|
189
|
+
return parent.id.name;
|
|
190
|
+
}
|
|
191
|
+
if (parent.type === "MethodDefinition") {
|
|
192
|
+
return sourceKeyName(sourceCode, parent.key);
|
|
193
|
+
}
|
|
194
|
+
return "anonymous function";
|
|
195
|
+
}
|
|
196
|
+
function isEmptyObjectExpression(expression) {
|
|
197
|
+
const unwrapped = unwrapExpression(expression);
|
|
198
|
+
return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0;
|
|
199
|
+
}
|
|
200
|
+
function isDictionaryAccumulatorTarget(destination) {
|
|
201
|
+
return destination.kind === "open dictionary" || destination.kind === "generic container";
|
|
202
|
+
}
|
|
203
|
+
function hasParentAssertion(node) {
|
|
204
|
+
return node.parent.type === "TSAsExpression" || node.parent.type === "TSTypeAssertion";
|
|
205
|
+
}
|
|
206
|
+
export const noKnownValueWidening = defineRule({
|
|
207
|
+
meta: {
|
|
208
|
+
type: "problem",
|
|
209
|
+
docs: {
|
|
210
|
+
description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.",
|
|
211
|
+
},
|
|
212
|
+
messages: {
|
|
213
|
+
widening: "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
createOnce(context) {
|
|
217
|
+
let environment = null;
|
|
218
|
+
const reportFlow = (expression, destination, subject) => {
|
|
219
|
+
if (destination === null) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (isDictionaryAccumulatorTarget(destination) && isEmptyObjectExpression(expression)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!hasKnownEvidence(context.sourceCode, expression)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
context.report({
|
|
229
|
+
node: expression,
|
|
230
|
+
messageId: "widening",
|
|
231
|
+
data: { subject, target: destination.kind },
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
|
|
235
|
+
return {
|
|
236
|
+
Program(node) {
|
|
237
|
+
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
|
|
238
|
+
},
|
|
239
|
+
VariableDeclarator(node) {
|
|
240
|
+
if (node.init === null || node.id.type !== "Identifier") {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
reportFlow(node.init, targetFromAnnotation(node.id.typeAnnotation), `binding \`${node.id.name}\``);
|
|
244
|
+
},
|
|
245
|
+
PropertyDefinition(node) {
|
|
246
|
+
if (node.value === null) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
250
|
+
},
|
|
251
|
+
AccessorProperty(node) {
|
|
252
|
+
if (node.value === null) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
256
|
+
},
|
|
257
|
+
AssignmentExpression(node) {
|
|
258
|
+
if (node.operator !== "=" || node.left.type !== "Identifier") {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const variable = resolveVariable(context.sourceCode, node.left);
|
|
262
|
+
if (variable === null) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const declarator = variableDeclarator(variable);
|
|
266
|
+
if (declarator === null || declarator.id.type !== "Identifier") {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
reportFlow(node.right, targetFromAnnotation(declarator.id.typeAnnotation), `binding \`${declarator.id.name}\``);
|
|
270
|
+
},
|
|
271
|
+
CallExpression(node) {
|
|
272
|
+
if (environment === null) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const owner = localFunctionForCall(context.sourceCode, node.callee);
|
|
276
|
+
if (owner === null) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const parameterIndex = typePredicateSubjectIndex(context.sourceCode, owner);
|
|
280
|
+
if (parameterIndex === null) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const parameter = owner.params[parameterIndex];
|
|
284
|
+
const argument = node.arguments[parameterIndex];
|
|
285
|
+
if (parameter === void 0 || argument === void 0 || argument.type === "SpreadElement") {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const parameterAnnotation = functionParameterTypeAnnotation(parameter);
|
|
289
|
+
if (parameterAnnotation === null ||
|
|
290
|
+
parameterAnnotation === void 0 ||
|
|
291
|
+
!containsUnknownType(parameterAnnotation.typeAnnotation)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!hasKnownCallArgumentEvidence(context.sourceCode, argument, environment)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
context.report({
|
|
298
|
+
node: argument,
|
|
299
|
+
messageId: "widening",
|
|
300
|
+
data: {
|
|
301
|
+
subject: `argument for parameter \`${functionParameterBindingName(parameter, context.sourceCode)}\` of \`${functionName(context.sourceCode, owner)}\``,
|
|
302
|
+
target: "unknown",
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
ReturnStatement(node) {
|
|
307
|
+
if (node.argument === null) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const owner = enclosingFunction(node);
|
|
311
|
+
reportFlow(node.argument, targetFromAnnotation(owner?.returnType), `return value of \`${functionName(context.sourceCode, owner)}\``);
|
|
312
|
+
},
|
|
313
|
+
ArrowFunctionExpression(node) {
|
|
314
|
+
if (node.body.type === "BlockStatement") {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
reportFlow(node.body, targetFromAnnotation(node.returnType), `return value of \`${functionName(context.sourceCode, node)}\``);
|
|
318
|
+
},
|
|
319
|
+
TSAsExpression(node) {
|
|
320
|
+
if (environment === null || hasParentAssertion(node)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion");
|
|
324
|
+
},
|
|
325
|
+
TSTypeAssertion(node) {
|
|
326
|
+
if (environment === null || hasParentAssertion(node)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion");
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noModuleMocking: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Adapted from dmmulroy/anti-slop
|
|
2
|
+
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { resolveVariable } from "../shared/scope.js";
|
|
4
|
+
const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
|
|
5
|
+
function importedName(node) {
|
|
6
|
+
if (node.type !== "ImportSpecifier") {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
|
|
10
|
+
}
|
|
11
|
+
function isTestFrameworkObject(sourceCode, expression) {
|
|
12
|
+
if (expression.type !== "Identifier") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if ((expression.name === "vi" || expression.name === "jest") &&
|
|
16
|
+
sourceCode.isGlobalReference(expression)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
20
|
+
if (variable === null || variable.defs.length === 0) {
|
|
21
|
+
return expression.name === "vi" || expression.name === "jest";
|
|
22
|
+
}
|
|
23
|
+
return variable.defs.some((definition) => {
|
|
24
|
+
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const source = definition.parent.source.value;
|
|
28
|
+
const name = importedName(definition.node);
|
|
29
|
+
return ((source === "vitest" && name === "vi") || (source === "@jest/globals" && name === "jest"));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function moduleMockCall(sourceCode, callee) {
|
|
33
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (!isTestFrameworkObject(sourceCode, callee.object)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const property = callee.property;
|
|
40
|
+
const method = callee.computed
|
|
41
|
+
? property.type === "Literal" &&
|
|
42
|
+
(property.value === "doMock" ||
|
|
43
|
+
property.value === "mock" ||
|
|
44
|
+
property.value === "unstable_mockModule")
|
|
45
|
+
? property.value
|
|
46
|
+
: null
|
|
47
|
+
: property.type === "Identifier"
|
|
48
|
+
? property.name
|
|
49
|
+
: null;
|
|
50
|
+
return method !== null && moduleMockMethods.has(method);
|
|
51
|
+
}
|
|
52
|
+
export const noModuleMocking = defineRule({
|
|
53
|
+
meta: {
|
|
54
|
+
type: "problem",
|
|
55
|
+
docs: {
|
|
56
|
+
description: "Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces.",
|
|
57
|
+
},
|
|
58
|
+
messages: {
|
|
59
|
+
moduleMock: "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
createOnce(context) {
|
|
63
|
+
return {
|
|
64
|
+
CallExpression(node) {
|
|
65
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (moduleMockCall(context.sourceCode, node.callee)) {
|
|
69
|
+
context.report({ node, messageId: "moduleMock" });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noObjectParameters: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Adapted from dmmulroy/anti-slop
|
|
2
|
+
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { functionParameterBindingName, functionParameterTypeAnnotation, } from "../shared/function-parameters.js";
|
|
4
|
+
import { createTypeAliasEnvironment, resolvedTypeMatches, } from "../shared/type-alias-resolution.js";
|
|
5
|
+
export const noObjectParameters = defineRule({
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary.",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
objectParameter: "Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function.",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
createOnce(context) {
|
|
16
|
+
let environment = null;
|
|
17
|
+
const resolvesToObject = (type) => environment !== null &&
|
|
18
|
+
resolvedTypeMatches(type, environment, (resolved, matches) => {
|
|
19
|
+
if (resolved.type === "TSObjectKeyword") {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (resolved.type === "TSParenthesizedType") {
|
|
23
|
+
return matches(resolved.typeAnnotation);
|
|
24
|
+
}
|
|
25
|
+
return resolved.type === "TSUnionType" && resolved.types.some(matches);
|
|
26
|
+
});
|
|
27
|
+
const checkParameters = (node) => {
|
|
28
|
+
for (const parameter of node.params) {
|
|
29
|
+
const annotation = functionParameterTypeAnnotation(parameter);
|
|
30
|
+
if (annotation === null || annotation === void 0) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!resolvesToObject(annotation.typeAnnotation)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
context.report({
|
|
37
|
+
node: annotation.typeAnnotation,
|
|
38
|
+
messageId: "objectParameter",
|
|
39
|
+
data: { parameter: functionParameterBindingName(parameter, context.sourceCode) },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
Program(node) {
|
|
45
|
+
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
46
|
+
},
|
|
47
|
+
ArrowFunctionExpression: checkParameters,
|
|
48
|
+
FunctionDeclaration: checkParameters,
|
|
49
|
+
FunctionExpression: checkParameters,
|
|
50
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
51
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
52
|
+
TSConstructorType: checkParameters,
|
|
53
|
+
TSDeclareFunction: checkParameters,
|
|
54
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
55
|
+
TSFunctionType: checkParameters,
|
|
56
|
+
TSMethodSignature: checkParameters,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noReduceAccumulatorCopy: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Adapted from dmmulroy/anti-slop
|
|
2
|
+
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { arrayMethodTarget, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression, } from "../shared/array-method.js";
|
|
4
|
+
function enclosingReducer(node) {
|
|
5
|
+
let parent = node.parent;
|
|
6
|
+
while (parent !== null) {
|
|
7
|
+
if (parent.type === "FunctionDeclaration") {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
|
|
11
|
+
const callback = parent;
|
|
12
|
+
let owner = callback.parent;
|
|
13
|
+
while (owner !== null && unwrapArrayExpression(owner) === callback) {
|
|
14
|
+
owner = owner.parent;
|
|
15
|
+
}
|
|
16
|
+
if (owner?.type !== "CallExpression") {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const method = arrayMethodTarget(owner.callee);
|
|
20
|
+
const firstArgument = owner.arguments[0];
|
|
21
|
+
if (method === null ||
|
|
22
|
+
(method.name !== "reduce" && method.name !== "reduceRight") ||
|
|
23
|
+
owner.arguments.length > 2 ||
|
|
24
|
+
firstArgument === void 0 ||
|
|
25
|
+
unwrapArrayExpression(firstArgument) !== callback) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const firstParameter = callback.params[0];
|
|
29
|
+
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
|
|
30
|
+
if (accumulator?.type !== "Identifier") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return { callback, accumulator, initialValue: owner.arguments[1] };
|
|
34
|
+
}
|
|
35
|
+
parent = parent.parent;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function referencesAccumulator(sourceCode, node, accumulator, visited = new Set()) {
|
|
40
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
41
|
+
if (variable === null || visited.has(variable)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (variable === accumulator) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
visited.add(variable);
|
|
48
|
+
if (variable.references.some((reference) => reference.isWrite() && !reference.init)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
for (const definition of variable.defs) {
|
|
52
|
+
if (definition.type === "Variable" &&
|
|
53
|
+
definition.node.type === "VariableDeclarator" &&
|
|
54
|
+
definition.node.id.type === "Identifier" &&
|
|
55
|
+
definition.node.init !== null &&
|
|
56
|
+
definition.node.parent.type === "VariableDeclaration" &&
|
|
57
|
+
definition.node.parent.kind === "const") {
|
|
58
|
+
return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function isGlobalCopyOwner(sourceCode, expression, name) {
|
|
64
|
+
const node = unwrapArrayExpression(expression);
|
|
65
|
+
if (node.type !== "Identifier" || node.name !== name) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
69
|
+
return variable === null || variable.defs.length === 0;
|
|
70
|
+
}
|
|
71
|
+
export const noReduceAccumulatorCopy = defineRule({
|
|
72
|
+
meta: {
|
|
73
|
+
type: "problem",
|
|
74
|
+
docs: {
|
|
75
|
+
description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods.",
|
|
76
|
+
},
|
|
77
|
+
messages: {
|
|
78
|
+
accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
createOnce(context) {
|
|
82
|
+
return {
|
|
83
|
+
CallExpression(node) {
|
|
84
|
+
const method = arrayMethodTarget(node.callee);
|
|
85
|
+
if (method === null) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const reducer = enclosingReducer(node);
|
|
89
|
+
if (reducer === null) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const accumulator = context.sourceCode
|
|
93
|
+
.getDeclaredVariables(reducer.callback)
|
|
94
|
+
.find((variable) => variable.identifiers.some((identifier) => identifier.start === reducer.accumulator.start));
|
|
95
|
+
if (accumulator === void 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const isAccumulator = (expression) => referencesAccumulator(context.sourceCode, expression, accumulator);
|
|
99
|
+
let copiesAccumulator = false;
|
|
100
|
+
if (method.name === "assign" &&
|
|
101
|
+
isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
|
|
102
|
+
const target = node.arguments[0];
|
|
103
|
+
copiesAccumulator =
|
|
104
|
+
target !== void 0 &&
|
|
105
|
+
unwrapArrayExpression(target).type === "ObjectExpression" &&
|
|
106
|
+
node.arguments.slice(1).some(isAccumulator);
|
|
107
|
+
}
|
|
108
|
+
else if (method.name === "from" &&
|
|
109
|
+
isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
|
|
110
|
+
const source = node.arguments[0];
|
|
111
|
+
copiesAccumulator = source !== void 0 && isAccumulator(source);
|
|
112
|
+
}
|
|
113
|
+
else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) {
|
|
114
|
+
const initialValue = reducer.initialValue;
|
|
115
|
+
const arrayAccumulator = initialValue !== void 0 && isKnownArrayExpression(context.sourceCode, initialValue);
|
|
116
|
+
copiesAccumulator = arrayAccumulator && isAccumulator(method.object);
|
|
117
|
+
}
|
|
118
|
+
if (copiesAccumulator) {
|
|
119
|
+
context.report({ node, messageId: "accumulatorCopy" });
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noReflectApply: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Adapted from dmmulroy/anti-slop
|
|
2
|
+
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { isGlobalReflectMethodCall } from "../shared/reflect-method.js";
|
|
4
|
+
export const noReflectApply = defineRule({
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface.",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
reflectApply: "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
createOnce(context) {
|
|
15
|
+
return {
|
|
16
|
+
CallExpression(node) {
|
|
17
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) {
|
|
21
|
+
context.report({ node, messageId: "reflectApply" });
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noReflectGet: import("@oxlint/plugins").Rule;
|