kensington-eslint-plugin 0.3.2 → 0.4.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 +82 -13
- package/index.js +4 -1
- package/package.json +2 -2
- package/rules/no-new-computed-in-computed.js +33 -11
- package/rules/no-out-of-scope-reactive-reference.js +160 -0
package/README.md
CHANGED
|
@@ -96,9 +96,10 @@ Because this is a standard ESLint plugin, it works anywhere ESLint runs with no
|
|
|
96
96
|
| [`no-ignored-effect-return`](#no-ignored-effect-return) | Require capturing the return value of `effect()` inside a function | warn |
|
|
97
97
|
| [`prefer-value-in-async`](#prefer-value-in-async) | Prefer `.value` over `.get()` inside async callbacks within an `effect()` | warn |
|
|
98
98
|
| [`no-new-computed-in-effect`](#no-new-computed-in-effect) | Disallow creating a new `computed()` inside an `effect()` body | error |
|
|
99
|
-
| [`no-new-signal-in-computed`](#no-new-signal-in-computed) |
|
|
99
|
+
| [`no-new-signal-in-computed`](#no-new-signal-in-computed) | Require a stable key for `signal()` calls inside a `computed()` body | error |
|
|
100
100
|
| [`no-unsafe-literal`](#no-unsafe-literal) | Disallow `.unsafeLiteral()` calls that bypass XSS protection | error |
|
|
101
|
-
| [`no-new-computed-in-computed`](#no-new-computed-in-computed) |
|
|
101
|
+
| [`no-new-computed-in-computed`](#no-new-computed-in-computed) | Require a stable key for `computed()` and `.transform()` calls inside a `computed()` body | warn |
|
|
102
|
+
| [`no-out-of-scope-reactive-reference`](#no-out-of-scope-reactive-reference) | Disallow referencing a `signal()`, `computed()`, or `.transform()` from outside the computed scope where it was created | warn |
|
|
102
103
|
| [`no-effect-in-effect`](#no-effect-in-effect) | Disallow creating a new `effect()` inside an `effect()` body | error |
|
|
103
104
|
| [`no-async-effect`](#no-async-effect) | Disallow async callbacks passed to `effect()` | error |
|
|
104
105
|
| [`no-async-computed`](#no-async-computed) | Disallow async callbacks passed to `computed()` | error |
|
|
@@ -325,18 +326,86 @@ t.literal(userContent);
|
|
|
325
326
|
|
|
326
327
|
### `no-new-computed-in-computed`
|
|
327
328
|
|
|
328
|
-
Creating `computed()` inside a `computed()` body creates a new orphaned derived signal on every recompute.
|
|
329
|
+
Creating `computed()` or `.transform()` inside a `computed()` body without a key creates a new orphaned derived signal on every recompute. Pass a stable key as the second argument to reuse the same instance across outer re-runs.
|
|
329
330
|
|
|
330
331
|
```js
|
|
331
|
-
// Bad
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
});
|
|
332
|
+
// Bad. New instance on every outer re-run, inner state lost
|
|
333
|
+
const list = computed(() =>
|
|
334
|
+
items.get().map(item => {
|
|
335
|
+
const cls = computed(() => filter.get() === item.cat ? 'on' : ''); // warn
|
|
336
|
+
return t.li({ dataKey: item.id, class: cls }, item.name);
|
|
337
|
+
})
|
|
338
|
+
);
|
|
336
339
|
|
|
337
|
-
//
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
+
// Bad. Same problem with .transform()
|
|
341
|
+
const list = computed(() =>
|
|
342
|
+
items.get().map(item =>
|
|
343
|
+
t.li({ dataKey: item.id, class: filter.transform(f => f === item.cat ? 'on' : '') }, item.name) // warn
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Good. Keyed computed
|
|
348
|
+
const list = computed(() =>
|
|
349
|
+
items.get().map(item =>
|
|
350
|
+
t.li({ dataKey: item.id, class: computed(() => filter.get() === item.cat ? 'on' : '', item.id) }, item.name)
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Good. Keyed transform
|
|
355
|
+
const list = computed(() =>
|
|
356
|
+
items.get().map(item =>
|
|
357
|
+
t.li({ dataKey: item.id, class: filter.transform(f => f === item.cat ? 'on' : '', item.id) }, item.name)
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Also good. Declare outside when fn has no per-item closure
|
|
362
|
+
const upper = computed(() => name.get().toUpperCase());
|
|
363
|
+
const outer = computed(() => upper.get() + '!');
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
### `no-out-of-scope-reactive-reference`
|
|
369
|
+
|
|
370
|
+
A reactive primitive (`signal()`, `computed()`, or `.transform()`) created inside a `computed()` body is owned by the surrounding computed. The owner can stop it at any time. When a re-run doesn't access the key, the instance is swept from the registry and stopped. Any reference held outside the owner's scope silently drops subscribers and produces out-of-sync state.
|
|
371
|
+
|
|
372
|
+
Two inline-consumption patterns are safe and allowed:
|
|
373
|
+
|
|
374
|
+
1. The result is consumed by an immediate method chain (`.get()`, `.transform()`, `.toString()`, etc.). The chain consumes the instance; the instance itself never escapes.
|
|
375
|
+
2. The result is passed directly to a tag call as content or an attribute value. The DOM binding effect created by `toElement()` is part of the owner's own render cycle, so its lifetime is tied to the DOM.
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
// Bad. Instance escapes via module-level cache; external code can hold a dead signal
|
|
379
|
+
const editingSignals = new Map();
|
|
380
|
+
const list = computed(() =>
|
|
381
|
+
items.get().map(item => {
|
|
382
|
+
const editing = signal(false, item.id);
|
|
383
|
+
editingSignals.set(item.id, editing); // warn
|
|
384
|
+
return t.li({ dataKey: item.id, class: editing.transform(v => v ? 'on' : '') }, item.name);
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Bad. Instance returned from map; consumers see a stale signal after a sweep
|
|
389
|
+
const list = computed(() =>
|
|
390
|
+
items.get().map(item => computed(() => item.v * 2, item.id)) // warn
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Good. Consumed via method chain
|
|
394
|
+
const list = computed(() =>
|
|
395
|
+
items.get().map(item =>
|
|
396
|
+
computed(() => filter.get() === item.cat ? 'on' : '', item.id).get()
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Good. Passed directly to a tag; DOM binding owns lifetime
|
|
401
|
+
const list = computed(() =>
|
|
402
|
+
items.get().map(item =>
|
|
403
|
+
t.li({
|
|
404
|
+
dataKey: item.id,
|
|
405
|
+
class: computed(() => filter.get() === item.cat ? 'on' : '', item.id),
|
|
406
|
+
}, item.name)
|
|
407
|
+
)
|
|
408
|
+
);
|
|
340
409
|
```
|
|
341
410
|
|
|
342
411
|
---
|
|
@@ -470,7 +539,7 @@ Auto-fixable when the group's members are contiguous in the source. Non-contiguo
|
|
|
470
539
|
|
|
471
540
|
### `prefer-array-for-multiline-content`
|
|
472
541
|
|
|
473
|
-
Mirrors what `html-to-kensington` emits: when a tag's content occupies its own line(s)
|
|
542
|
+
Mirrors what `html-to-kensington` emits: when a tag's content occupies its own line(s). Separated from both the opening and closing paren. It goes in an array, even when it's the only item. The array form makes line-by-line edits easier (no need to add `[ ]` when adding a sibling).
|
|
474
543
|
|
|
475
544
|
```js
|
|
476
545
|
// Bad
|
|
@@ -483,7 +552,7 @@ t.div({ class: 'x' }, [
|
|
|
483
552
|
t.p('only'),
|
|
484
553
|
]);
|
|
485
554
|
|
|
486
|
-
// Also good
|
|
555
|
+
// Also good. Content trails on the closing-paren line, no array needed
|
|
487
556
|
t.a({
|
|
488
557
|
href: 'https://example.com',
|
|
489
558
|
target: '_blank',
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import noNewSignalInComputed from './rules/no-new-signal-in-computed.js';
|
|
|
11
11
|
import noUnsafeLiteral from './rules/no-unsafe-literal.js';
|
|
12
12
|
import noNewComputedInComputed from './rules/no-new-computed-in-computed.js';
|
|
13
13
|
import noEffectInEffect from './rules/no-effect-in-effect.js';
|
|
14
|
+
import noOutOfScopeReactiveReference from './rules/no-out-of-scope-reactive-reference.js';
|
|
14
15
|
import noAsyncEffect from './rules/no-async-effect.js';
|
|
15
16
|
import noAsyncComputed from './rules/no-async-computed.js';
|
|
16
17
|
import preferBooleanAttributeTrue from './rules/prefer-boolean-attribute-true.js';
|
|
@@ -38,6 +39,7 @@ const plugin = {
|
|
|
38
39
|
'no-unsafe-literal': noUnsafeLiteral,
|
|
39
40
|
'no-new-computed-in-computed': noNewComputedInComputed,
|
|
40
41
|
'no-effect-in-effect': noEffectInEffect,
|
|
42
|
+
'no-out-of-scope-reactive-reference': noOutOfScopeReactiveReference,
|
|
41
43
|
'no-async-effect': noAsyncEffect,
|
|
42
44
|
'no-async-computed': noAsyncComputed,
|
|
43
45
|
'prefer-boolean-attribute-true': preferBooleanAttributeTrue,
|
|
@@ -66,10 +68,11 @@ plugin.configs.recommended = {
|
|
|
66
68
|
'kensington/no-new-computed-in-effect': 'error',
|
|
67
69
|
'kensington/no-new-signal-in-computed': 'error',
|
|
68
70
|
'kensington/no-unsafe-literal': 'error',
|
|
69
|
-
'kensington/no-new-computed-in-computed': '
|
|
71
|
+
'kensington/no-new-computed-in-computed': 'warn',
|
|
70
72
|
'kensington/no-effect-in-effect': 'error',
|
|
71
73
|
'kensington/no-async-effect': 'error',
|
|
72
74
|
'kensington/no-async-computed': 'error',
|
|
75
|
+
'kensington/no-out-of-scope-reactive-reference': 'warn',
|
|
73
76
|
},
|
|
74
77
|
};
|
|
75
78
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kensington-eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "ESLint rules for kensington signal correctness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node --test tests/no-set-in-derivation.test.js tests/no-self-read-write.test.js tests/no-signal-async-write.test.js tests/no-set-on-derived-signal.test.js tests/no-new-signal-in-effect.test.js tests/no-effect-in-computed.test.js tests/no-ignored-effect-return.test.js tests/prefer-value-in-async.test.js tests/no-new-computed-in-effect.test.js tests/no-new-signal-in-computed.test.js tests/no-unsafe-literal.test.js tests/no-new-computed-in-computed.test.js tests/no-effect-in-effect.test.js tests/no-async-effect.test.js tests/no-async-computed.test.js tests/prefer-boolean-attribute-true.test.js tests/prefer-camelcase-attrs.test.js tests/prefer-style-object.test.js tests/prefer-nested-attr-groups.test.js tests/prefer-array-for-multiline-content.test.js tests/attrs-on-call-line.test.js tests/attrs-canonical-shape.test.js tests/consistent-content-layout.test.js"
|
|
8
|
+
"test": "node --test tests/no-set-in-derivation.test.js tests/no-self-read-write.test.js tests/no-signal-async-write.test.js tests/no-set-on-derived-signal.test.js tests/no-new-signal-in-effect.test.js tests/no-effect-in-computed.test.js tests/no-ignored-effect-return.test.js tests/prefer-value-in-async.test.js tests/no-new-computed-in-effect.test.js tests/no-new-signal-in-computed.test.js tests/no-unsafe-literal.test.js tests/no-new-computed-in-computed.test.js tests/no-effect-in-effect.test.js tests/no-async-effect.test.js tests/no-async-computed.test.js tests/prefer-boolean-attribute-true.test.js tests/prefer-camelcase-attrs.test.js tests/prefer-style-object.test.js tests/prefer-nested-attr-groups.test.js tests/prefer-array-for-multiline-content.test.js tests/attrs-on-call-line.test.js tests/attrs-canonical-shape.test.js tests/consistent-content-layout.test.js tests/no-out-of-scope-reactive-reference.test.js"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
11
|
"eslint": ">=9"
|
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
// Reports computed()
|
|
2
|
-
// orphaned derived signal with no cleanup path
|
|
1
|
+
// Reports unkeyed computed() or .transform() calls inside a computed() callback. Each
|
|
2
|
+
// recompute creates a new orphaned derived signal with no cleanup path. Pass a stable key
|
|
3
|
+
// as the second argument (e.g. computed(fn, item.id) or sig.transform(fn, item.id)) to
|
|
4
|
+
// scope the derived signal to the surrounding computed so the same instance is reused
|
|
5
|
+
// across re-runs.
|
|
3
6
|
export default {
|
|
4
7
|
meta: {
|
|
5
|
-
type: '
|
|
8
|
+
type: 'suggestion',
|
|
6
9
|
docs: {
|
|
7
|
-
description: '
|
|
10
|
+
description: 'require a stable key for computed() and .transform() calls inside a computed() body',
|
|
8
11
|
},
|
|
9
12
|
messages: {
|
|
10
13
|
noNewComputedInComputed:
|
|
11
|
-
'computed() called inside a computed() body
|
|
12
|
-
'
|
|
14
|
+
'computed() called inside a computed() body without a key. The DOM node will be replaced ' +
|
|
15
|
+
'on every outer re-render. Pass a stable key as the second argument ' +
|
|
16
|
+
'(e.g. computed(fn, item.id)) so the same instance is reused across computed re-runs.',
|
|
17
|
+
noNewTransformInComputed:
|
|
18
|
+
'.transform() called inside a computed() body without a key. The DOM node will be replaced ' +
|
|
19
|
+
'on every outer re-render. Pass a stable key as the second argument ' +
|
|
20
|
+
'(e.g. sig.transform(fn, item.id)) so the same instance is reused across computed re-runs.',
|
|
13
21
|
},
|
|
14
22
|
},
|
|
15
23
|
|
|
16
24
|
create(context) {
|
|
17
25
|
const computedNames = new Set();
|
|
18
26
|
const effectNames = new Set();
|
|
19
|
-
// Each entry is 'computed', 'effect', or 'other'
|
|
27
|
+
// Each entry is 'computed', 'effect', or 'other'. Innermost frame is last.
|
|
20
28
|
const fnStack = [];
|
|
21
29
|
|
|
22
30
|
return {
|
|
@@ -53,14 +61,28 @@ export default {
|
|
|
53
61
|
},
|
|
54
62
|
|
|
55
63
|
CallExpression(node) {
|
|
64
|
+
let messageId;
|
|
56
65
|
if (
|
|
57
|
-
node.callee.type
|
|
58
|
-
|
|
59
|
-
) {
|
|
66
|
+
node.callee.type === 'Identifier'
|
|
67
|
+
&& computedNames.has(node.callee.name)
|
|
68
|
+
) {
|
|
69
|
+
messageId = 'noNewComputedInComputed';
|
|
70
|
+
} else if (
|
|
71
|
+
node.callee.type === 'MemberExpression'
|
|
72
|
+
&& !node.callee.computed
|
|
73
|
+
&& node.callee.property.type === 'Identifier'
|
|
74
|
+
&& node.callee.property.name === 'transform'
|
|
75
|
+
) {
|
|
76
|
+
messageId = 'noNewTransformInComputed';
|
|
77
|
+
} else {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// A key was supplied. This is the intended keyed pattern, not a problem.
|
|
81
|
+
if (node.arguments.length >= 2) { return; }
|
|
60
82
|
|
|
61
83
|
for (let i = fnStack.length - 1; i >= 0; i--) {
|
|
62
84
|
if (fnStack[i] === 'computed') {
|
|
63
|
-
context.report({ node, messageId
|
|
85
|
+
context.report({ node, messageId });
|
|
64
86
|
return;
|
|
65
87
|
}
|
|
66
88
|
if (fnStack[i] === 'effect') { return; }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Reports reactive primitives (signal, computed, .transform) that are created inside a
|
|
2
|
+
// `computed()` callback but escape its scope. Assigned to a variable that survives the
|
|
3
|
+
// callback, returned for external use, captured by module-level state, etc. Even with a
|
|
4
|
+
// stable key, the owning computed can stop the inner instance at any time (when its key
|
|
5
|
+
// isn't accessed during a re-run), so external references silently drop subscribers and
|
|
6
|
+
// produce out-of-sync state.
|
|
7
|
+
//
|
|
8
|
+
// Two consumption patterns are safe and allowed:
|
|
9
|
+
// 1. Calling .get() immediately. Extracts the value as a plain JS value
|
|
10
|
+
// 2. Passing the result directly to a tag call as content or an attribute value.
|
|
11
|
+
// the DOM-binding effect created by toElement() is flagged as internal at runtime
|
|
12
|
+
// and is part of the owner's own render cycle, so its lifetime is tied to the DOM.
|
|
13
|
+
|
|
14
|
+
// Returns true when `node` is being used as an argument to a tag-builder method call
|
|
15
|
+
// (t.li(node), t.div({ class: node }), t.span([..., node, ...])). Recurses through
|
|
16
|
+
// containing object/array literals to handle attribute objects and content arrays.
|
|
17
|
+
function isTagArgument(node) {
|
|
18
|
+
let current = node.parent;
|
|
19
|
+
let inner = node;
|
|
20
|
+
while (current) {
|
|
21
|
+
if (current.type === 'CallExpression') {
|
|
22
|
+
const c = current.callee;
|
|
23
|
+
if (
|
|
24
|
+
c.type === 'MemberExpression'
|
|
25
|
+
&& !c.computed
|
|
26
|
+
&& c.object.type === 'Identifier'
|
|
27
|
+
&& c.property.type === 'Identifier'
|
|
28
|
+
&& /^[a-z]/.test(c.property.name)
|
|
29
|
+
) {
|
|
30
|
+
if (current.arguments.includes(inner)) { return true; }
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (
|
|
35
|
+
current.type === 'ArrayExpression'
|
|
36
|
+
|| current.type === 'ObjectExpression'
|
|
37
|
+
|| current.type === 'Property'
|
|
38
|
+
|| current.type === 'SpreadElement'
|
|
39
|
+
) {
|
|
40
|
+
inner = current;
|
|
41
|
+
current = current.parent;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
meta: {
|
|
51
|
+
type: 'suggestion',
|
|
52
|
+
docs: {
|
|
53
|
+
description: 'disallow referencing a signal/computed/transform from outside the computed scope where it was created',
|
|
54
|
+
},
|
|
55
|
+
messages: {
|
|
56
|
+
noOutOfScopeSignal:
|
|
57
|
+
'signal() created inside a computed() body is referenced out of scope. ' +
|
|
58
|
+
'The instance is owned by the surrounding computed and may be stopped at any time. ' +
|
|
59
|
+
'Consume inline: call .get() on it, or pass it directly to a tag.',
|
|
60
|
+
noOutOfScopeComputed:
|
|
61
|
+
'computed() created inside a computed() body is referenced out of scope. ' +
|
|
62
|
+
'The instance is owned by the surrounding computed and may be stopped at any time. ' +
|
|
63
|
+
'Consume inline: call .get() on it, or pass it directly to a tag.',
|
|
64
|
+
noOutOfScopeTransform:
|
|
65
|
+
'.transform() called inside a computed() body is referenced out of scope. ' +
|
|
66
|
+
'The instance is owned by the surrounding computed and may be stopped at any time. ' +
|
|
67
|
+
'Consume inline: call .get() on it, or pass it directly to a tag.',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
create(context) {
|
|
72
|
+
const signalNames = new Set();
|
|
73
|
+
const computedNames = new Set();
|
|
74
|
+
const effectNames = new Set();
|
|
75
|
+
// Each entry is 'computed', 'effect', or 'other'. Innermost frame is last.
|
|
76
|
+
const fnStack = [];
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
ImportDeclaration(node) {
|
|
80
|
+
if (node.source.value !== 'kensington') { return; }
|
|
81
|
+
for (const spec of node.specifiers) {
|
|
82
|
+
if (spec.type !== 'ImportSpecifier') { continue; }
|
|
83
|
+
if (spec.imported.name === 'signal') { signalNames.add(spec.local.name); }
|
|
84
|
+
if (spec.imported.name === 'computed') { computedNames.add(spec.local.name); }
|
|
85
|
+
if (spec.imported.name === 'effect') { effectNames.add(spec.local.name); }
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
':matches(ArrowFunctionExpression, FunctionExpression)'(node) {
|
|
90
|
+
const { parent } = node;
|
|
91
|
+
if (
|
|
92
|
+
parent.type === 'CallExpression' &&
|
|
93
|
+
parent.arguments[0] === node &&
|
|
94
|
+
parent.callee.type === 'Identifier'
|
|
95
|
+
) {
|
|
96
|
+
if (computedNames.has(parent.callee.name)) {
|
|
97
|
+
fnStack.push('computed');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (effectNames.has(parent.callee.name)) {
|
|
101
|
+
fnStack.push('effect');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
fnStack.push('other');
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
':matches(ArrowFunctionExpression, FunctionExpression):exit'() {
|
|
109
|
+
fnStack.pop();
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
CallExpression(node) {
|
|
113
|
+
let messageId;
|
|
114
|
+
if (
|
|
115
|
+
node.callee.type === 'Identifier'
|
|
116
|
+
&& signalNames.has(node.callee.name)
|
|
117
|
+
) {
|
|
118
|
+
messageId = 'noOutOfScopeSignal';
|
|
119
|
+
} else if (
|
|
120
|
+
node.callee.type === 'Identifier'
|
|
121
|
+
&& computedNames.has(node.callee.name)
|
|
122
|
+
) {
|
|
123
|
+
messageId = 'noOutOfScopeComputed';
|
|
124
|
+
} else if (
|
|
125
|
+
node.callee.type === 'MemberExpression'
|
|
126
|
+
&& !node.callee.computed
|
|
127
|
+
&& node.callee.property.type === 'Identifier'
|
|
128
|
+
&& node.callee.property.name === 'transform'
|
|
129
|
+
) {
|
|
130
|
+
messageId = 'noOutOfScopeTransform';
|
|
131
|
+
} else {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Must be inside a computed callback. Inside an effect is a different concern.
|
|
136
|
+
let insideComputed = false;
|
|
137
|
+
for (let i = fnStack.length - 1; i >= 0; i--) {
|
|
138
|
+
if (fnStack[i] === 'computed') { insideComputed = true; break; }
|
|
139
|
+
if (fnStack[i] === 'effect') { return; }
|
|
140
|
+
}
|
|
141
|
+
if (!insideComputed) { return; }
|
|
142
|
+
|
|
143
|
+
// Safe: result is consumed immediately by a method chain. Call.get(),
|
|
144
|
+
// call.transform(...), call.toString(), etc. The chain consumes the instance;
|
|
145
|
+
// the instance itself never escapes the scope.
|
|
146
|
+
const { parent } = node;
|
|
147
|
+
if (
|
|
148
|
+
parent.type === 'MemberExpression'
|
|
149
|
+
&& !parent.computed
|
|
150
|
+
&& parent.object === node
|
|
151
|
+
) { return; }
|
|
152
|
+
|
|
153
|
+
// Safe: passed directly to a tag call as content or an attribute value.
|
|
154
|
+
if (isTagArgument(node)) { return; }
|
|
155
|
+
|
|
156
|
+
context.report({ node, messageId });
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|