kensington-eslint-plugin 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/README.md +347 -0
- package/index.js +60 -0
- package/package.json +28 -0
- package/rules/no-async-computed.js +47 -0
- package/rules/no-async-effect.js +49 -0
- package/rules/no-effect-in-computed.js +72 -0
- package/rules/no-effect-in-effect.js +74 -0
- package/rules/no-ignored-effect-return.js +60 -0
- package/rules/no-new-computed-in-computed.js +71 -0
- package/rules/no-new-computed-in-effect.js +71 -0
- package/rules/no-new-signal-in-computed.js +73 -0
- package/rules/no-new-signal-in-effect.js +74 -0
- package/rules/no-self-read-write.js +81 -0
- package/rules/no-set-in-computed.js +74 -0
- package/rules/no-set-on-computed.js +56 -0
- package/rules/no-signal-async-write.js +105 -0
- package/rules/no-unsafe-literal.js +28 -0
- package/rules/prefer-value-in-async.js +92 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Reports .unsafeLiteral() calls on any object. Unlike .literal(), unsafeLiteral skips
|
|
2
|
+
// the script-tag check and injects raw HTML directly — a potential XSS vector.
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'disallow .unsafeLiteral() calls that bypass XSS protection',
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
noUnsafeLiteral:
|
|
11
|
+
'.unsafeLiteral() bypasses XSS protection. Use .literal() instead, which validates ' +
|
|
12
|
+
'that the string does not contain script tags.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (
|
|
20
|
+
node.callee.type !== 'MemberExpression' ||
|
|
21
|
+
node.callee.property.type !== 'Identifier' ||
|
|
22
|
+
node.callee.property.name !== 'unsafeLiteral'
|
|
23
|
+
) { return; }
|
|
24
|
+
context.report({ node, messageId: 'noUnsafeLiteral' });
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Reports .get() calls inside async callbacks within an effect() body.
|
|
2
|
+
// The effect has already completed by the time the async callback runs, so .get()
|
|
3
|
+
// registers no subscription — it behaves like .value but looks like a reactive read.
|
|
4
|
+
// Use .value instead to make the intent clear.
|
|
5
|
+
|
|
6
|
+
const ASYNC_GLOBAL_FNS = new Set([
|
|
7
|
+
'setTimeout', 'setInterval', 'setImmediate',
|
|
8
|
+
'queueMicrotask', 'requestAnimationFrame', 'requestIdleCallback',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const ASYNC_METHODS = new Set(['then', 'catch', 'finally']);
|
|
12
|
+
|
|
13
|
+
function isAsyncParent(fnNode) {
|
|
14
|
+
const { parent } = fnNode;
|
|
15
|
+
if (parent.type !== 'CallExpression' || !parent.arguments.includes(fnNode)) { return false; }
|
|
16
|
+
const { callee } = parent;
|
|
17
|
+
if (callee.type === 'Identifier' && ASYNC_GLOBAL_FNS.has(callee.name)) { return true; }
|
|
18
|
+
if (
|
|
19
|
+
callee.type === 'MemberExpression' &&
|
|
20
|
+
callee.property.type === 'Identifier' &&
|
|
21
|
+
ASYNC_METHODS.has(callee.property.name)
|
|
22
|
+
) { return true; }
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
meta: {
|
|
28
|
+
type: 'suggestion',
|
|
29
|
+
docs: {
|
|
30
|
+
description: 'prefer .value over .get() inside async callbacks within an effect()',
|
|
31
|
+
},
|
|
32
|
+
messages: {
|
|
33
|
+
preferValueInAsync:
|
|
34
|
+
"Use .value instead of .get() inside an async callback. " +
|
|
35
|
+
'The effect has already completed by the time this callback runs, so .get() ' +
|
|
36
|
+
'registers no subscription. .value makes that intent explicit.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const effectNames = new Set();
|
|
42
|
+
// Each frame: { type: 'effectTop'|'asyncCb'|'other' }
|
|
43
|
+
const fnStack = [];
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
ImportDeclaration(node) {
|
|
47
|
+
if (node.source.value !== 'kensington') { return; }
|
|
48
|
+
for (const spec of node.specifiers) {
|
|
49
|
+
if (spec.type !== 'ImportSpecifier') { continue; }
|
|
50
|
+
if (spec.imported.name === 'effect') { effectNames.add(spec.local.name); }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
':matches(ArrowFunctionExpression, FunctionExpression)'(node) {
|
|
55
|
+
const { parent } = node;
|
|
56
|
+
if (
|
|
57
|
+
parent.type === 'CallExpression' &&
|
|
58
|
+
parent.arguments[0] === node &&
|
|
59
|
+
parent.callee.type === 'Identifier' &&
|
|
60
|
+
effectNames.has(parent.callee.name)
|
|
61
|
+
) {
|
|
62
|
+
fnStack.push('effectTop');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const top = fnStack[fnStack.length - 1];
|
|
67
|
+
if (top && (top === 'effectTop' || top === 'asyncCb') && isAsyncParent(node)) {
|
|
68
|
+
fnStack.push('asyncCb');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fnStack.push('other');
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
':matches(ArrowFunctionExpression, FunctionExpression):exit'() {
|
|
76
|
+
fnStack.pop();
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
CallExpression(node) {
|
|
80
|
+
if (fnStack[fnStack.length - 1] !== 'asyncCb') { return; }
|
|
81
|
+
if (
|
|
82
|
+
node.callee.type !== 'MemberExpression' ||
|
|
83
|
+
node.callee.object.type !== 'Identifier' ||
|
|
84
|
+
node.callee.property.type !== 'Identifier' ||
|
|
85
|
+
node.callee.property.name !== 'get' ||
|
|
86
|
+
node.arguments.length !== 0
|
|
87
|
+
) { return; }
|
|
88
|
+
context.report({ node, messageId: 'preferValueInAsync' });
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
};
|