Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 608a750

Browse files
authored
feat(eslint-plugin): add rule no-unsafe-member-access (typescript-eslint#1643)
1 parent 3b40231 commit 608a750

File tree

9 files changed

+227
-3
lines changed

9 files changed

+227
-3
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
132132
| [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: |
133133
| [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: |
134134
| [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: |
135+
| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | | | :thought_balloon: |
135136
| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: |
136137
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | |
137138
| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | | :wrench: | |

packages/eslint-plugin/ROADMAP.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th
9090
| [`no-this-assignment`] || [`@typescript-eslint/no-this-alias`] |
9191
| [`no-unbound-method`] || [`@typescript-eslint/unbound-method`] |
9292
| [`no-unnecessary-class`] || [`@typescript-eslint/no-extraneous-class`] |
93-
| [`no-unsafe-any`] | 🛑 | N/A |
93+
| [`no-unsafe-any`] | 🌓 | [`@typescript-eslint/no-unsafe-member-access`]<sup>[2]</sup> |
9494
| [`no-unsafe-finally`] | 🌟 | [`no-unsafe-finally`][no-unsafe-finally] |
9595
| [`no-unused-expression`] | 🌟 | [`no-unused-expressions`][no-unused-expressions] |
9696
| [`no-unused-variable`] | 🌓 | [`@typescript-eslint/no-unused-vars`] |
@@ -113,6 +113,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th
113113
| [`use-isnan`] | 🌟 | [`use-isnan`][use-isnan] |
114114

115115
<sup>[1]</sup> The ESLint rule also supports silencing with an extra set of parentheses (`if ((foo = bar)) {}`)<br>
116+
<sup>[2]</sup> Only checks member expressions
116117

117118
### Maintainability
118119

@@ -136,7 +137,6 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th
136137
| [`prefer-readonly`] || [`@typescript-eslint/prefer-readonly`] |
137138
| [`trailing-comma`] | 🌓 | [`comma-dangle`][comma-dangle] or [Prettier] |
138139

139-
<sup>[1]</sup> Only warns when importing deprecated symbols<br>
140140
<sup>[2]</sup> Missing support for blank-line-delimited sections
141141

142142
### Style
@@ -174,7 +174,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th
174174
| [`no-reference-import`] || [`@typescript-eslint/triple-slash-reference`] |
175175
| [`no-trailing-whitespace`] | 🌟 | [`no-trailing-spaces`][no-trailing-spaces] |
176176
| [`no-unnecessary-callback-wrapper`] | 🛑 | N/A and this might be unsafe (i.e. with `forEach`) |
177-
| [`no-unnecessary-else`] | 🌟 | [`no-else-return`][no-else-return] <sup>[2]</sup |
177+
| [`no-unnecessary-else`] | 🌟 | [`no-else-return`][no-else-return] <sup>[2]</sup> |
178178
| [`no-unnecessary-initializer`] | 🌟 | [`no-undef-init`][no-undef-init] |
179179
| [`no-unnecessary-qualifier`] || [`@typescript-eslint/no-unnecessary-qualifier`] |
180180
| [`number-literal-format`] | 🛑 | N/A |
@@ -640,6 +640,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
640640
[`@typescript-eslint/semi`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/semi.md
641641
[`@typescript-eslint/no-floating-promises`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-floating-promises.md
642642
[`@typescript-eslint/no-magic-numbers`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-magic-numbers.md
643+
[`@typescript-eslint/no-unsafe-member-access`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unsafe-member-access.md
643644

644645
<!-- eslint-plugin-import -->
645646

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Disallows member access on any typed variables (`no-unsafe-member-access`)
2+
3+
Despite your best intentions, the `any` type can sometimes leak into your codebase.
4+
Member access on `any` typed variables is not checked at all by TypeScript, so it creates a potential safety hole, and source of bugs in your codebase.
5+
6+
## Rule Details
7+
8+
This rule disallows member access on any variable that is typed as `any`.
9+
10+
Examples of **incorrect** code for this rule:
11+
12+
```ts
13+
declare const anyVar: any;
14+
declare const nestedAny: { prop: any };
15+
16+
anyVar.a;
17+
anyVar.a.b;
18+
anyVar['a'];
19+
anyVar['a']['b'];
20+
21+
nestedAny.prop.a;
22+
nestedAny.prop['a'];
23+
24+
const key = 'a';
25+
nestedAny.prop[key];
26+
```
27+
28+
Examples of **correct** code for this rule:
29+
30+
```ts
31+
declare const properlyTyped: { prop: { a: string } };
32+
33+
nestedAny.prop.a;
34+
nestedAny.prop['a'];
35+
36+
const key = 'a';
37+
nestedAny.prop[key];
38+
```
39+
40+
## Related to
41+
42+
- [`no-explicit-any`](./no-explicit-any.md)
43+
- TSLint: [`no-unsafe-any`](https://palantir.github.io/tslint/rules/no-unsafe-any/)

packages/eslint-plugin/src/configs/all.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@typescript-eslint/no-unnecessary-qualifier": "error",
6262
"@typescript-eslint/no-unnecessary-type-arguments": "error",
6363
"@typescript-eslint/no-unnecessary-type-assertion": "error",
64+
"@typescript-eslint/no-unsafe-member-access": "error",
6465
"no-unused-expressions": "off",
6566
"@typescript-eslint/no-unused-expressions": "error",
6667
"no-unused-vars": "off",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import noUnnecessaryCondition from './no-unnecessary-condition';
5353
import noUnnecessaryQualifier from './no-unnecessary-qualifier';
5454
import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments';
5555
import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion';
56+
import noUnsafeMemberAccess from './no-unsafe-member-access';
5657
import noUntypedPublicSignature from './no-untyped-public-signature';
5758
import noUnusedExpressions from './no-unused-expressions';
5859
import noUnusedVars from './no-unused-vars';
@@ -144,6 +145,7 @@ export default {
144145
'no-unnecessary-qualifier': noUnnecessaryQualifier,
145146
'no-unnecessary-type-arguments': noUnnecessaryTypeArguments,
146147
'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion,
148+
'no-unsafe-member-access': noUnsafeMemberAccess,
147149
'no-untyped-public-signature': noUntypedPublicSignature,
148150
'no-unused-expressions': noUnusedExpressions,
149151
'no-unused-vars-experimental': noUnusedVarsExperimental,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { TSESTree } from '@typescript-eslint/experimental-utils';
2+
import * as util from '../util';
3+
4+
const enum State {
5+
Unsafe = 1,
6+
Safe = 2,
7+
}
8+
9+
export default util.createRule({
10+
name: 'no-unsafe-member-access',
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'Disallows member access on any typed variables',
15+
category: 'Possible Errors',
16+
recommended: false,
17+
requiresTypeChecking: true,
18+
},
19+
messages: {
20+
unsafeMemberExpression:
21+
'Unsafe member access {{property}} on an any value',
22+
},
23+
schema: [],
24+
},
25+
defaultOptions: [],
26+
create(context) {
27+
const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context);
28+
const checker = program.getTypeChecker();
29+
const sourceCode = context.getSourceCode();
30+
31+
const stateCache = new Map<TSESTree.Node, State>();
32+
33+
function checkMemberExpression(
34+
node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
35+
): State {
36+
const cachedState = stateCache.get(node);
37+
if (cachedState) {
38+
return cachedState;
39+
}
40+
41+
if (util.isMemberOrOptionalMemberExpression(node.object)) {
42+
const objectState = checkMemberExpression(node.object);
43+
if (objectState === State.Unsafe) {
44+
// if the object is unsafe, we know this will be unsafe as well
45+
// we don't need to report, as we have already reported on the inner member expr
46+
stateCache.set(node, objectState);
47+
return objectState;
48+
}
49+
}
50+
51+
const tsNode = esTreeNodeToTSNodeMap.get(node.object);
52+
const type = checker.getTypeAtLocation(tsNode);
53+
const state = util.isTypeAnyType(type) ? State.Unsafe : State.Safe;
54+
stateCache.set(node, state);
55+
56+
if (state === State.Unsafe) {
57+
const propertyName = sourceCode.getText(node.property);
58+
context.report({
59+
node,
60+
messageId: 'unsafeMemberExpression',
61+
data: {
62+
property: node.computed ? `[${propertyName}]` : `.${propertyName}`,
63+
},
64+
});
65+
}
66+
67+
return state;
68+
}
69+
70+
return {
71+
'MemberExpression, OptionalMemberExpression': checkMemberExpression,
72+
};
73+
},
74+
});

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,15 @@ function isAwaitKeyword(
221221
return node?.type === AST_TOKEN_TYPES.Identifier && node.value === 'await';
222222
}
223223

224+
function isMemberOrOptionalMemberExpression(
225+
node: TSESTree.Node,
226+
): node is TSESTree.MemberExpression | TSESTree.OptionalMemberExpression {
227+
return (
228+
node.type === AST_NODE_TYPES.MemberExpression ||
229+
node.type === AST_NODE_TYPES.OptionalMemberExpression
230+
);
231+
}
232+
224233
export {
225234
isAwaitExpression,
226235
isAwaitKeyword,
@@ -231,6 +240,7 @@ export {
231240
isFunctionType,
232241
isIdentifier,
233242
isLogicalOrOperator,
243+
isMemberOrOptionalMemberExpression,
234244
isNonNullAssertionPunctuator,
235245
isNotNonNullAssertionPunctuator,
236246
isNotOptionalChainPunctuator,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,10 @@ export function getEqualsKind(operator: string): EqualsKind | undefined {
290290
return undefined;
291291
}
292292
}
293+
294+
/**
295+
* @returns true if the type is `any`
296+
*/
297+
export function isTypeAnyType(type: ts.Type): boolean {
298+
return isTypeFlagSet(type, ts.TypeFlags.Any);
299+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import rule from '../../src/rules/no-unsafe-member-access';
2+
import {
3+
RuleTester,
4+
batchedSingleLineTests,
5+
getFixturesRootDir,
6+
} from '../RuleTester';
7+
8+
const ruleTester = new RuleTester({
9+
parser: '@typescript-eslint/parser',
10+
parserOptions: {
11+
project: './tsconfig.json',
12+
tsconfigRootDir: getFixturesRootDir(),
13+
},
14+
});
15+
16+
ruleTester.run('no-unsafe-member-access', rule, {
17+
valid: [
18+
'function foo(x: { a: number }) { x.a }',
19+
'function foo(x?: { a: number }) { x?.a }',
20+
],
21+
invalid: [
22+
...batchedSingleLineTests({
23+
code: `
24+
function foo(x: any) { x.a }
25+
function foo(x: any) { x.a.b.c.d.e.f.g }
26+
function foo(x: { a: any }) { x.a.b.c.d.e.f.g }
27+
`,
28+
errors: [
29+
{
30+
messageId: 'unsafeMemberExpression',
31+
data: {
32+
property: '.a',
33+
},
34+
line: 2,
35+
column: 24,
36+
endColumn: 27,
37+
},
38+
{
39+
messageId: 'unsafeMemberExpression',
40+
data: {
41+
property: '.a',
42+
},
43+
line: 3,
44+
column: 24,
45+
endColumn: 27,
46+
},
47+
{
48+
messageId: 'unsafeMemberExpression',
49+
data: {
50+
property: '.b',
51+
},
52+
line: 4,
53+
column: 31,
54+
endColumn: 36,
55+
},
56+
],
57+
}),
58+
...batchedSingleLineTests({
59+
code: `
60+
function foo(x: any) { x['a'] }
61+
function foo(x: any) { x['a']['b']['c'] }
62+
`,
63+
errors: [
64+
{
65+
messageId: 'unsafeMemberExpression',
66+
data: {
67+
property: "['a']",
68+
},
69+
line: 2,
70+
column: 24,
71+
endColumn: 30,
72+
},
73+
{
74+
messageId: 'unsafeMemberExpression',
75+
data: {
76+
property: "['a']",
77+
},
78+
line: 3,
79+
column: 24,
80+
endColumn: 30,
81+
},
82+
],
83+
}),
84+
],
85+
});

0 commit comments

Comments
 (0)