eslint-plugin-no-mistakes 0.96.0 → 0.97.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/package.json +1 -1
- package/src/rules/react-no-use-promise-resolve-alias.js +168 -0
- package/src/rules/react-no-use-promise-resolve.js +33 -1
- package/src/rules/test-no-delayed-rejects-promise-alias.js +69 -0
- package/src/rules/test-no-delayed-rejects.js +4 -33
- package/src/rules/ts-no-function-aliases-ast.js +7 -1
- package/src/rules/ts-no-function-aliases-cast-proof.js +145 -0
- package/src/rules/ts-no-function-aliases-contract.js +8 -30
- package/src/rules/ts-no-function-aliases.js +7 -1
package/package.json
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { resolveVariable } = require("./test-no-shared-state-aliases");
|
|
4
|
+
const { literalString } = require("./module-mock-helpers");
|
|
5
|
+
|
|
6
|
+
function writesNamespaceMember(variable) {
|
|
7
|
+
return variable.references.some((reference) => {
|
|
8
|
+
const member = reference.identifier.parent;
|
|
9
|
+
if (member?.type !== "MemberExpression" || member.object !== reference.identifier) return false;
|
|
10
|
+
const parent = member.parent;
|
|
11
|
+
return (
|
|
12
|
+
(parent.type === "AssignmentExpression" && parent.left === member) ||
|
|
13
|
+
(parent.type === "UpdateExpression" && parent.argument === member) ||
|
|
14
|
+
(parent.type === "UnaryExpression" && parent.operator === "delete")
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function aliasUsesStayLocal(variable, context, seen = new Set()) {
|
|
20
|
+
if (
|
|
21
|
+
seen.has(variable) ||
|
|
22
|
+
variable.defs[0].node?.parent?.parent?.type === "ExportNamedDeclaration" ||
|
|
23
|
+
writesNamespaceMember(variable)
|
|
24
|
+
)
|
|
25
|
+
return false;
|
|
26
|
+
seen.add(variable);
|
|
27
|
+
return variable.references.every((reference) => {
|
|
28
|
+
if (reference.init) return true;
|
|
29
|
+
if (reference.isWrite() || crossesWith(reference.identifier, variable.scope, context))
|
|
30
|
+
return false;
|
|
31
|
+
const parent = reference.identifier.parent;
|
|
32
|
+
if (parent?.type === "MemberExpression" && parent.object === reference.identifier) {
|
|
33
|
+
const name = parent.computed ? literalString(parent.property) : parent.property.name;
|
|
34
|
+
return (
|
|
35
|
+
name === "resolve" &&
|
|
36
|
+
parent.parent?.type === "CallExpression" &&
|
|
37
|
+
parent.parent.callee === parent
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
parent?.type !== "VariableDeclarator" ||
|
|
42
|
+
parent.init !== reference.identifier ||
|
|
43
|
+
parent.parent?.kind !== "const" ||
|
|
44
|
+
parent.id.type !== "Identifier" ||
|
|
45
|
+
parent.parent.parent?.type === "ExportNamedDeclaration" ||
|
|
46
|
+
variable.defs[0].node.init.range[1] > parent.init.range[0]
|
|
47
|
+
)
|
|
48
|
+
return false;
|
|
49
|
+
const child = resolveVariable(parent.id, context);
|
|
50
|
+
return child?.defs.length === 1 && aliasUsesStayLocal(child, context, seen);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function globalUsesStayLocal(variable, context) {
|
|
55
|
+
return variable.references.every((reference) => {
|
|
56
|
+
if (reference.isTypeReference && !reference.isValueReference) return true;
|
|
57
|
+
if (reference.isWrite()) return false;
|
|
58
|
+
const parent = reference.identifier.parent;
|
|
59
|
+
if (parent?.type === "MemberExpression" && parent.object === reference.identifier) {
|
|
60
|
+
const name = parent.computed ? literalString(parent.property) : parent.property.name;
|
|
61
|
+
return (
|
|
62
|
+
["resolve", "reject", "all", "allSettled", "any", "race"].includes(name) &&
|
|
63
|
+
parent.parent?.type === "CallExpression" &&
|
|
64
|
+
parent.parent.callee === parent
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (parent?.type === "NewExpression" && parent.callee === reference.identifier) return true;
|
|
68
|
+
if (
|
|
69
|
+
parent?.type !== "VariableDeclarator" ||
|
|
70
|
+
parent.init !== reference.identifier ||
|
|
71
|
+
parent.parent?.kind !== "const" ||
|
|
72
|
+
parent.id.type !== "Identifier"
|
|
73
|
+
)
|
|
74
|
+
return false;
|
|
75
|
+
const child = resolveVariable(parent.id, context);
|
|
76
|
+
return child?.defs.length === 1 && aliasUsesStayLocal(child, context);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasGlobalObjectPromiseAccess(context) {
|
|
81
|
+
const globalScope = context.sourceCode.scopeManager.globalScope;
|
|
82
|
+
for (const name of ["globalThis", "window", "self", "global"]) {
|
|
83
|
+
const variable = globalScope.set.get(name);
|
|
84
|
+
const references =
|
|
85
|
+
variable && variable.defs.length === 0
|
|
86
|
+
? variable.references
|
|
87
|
+
: globalScope.through.filter(
|
|
88
|
+
(reference) => reference.identifier.name === name && !reference.resolved,
|
|
89
|
+
);
|
|
90
|
+
if (
|
|
91
|
+
references.some((reference) => {
|
|
92
|
+
const member = reference.identifier.parent;
|
|
93
|
+
if (member?.type !== "MemberExpression" || member.object !== reference.identifier)
|
|
94
|
+
return true; // The global object escaped or was used as a value.
|
|
95
|
+
const property = member.computed ? literalString(member.property) : member.property.name;
|
|
96
|
+
if (property === null || property === "Promise") return true;
|
|
97
|
+
const parent = member.parent;
|
|
98
|
+
return (
|
|
99
|
+
(parent.type === "CallExpression" && parent.callee === member) ||
|
|
100
|
+
(parent.type === "AssignmentExpression" && parent.left === member) ||
|
|
101
|
+
(parent.type === "UpdateExpression" && parent.argument === member) ||
|
|
102
|
+
(parent.type === "UnaryExpression" && parent.operator === "delete")
|
|
103
|
+
);
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function crossesWith(identifier, bindingScope, context) {
|
|
112
|
+
for (
|
|
113
|
+
let scope = context.sourceCode.getScope(identifier);
|
|
114
|
+
scope && scope !== bindingScope;
|
|
115
|
+
scope = scope.upper
|
|
116
|
+
) {
|
|
117
|
+
if (scope.type === "with") return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function immutableAlias(identifier, context) {
|
|
123
|
+
const variable = resolveVariable(identifier, context);
|
|
124
|
+
if (variable?.defs.length !== 1) return null;
|
|
125
|
+
const [definition] = variable.defs;
|
|
126
|
+
const declarator = definition.node;
|
|
127
|
+
if (
|
|
128
|
+
definition.type !== "Variable" ||
|
|
129
|
+
declarator?.parent?.kind !== "const" ||
|
|
130
|
+
declarator.id.type !== "Identifier" ||
|
|
131
|
+
declarator.id.name !== identifier.name ||
|
|
132
|
+
declarator.init?.type !== "Identifier" ||
|
|
133
|
+
declarator.parent.parent?.type === "ExportNamedDeclaration" ||
|
|
134
|
+
variable.references.some((reference) => reference.isWrite() && !reference.init) ||
|
|
135
|
+
!aliasUsesStayLocal(variable, context) ||
|
|
136
|
+
declarator.init.range[1] > identifier.range[0] ||
|
|
137
|
+
crossesWith(identifier, variable.scope, context)
|
|
138
|
+
)
|
|
139
|
+
return null;
|
|
140
|
+
return declarator;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isGlobalPromiseAlias(identifier, context) {
|
|
144
|
+
let current = identifier;
|
|
145
|
+
while (true) {
|
|
146
|
+
const declarator = immutableAlias(current, context);
|
|
147
|
+
if (!declarator) return false;
|
|
148
|
+
const next = declarator.init;
|
|
149
|
+
const binding = resolveVariable(next, context);
|
|
150
|
+
// A later const initializer is still in its TDZ at this read. This
|
|
151
|
+
// decreasing source-order invariant also rules out alias cycles.
|
|
152
|
+
if (binding?.defs.length && binding.defs[0].node?.init?.range[1] > next.range[0]) return false;
|
|
153
|
+
if (next.name === "Promise") {
|
|
154
|
+
return (
|
|
155
|
+
binding?.defs.length === 0 &&
|
|
156
|
+
binding.scope.type === "global" &&
|
|
157
|
+
!writesNamespaceMember(binding) &&
|
|
158
|
+
globalUsesStayLocal(binding, context) &&
|
|
159
|
+
!hasGlobalObjectPromiseAccess(context) &&
|
|
160
|
+
!crossesWith(next, binding?.scope, context)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (crossesWith(next, binding?.scope, context)) return false;
|
|
164
|
+
current = next;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { isGlobalPromiseAlias };
|
|
@@ -4,6 +4,7 @@ const { rule } = require("../helpers");
|
|
|
4
4
|
const { ruleSuppression } = require("../rule-suppression");
|
|
5
5
|
const { literalString } = require("./module-mock-helpers");
|
|
6
6
|
const { resolveVariable } = require("./test-no-shared-state-aliases");
|
|
7
|
+
const { isGlobalPromiseAlias } = require("./react-no-use-promise-resolve-alias");
|
|
7
8
|
|
|
8
9
|
function unwrap(node) {
|
|
9
10
|
while (node?.type === "ChainExpression") node = node.expression;
|
|
@@ -106,6 +107,36 @@ function isPromiseResolve(node) {
|
|
|
106
107
|
);
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
function isAliasedPromiseResolve(node, context) {
|
|
111
|
+
if (
|
|
112
|
+
node?.type !== "CallExpression" ||
|
|
113
|
+
node.optional ||
|
|
114
|
+
node.arguments.length !== 1 ||
|
|
115
|
+
node.arguments[0].type === "SpreadElement"
|
|
116
|
+
)
|
|
117
|
+
return false;
|
|
118
|
+
const callee = node.callee;
|
|
119
|
+
return (
|
|
120
|
+
callee.type === "MemberExpression" &&
|
|
121
|
+
!callee.optional &&
|
|
122
|
+
callee.object.type === "Identifier" &&
|
|
123
|
+
staticName(callee.property, callee.computed) === "resolve" &&
|
|
124
|
+
isGlobalPromiseAlias(callee.object, context)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ordinaryUseCall(node) {
|
|
129
|
+
if (node.optional || node.arguments.length !== 1 || node.arguments[0].type === "SpreadElement")
|
|
130
|
+
return false;
|
|
131
|
+
const callee = node.callee;
|
|
132
|
+
return (
|
|
133
|
+
callee.type === "Identifier" ||
|
|
134
|
+
(callee.type === "MemberExpression" &&
|
|
135
|
+
!callee.optional &&
|
|
136
|
+
!["call", "apply"].includes(staticName(callee.property, callee.computed)))
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
109
140
|
module.exports = Object.assign(
|
|
110
141
|
rule(
|
|
111
142
|
{
|
|
@@ -141,7 +172,8 @@ module.exports = Object.assign(
|
|
|
141
172
|
if (
|
|
142
173
|
suppressed(node) ||
|
|
143
174
|
!isReactUse(node.callee, context, useNames) ||
|
|
144
|
-
!isPromiseResolve(resolvedArgument)
|
|
175
|
+
(!isPromiseResolve(resolvedArgument) &&
|
|
176
|
+
!(ordinaryUseCall(node) && isAliasedPromiseResolve(node.arguments[0], context)))
|
|
145
177
|
) {
|
|
146
178
|
return;
|
|
147
179
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { unwrapExpression } = require("./async-ast");
|
|
4
|
+
|
|
5
|
+
function findVariable(scope, name) {
|
|
6
|
+
for (let current = scope; current; current = current.upper) {
|
|
7
|
+
const variable =
|
|
8
|
+
current.set?.get?.(name) ?? current.variables?.find((item) => item.name === name);
|
|
9
|
+
if (variable) return variable;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function crossesWith(identifier, variable, context) {
|
|
15
|
+
for (
|
|
16
|
+
let scope = context.sourceCode.getScope(identifier);
|
|
17
|
+
scope !== variable.scope;
|
|
18
|
+
scope = scope.upper
|
|
19
|
+
) {
|
|
20
|
+
if (scope.type === "with") return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function immutableDeclarator(identifier, context) {
|
|
26
|
+
const variable = findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
27
|
+
if (variable?.defs.length !== 1) return null;
|
|
28
|
+
const [definition] = variable.defs;
|
|
29
|
+
if (definition.type !== "Variable") return null;
|
|
30
|
+
const declarator = definition.node;
|
|
31
|
+
if (
|
|
32
|
+
!declarator?.init ||
|
|
33
|
+
declarator.id.type !== "Identifier" ||
|
|
34
|
+
declarator.id.name !== identifier.name ||
|
|
35
|
+
declarator.parent?.type !== "VariableDeclaration" ||
|
|
36
|
+
declarator.parent.kind !== "const"
|
|
37
|
+
)
|
|
38
|
+
return null;
|
|
39
|
+
return { variable, declarator };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function originDeclarator(identifier, context) {
|
|
43
|
+
let reference = identifier;
|
|
44
|
+
while (true) {
|
|
45
|
+
const binding = immutableDeclarator(reference, context);
|
|
46
|
+
if (!binding) return null;
|
|
47
|
+
const { declarator } = binding;
|
|
48
|
+
const value = unwrapExpression(declarator.init);
|
|
49
|
+
if (value.type !== "Identifier") return declarator;
|
|
50
|
+
// A with-object getter can replace an outer lexical value at either read.
|
|
51
|
+
if (crossesWith(reference, binding.variable, context)) return null;
|
|
52
|
+
if (binding.variable.references.some((ref) => ref.isWrite() && !ref.init)) return null;
|
|
53
|
+
// Reading an alias before initialization, or creating it from a binding
|
|
54
|
+
// still in the TDZ, does not establish promise identity.
|
|
55
|
+
if (declarator.init.range[1] > reference.range[0]) return null;
|
|
56
|
+
const target = immutableDeclarator(value, context);
|
|
57
|
+
// Each hop moves to an earlier initializer. That also rules out cycles.
|
|
58
|
+
if (
|
|
59
|
+
!target ||
|
|
60
|
+
crossesWith(value, target.variable, context) ||
|
|
61
|
+
target.variable.references.some((ref) => ref.isWrite() && !ref.init) ||
|
|
62
|
+
target.declarator.init.range[1] > declarator.init.range[0]
|
|
63
|
+
)
|
|
64
|
+
return null;
|
|
65
|
+
reference = value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { originDeclarator };
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { rule } = require("../helpers");
|
|
4
4
|
const { ruleSuppression } = require("../rule-suppression");
|
|
5
|
-
const { findContainingRoot, traverse
|
|
5
|
+
const { findContainingRoot, traverse } = require("./async-ast");
|
|
6
|
+
const { originDeclarator } = require("./test-no-delayed-rejects-promise-alias");
|
|
6
7
|
const { chainIsSafelyObserved, promiseChainBase } = require("./test-no-delayed-rejects-chains");
|
|
7
8
|
const { canReachMatcher, contains, executesBefore } = require("./test-no-delayed-rejects-flow");
|
|
8
9
|
const { isImmediateObserver } = require("./test-no-delayed-rejects-observers");
|
|
@@ -14,17 +15,6 @@ const {
|
|
|
14
15
|
|
|
15
16
|
const { expectedIdentifier } = require("./test-no-delayed-rejects-expect");
|
|
16
17
|
|
|
17
|
-
function findVariable(scope, name) {
|
|
18
|
-
let current = scope;
|
|
19
|
-
while (current) {
|
|
20
|
-
const variable =
|
|
21
|
-
current.set?.get?.(name) ?? current.variables?.find((item) => item.name === name);
|
|
22
|
-
if (variable) return variable;
|
|
23
|
-
current = current.upper;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
18
|
function matcherCall(rejects) {
|
|
29
19
|
let current = rejects;
|
|
30
20
|
while (
|
|
@@ -38,27 +28,8 @@ function matcherCall(rejects) {
|
|
|
38
28
|
: null;
|
|
39
29
|
}
|
|
40
30
|
|
|
41
|
-
function constDeclarator(identifier, context) {
|
|
42
|
-
const variable = findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
43
|
-
if (!variable || variable.defs.length !== 1) return null;
|
|
44
|
-
const [definition] = variable.defs;
|
|
45
|
-
if (definition.type !== "Variable") return null;
|
|
46
|
-
const declarator = definition.node;
|
|
47
|
-
if (
|
|
48
|
-
!declarator?.init ||
|
|
49
|
-
declarator.id.type !== "Identifier" ||
|
|
50
|
-
declarator.id.name !== identifier.name ||
|
|
51
|
-
declarator.parent?.type !== "VariableDeclaration" ||
|
|
52
|
-
declarator.parent.kind !== "const"
|
|
53
|
-
) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
if (unwrapExpression(declarator.init).type === "Identifier") return null;
|
|
57
|
-
return declarator;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
31
|
function isSameConst(identifier, declarator, context) {
|
|
61
|
-
return identifier.type === "Identifier" &&
|
|
32
|
+
return identifier.type === "Identifier" && originDeclarator(identifier, context) === declarator;
|
|
62
33
|
}
|
|
63
34
|
|
|
64
35
|
function isRejectionHandlerCall(node, declarator, context) {
|
|
@@ -159,7 +130,7 @@ module.exports = rule(
|
|
|
159
130
|
MemberExpression(node) {
|
|
160
131
|
const identifier = expectedIdentifier(node, context);
|
|
161
132
|
if (!identifier) return;
|
|
162
|
-
const declarator =
|
|
133
|
+
const declarator = originDeclarator(identifier, context);
|
|
163
134
|
const call = matcherCall(node);
|
|
164
135
|
if (
|
|
165
136
|
!declarator ||
|
|
@@ -6,7 +6,13 @@ function unwrapExpression(expression) {
|
|
|
6
6
|
current &&
|
|
7
7
|
(current.type === "AwaitExpression" ||
|
|
8
8
|
current.type === "ChainExpression" ||
|
|
9
|
-
|
|
9
|
+
[
|
|
10
|
+
"TSAsExpression",
|
|
11
|
+
"TSNonNullExpression",
|
|
12
|
+
"TSSatisfiesExpression",
|
|
13
|
+
"TSTypeAssertion",
|
|
14
|
+
"TSInstantiationExpression",
|
|
15
|
+
].includes(current.type))
|
|
10
16
|
) {
|
|
11
17
|
current = current.type === "AwaitExpression" ? current.argument : current.expression;
|
|
12
18
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { staticMemberName } = require("./ts-no-function-aliases-ast");
|
|
4
|
+
const { resolveVariable } = require("./ts-no-function-aliases-self");
|
|
5
|
+
|
|
6
|
+
function unassertedPath(expression) {
|
|
7
|
+
let current = expression;
|
|
8
|
+
while (["ChainExpression", "MemberExpression", "AwaitExpression"].includes(current.type)) {
|
|
9
|
+
if (current.type === "ChainExpression") current = current.expression;
|
|
10
|
+
else current = current.type === "AwaitExpression" ? current.argument : current.object;
|
|
11
|
+
}
|
|
12
|
+
return !current.type.startsWith("TS");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function typeQueryPath(node) {
|
|
16
|
+
if (node.type === "Identifier") return [node.name];
|
|
17
|
+
if (node.type === "TSQualifiedName") return [...typeQueryPath(node.left), node.right.name];
|
|
18
|
+
// A typeof import() query has no static value path in this file.
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pathRoot(node) {
|
|
23
|
+
let current = node;
|
|
24
|
+
while (current?.type === "MemberExpression" || current?.type === "TSQualifiedName")
|
|
25
|
+
current = current.type === "MemberExpression" ? current.object : current.left;
|
|
26
|
+
return current?.type === "Identifier" ? current : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function valuePath(node) {
|
|
30
|
+
if (node?.type === "Identifier") return [node.name];
|
|
31
|
+
if (node?.type === "MemberExpression") {
|
|
32
|
+
const left = valuePath(node.object);
|
|
33
|
+
const right = staticMemberName(node);
|
|
34
|
+
return left && right !== null ? [...left, right] : null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function samePath(left, right) {
|
|
40
|
+
return (
|
|
41
|
+
left &&
|
|
42
|
+
right &&
|
|
43
|
+
left.length === right.length &&
|
|
44
|
+
left.every((segment, index) => segment === right[index])
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assertionType(annotation, sourceCode) {
|
|
49
|
+
if (
|
|
50
|
+
annotation.type !== "TSTypeReference" ||
|
|
51
|
+
annotation.typeName.type !== "Identifier" ||
|
|
52
|
+
annotation.typeArguments
|
|
53
|
+
)
|
|
54
|
+
return annotation;
|
|
55
|
+
const variable = resolveVariable(sourceCode, annotation.typeName);
|
|
56
|
+
const declaration = variable?.defs.length === 1 ? variable.defs[0].node : null;
|
|
57
|
+
if (declaration?.type !== "TSTypeAliasDeclaration" || declaration.typeParameters)
|
|
58
|
+
return annotation;
|
|
59
|
+
// One non-generic, same-file alias is a local proof. Chained aliases and
|
|
60
|
+
// imported type names need type resolution that this rule does not own.
|
|
61
|
+
return declaration.typeAnnotation.type === "TSTypeReference"
|
|
62
|
+
? annotation
|
|
63
|
+
: declaration.typeAnnotation;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function preservedCalleeCast(node, target, sourceCode) {
|
|
67
|
+
if (node.type !== "TSAsExpression" && node.type !== "TSTypeAssertion") return false;
|
|
68
|
+
const annotation = assertionType(node.typeAnnotation, sourceCode);
|
|
69
|
+
return (
|
|
70
|
+
samePath(valuePath(node.expression), valuePath(target)) &&
|
|
71
|
+
annotation.type === "TSTypeQuery" &&
|
|
72
|
+
samePath(typeQueryPath(annotation.exprName), valuePath(target)) &&
|
|
73
|
+
resolveVariable(sourceCode, pathRoot(annotation.exprName)) ===
|
|
74
|
+
resolveVariable(sourceCode, pathRoot(target)) &&
|
|
75
|
+
!annotation.typeArguments
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resultCastPreserved(node, target, sourceCode) {
|
|
80
|
+
const castType = assertionType(node.typeAnnotation, sourceCode).type;
|
|
81
|
+
return (
|
|
82
|
+
(node.type === "TSAsExpression" || node.type === "TSTypeAssertion") &&
|
|
83
|
+
target.callable &&
|
|
84
|
+
// The binding annotation can expose a wider contract than the initializer.
|
|
85
|
+
!target.annotation &&
|
|
86
|
+
!wrapperHasOverloads(target.callable, sourceCode) &&
|
|
87
|
+
!target.callable.typeParameters &&
|
|
88
|
+
!target.callable.async &&
|
|
89
|
+
!target.callable.generator &&
|
|
90
|
+
target.callable?.returnType?.typeAnnotation.type === castType &&
|
|
91
|
+
[
|
|
92
|
+
"TSVoidKeyword",
|
|
93
|
+
"TSUndefinedKeyword",
|
|
94
|
+
"TSNullKeyword",
|
|
95
|
+
"TSStringKeyword",
|
|
96
|
+
"TSNumberKeyword",
|
|
97
|
+
"TSBooleanKeyword",
|
|
98
|
+
"TSBigIntKeyword",
|
|
99
|
+
"TSSymbolKeyword",
|
|
100
|
+
].includes(castType)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertedContractPreserved(node, call, candidate, target, sourceCode) {
|
|
105
|
+
let result = node.body;
|
|
106
|
+
if (result.type === "BlockStatement") {
|
|
107
|
+
const statement = result.body[0];
|
|
108
|
+
result = statement.argument || statement.expression;
|
|
109
|
+
}
|
|
110
|
+
while (result !== call) {
|
|
111
|
+
if (!result) return false;
|
|
112
|
+
if (["TSAsExpression", "TSTypeAssertion"].includes(result.type)) {
|
|
113
|
+
if (!resultCastPreserved(result, target, sourceCode)) return false;
|
|
114
|
+
result = result.expression;
|
|
115
|
+
} else if (result.type === "ChainExpression" || result.type === "TSNonNullExpression") {
|
|
116
|
+
if (result.type === "TSNonNullExpression") return false;
|
|
117
|
+
result = result.expression;
|
|
118
|
+
} else return false; // Async wrappers were rejected before this walk.
|
|
119
|
+
}
|
|
120
|
+
let callee = call.callee;
|
|
121
|
+
while (callee && callee !== candidate.target) {
|
|
122
|
+
if (["TSAsExpression", "TSTypeAssertion"].includes(callee.type)) {
|
|
123
|
+
if (!preservedCalleeCast(callee, candidate.target, sourceCode)) return false;
|
|
124
|
+
callee = callee.expression;
|
|
125
|
+
} else if (callee.type === "ChainExpression") {
|
|
126
|
+
callee = callee.expression;
|
|
127
|
+
} else if (callee.type === "MemberExpression") {
|
|
128
|
+
callee = callee.object;
|
|
129
|
+
} else return false;
|
|
130
|
+
}
|
|
131
|
+
return callee === candidate.target && unassertedPath(candidate.target);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function wrapperHasOverloads(node, sourceCode) {
|
|
135
|
+
if (node.type !== "FunctionDeclaration") return false;
|
|
136
|
+
return sourceCode
|
|
137
|
+
.getDeclaredVariables(node)
|
|
138
|
+
.some((variable) =>
|
|
139
|
+
variable.defs.some(
|
|
140
|
+
(definition) => definition.type === "FunctionName" && definition.node !== node,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { unassertedPath, assertedContractPreserved, wrapperHasOverloads };
|
|
@@ -2,28 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const { staticMemberName } = require("./ts-no-function-aliases-ast");
|
|
4
4
|
const { resolveVariable } = require("./ts-no-function-aliases-self");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
else current = current.type === "AwaitExpression" ? current.argument : current.object;
|
|
11
|
-
}
|
|
12
|
-
return !current.type.startsWith("TS");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function unassertedResult(node) {
|
|
16
|
-
let result = node.body;
|
|
17
|
-
if (result.type === "BlockStatement") {
|
|
18
|
-
const statement = result.body[0];
|
|
19
|
-
result = statement.argument || statement.expression;
|
|
20
|
-
}
|
|
21
|
-
return unassertedPath(result);
|
|
22
|
-
}
|
|
5
|
+
const {
|
|
6
|
+
unassertedPath,
|
|
7
|
+
assertedContractPreserved,
|
|
8
|
+
wrapperHasOverloads,
|
|
9
|
+
} = require("./ts-no-function-aliases-cast-proof");
|
|
23
10
|
|
|
24
11
|
function unassertedCall(node, call) {
|
|
25
12
|
if (call.typeArguments || call.typeParameters) return false;
|
|
26
|
-
|
|
13
|
+
// Assertions are checked against the reachable target after candidate
|
|
14
|
+
// selection. This guard still rejects casts on an explicit receiver.
|
|
27
15
|
const callee = call.callee.type === "ChainExpression" ? call.callee.expression : call.callee;
|
|
28
16
|
const method = callee.type === "MemberExpression" ? staticMemberName(callee) : null;
|
|
29
17
|
return (
|
|
@@ -31,17 +19,6 @@ function unassertedCall(node, call) {
|
|
|
31
19
|
);
|
|
32
20
|
}
|
|
33
21
|
|
|
34
|
-
function wrapperHasOverloads(node, sourceCode) {
|
|
35
|
-
if (node.type !== "FunctionDeclaration") return false;
|
|
36
|
-
return sourceCode
|
|
37
|
-
.getDeclaredVariables(node)
|
|
38
|
-
.some((variable) =>
|
|
39
|
-
variable.defs.some(
|
|
40
|
-
(definition) => definition.type === "FunctionName" && definition.node !== node,
|
|
41
|
-
),
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
22
|
function directIntrinsic(target, sourceCode) {
|
|
46
23
|
if (target.type !== "MemberExpression") return false;
|
|
47
24
|
const name = staticMemberName(target);
|
|
@@ -86,6 +63,7 @@ function documentedSignature(node, sourceCode) {
|
|
|
86
63
|
|
|
87
64
|
module.exports = {
|
|
88
65
|
unassertedCall,
|
|
66
|
+
assertedContractPreserved,
|
|
89
67
|
wrapperHasOverloads,
|
|
90
68
|
directIntrinsic,
|
|
91
69
|
documentedSignature,
|
|
@@ -12,6 +12,7 @@ const { legacyIdentifierCall } = require("./ts-no-function-aliases-legacy");
|
|
|
12
12
|
const { exportedDeclarations, reachableTarget } = require("./ts-no-function-aliases-reachable");
|
|
13
13
|
const {
|
|
14
14
|
unassertedCall,
|
|
15
|
+
assertedContractPreserved,
|
|
15
16
|
wrapperHasOverloads,
|
|
16
17
|
directIntrinsic,
|
|
17
18
|
documentedSignature,
|
|
@@ -63,7 +64,12 @@ function reportIfAlias(node, context, suppressed, declarations) {
|
|
|
63
64
|
if (!candidate) continue;
|
|
64
65
|
if (!isSameArgumentList(node.params, candidate.args)) continue;
|
|
65
66
|
const reachable = reachableTarget(candidate.target, sourceCode, declarations);
|
|
66
|
-
if (
|
|
67
|
+
if (
|
|
68
|
+
!reachable ||
|
|
69
|
+
!signaturePreserved(node, reachable) ||
|
|
70
|
+
!assertedContractPreserved(node, call, candidate, reachable, sourceCode)
|
|
71
|
+
)
|
|
72
|
+
continue;
|
|
67
73
|
context.report({ node, messageId: "alias" });
|
|
68
74
|
return;
|
|
69
75
|
}
|