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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unsafe-member-access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,43 @@ arr[idx++];
</TabItem>
</Tabs>

## Options

### `allowOptionalChaining`

{/* insert option description */}

Examples of code for this rule with `{ allowOptionalChaining: true }`:

<Tabs>
<TabItem value="❌ Incorrect">

```ts
declare const outer: any;

outer.inner;
outer.middle.inner;
```

</TabItem>
<TabItem value="✅ Correct">

```ts option='{ "allowOptionalChaining": true }'
declare const outer: any;

outer?.inner;
outer?.middle?.inner;
```

</TabItem>
</Tabs>

:::caution
We only recommend using `allowOptionalChaining` to help transition an existing project towards fully enabling `no-unsafe-member-access`.
Optional chaining makes it safer than normal property accesses in that you won't get a runtime error if the parent value is `null` or `undefined`.
However, it still results in an `any`-typed value, which is unsafe.
:::

## When Not To Use It

If your codebase has many existing `any`s or areas of unsafe code, it may be difficult to enable this rule.
Expand Down
59 changes: 53 additions & 6 deletions packages/eslint-plugin/src/rules/no-unsafe-member-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,26 @@ import {
const enum State {
Unsafe = 1,
Safe = 2,
Chained = 3,
}

function createDataType(type: ts.Type): '`any`' | '`error` typed' {
const isErrorType = tsutils.isIntrinsicErrorType(type);
return isErrorType ? '`error` typed' : '`any`';
}

export default createRule({
export type Options = [
{
allowOptionalChaining?: boolean;
},
];

export type MessageIds =
| 'unsafeComputedMemberAccess'
| 'unsafeMemberExpression'
| 'unsafeThisMemberExpression';

export default createRule<Options, MessageIds>({
name: 'no-unsafe-member-access',
meta: {
type: 'problem',
Expand All @@ -41,10 +53,26 @@ export default createRule({
'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.',
].join('\n'),
},
schema: [],
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
allowOptionalChaining: {
type: 'boolean',
description:
'Whether to allow `?.` optional chains on `any` values.',
},
},
},
],
},
defaultOptions: [],
create(context) {
defaultOptions: [
{
allowOptionalChaining: false,
},
],
create(context, [{ allowOptionalChaining }]) {
const services = getParserServices(context);
const compilerOptions = services.program.getCompilerOptions();
const isNoImplicitThis = tsutils.isStrictCompilerOptionEnabled(
Expand All @@ -54,7 +82,20 @@ export default createRule({

const stateCache = new Map<TSESTree.Node, State>();

// Case notes:
// value?.outer.middle.inner
// The ChainExpression is a child of the root expression, and a parent of all the MemberExpressions.
// But the left-most expression is what we want to report on: the inner-most expressions.
// In fact, this is true even if the chain is on the inside!
// value.outer.middle?.inner;
// It was already true that every `object` (MemberExpression) has optional: boolean

function checkMemberExpression(node: TSESTree.MemberExpression): State {
if (allowOptionalChaining && node.optional) {
stateCache.set(node, State.Chained);
return State.Chained;
}

const cachedState = stateCache.get(node);
if (cachedState) {
return cachedState;
Expand All @@ -77,8 +118,7 @@ export default createRule({
if (state === State.Unsafe) {
const propertyName = context.sourceCode.getText(node.property);

let messageId: 'unsafeMemberExpression' | 'unsafeThisMemberExpression' =
'unsafeMemberExpression';
let messageId: MessageIds = 'unsafeMemberExpression';

if (!isNoImplicitThis) {
// `this.foo` or `this.foo[bar]`
Expand Down Expand Up @@ -114,6 +154,13 @@ export default createRule({
'MemberExpression[computed = true] > *.property'(
node: TSESTree.Expression,
): void {
if (
allowOptionalChaining &&
(node.parent as TSESTree.MemberExpression).optional
) {
return;
}

if (
// x[1]
node.type === AST_NODE_TYPES.Literal ||
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 142 additions & 0 deletions packages/eslint-plugin/tests/rules/no-unsafe-member-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,38 @@ class B implements F.S.T.A {}
`
interface B extends F.S.T.A {}
`,
{
code: `
function foo(x?: { a: number }) {
x?.a;
}
`,
options: [{ allowOptionalChaining: true }],
},
{
code: `
function foo(x?: { a: number }, y: string) {
x?.[y];
}
`,
options: [{ allowOptionalChaining: true }],
},
{
code: `
function foo(x: { a: number }, y: 'a') {
x?.[y];
}
`,
options: [{ allowOptionalChaining: true }],
},
{
code: `
function foo(x: { a: number }, y: NotKnown) {
x?.[y];
}
`,
options: [{ allowOptionalChaining: true }],
},
],
invalid: [
{
Expand Down Expand Up @@ -382,5 +414,115 @@ class C {
},
],
},
{
code: `
let value: any;

value?.middle.inner;
`,
errors: [
{
column: 15,
data: {
property: '.inner',
type: '`any`',
},
endColumn: 20,
line: 4,
messageId: 'unsafeMemberExpression',
},
],
options: [{ allowOptionalChaining: true }],
},
{
code: `
let value: any;

value?.outer.middle.inner;
`,
errors: [
{
column: 14,
data: {
property: '.middle',
type: '`any`',
},
endColumn: 20,
line: 4,
messageId: 'unsafeMemberExpression',
},
],
options: [{ allowOptionalChaining: true }],
},
{
code: `
let value: any;

value.outer?.middle.inner;
`,
errors: [
{
column: 7,
data: {
property: '.outer',
type: '`any`',
},
endColumn: 12,
line: 4,
messageId: 'unsafeMemberExpression',
},
{
column: 21,
data: {
property: '.inner',
type: '`any`',
},
endColumn: 26,
line: 4,
messageId: 'unsafeMemberExpression',
},
],
options: [{ allowOptionalChaining: true }],
},
{
code: `
let value: any;

value.outer.middle?.inner;
`,
errors: [
{
column: 7,
data: {
property: '.outer',
type: '`any`',
},
endColumn: 12,
line: 4,
messageId: 'unsafeMemberExpression',
},
],
options: [{ allowOptionalChaining: true }],
},
{
code: `
function foo(x: { a: number }, y: NotKnown) {
x[y];
}
`,
errors: [
{
column: 5,
data: {
property: '[y]',
type: '`error` typed',
},
endColumn: 6,
line: 3,
messageId: 'unsafeComputedMemberAccess',
},
],
options: [{ allowOptionalChaining: true }],
},
],
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.