8000 feat(eslint-plugin): [no-unsafe-call][no-unsafe-member-access] improv… · lorimccurry/typescript-eslint@b1b26c4 · GitHub
[go: up one dir, main page]

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 t 10000 hisExpression = 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) {
10000 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)
0