@doubleloop-io/oxlint-plugin-effect 0.1.1 → 0.1.2
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @doubleloop-io/oxlint-plugin-effect
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#42](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/42) [`00de7c6`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/00de7c6780be261181bd49e34c183f020733754f) Thanks [@iacoware](https://github.com/iacoware)! - `no-inline-yield-star` adds a hint when the `yield*` sits inside a larger conditional branch, where binding it first would run it unconditionally: extract the effect into a named function and transform its result inside it (`Effect.map`), so the branch is a single `yield*`.
|
|
8
|
+
|
|
9
|
+
- [#41](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/41) [`a71cd0f`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/a71cd0fbcc82be7e9961550ec4924ae483b64016) Thanks [@iacoware](https://github.com/iacoware)! - `no-inline-yield-star` autofix names a `const` whose name is taken after the object it is read from (`reader.rows` → `readerRows`, `Effect.scope` → `effectScope`) before falling back to a numeric suffix.
|
|
10
|
+
|
|
11
|
+
- [#39](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/39) [`469f03d`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/469f03d1eb92126a918d8cf456e8483b05c4fdd9) Thanks [@d4v1d82](https://github.com/d4v1d82)! - Prevent `no-inline-yield-star` autofixes from generating duplicate `const` bindings when multiple statements in the same generator extract the same base name.
|
|
12
|
+
|
|
3
13
|
## 0.1.1
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @doubleloop-io/oxlint-plugin-effect
|
|
2
2
|
|
|
3
|
-
An oxlint plugin that enforces a catalog of patterns for Effect 4 code.
|
|
3
|
+
An [oxlint](https://github.com/oxc-project/oxc) plugin that enforces a catalog of patterns for [Effect](https://github.com/Effect-TS/effect/) 4 code.
|
|
4
4
|
|
|
5
5
|
The rules are written and tested against Effect 4. Where an Effect 3 API has the same intent, the rules recognize its v3 name too: each rule's Trigger names them. The [Rules overview](https://doubleloop-io.github.io/oxlint-plugin-effect/rules-overview/) lists every rule.
|
|
6
6
|
|
|
@@ -35,6 +35,29 @@ const statementOf = (node) => {
|
|
|
35
35
|
return current;
|
|
36
36
|
};
|
|
37
37
|
const logicalAssignments = new Set(["&&=", "||=", "??="]);
|
|
38
|
+
const isConditionalBranch = (child, parent) => {
|
|
39
|
+
switch (parent.type) {
|
|
40
|
+
case "ConditionalExpression":
|
|
41
|
+
return parent.test !== child;
|
|
42
|
+
case "LogicalExpression":
|
|
43
|
+
return parent.right === child;
|
|
44
|
+
case "AssignmentExpression":
|
|
45
|
+
return parent.right === child && logicalAssignments.has(parent.operator);
|
|
46
|
+
default:
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// A branch that is the `yield*` itself can be bound whole; a larger branch cannot be bound first
|
|
51
|
+
// without running the effect unconditionally.
|
|
52
|
+
const isNestedInConditionalBranch = (node, statement) => {
|
|
53
|
+
let child = node.parent;
|
|
54
|
+
while (child !== statement && child.parent) {
|
|
55
|
+
if (isConditionalBranch(child, child.parent))
|
|
56
|
+
return true;
|
|
57
|
+
child = child.parent;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
38
61
|
// The extracted `const` runs before its statement, once and unconditionally: any position that
|
|
39
62
|
// short-circuits, repeats or belongs to a nested statement would change when the effect runs.
|
|
40
63
|
const runsOnceBeforeStatement = (child, parent, statement) => {
|
|
@@ -89,13 +112,32 @@ const trailingName = (node) => {
|
|
|
89
112
|
return undefined;
|
|
90
113
|
}
|
|
91
114
|
};
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
const memberOwner = (node) => {
|
|
116
|
+
switch (node.type) {
|
|
117
|
+
case "MemberExpression":
|
|
118
|
+
return node.computed ? undefined : node.object;
|
|
119
|
+
case "CallExpression":
|
|
120
|
+
case "NewExpression":
|
|
121
|
+
return memberOwner(node.callee);
|
|
122
|
+
default:
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1);
|
|
127
|
+
const upperFirst = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
128
|
+
// `yield* readSheet()` becomes `sheet`, `yield* reader.rows` becomes `rows`, or `readerRows`
|
|
129
|
+
// when `rows` is taken: the owner tells the reader where the value comes from, a suffix does not.
|
|
130
|
+
const names = (node) => {
|
|
131
|
+
const { argument } = node;
|
|
132
|
+
const name = argument ? trailingName(argument)?.replace(verbPrefix, "") : undefined;
|
|
133
|
+
if (!argument || name === undefined || name === "")
|
|
134
|
+
return { base: "value" };
|
|
135
|
+
const base = lowerFirst(name);
|
|
136
|
+
if (reservedWords.has(base))
|
|
137
|
+
return { base: "value" };
|
|
138
|
+
const owner = memberOwner(argument);
|
|
139
|
+
const ownerName = owner ? trailingName(owner) : undefined;
|
|
140
|
+
return ownerName ? { base, qualified: lowerFirst(ownerName) + upperFirst(base) } : { base };
|
|
99
141
|
};
|
|
100
142
|
const namesInUse = (scope) => {
|
|
101
143
|
const names = new Set();
|
|
@@ -130,12 +172,14 @@ const replacedRange = (node, sourceCode) => {
|
|
|
130
172
|
}
|
|
131
173
|
return wrappers[wrappers.length - 1 - callParens] ?? node.range;
|
|
132
174
|
};
|
|
133
|
-
const extraction = (statement, hoisted, sourceCode) => {
|
|
134
|
-
const taken = namesInUse(sourceCode.getScope(statement).variableScope);
|
|
175
|
+
const extraction = (statement, hoisted, sourceCode, namesByOwner, owner) => {
|
|
176
|
+
const taken = namesByOwner.get(owner) ?? namesInUse(sourceCode.getScope(statement).variableScope);
|
|
177
|
+
namesByOwner.set(owner, taken);
|
|
135
178
|
const replacements = hoisted.map((node) => {
|
|
136
|
-
|
|
179
|
+
const { base, qualified } = names(node);
|
|
180
|
+
let name = taken.has(base) && qualified !== undefined ? qualified : base;
|
|
137
181
|
for (let suffix = 2; taken.has(name); suffix++)
|
|
138
|
-
name = `${
|
|
182
|
+
name = `${base}${suffix}`;
|
|
139
183
|
taken.add(name);
|
|
140
184
|
return { node, name, range: replacedRange(node, sourceCode) };
|
|
141
185
|
});
|
|
@@ -161,6 +205,7 @@ const extraction = (statement, hoisted, sourceCode) => {
|
|
|
161
205
|
const guarded = startsLine && /^[[(`+\-/]/.test(remainder) ? `;${remainder}` : remainder;
|
|
162
206
|
return [...declarations, guarded].join(startsLine ? `\n${indentation}` : "; ");
|
|
163
207
|
};
|
|
208
|
+
const inlineYieldStarMessage = "This `yield*` runs inside a larger expression. Bind its result first (`const rows = yield* reader.rows`), so each statement runs one effect in reading order.";
|
|
164
209
|
export const noInlineYieldStarRule = defineRule({
|
|
165
210
|
meta: {
|
|
166
211
|
type: "suggestion",
|
|
@@ -169,7 +214,8 @@ export const noInlineYieldStarRule = defineRule({
|
|
|
169
214
|
description: "Bind the result of a `yield*` to a `const` instead of using it inside a larger expression.",
|
|
170
215
|
},
|
|
171
216
|
messages: {
|
|
172
|
-
inlineYieldStar:
|
|
217
|
+
inlineYieldStar: inlineYieldStarMessage,
|
|
218
|
+
conditionalYieldStar: `${inlineYieldStarMessage} When inside a conditional branch, binding it first would run it unconditionally. Instead, extract the effect into a named function and transform its result inside it (\`Effect.map\`), so the branch is a single \`yield*\`.`,
|
|
173
219
|
},
|
|
174
220
|
},
|
|
175
221
|
createOnce(context) {
|
|
@@ -197,6 +243,7 @@ export const noInlineYieldStarRule = defineRule({
|
|
|
197
243
|
VariableDeclarator: collect,
|
|
198
244
|
"Program:exit"() {
|
|
199
245
|
const byStatement = new Map();
|
|
246
|
+
const namesByOwner = new Map();
|
|
200
247
|
for (const node of inline) {
|
|
201
248
|
const statement = statementOf(node);
|
|
202
249
|
byStatement.set(statement, [...(byStatement.get(statement) ?? []), node]);
|
|
@@ -212,12 +259,14 @@ export const noInlineYieldStarRule = defineRule({
|
|
|
212
259
|
hoisted.length > 0 &&
|
|
213
260
|
!reordersEffects(hoisted, evaluatedInStatement);
|
|
214
261
|
const replacement = fixable
|
|
215
|
-
? extraction(statement, hoisted, context.sourceCode)
|
|
262
|
+
? extraction(statement, hoisted, context.sourceCode, namesByOwner, owner)
|
|
216
263
|
: undefined;
|
|
217
264
|
for (const node of yields) {
|
|
218
265
|
context.report({
|
|
219
266
|
node,
|
|
220
|
-
messageId:
|
|
267
|
+
messageId: isNestedInConditionalBranch(node, statement)
|
|
268
|
+
? "conditionalYieldStar"
|
|
269
|
+
: "inlineYieldStar",
|
|
221
270
|
...(replacement !== undefined && hoisted.includes(node)
|
|
222
271
|
? { fix: (fixer) => fixer.replaceText(statement, replacement) }
|
|
223
272
|
: {}),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: "@doubleloop-io/oxlint-plugin-effect"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
An oxlint plugin that enforces a catalog of patterns for Effect 4 code.
|
|
5
|
+
An [oxlint](https://github.com/oxc-project/oxc) plugin that enforces a catalog of patterns for [Effect](https://github.com/Effect-TS/effect/) 4 code.
|
|
6
6
|
|
|
7
7
|
The rules are written and tested against Effect 4. Where an Effect 3 API has the same intent, the rules recognize its v3 name too: each rule's Trigger names them.
|
|
8
8
|
|
|
@@ -20,7 +20,7 @@ function* () {
|
|
|
20
20
|
|
|
21
21
|
**Trigger**: a `yield*` whose result is used inside a larger expression: argument, operand, member, property, template.
|
|
22
22
|
|
|
23
|
-
**Exemptions and blind spots**: a `yield*` that is the whole value of a statement (`yield* x`, `return yield* x`, `if (yield* x)`, `for (… of yield* x)`), a declaration or an assignment. Also the whole branch of a ternary, or the right operand of `&&`/`||`/`??`, in one of those positions (`const a = cached ?? (yield* x)`): binding first would run the effect unconditionally. The autofix extracts every nested `yield*` into `const`s, innermost first, named after the argument's last identifier without a verb prefix (`readSheet()` → `sheet`). There is no autofix when moving the effect could change when or how often it runs: branches of a ternary or `&&`/`||`/`??` inside a larger expression, optional chaining, `while`/`for` conditions, statements without braces, another call evaluated before the `yield*`.
|
|
23
|
+
**Exemptions and blind spots**: a `yield*` that is the whole value of a statement (`yield* x`, `return yield* x`, `if (yield* x)`, `for (… of yield* x)`), a declaration or an assignment. Also the whole branch of a ternary, or the right operand of `&&`/`||`/`??`, in one of those positions (`const a = cached ?? (yield* x)`): binding first would run the effect unconditionally. The autofix extracts every nested `yield*` into `const`s, innermost first, named after the argument's last identifier without a verb prefix (`readSheet()` → `sheet`); when that name is taken, prefixed by the object it is read from (`reader.rows` → `readerRows`), else numbered (`sheet2`). There is no autofix when moving the effect could change when or how often it runs: branches of a ternary or `&&`/`||`/`??` inside a larger expression, optional chaining, `while`/`for` conditions, statements without braces, another call evaluated before the `yield*`.
|
|
24
24
|
|
|
25
25
|
**Why**: with several `yield*`s in one expression the order of effects can only be inferred from precedence. One `const` per effect makes the order the reading order and names the intermediate value.
|
|
26
26
|
|
|
@@ -5,4 +5,4 @@ order: 7
|
|
|
5
5
|
|
|
6
6
|
Versions are `0.x`: any minor release may add, remove or change rules, options, settings and messages.
|
|
7
7
|
|
|
8
|
-
Supported oxlint is the `peerDependencies` range, `^1.82.0`; CI tests the tarball against `1.82.0
|
|
8
|
+
Supported oxlint is the `peerDependencies` range, `^1.82.0`; CI tests the tarball against `1.82.0`, and a weekly workflow runs the tests and the tarball against the latest oxlint. oxlint's JS plugin API is still experimental, so a newer oxlint minor may break the plugin before the range is narrowed.
|