@simbiat/eslint-plugin-simbiat 1.0.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/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/Plugin.d.mts +19 -0
- package/dist/Plugin.mjs +29 -0
- package/dist/rules/NoExternalListenersInConstructor.d.mts +18 -0
- package/dist/rules/NoExternalListenersInConstructor.mjs +81 -0
- package/dist/rules/NoForbiddenInConstructor.d.mts +28 -0
- package/dist/rules/NoForbiddenInConstructor.mjs +348 -0
- package/dist/rules/NoKeypressEvent.d.mts +13 -0
- package/dist/rules/NoKeypressEvent.mjs +136 -0
- package/dist/rules/PreferFieldInitializer.d.mts +21 -0
- package/dist/rules/PreferFieldInitializer.mjs +289 -0
- package/dist/rules/RequireListenerCleanup.d.mts +28 -0
- package/dist/rules/RequireListenerCleanup.mjs +361 -0
- package/dist/rules/RequireSuperFirstInConstructor.d.mts +18 -0
- package/dist/rules/RequireSuperFirstInConstructor.mjs +117 -0
- package/dist/rules/RequireTypeParameter.d.mts +13 -0
- package/dist/rules/RequireTypeParameter.mjs +71 -0
- package/dist/utils/ASTHelpers.d.mts +70 -0
- package/dist/utils/ASTHelpers.mjs +275 -0
- package/dist/utils/Adapters.d.mts +21 -0
- package/dist/utils/Adapters.mjs +29 -0
- package/dist/utils/CustomElementsScope.d.mts +80 -0
- package/dist/utils/CustomElementsScope.mjs +270 -0
- package/package.json +50 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: simbiat/require-super-first-in-constructor.
|
|
3
|
+
*
|
|
4
|
+
* Enforces that the constructor of a Custom Element class:
|
|
5
|
+
*
|
|
6
|
+
* 1. Has `super()` as its very first statement.
|
|
7
|
+
* 2. Calls `super()` with no arguments.
|
|
8
|
+
*
|
|
9
|
+
* Per the Custom Elements spec: "A parameter-less call to super() must be the first statement in the constructor body, to establish the correct prototype chain and this value before any further code is run."
|
|
10
|
+
*
|
|
11
|
+
* Only applies to classes that directly extend HTMLElement (or configured
|
|
12
|
+
* base classes). Does not recurse into nested classes.
|
|
13
|
+
*
|
|
14
|
+
* Options: baseClasses: string[] – additional class names to treat as HTMLElement. Defaults to ['HTMLElement'].
|
|
15
|
+
*/
|
|
16
|
+
import type { Rule } from 'eslint';
|
|
17
|
+
declare const requireSuperFirstInConstructor: Rule.RuleModule;
|
|
18
|
+
export default requireSuperFirstInConstructor;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: simbiat/require-super-first-in-constructor.
|
|
3
|
+
*
|
|
4
|
+
* Enforces that the constructor of a Custom Element class:
|
|
5
|
+
*
|
|
6
|
+
* 1. Has `super()` as its very first statement.
|
|
7
|
+
* 2. Calls `super()` with no arguments.
|
|
8
|
+
*
|
|
9
|
+
* Per the Custom Elements spec: "A parameter-less call to super() must be the first statement in the constructor body, to establish the correct prototype chain and this value before any further code is run."
|
|
10
|
+
*
|
|
11
|
+
* Only applies to classes that directly extend HTMLElement (or configured
|
|
12
|
+
* base classes). Does not recurse into nested classes.
|
|
13
|
+
*
|
|
14
|
+
* Options: baseClasses: string[] – additional class names to treat as HTMLElement. Defaults to ['HTMLElement'].
|
|
15
|
+
*/
|
|
16
|
+
import { adaptNodeHandler } from '../utils/Adapters.mjs';
|
|
17
|
+
import { baseClassesSchema } from '../utils/CustomElementsScope.mjs';
|
|
18
|
+
// Helpers
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if `stmt` is an expression statement containing a bare
|
|
21
|
+
* `super(…)` call (not `super.method(…)` or any other form).
|
|
22
|
+
* @param stmt - Statement node to test.
|
|
23
|
+
* @returns True when the statement is a bare super() call.
|
|
24
|
+
*/
|
|
25
|
+
function isSuperCallStatement(stmt) {
|
|
26
|
+
return (stmt?.type === 'ExpressionStatement'
|
|
27
|
+
&& stmt.expression?.type === 'CallExpression'
|
|
28
|
+
&& stmt.expression.callee?.type === 'Super');
|
|
29
|
+
}
|
|
30
|
+
// Rule visitor handler
|
|
31
|
+
/**
|
|
32
|
+
* Checks a MethodDefinition node and reports if super() is not the first
|
|
33
|
+
* statement or is called with arguments in a Custom Element constructor.
|
|
34
|
+
* @param context - ESLint rule context for reporting.
|
|
35
|
+
* @param base_classes - Class names to treat as HTMLElement base classes.
|
|
36
|
+
* @param node - MethodDefinition node to inspect.
|
|
37
|
+
*/
|
|
38
|
+
function checkMethodDefinition(context, base_classes, node) {
|
|
39
|
+
const method = node;
|
|
40
|
+
if (method.kind !== 'constructor') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// node → MethodDefinition, node.parent → ClassBody, node.parent.parent → Class
|
|
44
|
+
const class_node = method.parent?.parent;
|
|
45
|
+
if (!class_node) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const super_class = class_node.superClass;
|
|
49
|
+
if (super_class === null || typeof super_class === 'undefined') {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (super_class.type !== 'Identifier') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!base_classes.includes(super_class.name ?? '')) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const body = method.value.body.body;
|
|
59
|
+
// Check 1: super() must be the first statement.
|
|
60
|
+
if (!isSuperCallStatement(body[0])) {
|
|
61
|
+
context.report({
|
|
62
|
+
node,
|
|
63
|
+
messageId: 'missingSuperFirst',
|
|
64
|
+
});
|
|
65
|
+
return; // no point checking arguments if super() isn't first
|
|
66
|
+
}
|
|
67
|
+
// Check 2: super() must have no arguments.
|
|
68
|
+
const super_call = body[0]?.expression;
|
|
69
|
+
if ((super_call?.arguments.length ?? 0) > 0) {
|
|
70
|
+
context.report({
|
|
71
|
+
node: super_call,
|
|
72
|
+
messageId: 'superHasArguments',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Rule visitor handler (top-level, adapted via adaptNodeHandler in `create`)
|
|
77
|
+
/**
|
|
78
|
+
* Top-level ESLint visitor for MethodDefinition nodes.
|
|
79
|
+
* @param state - Rule check state containing context and base classes.
|
|
80
|
+
* @param node - MethodDefinition node from ESLint.
|
|
81
|
+
*/
|
|
82
|
+
function onMethodDefinition(state, node) {
|
|
83
|
+
checkMethodDefinition(state.context, state.base_classes, node);
|
|
84
|
+
}
|
|
85
|
+
// Rule definition
|
|
86
|
+
const requireSuperFirstInConstructor = {
|
|
87
|
+
meta: {
|
|
88
|
+
type: 'problem',
|
|
89
|
+
docs: {
|
|
90
|
+
description: 'Require a parameter-less super() as the first statement in Custom Element constructors.',
|
|
91
|
+
url: 'https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance',
|
|
92
|
+
},
|
|
93
|
+
messages: {
|
|
94
|
+
missingSuperFirst: 'The first statement in a Custom Element constructor must be a bare super() call.',
|
|
95
|
+
superHasArguments: 'super() in a Custom Element constructor must be called without arguments.',
|
|
96
|
+
},
|
|
97
|
+
schema: baseClassesSchema,
|
|
98
|
+
hasSuggestions: false,
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* Creates the rule listeners.
|
|
102
|
+
* @param context - ESLint rule context.
|
|
103
|
+
* @returns Rule listener object.
|
|
104
|
+
*/
|
|
105
|
+
create(context) {
|
|
106
|
+
const options = context.options[0];
|
|
107
|
+
const base_classes = options?.baseClasses ?? ['HTMLElement'];
|
|
108
|
+
const check_state = {
|
|
109
|
+
context,
|
|
110
|
+
base_classes,
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
MethodDefinition: adaptNodeHandler(check_state, onMethodDefinition),
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
export default requireSuperFirstInConstructor;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: simbiat/require-type-parameter.
|
|
3
|
+
*
|
|
4
|
+
* Flags `querySelector`, `querySelectorAll`, and `closest` calls in TypeScript
|
|
5
|
+
* files that lack a type parameter, e.g. `querySelector<HTMLAnchorElement>('.link')`.
|
|
6
|
+
*
|
|
7
|
+
* Only activates on .ts / .tsx files; JS files are left alone.
|
|
8
|
+
* No auto-fix is provided: the correct type depends on the selector and must
|
|
9
|
+
* be supplied by the developer.
|
|
10
|
+
*/
|
|
11
|
+
import type { Rule } from 'eslint';
|
|
12
|
+
declare const requireTypeParameter: Rule.RuleModule;
|
|
13
|
+
export default requireTypeParameter;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: simbiat/require-type-parameter.
|
|
3
|
+
*
|
|
4
|
+
* Flags `querySelector`, `querySelectorAll`, and `closest` calls in TypeScript
|
|
5
|
+
* files that lack a type parameter, e.g. `querySelector<HTMLAnchorElement>('.link')`.
|
|
6
|
+
*
|
|
7
|
+
* Only activates on .ts / .tsx files; JS files are left alone.
|
|
8
|
+
* No auto-fix is provided: the correct type depends on the selector and must
|
|
9
|
+
* be supplied by the developer.
|
|
10
|
+
*/
|
|
11
|
+
import { adaptNodeHandler } from '../utils/Adapters.mjs';
|
|
12
|
+
/**
|
|
13
|
+
* Top-level ESLint visitor for CallExpression nodes.
|
|
14
|
+
* Checks `querySelector`/`querySelectorAll`/`closest` calls for a missing type parameter.
|
|
15
|
+
* @param context - ESLint rule context.
|
|
16
|
+
* @param node - CallExpression node from ESLint.
|
|
17
|
+
*/
|
|
18
|
+
function onCallExpression(context, node) {
|
|
19
|
+
const call = node;
|
|
20
|
+
if (call.callee.type !== 'MemberExpression') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const { property } = call.callee;
|
|
24
|
+
if (property.type !== 'Identifier') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (property.name !== 'querySelector'
|
|
28
|
+
&& property.name !== 'querySelectorAll'
|
|
29
|
+
&& property.name !== 'closest') {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// @typescript-eslint/parser attaches type arguments as either
|
|
33
|
+
// `typeParameters` (older versions) or `typeArguments` (v6+).
|
|
34
|
+
const has_type_arg = (call.typeParameters?.params.length ?? 0) > 0 || (call.typeArguments?.params.length ?? 0) > 0;
|
|
35
|
+
if (!has_type_arg) {
|
|
36
|
+
context.report({
|
|
37
|
+
node: property,
|
|
38
|
+
messageId: 'missingTypeParam',
|
|
39
|
+
data: { method: property.name },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const requireTypeParameter = {
|
|
44
|
+
meta: {
|
|
45
|
+
type: 'suggestion',
|
|
46
|
+
docs: {
|
|
47
|
+
description: 'Require a type parameter on querySelector / querySelectorAll / closest calls in TypeScript files.',
|
|
48
|
+
},
|
|
49
|
+
messages: {
|
|
50
|
+
missingTypeParam: 'Provide a type parameter to {{method}} to be more explicit and reduce casting.',
|
|
51
|
+
},
|
|
52
|
+
schema: [],
|
|
53
|
+
hasSuggestions: false,
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* Creates the rule listeners.
|
|
57
|
+
* @param context - ESLint rule context.
|
|
58
|
+
* @returns Rule listener object.
|
|
59
|
+
*/
|
|
60
|
+
create(context) {
|
|
61
|
+
// Limit to TypeScript source files only.
|
|
62
|
+
const filename = context.filename ?? '';
|
|
63
|
+
if (!filename.endsWith('.ts') && !filename.endsWith('.tsx')) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
CallExpression: adaptNodeHandler(context, onCallExpression),
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
export default requireTypeParameter;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AST helper utilities for the prefer-field-initializer and related rules.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Collects all declared field names (PropertyDefinition keys) from a class
|
|
6
|
+
* node's body into a `Set<string>`.
|
|
7
|
+
* @param class_node - ClassDeclaration or ClassExpression node.
|
|
8
|
+
* @returns Set of declared field name strings.
|
|
9
|
+
*/
|
|
10
|
+
export declare function collectFieldNames(class_node: unknown): Set<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Recursively collects all binding names introduced by a parameter or
|
|
13
|
+
* destructuring pattern node into `out`.
|
|
14
|
+
* Handles: Identifier, AssignmentPattern, RestElement, ObjectPattern,
|
|
15
|
+
* ArrayPattern, and TypeScript's TSParameterProperty.
|
|
16
|
+
* @param param - Parameter or pattern node to collect names from.
|
|
17
|
+
* @param out - Set to accumulate discovered binding names into.
|
|
18
|
+
*/
|
|
19
|
+
export declare function collectParamNames(param: unknown, out: Set<string>): void;
|
|
20
|
+
/**
|
|
21
|
+
* Pre-scans a constructor body (BlockStatement) and collects the binding
|
|
22
|
+
* names of every VariableDeclarator into `out`, without descending into
|
|
23
|
+
* nested FunctionExpression / FunctionDeclaration / ArrowFunctionExpression
|
|
24
|
+
* nodes (those have their own scope and are irrelevant here).
|
|
25
|
+
*
|
|
26
|
+
* This is called once at constructor-entry time so that later
|
|
27
|
+
* `this.x = rhs` checks can suppress false positives when `rhs` references
|
|
28
|
+
* a locally declared variable rather than a constructor parameter.
|
|
29
|
+
* @param body_node - BlockStatement (the constructor body).
|
|
30
|
+
* @param out - Set to accumulate discovered local names into.
|
|
31
|
+
*/
|
|
32
|
+
export declare function collectLocalNames(body_node: unknown, out: Set<string>): void;
|
|
33
|
+
/**
|
|
34
|
+
* Returns true if `node` (or any descendant) is a MemberExpression whose
|
|
35
|
+
* object is a ThisExpression, e.g. `this.foo`.
|
|
36
|
+
*
|
|
37
|
+
* Does NOT recurse into regular FunctionExpression / FunctionDeclaration
|
|
38
|
+
* because `this` is rebound there. DOES recurse into ArrowFunctionExpression
|
|
39
|
+
* because arrow functions inherit `this` lexically.
|
|
40
|
+
* @param node - AST node to inspect.
|
|
41
|
+
* @returns True if the node or a descendant accesses `this`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function containsThisAccess(node: unknown): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Returns true if `node` (or any descendant) contains an Identifier whose
|
|
46
|
+
* name is in `names`.
|
|
47
|
+
*
|
|
48
|
+
* Same `this`-rebinding rules as `containsThisAccess`.
|
|
49
|
+
* Stops at non-computed property keys in MemberExpression and Property to
|
|
50
|
+
* avoid false positives on `{ foo: bar }` or `obj.foo` where `foo` is in names.
|
|
51
|
+
* @param node - AST node to inspect.
|
|
52
|
+
* @param names - Set of identifier names to search for.
|
|
53
|
+
* @returns True if the node or a descendant references one of the given names.
|
|
54
|
+
*/
|
|
55
|
+
export declare function containsIdentifierRef(node: unknown, names: Set<string>): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Human-readable stringification of a known external-target node.
|
|
58
|
+
* @param node - AST node representing the target.
|
|
59
|
+
* @param text - Fallback text when the target cannot be stringified.
|
|
60
|
+
* @returns Human-readable name for the target node.
|
|
61
|
+
*/
|
|
62
|
+
export declare function targetName(node: unknown, text?: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Returns true for nodes that represent a "global" event-listener target:
|
|
65
|
+
* `document`, `window`, `document.body`, `document.documentElement`,
|
|
66
|
+
* `document.head`.
|
|
67
|
+
* @param node - AST node to test.
|
|
68
|
+
* @returns True when the node is a known external event-listener target.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isExternalTarget(node: unknown): boolean;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AST helper utilities for the prefer-field-initializer and related rules.
|
|
3
|
+
*/
|
|
4
|
+
// Field / parameter name collection
|
|
5
|
+
/**
|
|
6
|
+
* Collects all declared field names (PropertyDefinition keys) from a class
|
|
7
|
+
* node's body into a `Set<string>`.
|
|
8
|
+
* @param class_node - ClassDeclaration or ClassExpression node.
|
|
9
|
+
* @returns Set of declared field name strings.
|
|
10
|
+
*/
|
|
11
|
+
export function collectFieldNames(class_node) {
|
|
12
|
+
const node = class_node;
|
|
13
|
+
const names = new Set();
|
|
14
|
+
for (const member of node.body?.body ?? []) {
|
|
15
|
+
if (member.type !== 'PropertyDefinition') {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const key = member.key;
|
|
19
|
+
let name;
|
|
20
|
+
if (key?.type === 'Identifier' && typeof key.name === 'string') {
|
|
21
|
+
name = key.name;
|
|
22
|
+
}
|
|
23
|
+
else if (key?.type === 'Literal') {
|
|
24
|
+
name = String(key.value);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
name = null;
|
|
28
|
+
}
|
|
29
|
+
if (name !== null) {
|
|
30
|
+
names.add(name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return names;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Recursively collects all binding names introduced by a parameter or
|
|
37
|
+
* destructuring pattern node into `out`.
|
|
38
|
+
* Handles: Identifier, AssignmentPattern, RestElement, ObjectPattern,
|
|
39
|
+
* ArrayPattern, and TypeScript's TSParameterProperty.
|
|
40
|
+
* @param param - Parameter or pattern node to collect names from.
|
|
41
|
+
* @param out - Set to accumulate discovered binding names into.
|
|
42
|
+
*/
|
|
43
|
+
export function collectParamNames(param, out) {
|
|
44
|
+
if (param === null || typeof param === 'undefined' || typeof param !== 'object') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const p = param;
|
|
48
|
+
switch (p.type) {
|
|
49
|
+
case 'Identifier':
|
|
50
|
+
if (typeof p.name === 'string') {
|
|
51
|
+
out.add(p.name);
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case 'AssignmentPattern':
|
|
55
|
+
collectParamNames(p.left, out);
|
|
56
|
+
break;
|
|
57
|
+
case 'RestElement':
|
|
58
|
+
collectParamNames(p.argument, out);
|
|
59
|
+
break;
|
|
60
|
+
case 'ObjectPattern':
|
|
61
|
+
for (const prop of p.properties ?? []) {
|
|
62
|
+
collectParamNames(prop.type === 'RestElement' ? prop : prop.value, out);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
case 'ArrayPattern':
|
|
66
|
+
for (const el of p.elements ?? []) {
|
|
67
|
+
collectParamNames(el, out); // el may be null for holes
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case 'TSParameterProperty':
|
|
71
|
+
// TypeScript: constructor(private foo: string) – foo is both param and field.
|
|
72
|
+
collectParamNames(p.parameter, out);
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// AST traversal predicates
|
|
79
|
+
/** Keys that should never be traversed as child AST nodes. */
|
|
80
|
+
const SKIP_KEYS = new Set(['type', 'parent', 'loc', 'range', 'start', 'end']);
|
|
81
|
+
/**
|
|
82
|
+
* Recursively walks an AST node, collecting variable declarator binding names into `out`,
|
|
83
|
+
* without descending into nested function boundaries.
|
|
84
|
+
* @param node - AST node to walk.
|
|
85
|
+
* @param out - Set to accumulate discovered local variable names into.
|
|
86
|
+
*/
|
|
87
|
+
function walkNode(node, out) {
|
|
88
|
+
if (node === null || typeof node === 'undefined' || typeof node !== 'object') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const n = node;
|
|
92
|
+
// Stop at nested-function boundaries – their locals are a different scope.
|
|
93
|
+
if (n.type === 'FunctionExpression'
|
|
94
|
+
|| n.type === 'FunctionDeclaration'
|
|
95
|
+
|| n.type === 'ArrowFunctionExpression') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (n.type === 'VariableDeclarator') {
|
|
99
|
+
// id may be Identifier, ObjectPattern, ArrayPattern, etc.
|
|
100
|
+
collectParamNames(n.id, out);
|
|
101
|
+
// Don't walk the init expression – we only care about declared names.
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const key of Object.keys(n)) {
|
|
105
|
+
if (SKIP_KEYS.has(key)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
109
|
+
const val = n[key];
|
|
110
|
+
if (Array.isArray(val)) {
|
|
111
|
+
for (const child of val) {
|
|
112
|
+
walkNode(child, out);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (val !== null && typeof val === 'object' && typeof val.type === 'string') {
|
|
116
|
+
walkNode(val, out);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Pre-scans a constructor body (BlockStatement) and collects the binding
|
|
122
|
+
* names of every VariableDeclarator into `out`, without descending into
|
|
123
|
+
* nested FunctionExpression / FunctionDeclaration / ArrowFunctionExpression
|
|
124
|
+
* nodes (those have their own scope and are irrelevant here).
|
|
125
|
+
*
|
|
126
|
+
* This is called once at constructor-entry time so that later
|
|
127
|
+
* `this.x = rhs` checks can suppress false positives when `rhs` references
|
|
128
|
+
* a locally declared variable rather than a constructor parameter.
|
|
129
|
+
* @param body_node - BlockStatement (the constructor body).
|
|
130
|
+
* @param out - Set to accumulate discovered local names into.
|
|
131
|
+
*/
|
|
132
|
+
export function collectLocalNames(body_node, out) {
|
|
133
|
+
walkNode(body_node, out);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Returns true if `node` (or any descendant) is a MemberExpression whose
|
|
137
|
+
* object is a ThisExpression, e.g. `this.foo`.
|
|
138
|
+
*
|
|
139
|
+
* Does NOT recurse into regular FunctionExpression / FunctionDeclaration
|
|
140
|
+
* because `this` is rebound there. DOES recurse into ArrowFunctionExpression
|
|
141
|
+
* because arrow functions inherit `this` lexically.
|
|
142
|
+
* @param node - AST node to inspect.
|
|
143
|
+
* @returns True if the node or a descendant accesses `this`.
|
|
144
|
+
*/
|
|
145
|
+
export function containsThisAccess(node) {
|
|
146
|
+
if (node === null || typeof node === 'undefined' || typeof node !== 'object') {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const n = node;
|
|
150
|
+
// `this` is rebound in regular functions – stop recursing.
|
|
151
|
+
if (n.type === 'FunctionExpression' || n.type === 'FunctionDeclaration') {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (n.type === 'MemberExpression'
|
|
155
|
+
&& n.object?.type === 'ThisExpression') {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
for (const key of Object.keys(n)) {
|
|
159
|
+
if (SKIP_KEYS.has(key)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
163
|
+
const val = n[key];
|
|
164
|
+
if (Array.isArray(val)) {
|
|
165
|
+
for (const child of val) {
|
|
166
|
+
if (containsThisAccess(child)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (val !== null
|
|
172
|
+
&& typeof val === 'object'
|
|
173
|
+
&& typeof val.type === 'string'
|
|
174
|
+
&& containsThisAccess(val)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Returns true if `node` (or any descendant) contains an Identifier whose
|
|
182
|
+
* name is in `names`.
|
|
183
|
+
*
|
|
184
|
+
* Same `this`-rebinding rules as `containsThisAccess`.
|
|
185
|
+
* Stops at non-computed property keys in MemberExpression and Property to
|
|
186
|
+
* avoid false positives on `{ foo: bar }` or `obj.foo` where `foo` is in names.
|
|
187
|
+
* @param node - AST node to inspect.
|
|
188
|
+
* @param names - Set of identifier names to search for.
|
|
189
|
+
* @returns True if the node or a descendant references one of the given names.
|
|
190
|
+
*/
|
|
191
|
+
export function containsIdentifierRef(node, names) {
|
|
192
|
+
if (names.size === 0) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (node === null || typeof node === 'undefined' || typeof node !== 'object') {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const n = node;
|
|
199
|
+
if (n.type === 'FunctionExpression' || n.type === 'FunctionDeclaration') {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (n.type === 'Identifier') {
|
|
203
|
+
return typeof n.name === 'string' && names.has(n.name);
|
|
204
|
+
}
|
|
205
|
+
for (const key of Object.keys(n)) {
|
|
206
|
+
if (SKIP_KEYS.has(key)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// Skip non-computed property keys to avoid treating `{ paramName: val }`
|
|
210
|
+
// or `obj.paramName` as a reference to the parameter.
|
|
211
|
+
if ((n.type === 'Property' && key === 'key' && n.computed !== true)
|
|
212
|
+
|| (n.type === 'MemberExpression' && key === 'property' && n.computed !== true)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
216
|
+
const val = n[key];
|
|
217
|
+
if (Array.isArray(val)) {
|
|
218
|
+
for (const child of val) {
|
|
219
|
+
if (containsIdentifierRef(child, names)) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (val !== null
|
|
225
|
+
&& typeof val === 'object'
|
|
226
|
+
&& typeof val.type === 'string'
|
|
227
|
+
&& containsIdentifierRef(val, names)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Human-readable stringification of a known external-target node.
|
|
235
|
+
* @param node - AST node representing the target.
|
|
236
|
+
* @param text - Fallback text when the target cannot be stringified.
|
|
237
|
+
* @returns Human-readable name for the target node.
|
|
238
|
+
*/
|
|
239
|
+
export function targetName(node, text = 'external target') {
|
|
240
|
+
const n = node;
|
|
241
|
+
if (n.type === 'Identifier') {
|
|
242
|
+
return typeof n.name === 'string' ? n.name : text;
|
|
243
|
+
}
|
|
244
|
+
if (n.type === 'MemberExpression' && n.computed !== true) {
|
|
245
|
+
const obj = n.object?.type === 'Identifier' && typeof (n.object).name === 'string'
|
|
246
|
+
? String((n.object).name)
|
|
247
|
+
: '…';
|
|
248
|
+
const prop = n.property?.type === 'Identifier' && typeof (n.property).name === 'string'
|
|
249
|
+
? String((n.property).name)
|
|
250
|
+
: '…';
|
|
251
|
+
return `${obj}.${prop}`;
|
|
252
|
+
}
|
|
253
|
+
return text;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Returns true for nodes that represent a "global" event-listener target:
|
|
257
|
+
* `document`, `window`, `document.body`, `document.documentElement`,
|
|
258
|
+
* `document.head`.
|
|
259
|
+
* @param node - AST node to test.
|
|
260
|
+
* @returns True when the node is a known external event-listener target.
|
|
261
|
+
*/
|
|
262
|
+
export function isExternalTarget(node) {
|
|
263
|
+
const n = node;
|
|
264
|
+
if (n.type === 'Identifier') {
|
|
265
|
+
return n.name === 'document' || n.name === 'window';
|
|
266
|
+
}
|
|
267
|
+
if (n.type === 'MemberExpression'
|
|
268
|
+
&& n.computed !== true
|
|
269
|
+
&& n.object?.type === 'Identifier'
|
|
270
|
+
&& (n.object).name === 'document'
|
|
271
|
+
&& n.property?.type === 'Identifier') {
|
|
272
|
+
return ['body', 'documentElement', 'head'].includes(String((n.property).name));
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Adapter utilities to wrap stateful handlers into ESLint-compatible visitor callbacks.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a handler that expects (state, node) into the single-argument
|
|
6
|
+
* signature ESLint passes to visitor callbacks.
|
|
7
|
+
* @template S - The state type.
|
|
8
|
+
* @param state - Rule state object to be threaded through.
|
|
9
|
+
* @param handler - Two-argument handler to wrap.
|
|
10
|
+
* @returns A single-argument visitor callback suitable for ESLint.
|
|
11
|
+
*/
|
|
12
|
+
export declare function adaptNodeHandler<S>(state: S, handler: (state: S, node: unknown) => void): (node: unknown) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Wraps a handler that expects only (state) into the no-argument
|
|
15
|
+
* signature ESLint passes to `:exit` visitor callbacks.
|
|
16
|
+
* @template S - The state type.
|
|
17
|
+
* @param state - Rule state object to be threaded through.
|
|
18
|
+
* @param handler - Single-argument handler to wrap.
|
|
19
|
+
* @returns A no-argument visitor callback suitable for ESLint.
|
|
20
|
+
*/
|
|
21
|
+
export declare function adaptStateHandler<S>(state: S, handler: (state: S) => void): () => void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Adapter utilities to wrap stateful handlers into ESLint-compatible visitor callbacks.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a handler that expects (state, node) into the single-argument
|
|
6
|
+
* signature ESLint passes to visitor callbacks.
|
|
7
|
+
* @template S - The state type.
|
|
8
|
+
* @param state - Rule state object to be threaded through.
|
|
9
|
+
* @param handler - Two-argument handler to wrap.
|
|
10
|
+
* @returns A single-argument visitor callback suitable for ESLint.
|
|
11
|
+
*/
|
|
12
|
+
export function adaptNodeHandler(state, handler) {
|
|
13
|
+
return function adaptedNodeHandler(node) {
|
|
14
|
+
handler(state, node);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a handler that expects only (state) into the no-argument
|
|
19
|
+
* signature ESLint passes to `:exit` visitor callbacks.
|
|
20
|
+
* @template S - The state type.
|
|
21
|
+
* @param state - Rule state object to be threaded through.
|
|
22
|
+
* @param handler - Single-argument handler to wrap.
|
|
23
|
+
* @returns A no-argument visitor callback suitable for ESLint.
|
|
24
|
+
*/
|
|
25
|
+
export function adaptStateHandler(state, handler) {
|
|
26
|
+
return function adaptedStateHandler() {
|
|
27
|
+
handler(state);
|
|
28
|
+
};
|
|
29
|
+
}
|