oxlint-plugin-effect 0.7.0 → 0.8.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/README.md +21 -0
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/dist/presets/recommended.d.ts +11 -0
- package/dist/presets/recommended.js +12 -1
- package/dist/rules/_anti-slop-dictionary-types.d.ts +24 -0
- package/dist/rules/_anti-slop-dictionary-types.js +248 -0
- package/dist/rules/index.d.ts +13 -2
- package/dist/rules/index.js +24 -2
- package/dist/rules/no-chained-type-assertions.d.ts +7 -0
- package/dist/rules/no-chained-type-assertions.js +60 -0
- package/dist/rules/no-conditional-empty-object-spread.d.ts +7 -0
- package/dist/rules/no-conditional-empty-object-spread.js +79 -0
- package/dist/rules/no-known-value-widening.d.ts +7 -0
- package/dist/rules/no-known-value-widening.js +144 -0
- package/dist/rules/no-object-parameters.d.ts +7 -0
- package/dist/rules/no-object-parameters.js +70 -0
- package/dist/rules/no-runtime-typeof.d.ts +7 -0
- package/dist/rules/no-runtime-typeof.js +24 -0
- package/dist/rules/no-shape-in-symbol-names.d.ts +7 -0
- package/dist/rules/no-shape-in-symbol-names.js +34 -0
- package/dist/rules/no-unknown-parameters.d.ts +7 -0
- package/dist/rules/no-unknown-parameters.js +55 -0
- package/dist/rules/no-unknown-type-aliases.d.ts +7 -0
- package/dist/rules/no-unknown-type-aliases.js +48 -0
- package/dist/rules/no-unsafe-dictionary-type.d.ts +7 -0
- package/dist/rules/no-unsafe-dictionary-type.js +75 -0
- package/dist/rules/no-widen-then-assert.d.ts +7 -0
- package/dist/rules/no-widen-then-assert.js +199 -0
- package/dist/rules/prefer-effect-fn.d.ts +6 -0
- package/dist/rules/prefer-effect-fn.js +31 -0
- package/package.json +3 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-widen-then-assert.ts
|
|
7
|
+
/**
|
|
8
|
+
* Ported from dmmulroy/anti-slop at
|
|
9
|
+
* b5d2288db1f00469a1d5f2e3b0e265e5a5676fd0.
|
|
10
|
+
*/
|
|
11
|
+
const functionBoundaryTypes = new Set([
|
|
12
|
+
"ArrowFunctionExpression",
|
|
13
|
+
"FunctionDeclaration",
|
|
14
|
+
"FunctionExpression",
|
|
15
|
+
"TSDeclareFunction",
|
|
16
|
+
"TSEmptyBodyFunctionExpression"
|
|
17
|
+
]);
|
|
18
|
+
function unwrapExpressionParentheses(expression) {
|
|
19
|
+
let current = expression;
|
|
20
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
function unwrapTypeParentheses(type) {
|
|
24
|
+
let current = type;
|
|
25
|
+
while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
function typeReferenceName(type) {
|
|
29
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
30
|
+
}
|
|
31
|
+
function isUnknownOrAnyType(type) {
|
|
32
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
33
|
+
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
|
|
34
|
+
}
|
|
35
|
+
function isBroadRecordKeyType(type) {
|
|
36
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
37
|
+
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
38
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
|
|
39
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
|
|
40
|
+
}
|
|
41
|
+
function isBroadRecordType(type) {
|
|
42
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
43
|
+
if (unwrapped.type === "TSTypeReference") {
|
|
44
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
45
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
46
|
+
return inner !== void 0 && isBroadRecordType(inner);
|
|
47
|
+
}
|
|
48
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
49
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
50
|
+
return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
|
|
51
|
+
}
|
|
52
|
+
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
|
|
53
|
+
const [member] = unwrapped.members;
|
|
54
|
+
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
|
|
55
|
+
return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
|
|
56
|
+
}
|
|
57
|
+
function broadTypeKind(type) {
|
|
58
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
59
|
+
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
|
|
60
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
61
|
+
return isBroadRecordType(unwrapped) ? "record" : null;
|
|
62
|
+
}
|
|
63
|
+
function assertedExpression(node) {
|
|
64
|
+
return unwrapExpressionParentheses(node.expression);
|
|
65
|
+
}
|
|
66
|
+
function assertionFromExpression(expression) {
|
|
67
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
68
|
+
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" ? unwrapped : null;
|
|
69
|
+
}
|
|
70
|
+
function normalizedTypeText(sourceText, type) {
|
|
71
|
+
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
|
|
72
|
+
}
|
|
73
|
+
function typesHaveSameSyntax(sourceText, left, right) {
|
|
74
|
+
return left !== null && normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right));
|
|
75
|
+
}
|
|
76
|
+
function isDefinitelyObjectType(type) {
|
|
77
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
78
|
+
switch (unwrapped.type) {
|
|
79
|
+
case "TSArrayType":
|
|
80
|
+
case "TSConstructorType":
|
|
81
|
+
case "TSFunctionType":
|
|
82
|
+
case "TSMappedType":
|
|
83
|
+
case "TSObjectKeyword":
|
|
84
|
+
case "TSTupleType": return true;
|
|
85
|
+
case "TSTypeLiteral": return unwrapped.members.length > 0;
|
|
86
|
+
case "TSIntersectionType": return unwrapped.types.every(isDefinitelyObjectType);
|
|
87
|
+
case "TSTypeOperator": return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
|
|
88
|
+
default: return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isDefinitelyNarrowerRecordType(type) {
|
|
92
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
93
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
|
|
94
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
95
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
96
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
97
|
+
return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
|
|
98
|
+
}
|
|
99
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
100
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
101
|
+
return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
|
|
102
|
+
}
|
|
103
|
+
function functionBoundary(node) {
|
|
104
|
+
let current = node.parent;
|
|
105
|
+
while (current !== null && current.type !== "Program") {
|
|
106
|
+
if (functionBoundaryTypes.has(current.type)) return current;
|
|
107
|
+
current = current.parent;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function resolvedVariableForIdentifier(scopes, identifier) {
|
|
112
|
+
for (const scope of scopes) {
|
|
113
|
+
const reference = scope.references.find((candidate) => candidate.identifier.start === identifier.start && candidate.identifier.end === identifier.end);
|
|
114
|
+
if (reference !== void 0) return reference.resolved;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function variableDeclarator(variable) {
|
|
119
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
|
|
123
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
124
|
+
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
|
|
125
|
+
if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
|
|
126
|
+
return { type: unwrapped.typeAnnotation };
|
|
127
|
+
}
|
|
128
|
+
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type: null };
|
|
129
|
+
if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type: null };
|
|
130
|
+
if (unwrapped.type !== "Identifier") return null;
|
|
131
|
+
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
|
|
132
|
+
if (variable === null || visitedVariables.has(variable)) return null;
|
|
133
|
+
const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
|
|
134
|
+
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
|
|
135
|
+
if (annotation !== void 0 && annotatedIdentifier !== void 0) {
|
|
136
|
+
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) return null;
|
|
137
|
+
return { type: annotation };
|
|
138
|
+
}
|
|
139
|
+
const declarator = variableDeclarator(variable);
|
|
140
|
+
if (declarator === null || declarator.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init) || functionBoundary(declarator) !== boundary) return null;
|
|
141
|
+
return knownValueEvidence(declarator.init, scopes, boundary, new Set([...visitedVariables, variable]));
|
|
142
|
+
}
|
|
143
|
+
function widenedBinding(variable, scopes) {
|
|
144
|
+
const declarator = variableDeclarator(variable);
|
|
145
|
+
if (declarator === null || declarator.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.id.type !== "Identifier" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
|
|
146
|
+
const identifier = variable.identifiers.find((candidate) => candidate.start === declarator.id.start && candidate.end === declarator.id.end);
|
|
147
|
+
const boundary = functionBoundary(declarator);
|
|
148
|
+
const declaredType = identifier?.typeAnnotation?.typeAnnotation;
|
|
149
|
+
const initializerAssertion = assertionFromExpression(declarator.init);
|
|
150
|
+
const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
|
|
151
|
+
const broadKind = (declaredType === void 0 ? null : broadTypeKind(declaredType)) ?? initializerBroadKind;
|
|
152
|
+
if (broadKind === null) return null;
|
|
153
|
+
const evidence = knownValueEvidence(initializerAssertion !== null && initializerBroadKind !== null ? assertedExpression(initializerAssertion) : declarator.init, scopes, boundary, new Set([variable]));
|
|
154
|
+
return evidence === null ? null : {
|
|
155
|
+
broadKind,
|
|
156
|
+
evidence,
|
|
157
|
+
declaredAt: declarator.end,
|
|
158
|
+
boundary
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
|
|
162
|
+
if (broadTypeKind(assertedType) !== null) return false;
|
|
163
|
+
if (broadKind === "top") return true;
|
|
164
|
+
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
|
|
165
|
+
if (broadKind === "object") return isDefinitelyObjectType(assertedType);
|
|
166
|
+
return isDefinitelyNarrowerRecordType(assertedType);
|
|
167
|
+
}
|
|
168
|
+
/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
|
|
169
|
+
const noWidenThenAssert = define({
|
|
170
|
+
name: "no-widen-then-assert",
|
|
171
|
+
meta: meta({
|
|
172
|
+
type: "problem",
|
|
173
|
+
description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
|
|
174
|
+
messages: { widenThenAssert: "Binding \"{{name}}\" erases established type evidence by widening the value, then reconstructs that evidence with a type assertion. Preserve the precise type end-to-end; if the input is genuinely unknown, parse it once at the boundary instead." }
|
|
175
|
+
}),
|
|
176
|
+
create: function* () {
|
|
177
|
+
const context = yield* RuleContext;
|
|
178
|
+
const scopes = context.sourceCode.scopeManager.scopes;
|
|
179
|
+
const checkAssertion = (node) => {
|
|
180
|
+
const expression = assertedExpression(node);
|
|
181
|
+
if (expression.type !== "Identifier") return Effect.void;
|
|
182
|
+
const variable = resolvedVariableForIdentifier(scopes, expression);
|
|
183
|
+
if (variable === null) return Effect.void;
|
|
184
|
+
const widened = widenedBinding(variable, scopes);
|
|
185
|
+
if (widened === null || node.start <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) return Effect.void;
|
|
186
|
+
return context.report(fromId({
|
|
187
|
+
node,
|
|
188
|
+
messageId: "widenThenAssert",
|
|
189
|
+
data: { name: expression.name }
|
|
190
|
+
}));
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
TSAsExpression: checkAssertion,
|
|
194
|
+
TSTypeAssertion: checkAssertion
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
//#endregion
|
|
199
|
+
export { noWidenThenAssert };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/prefer-effect-fn.ts
|
|
7
|
+
const isStaticMember = (node, object, property) => node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && node.object.name === object && node.property.type === "Identifier" && node.property.name === property;
|
|
8
|
+
const isEffectGenCall = (node) => node.type === "CallExpression" && node.callee.type !== "Super" && isStaticMember(node.callee, "Effect", "gen");
|
|
9
|
+
const isEffectWithSpanCall = (node) => node.type === "CallExpression" && node.callee.type !== "Super" && isStaticMember(node.callee, "Effect", "withSpan");
|
|
10
|
+
const spansEffectGenDirectly = (node) => node.callee.type !== "Super" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && node.callee.property.name === "withSpan" && isEffectGenCall(node.callee.object);
|
|
11
|
+
const pipesOnlyWithSpanFromEffectGen = (node) => node.callee.type !== "Super" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && node.callee.property.name === "pipe" && isEffectGenCall(node.callee.object) && node.arguments.length === 1 && node.arguments[0] !== void 0 && isEffectWithSpanCall(node.arguments[0]);
|
|
12
|
+
const preferEffectFn = define({
|
|
13
|
+
name: "prefer-effect-fn",
|
|
14
|
+
meta: meta({
|
|
15
|
+
type: "suggestion",
|
|
16
|
+
description: "Use Effect.fn for a named generator operation."
|
|
17
|
+
}),
|
|
18
|
+
create: function* () {
|
|
19
|
+
const ctx = yield* RuleContext;
|
|
20
|
+
return { CallExpression: (node) => {
|
|
21
|
+
if (node.type !== "CallExpression") return Effect.void;
|
|
22
|
+
if (!spansEffectGenDirectly(node) && !pipesOnlyWithSpanFromEffectGen(node)) return Effect.void;
|
|
23
|
+
return ctx.report(make({
|
|
24
|
+
node,
|
|
25
|
+
message: "Avoid a span on Effect.gen. Define the named operation with Effect.fn."
|
|
26
|
+
}));
|
|
27
|
+
} };
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
//#endregion
|
|
31
|
+
export { preferEffectFn };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-effect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "oxlint plugin with Effect-specific lint rules",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"url": "https://github.com/cevr/effect-oxlint"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"THIRD_PARTY_NOTICES.md"
|
|
12
13
|
],
|
|
13
14
|
"type": "module",
|
|
14
15
|
"exports": {
|