Skip to content

Commit b1b26c4

Browse files
authored
feat(eslint-plugin): [no-unsafe-call][no-unsafe-member-access] improve report messages for this for noImplicitThis (typescript-eslint#3199)
1 parent b1aa7dc commit b1b26c4

11 files changed

+276
-18
lines changed

packages/eslint-plugin/src/rules/no-unsafe-assignment.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import {
22
TSESTree,
33
AST_NODE_TYPES,
44
} from '@typescript-eslint/experimental-utils';
5+
import * as tsutils from 'tsutils';
56
import * as ts from 'typescript';
67
import * as util from '../util';
8+
import { getThisExpression } from '../util';
79

810
const enum ComparisonType {
911
/** Do no assignment comparison */
@@ -25,20 +27,29 @@ export default util.createRule({
2527
requiresTypeChecking: true,
2628
},
2729
messages: {
28-
anyAssignment: 'Unsafe assignment of an any value.',
29-
unsafeArrayPattern: 'Unsafe array destructuring of an any array value.',
30+
anyAssignment: 'Unsafe assignment of an `any` value.',
31+
anyAssignmentThis: [
32+
'Unsafe assignment of an `any` value. `this` is typed as `any`.',
33+
'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.',
34+
].join('\n'),
35+
unsafeArrayPattern: 'Unsafe array destructuring of an `any` array value.',
3036
unsafeArrayPatternFromTuple:
31-
'Unsafe array destructuring of a tuple element with an any value.',
37+
'Unsafe array destructuring of a tuple element with an `any` value.',
3238
unsafeAssignment:
3339
'Unsafe assignment of type {{sender}} to a variable of type {{receiver}}.',
34-
unsafeArraySpread: 'Unsafe spread of an any value in an array.',
40+
unsafeArraySpread: 'Unsafe spread of an `any` value in an array.',
3541
},
3642
schema: [],
3743
},
3844
defaultOptions: [],
3945
create(context) {
4046
const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context);
4147
const checker = program.getTypeChecker();
48+
const compilerOptions = program.getCompilerOptions();
49+
const isNoImplicitThis = tsutils.isStrictCompilerOptionEnabled(
50+
compilerOptions,
51+
'noImplicitThis',
52+
);
4253

4354
// returns true if the assignment reported
4455
function checkArrayDestructureHelper(
@@ -243,9 +254,27 @@ export default util.createRule({
243254
return false;
244255
}
245256

257+
let messageId: 'anyAssignment' | 'anyAssignmentThis' = 'anyAssignment';
258+
259+
if (!isNoImplicitThis) {
260+
// `var foo = this`
261+
const thisExpression = getThisExpression(senderNode);
262+
if (
263+
thisExpression &&
264+
util.isTypeAnyType(
265+
util.getConstrainedTypeAtLocation(
266+
checker,
267+
esTreeNodeToTSNodeMap.get(thisExpression),
268+
),
269+
)
270+
) {
271+
messageId = 'anyAssignmentThis';
272+
}
273+
}
274+
246275
context.report({
247276
node: reportingNode,
248-
messageId: 'anyAssignment',
277+
messageId,
249278
});
250279
return true;
251280
}

packages/eslint-plugin/src/rules/no-unsafe-call.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { TSESTree } from '@typescript-eslint/experimental-utils';
2+
import * as tsutils from 'tsutils';
23
import * as util from '../util';
4+
import { getThisExpression } from '../util';
35

4-
type MessageIds = 'unsafeCall' | 'unsafeNew' | 'unsafeTemplateTag';
6+
type MessageIds =
7+
| 'unsafeCall'
8+
| 'unsafeCallThis'
9+
| 'unsafeNew'
10+
| 'unsafeTemplateTag';
511

612
export default util.createRule<[], MessageIds>({
713
name: 'no-unsafe-call',
@@ -14,7 +20,11 @@ export default util.createRule<[], MessageIds>({
1420
requiresTypeChecking: true,
1521
},
1622
messages: {
17-
unsafeCall: 'Unsafe call of an any typed value.',
23+
unsafeCall: 'Unsafe call of an `any` typed value.',
24+
unsafeCallThis: [
25+
'Unsafe call of an `any` typed value. `this` is typed as `any`.',
26+
'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.',
27+
].join('\n'),
1828
unsafeNew: 'Unsafe construction of an any type value.',
1929
unsafeTemplateTag: 'Unsafe any typed template tag.',
2030
},
@@ -24,6 +34,11 @@ export default util.createRule<[], MessageIds>({
2434
create(context) {
2535
const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context);
2636
const checker = program.getTypeChecker();
37+
const compilerOptions = program.getCompilerOptions();
38+
const isNoImplicitThis = tsutils.isStrictCompilerOptionEnabled(
39+
compilerOptions,
40+
'noImplicitThis',
41+
);
2742

2843
function checkCall(
2944
node: TSESTree.Node,
@@ -34,6 +49,21 @@ export default util.createRule<[], MessageIds>({
3449
const type = util.getConstrainedTypeAtLocation(checker, tsNode);
3550

3651
if (util.isTypeAnyType(type)) {
52+
if (!isNoImplicitThis) {
53+
// `this()` or `this.foo()` or `this.foo[bar]()`
54+
const thisExpression = getThisExpression(node);
55+
if (
56+
thisExpression &&
57+
util.isTypeAnyType(
58+
util.getConstrainedTypeAtLocation(
59+
checker,
60+
esTreeNodeToTSNodeMap.get(thisExpression),
61+
),
62+
)
63+
) {
64+
messageId = 'unsafeCallThis';
65+
}
66+
}
3767
context.report({
3868
node: reportingNode,
3969
messageId: messageId,

packages/eslint-plugin/src/rules/no-unsafe-member-access.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import {
22
TSESTree,
33
AST_NODE_TYPES,
44
} from '@typescript-eslint/experimental-utils';
5+
import * as tsutils from 'tsutils';
56
import * as util from '../util';
7+
import { getThisExpression } from '../util';
68

79
const enum State {
810
Unsafe = 1,
@@ -21,7 +23,11 @@ export default util.createRule({
2123
},
2224
messages: {
2325
unsafeMemberExpression:
24-
'Unsafe member access {{property}} on an any value.',
26+
'Unsafe member access {{property}} on an `any` value.',
27+
unsafeThisMemberExpression: [
28+
'Unsafe member access {{property}} on an `any` value. `this` is typed as `any`.',
29+
'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.',
30+
].join('\n'),
2531
unsafeComputedMemberAccess:
2632
'Computed name {{property}} resolves to an any value.',
2733
},
@@ -31,6 +37,11 @@ export default util.createRule({
3137
create(context) {
3238
const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context);
3339
const checker = program.getTypeChecker();
40+
const compilerOptions = program.getCompilerOptions();
41+
const isNoImplicitThis = tsutils.isStrictCompilerOptionEnabled(
42+
compilerOptions,
43+
'noImplicitThis',
44+
);
3445
const sourceCode = context.getSourceCode();
3546

3647
const stateCache = new Map<TSESTree.Node, State>();
@@ -58,9 +69,30 @@ export default util.createRule({
5869

5970
if (state === State.Unsafe) {
6071
const propertyName = sourceCode.getText(node.property);
72+
73+
let messageId: 'unsafeMemberExpression' | 'unsafeThisMemberExpression' =
74+
'unsafeMemberExpression';
75+
76+
if (!isNoImplicitThis) {
77+
// `this.foo` or `this.foo[bar]`
78+
const thisExpression = getThisExpression(node);
79+
80+
if (
81+
thisExpression &&
82+
util.isTypeAnyType(
83+
util.getConstrainedTypeAtLocation(
84+
checker,
85+
esTreeNodeToTSNodeMap.get(thisExpression),
86+
),
87+
)
88+
) {
89+
messageId = 'unsafeThisMemberExpression';
90+
}
91+
}
92+
6193
context.report({
6294
node,
63-
messageId: 'unsafeMemberExpression',
95+
messageId,
6496
data: {
6597
property: node.computed ? `[${propertyName}]` : `.${propertyName}`,
6698
},

packages/eslint-plugin/src/rules/no-unsafe-return.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import {
22
TSESTree,
33
AST_NODE_TYPES,
44
} from '@typescript-eslint/experimental-utils';
5-
import { isExpression } from 'tsutils';
5+
import * as tsutils from 'tsutils';
66
import * as util from '../util';
7+
import { getThisExpression } from '../util';
78

89
export default util.createRule({
910
name: 'no-unsafe-return',
@@ -16,16 +17,25 @@ export default util.createRule({
1617
requiresTypeChecking: true,
1718
},
1819
messages: {
19-
unsafeReturn: 'Unsafe return of an {{type}} typed value',
20+
unsafeReturn: 'Unsafe return of an `{{type}}` typed value.',
21+
unsafeReturnThis: [
22+
'Unsafe return of an `{{type}}` typed value. `this` is typed as `any`.',
23+
'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.',
24+
].join('\n'),
2025
unsafeReturnAssignment:
21-
'Unsafe return of type {{sender}} from function with return type {{receiver}}.',
26+
'Unsafe return of type `{{sender}}` from function with return type `{{receiver}}`.',
2227
},
2328
schema: [],
2429
},
2530
defaultOptions: [],
2631
create(context) {
2732
const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context);
2833
const checker = program.getTypeChecker();
34+
const compilerOptions = program.getCompilerOptions();
35+
const isNoImplicitThis = tsutils.isStrictCompilerOptionEnabled(
36+
compilerOptions,
37+
'noImplicitThis',
38+
);
2939

3040
function getParentFunctionNode(
3141
node: TSESTree.Node,
@@ -74,7 +84,7 @@ export default util.createRule({
7484
// so we have to use the contextual typing in these cases, i.e.
7585
// const foo1: () => Set<string> = () => new Set<any>();
7686
// the return type of the arrow function is Set<any> even though the variable is typed as Set<string>
77-
let functionType = isExpression(functionTSNode)
87+
let functionType = tsutils.isExpression(functionTSNode)
7888
? util.getContextualType(checker, functionTSNode)
7989
: checker.getTypeAtLocation(functionTSNode);
8090
if (!functionType) {
@@ -100,10 +110,28 @@ export default util.createRule({
100110
}
101111
}
102112

113+
let messageId: 'unsafeReturn' | 'unsafeReturnThis' = 'unsafeReturn';
114+
115+
if (!isNoImplicitThis) {
116+
// `return this`
117+
const thisExpression = getThisExpression(returnNode);
118+
if (
119+
thisExpression &&
120+
util.isTypeAnyType(
121+
util.getConstrainedTypeAtLocation(
122+
checker,
123+
esTreeNodeToTSNodeMap.get(thisExpression),
124+
),
125+
)
126+
) {
127+
messageId = 'unsafeReturnThis';
128+
}
129+
}
130+
103131
// If the function return type was not unknown/unknown[], mark usage as unsafeReturn.
104132
return context.report({
105133
node: reportingNode,
106-
messageId: 'unsafeReturn',
134+
messageId,
107135
data: {
108136
type: anyType === util.AnyType.Any ? 'any' : 'any[]',
109137
},
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESTree,
4+
} from '@typescript-eslint/experimental-utils';
5+
6+
export function getThisExpression(
7+
node: TSESTree.Node,
8+
): TSESTree.ThisExpression | undefined {
9+
while (node) {
10+
if (node.type === AST_NODE_TYPES.CallExpression) {
11+
node = node.callee;
12+
} else if (node.type === AST_NODE_TYPES.ThisExpression) {
13+
return node;
14+
} else if (node.type === AST_NODE_TYPES.MemberExpression) {
15+
node = node.object;
16+
} else if (node.type === AST_NODE_TYPES.ChainExpression) {
17+
node = node.expression;
18+
} else {
19+
break;
20+
}
21+
}
22+
23+
return;
24+
}

packages/eslint-plugin/src/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './astUtils';
44
export * from './collectUnusedVariables';
55
export * from './createRule';
66
export * from './getFunctionHeadLoc';
7+
export * from './getThisExpression';
78
export * from './getWrappingFixer';
89
export * from './isTypeReadonly';
910
export * from './misc';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"noImplicitThis": false
5+
}
6+
}

packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function assignmentTest(
6868
const ruleTester = new RuleTester({
6969
parser: '@typescript-eslint/parser',
7070
parserOptions: {
71-
project: './tsconfig.json',
71+
project: './tsconfig.noImplicitThis.json',
7272
tsconfigRootDir: getFixturesRootDir(),
7373
},
7474
});
@@ -347,5 +347,20 @@ declare function Foo(props: Props): never;
347347
},
348348
],
349349
},
350+
{
351+
code: `
352+
function foo() {
353+
const bar = this;
354+
}
355+
`,
356+
errors: [
357+
{
358+
messageId: 'anyAssignmentThis',
359+
line: 3,
360+
column: 9,
361+
endColumn: 19,
362+
},
363+
],
364+
},
350365
],
351366
});

packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
const ruleTester = new RuleTester({
1010
parser: '@typescript-eslint/parser',
1111
parserOptions: {
12-
project: './tsconfig.json',
12+
project: './tsconfig.noImplicitThis.json',
1313
tsconfigRootDir: getFixturesRootDir(),
1414
},
1515
});
@@ -148,5 +148,34 @@ function foo(x: { tag: any }) { x.tag\`foo\` }
148148
},
149149
],
150150
}),
151+
{
152+
code: noFormat`
153+
const methods = {
154+
methodA() {
155+
return this.methodB()
156+
},
157+
methodB() {
158+
return true
159+
},
160+
methodC() {
161+
return this()
162+
}
163+
};
164+
`,
165+
errors: [
166+
{
167+
messageId: 'unsafeCallThis',
168+
line: 4,
169+
column: 12,
170+
endColumn: 24,
171+
},
172+
{
173+
messageId: 'unsafeCallThis',
174+
line: 10,
175+
column: 12,
176+
endColumn: 16,
177+
},
178+
],
179+
},
151180
],
152181
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy