@azerothjs/eslint-plugin 0.5.0-beta.1
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/dist/ast.d.ts +42 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +55 -0
- package/dist/ast.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/handler-call.d.ts +3 -0
- package/dist/rules/handler-call.d.ts.map +1 -0
- package/dist/rules/handler-call.js +68 -0
- package/dist/rules/handler-call.js.map +1 -0
- package/dist/rules/no-self-write-in-effect.d.ts +3 -0
- package/dist/rules/no-self-write-in-effect.d.ts.map +1 -0
- package/dist/rules/no-self-write-in-effect.js +69 -0
- package/dist/rules/no-self-write-in-effect.js.map +1 -0
- package/dist/rules/require-effect-disposal.d.ts +3 -0
- package/dist/rules/require-effect-disposal.d.ts.map +1 -0
- package/dist/rules/require-effect-disposal.js +38 -0
- package/dist/rules/require-effect-disposal.js.map +1 -0
- package/package.json +54 -0
package/dist/ast.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** The slice of ESTree the rules touch, structurally typed. */
|
|
2
|
+
export interface AstNode {
|
|
3
|
+
type: string;
|
|
4
|
+
parent?: AstNode;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
/** A `name` carrier (Identifier). */
|
|
8
|
+
export interface IdentifierNode extends AstNode {
|
|
9
|
+
type: 'Identifier';
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
/** A call with a callee and arguments. */
|
|
13
|
+
export interface CallNode extends AstNode {
|
|
14
|
+
type: 'CallExpression';
|
|
15
|
+
callee: AstNode;
|
|
16
|
+
arguments: AstNode[];
|
|
17
|
+
}
|
|
18
|
+
/** True when `node` is an Identifier with the given name. */
|
|
19
|
+
export declare function isIdentifier(node: AstNode | undefined, name?: string): node is IdentifierNode;
|
|
20
|
+
/** True when `node` is a call of the named identifier: `name(...)`. */
|
|
21
|
+
export declare function isCallTo(node: AstNode | undefined, name: string): node is CallNode;
|
|
22
|
+
/** A function expression of any flavor. */
|
|
23
|
+
export declare function isFunctionNode(node: AstNode | undefined): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Walks ancestors from `node` upward, returning the first one matching
|
|
26
|
+
* `predicate`, stopping (exclusive) at `boundary`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findAncestor(node: AstNode, predicate: (ancestor: AstNode) => boolean, boundary?: AstNode): AstNode | null;
|
|
29
|
+
/**
|
|
30
|
+
* Collects the getter->setter pairs declared in the file via
|
|
31
|
+
* `const [get, set] = createSignal(...)`. Keyed both ways for the rules'
|
|
32
|
+
* lookups.
|
|
33
|
+
*/
|
|
34
|
+
export interface SignalPairs {
|
|
35
|
+
/** setter name -> getter name */
|
|
36
|
+
getterOf: Map<string, string>;
|
|
37
|
+
/** getter names */
|
|
38
|
+
getters: Set<string>;
|
|
39
|
+
}
|
|
40
|
+
/** Records a `const [g, s] = createSignal(...)` declarator into `pairs`. */
|
|
41
|
+
export declare function collectSignalPair(declarator: AstNode, pairs: SignalPairs): void;
|
|
42
|
+
//# sourceMappingURL=ast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAOA,+DAA+D;AAC/D,MAAM,WAAW,OAAO;IAEpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,qCAAqC;AACrC,MAAM,WAAW,cAAe,SAAQ,OAAO;IAE3C,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,QAAS,SAAQ,OAAO;IAErC,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,EAAE,CAAC;CACxB;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,IAAI,cAAc,CAI7F;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,QAAQ,CAIlF;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAOjE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CACxB,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,EACzC,QAAQ,CAAC,EAAE,OAAO,GACnB,OAAO,GAAG,IAAI,CAYhB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAExB,iCAAiC;IACjC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE9B,mBAAmB;IACnB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACxB;AAED,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAoB/E"}
|
package/dist/ast.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Minimal structural AST types and helpers shared by the rules. The rules
|
|
2
|
+
// are deliberately SYNTACTIC: they track signals from
|
|
3
|
+
// `const [x, setX] = createSignal(...)` destructuring by name, the same
|
|
4
|
+
// approach eslint-plugin-solid takes. That keeps them parser-agnostic (no
|
|
5
|
+
// type-services project wiring for consumers) at the cost of not seeing
|
|
6
|
+
// through aliases or imports-as - the trade is documented per rule.
|
|
7
|
+
/** True when `node` is an Identifier with the given name. */
|
|
8
|
+
export function isIdentifier(node, name) {
|
|
9
|
+
return node !== undefined && node.type === 'Identifier'
|
|
10
|
+
&& (name === undefined || node.name === name);
|
|
11
|
+
}
|
|
12
|
+
/** True when `node` is a call of the named identifier: `name(...)`. */
|
|
13
|
+
export function isCallTo(node, name) {
|
|
14
|
+
return node !== undefined && node.type === 'CallExpression'
|
|
15
|
+
&& isIdentifier(node.callee, name);
|
|
16
|
+
}
|
|
17
|
+
/** A function expression of any flavor. */
|
|
18
|
+
export function isFunctionNode(node) {
|
|
19
|
+
return node !== undefined && (node.type === 'ArrowFunctionExpression' ||
|
|
20
|
+
node.type === 'FunctionExpression' ||
|
|
21
|
+
node.type === 'FunctionDeclaration');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Walks ancestors from `node` upward, returning the first one matching
|
|
25
|
+
* `predicate`, stopping (exclusive) at `boundary`.
|
|
26
|
+
*/
|
|
27
|
+
export function findAncestor(node, predicate, boundary) {
|
|
28
|
+
let current = node.parent;
|
|
29
|
+
while (current !== undefined && current !== boundary) {
|
|
30
|
+
if (predicate(current)) {
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
current = current.parent;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/** Records a `const [g, s] = createSignal(...)` declarator into `pairs`. */
|
|
38
|
+
export function collectSignalPair(declarator, pairs) {
|
|
39
|
+
const init = declarator.init;
|
|
40
|
+
if (!isCallTo(init, 'createSignal')) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const id = declarator.id;
|
|
44
|
+
if (id === undefined || id.type !== 'ArrayPattern') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const elements = id.elements;
|
|
48
|
+
const getter = elements[0];
|
|
49
|
+
const setter = elements[1];
|
|
50
|
+
if (getter !== null && setter !== null && isIdentifier(getter ?? undefined) && isIdentifier(setter ?? undefined)) {
|
|
51
|
+
pairs.getterOf.set(setter.name, getter.name);
|
|
52
|
+
pairs.getters.add(getter.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=ast.js.map
|
package/dist/ast.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,sDAAsD;AACtD,wEAAwE;AACxE,0EAA0E;AAC1E,wEAAwE;AACxE,oEAAoE;AAyBpE,6DAA6D;AAC7D,MAAM,UAAU,YAAY,CAAC,IAAyB,EAAE,IAAa;IAEjE,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;WAChD,CAAC,IAAI,KAAK,SAAS,IAAK,IAAuB,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC1E,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,QAAQ,CAAC,IAAyB,EAAE,IAAY;IAE5D,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB;WACpD,YAAY,CAAE,IAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACzD,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,cAAc,CAAC,IAAyB;IAEpD,OAAO,IAAI,KAAK,SAAS,IAAI,CACzB,IAAI,CAAC,IAAI,KAAK,yBAAyB;QACvC,IAAI,CAAC,IAAI,KAAK,oBAAoB;QAClC,IAAI,CAAC,IAAI,KAAK,qBAAqB,CACtC,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CACxB,IAAa,EACb,SAAyC,EACzC,QAAkB;IAGlB,IAAI,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC;IAC1B,OAAO,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,QAAQ,EACpD,CAAC;QACG,IAAI,SAAS,CAAC,OAAO,CAAC,EACtB,CAAC;YACG,OAAO,OAAO,CAAC;QACnB,CAAC;QACD,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAC7B,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAgBD,4EAA4E;AAC5E,MAAM,UAAU,iBAAiB,CAAC,UAAmB,EAAE,KAAkB;IAErE,MAAM,IAAI,GAAG,UAAU,CAAC,IAA2B,CAAC;IACpD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,EACnC,CAAC;QACG,OAAO;IACX,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,CAAC,EAAyB,CAAC;IAChD,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,CAAC,IAAI,KAAK,cAAc,EAClD,CAAC;QACG,OAAO;IACX,CAAC;IACD,MAAM,QAAQ,GAAG,EAAE,CAAC,QAA8B,CAAC;IACnD,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,IAAI,YAAY,CAAC,MAAM,IAAI,SAAS,CAAC,IAAI,YAAY,CAAC,MAAM,IAAI,SAAS,CAAC,EAChH,CAAC;QACG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAE,MAAyB,CAAC,IAAI,EAAG,MAAyB,CAAC,IAAI,CAAC,CAAC;QACrF,KAAK,CAAC,OAAO,CAAC,GAAG,CAAE,MAAyB,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC;AACL,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ESLint, Linter, Rule } from 'eslint';
|
|
2
|
+
declare const rules: Record<string, Rule.RuleModule>;
|
|
3
|
+
declare const plugin: ESLint.Plugin & {
|
|
4
|
+
configs: {
|
|
5
|
+
recommended: Linter.Config;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export default plugin;
|
|
9
|
+
export { rules };
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAKnD,QAAA,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAI1C,CAAC;AAEF,QAAA,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG;IAAE,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,CAAA;KAAE,CAAA;CAWtE,CAAC;AAiBF,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @azerothjs/eslint-plugin: the reactivity foot-guns in plain .ts files
|
|
2
|
+
// (markup in .azeroth files is covered by the compiler's lint, surfaced in
|
|
3
|
+
// the editor and the Vite build). The rules are SYNTACTIC by design -
|
|
4
|
+
// signals are tracked from `const [x, setX] = createSignal(...)`
|
|
5
|
+
// destructuring by name, so consumers need no type-services project
|
|
6
|
+
// wiring. The trade: aliased or re-exported signals are invisible.
|
|
7
|
+
//
|
|
8
|
+
// Flat-config use:
|
|
9
|
+
//
|
|
10
|
+
// import azeroth from '@azerothjs/eslint-plugin';
|
|
11
|
+
// export default [azeroth.configs.recommended];
|
|
12
|
+
import { noSelfWriteInEffect } from "./rules/no-self-write-in-effect.js";
|
|
13
|
+
import { requireEffectDisposal } from "./rules/require-effect-disposal.js";
|
|
14
|
+
import { handlerCall } from "./rules/handler-call.js";
|
|
15
|
+
const rules = {
|
|
16
|
+
'no-self-write-in-effect': noSelfWriteInEffect,
|
|
17
|
+
'require-effect-disposal': requireEffectDisposal,
|
|
18
|
+
'handler-call': handlerCall
|
|
19
|
+
};
|
|
20
|
+
const plugin = {
|
|
21
|
+
meta: {
|
|
22
|
+
name: '@azerothjs/eslint-plugin',
|
|
23
|
+
version: '0.4.0-beta.3'
|
|
24
|
+
},
|
|
25
|
+
rules,
|
|
26
|
+
configs: {
|
|
27
|
+
recommended: {}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// The recommended config references the plugin object itself, so it is
|
|
31
|
+
// attached after construction.
|
|
32
|
+
plugin.configs.recommended = {
|
|
33
|
+
plugins: {
|
|
34
|
+
azeroth: plugin
|
|
35
|
+
},
|
|
36
|
+
rules: {
|
|
37
|
+
'azeroth/no-self-write-in-effect': 'warn',
|
|
38
|
+
'azeroth/require-effect-disposal': 'warn',
|
|
39
|
+
'azeroth/handler-call': 'warn'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
export default plugin;
|
|
43
|
+
export { rules };
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,2EAA2E;AAC3E,sEAAsE;AACtE,iEAAiE;AACjE,oEAAoE;AACpE,mEAAmE;AACnE,EAAE;AACF,mBAAmB;AACnB,EAAE;AACF,sDAAsD;AACtD,oDAAoD;AAGpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,MAAM,KAAK,GAAoC;IAC3C,yBAAyB,EAAE,mBAAmB;IAC9C,yBAAyB,EAAE,qBAAqB;IAChD,cAAc,EAAE,WAAW;CAC9B,CAAC;AAEF,MAAM,MAAM,GAAgE;IACxE,IAAI,EACJ;QACI,IAAI,EAAE,0BAA0B;QAChC,OAAO,EAAE,cAAc;KAC1B;IACD,KAAK;IACL,OAAO,EACP;QACI,WAAW,EAAE,EAAmB;KACnC;CACJ,CAAC;AAEF,uEAAuE;AACvE,+BAA+B;AAC/B,MAAM,CAAC,OAAO,CAAC,WAAW,GAAG;IACzB,OAAO,EACP;QACI,OAAO,EAAE,MAAM;KAClB;IACD,KAAK,EACL;QACI,iCAAiC,EAAE,MAAM;QACzC,iCAAiC,EAAE,MAAM;QACzC,sBAAsB,EAAE,MAAM;KACjC;CACJ,CAAC;AAEF,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-call.d.ts","sourceRoot":"","sources":["../../src/rules/handler-call.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAuBnC,eAAO,MAAM,WAAW,EAAE,IAAI,CAAC,UA8D9B,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// azeroth/handler-call: the hyperscript twin of the compiler's
|
|
2
|
+
// azeroth/handler-call markup rule. `h('button', { onClick: save() })`
|
|
3
|
+
// calls save() while BUILDING the element and passes its result as the
|
|
4
|
+
// handler. Zero-argument calls of a plain reference are flagged;
|
|
5
|
+
// `onClick: makeHandler(id)` (the factory idiom) stays silent.
|
|
6
|
+
import { isCallTo, isIdentifier } from "../ast.js";
|
|
7
|
+
/** `save` or `actions.reset` - a bare callable reference. */
|
|
8
|
+
function isBareReference(node) {
|
|
9
|
+
if (node === undefined) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (node.type === 'Identifier') {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (node.type === 'MemberExpression') {
|
|
16
|
+
return (node.computed !== true)
|
|
17
|
+
&& isBareReference(node.object)
|
|
18
|
+
&& isIdentifier(node.property);
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
export const handlerCall = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
docs: {
|
|
26
|
+
description: 'disallow zero-argument calls passed as h() event handlers'
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
called: '{{key}}: {{callee}}() runs while the element is being built and passes its RESULT as the handler. Use {{key}}: {{callee}} or {{key}}: () => {{callee}}().'
|
|
30
|
+
},
|
|
31
|
+
schema: []
|
|
32
|
+
},
|
|
33
|
+
create(context) {
|
|
34
|
+
return {
|
|
35
|
+
CallExpression(node) {
|
|
36
|
+
const call = node;
|
|
37
|
+
if (!isCallTo(call, 'h')) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const props = call.arguments[1];
|
|
41
|
+
if (props === undefined || props.type !== 'ObjectExpression') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const property of props.properties) {
|
|
45
|
+
if (property.type !== 'Property' || property.computed === true) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const key = property.key;
|
|
49
|
+
if (!isIdentifier(key) || !/^on[A-Z]/.test(key.name)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const value = property.value;
|
|
53
|
+
if (value.type === 'CallExpression' &&
|
|
54
|
+
value.arguments.length === 0 &&
|
|
55
|
+
isBareReference(value.callee)) {
|
|
56
|
+
const source = context.sourceCode.getText(value.callee);
|
|
57
|
+
context.report({
|
|
58
|
+
node: value,
|
|
59
|
+
messageId: 'called',
|
|
60
|
+
data: { key: key.name, callee: source }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=handler-call.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-call.js","sourceRoot":"","sources":["../../src/rules/handler-call.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,uEAAuE;AACvE,uEAAuE;AACvE,iEAAiE;AACjE,+DAA+D;AAG/D,OAAO,EAA+B,QAAQ,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEhF,6DAA6D;AAC7D,SAAS,eAAe,CAAC,IAAyB;IAE9C,IAAI,IAAI,KAAK,SAAS,EACtB,CAAC;QACG,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,EAC9B,CAAC;QACG,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,kBAAkB,EACpC,CAAC;QACG,OAAO,CAAC,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC;eACxB,eAAe,CAAC,IAAI,CAAC,MAAiB,CAAC;eACvC,YAAY,CAAC,IAAI,CAAC,QAAmB,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAoB;IACxC,IAAI,EACJ;QACI,IAAI,EAAE,SAAS;QACf,IAAI,EACJ;YACI,WAAW,EAAE,2DAA2D;SAC3E;QACD,QAAQ,EACR;YACI,MAAM,EAAE,2JAA2J;SACtK;QACD,MAAM,EAAE,EAAE;KACb;IAED,MAAM,CAAC,OAAO;QAEV,OAAO;YACH,cAAc,CAAC,IAAI;gBAEf,MAAM,IAAI,GAAG,IAA2B,CAAC;gBACzC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,EACxB,CAAC;oBACG,OAAO;gBACX,CAAC;gBACD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBAChC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,kBAAkB,EAC5D,CAAC;oBACG,OAAO;gBACX,CAAC;gBAED,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,UAAuB,EACpD,CAAC;oBACG,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU,IAAI,QAAQ,CAAC,QAAQ,KAAK,IAAI,EAC9D,CAAC;wBACG,SAAS;oBACb,CAAC;oBACD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAc,CAAC;oBACpC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EACpD,CAAC;wBACG,SAAS;oBACb,CAAC;oBACD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAgB,CAAC;oBACxC,IACI,KAAK,CAAC,IAAI,KAAK,gBAAgB;wBAC9B,KAAkB,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;wBAC1C,eAAe,CAAE,KAAkB,CAAC,MAAM,CAAC,EAE/C,CAAC;wBACG,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CACpC,KAAkB,CAAC,MAAqE,CAC5F,CAAC;wBACF,OAAO,CAAC,MAAM,CAAC;4BACX,IAAI,EAAE,KAAK;4BACX,SAAS,EAAE,QAAQ;4BACnB,IAAI,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE;yBACS,CAAC,CAAC;oBAC1D,CAAC;gBACL,CAAC;YACL,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-self-write-in-effect.d.ts","sourceRoot":"","sources":["../../src/rules/no-self-write-in-effect.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAwBnC,eAAO,MAAM,mBAAmB,EAAE,IAAI,CAAC,UAsFtC,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// azeroth/no-self-write-in-effect: inside one createEffect callback, calling
|
|
2
|
+
// a signal's setter while also reading its getter is the synchronous
|
|
3
|
+
// feedback loop - the write re-runs the effect that is currently running.
|
|
4
|
+
// Wrapping the write in untrack() (or restructuring) is the fix, and an
|
|
5
|
+
// untrack-wrapped write is exactly what the rule permits.
|
|
6
|
+
import { collectSignalPair, findAncestor, isCallTo, isFunctionNode, isIdentifier } from "../ast.js";
|
|
7
|
+
export const noSelfWriteInEffect = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'disallow writing a signal inside an effect that reads it (synchronous feedback loop)'
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
selfWrite: 'This effect reads {{getter}}() and writes it with {{setter}}() - a synchronous feedback loop. Wrap the write in untrack(() => ...) or derive the value with createMemo.'
|
|
15
|
+
},
|
|
16
|
+
schema: []
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const pairs = { getterOf: new Map(), getters: new Set() };
|
|
20
|
+
const stack = [];
|
|
21
|
+
function insideUntrack(node, boundary) {
|
|
22
|
+
return findAncestor(node, (a) => isCallTo(a, 'untrack'), boundary) !== null;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
VariableDeclarator(node) {
|
|
26
|
+
collectSignalPair(node, pairs);
|
|
27
|
+
},
|
|
28
|
+
CallExpression(node) {
|
|
29
|
+
const call = node;
|
|
30
|
+
// Entering an effect: the callback is argument 0.
|
|
31
|
+
if (isCallTo(call, 'createEffect') && isFunctionNode(call.arguments[0])) {
|
|
32
|
+
stack.push({ fn: call.arguments[0], reads: new Set(), writes: [] });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const frame = stack[stack.length - 1];
|
|
36
|
+
if (frame === undefined || !isIdentifier(call.callee)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const name = call.callee.name;
|
|
40
|
+
if (pairs.getters.has(name) && !insideUntrack(call, frame.fn)) {
|
|
41
|
+
frame.reads.add(name);
|
|
42
|
+
}
|
|
43
|
+
const getter = pairs.getterOf.get(name);
|
|
44
|
+
if (getter !== undefined && !insideUntrack(call, frame.fn)) {
|
|
45
|
+
frame.writes.push({ node: call, setterName: name });
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
'CallExpression:exit'(node) {
|
|
49
|
+
const call = node;
|
|
50
|
+
const frame = stack[stack.length - 1];
|
|
51
|
+
if (frame === undefined || !isCallTo(call, 'createEffect') || call.arguments[0] !== frame.fn) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
stack.pop();
|
|
55
|
+
for (const write of frame.writes) {
|
|
56
|
+
const getter = pairs.getterOf.get(write.setterName);
|
|
57
|
+
if (getter !== undefined && frame.reads.has(getter)) {
|
|
58
|
+
context.report({
|
|
59
|
+
node: write.node,
|
|
60
|
+
messageId: 'selfWrite',
|
|
61
|
+
data: { getter, setter: write.setterName }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=no-self-write-in-effect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-self-write-in-effect.js","sourceRoot":"","sources":["../../src/rules/no-self-write-in-effect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-effect-disposal.d.ts","sourceRoot":"","sources":["../../src/rules/require-effect-disposal.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,qBAAqB,EAAE,IAAI,CAAC,UAsCxC,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// azeroth/require-effect-disposal: a bare `createEffect(...)` statement at
|
|
2
|
+
// MODULE scope has no owner - no root collects its disposer and the caller
|
|
3
|
+
// discarded it, so the effect runs for the life of the page. Inside
|
|
4
|
+
// functions, ownership is unknowable syntactically (the surrounding
|
|
5
|
+
// component/render() usually provides a root), so the rule deliberately
|
|
6
|
+
// stays silent there - module scope is the case that is always wrong.
|
|
7
|
+
import { isCallTo } from "../ast.js";
|
|
8
|
+
export const requireEffectDisposal = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'disallow undisposable module-scope createEffect calls'
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
naked: 'Module-scope createEffect with a discarded disposer can never be stopped. Keep the returned dispose function, or create the effect inside a createRoot()/component.'
|
|
16
|
+
},
|
|
17
|
+
schema: []
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
ExpressionStatement(node) {
|
|
22
|
+
const statement = node;
|
|
23
|
+
const parent = statement.parent;
|
|
24
|
+
if (parent === undefined || parent.type !== 'Program') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const expression = statement.expression;
|
|
28
|
+
if (isCallTo(expression, 'createEffect')) {
|
|
29
|
+
context.report({
|
|
30
|
+
node: expression,
|
|
31
|
+
messageId: 'naked'
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=require-effect-disposal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-effect-disposal.js","sourceRoot":"","sources":["../../src/rules/require-effect-disposal.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,2EAA2E;AAC3E,oEAAoE;AACpE,oEAAoE;AACpE,wEAAwE;AACxE,sEAAsE;AAGtE,OAAO,EAAgB,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAoB;IAClD,IAAI,EACJ;QACI,IAAI,EAAE,SAAS;QACf,IAAI,EACJ;YACI,WAAW,EAAE,uDAAuD;SACvE;QACD,QAAQ,EACR;YACI,KAAK,EAAE,qKAAqK;SAC/K;QACD,MAAM,EAAE,EAAE;KACb;IAED,MAAM,CAAC,OAAO;QAEV,OAAO;YACH,mBAAmB,CAAC,IAAI;gBAEpB,MAAM,SAAS,GAAG,IAA0B,CAAC;gBAC7C,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC;gBAChC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EACrD,CAAC;oBACG,OAAO;gBACX,CAAC;gBAED,MAAM,UAAU,GAAG,SAAS,CAAC,UAAiC,CAAC;gBAC/D,IAAI,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAC,EACxC,CAAC;oBACG,OAAO,CAAC,MAAM,CAAC;wBACX,IAAI,EAAE,UAAU;wBAChB,SAAS,EAAE,OAAO;qBAC8B,CAAC,CAAC;gBAC1D,CAAC;YACL,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@azerothjs/eslint-plugin",
|
|
3
|
+
"version": "0.5.0-beta.1",
|
|
4
|
+
"description": "AzerothJS ESLint rules — reactivity foot-guns in plain .ts files",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports":
|
|
10
|
+
{
|
|
11
|
+
".":
|
|
12
|
+
{
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files":
|
|
19
|
+
[
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"scripts":
|
|
24
|
+
{
|
|
25
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies":
|
|
29
|
+
{
|
|
30
|
+
"eslint": ">=9"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig":
|
|
33
|
+
{
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"author":
|
|
37
|
+
{
|
|
38
|
+
"name": "IntelligentQuantum",
|
|
39
|
+
"email": "IntelligentQuantum@Gmail.Com",
|
|
40
|
+
"url": "https://IntelligentQuantum.Dev/"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository":
|
|
44
|
+
{
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/IntelligentQuantum-Dev/AzerothJS.git",
|
|
47
|
+
"directory": "packages/eslint-plugin"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/IntelligentQuantum-Dev/AzerothJS/tree/main/packages/eslint-plugin",
|
|
50
|
+
"bugs":
|
|
51
|
+
{
|
|
52
|
+
"url": "https://github.com/IntelligentQuantum-Dev/AzerothJS/issues"
|
|
53
|
+
}
|
|
54
|
+
}
|