Skip to content

Commit 5a8a19a

Browse files
authored
Merge branch 'master' into 2109-special-case-empty-map-unsafe-assignment
2 parents 0f0ffdf + 37ec2c2 commit 5a8a19a

File tree

7 files changed

+198
-16
lines changed

7 files changed

+198
-16
lines changed

Diff for: packages/eslint-plugin/docs/rules/dot-notation.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
## Rule Details
44

55
This rule extends the base [`eslint/dot-notation`](https://eslint.org/docs/rules/dot-notation) rule.
6-
It adds support for optionally ignoring computed `private` member access.
6+
It adds:
7+
8+
- Support for optionally ignoring computed `private` and/or `protected` member access.
9+
- Compatibility with TypeScript's `noPropertyAccessFromIndexSignature` option.
710

811
## How to use
912

@@ -24,14 +27,18 @@ This rule adds the following options:
2427
interface Options extends BaseDotNotationOptions {
2528
allowPrivateClassPropertyAccess?: boolean;
2629
allowProtectedClassPropertyAccess?: boolean;
30+
allowIndexSignaturePropertyAccess?: boolean;
2731
}
2832
const defaultOptions: Options = {
2933
...baseDotNotationDefaultOptions,
3034
allowPrivateClassPropertyAccess: false,
3135
allowProtectedClassPropertyAccess: false,
36+
allowIndexSignaturePropertyAccess: false,
3237
};
3338
```
3439

40+
If the TypeScript compiler option `noPropertyAccessFromIndexSignature` is set to `true`, then this rule always allows the use of square bracket notation to access properties of types that have a `string` index signature, even if `allowIndexSignaturePropertyAccess` is `false`.
41+
3542
### `allowPrivateClassPropertyAccess`
3643

3744
Example of a correct code when `allowPrivateClassPropertyAccess` is set to `true`
@@ -58,4 +65,19 @@ const x = new X();
5865
x['protected_prop'] = 123;
5966
```
6067

68+
### `allowIndexSignaturePropertyAccess`
69+
70+
Example of correct code when `allowIndexSignaturePropertyAccess` is set to `true`
71+
72+
```ts
73+
class X {
74+
[key: string]: number;
75+
}
76+
77+
const x = new X();
78+
x['hello'] = 123;
79+
```
80+
81+
If the TypeScript compiler option `noPropertyAccessFromIndexSignature` is set to `true`, then the above code is always allowed, even if `allowIndexSignaturePropertyAccess` is `false`.
82+
6183
<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/dot-notation.md)</sup>

Diff for: packages/eslint-plugin/docs/rules/prefer-regexp-exec.md

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
# Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided (`prefer-regexp-exec`)
22

3-
`RegExp#exec` is faster than `String#match` and both work the same when not using the `/g` flag.
3+
As `String#match` is defined to be the same as `RegExp#exec` when the regular expression does not include the `g` flag, prefer a consistent usage.
44

55
## Rule Details
66

7-
This rule is aimed at enforcing the more performant way of applying regular expressions on strings.
7+
This rule is aimed at enforcing a consistent way to apply regular expressions to strings.
88

99
From [`String#match` on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match):
1010

1111
> If the regular expression does not include the g flag, returns the same result as `RegExp.exec()`.
1212
13-
From [Stack Overflow](https://stackoverflow.com/questions/9214754/what-is-the-difference-between-regexp-s-exec-function-and-string-s-match-fun)
14-
15-
> `RegExp.prototype.exec` is a lot faster than `String.prototype.match`, but that’s because they are not exactly the same thing, they are different.
13+
`RegExp#exec` may also be slightly faster than `String#match`; this is the reason to choose it as the preferred usage.
1614

1715
Examples of **incorrect** code for this rule:
1816

Diff for: packages/eslint-plugin/src/rules/dot-notation.ts

+39-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { TSESTree } from '@typescript-eslint/experimental-utils';
22
import * as ts from 'typescript';
3+
import * as tsutils from 'tsutils';
34
import baseRule from 'eslint/lib/rules/dot-notation';
45
import {
5-
InferOptionsTypeFromRule,
6-
InferMessageIdsTypeFromRule,
76
createRule,
87
getParserServices,
8+
InferMessageIdsTypeFromRule,
9+
InferOptionsTypeFromRule,
910
} from '../util';
1011

1112
export type Options = InferOptionsTypeFromRule<typeof baseRule>;
@@ -42,6 +43,10 @@ export default createRule<Options, MessageIds>({
4243
type: 'boolean',
4344
default: false,
4445
},
46+
allowIndexSignaturePropertyAccess: {
47+
type: 'boolean',
48+
default: false,
49+
},
4550
},
4651
additionalProperties: false,
4752
},
@@ -53,32 +58,41 @@ export default createRule<Options, MessageIds>({
5358
{
5459
allowPrivateClassPropertyAccess: false,
5560
allowProtectedClassPropertyAccess: false,
61+
allowIndexSignaturePropertyAccess: false,
5662
allowKeywords: true,
5763
allowPattern: '',
5864
},
5965
],
6066
create(context, [options]) {
6167
const rules = baseRule.create(context);
68+
69+
const { program, esTreeNodeToTSNodeMap } = getParserServices(context);
70+
const typeChecker = program.getTypeChecker();
71+
6272
const allowPrivateClassPropertyAccess =
6373
options.allowPrivateClassPropertyAccess;
6474
const allowProtectedClassPropertyAccess =
6575
options.allowProtectedClassPropertyAccess;
66-
67-
const parserServices = getParserServices(context);
68-
const typeChecker = parserServices.program.getTypeChecker();
76+
const allowIndexSignaturePropertyAccess =
77+
(options.allowIndexSignaturePropertyAccess ?? false) ||
78+
tsutils.isCompilerOptionEnabled(
79+
program.getCompilerOptions(),
80+
'noPropertyAccessFromIndexSignature',
81+
);
6982

7083
return {
7184
MemberExpression(node: TSESTree.MemberExpression): void {
7285
if (
7386
(allowPrivateClassPropertyAccess ||
74-
allowProtectedClassPropertyAccess) &&
87+
allowProtectedClassPropertyAccess ||
88+
allowIndexSignaturePropertyAccess) &&
7589
node.computed
7690
) {
77-
// for perf reasons - only fetch the symbol if we have to
78-
const objectSymbol = typeChecker.getSymbolAtLocation(
79-
parserServices.esTreeNodeToTSNodeMap.get(node.property),
91+
// for perf reasons - only fetch symbols if we have to
92+
const propertySymbol = typeChecker.getSymbolAtLocation(
93+
esTreeNodeToTSNodeMap.get(node.property),
8094
);
81-
const modifierKind = objectSymbol?.getDeclarations()?.[0]
95+
const modifierKind = propertySymbol?.getDeclarations()?.[0]
8296
?.modifiers?.[0].kind;
8397
if (
8498
(allowPrivateClassPropertyAccess &&
@@ -88,6 +102,21 @@ export default createRule<Options, MessageIds>({
88102
) {
89103
return;
90104
}
105+
if (
106+
propertySymbol === undefined &&
107+
allowIndexSignaturePropertyAccess
108+
) {
109+
const objectType = typeChecker.getTypeAtLocation(
110+
esTreeNodeToTSNodeMap.get(node.object),
111+
);
112+
const indexType = typeChecker.getIndexTypeOfType(
113+
objectType,
114+
ts.IndexKind.String,
115+
);
116+
if (indexType != undefined) {
117+
return;
118+
}
119+
}
91120
}
92121
rules.MemberExpression(node);
93122
},

Diff for: packages/eslint-plugin/src/rules/no-shadow.ts

+85
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,84 @@ export default util.createRule<Options, MessageIds>({
126126
return util.isFunctionType(id.parent);
127127
}
128128

129+
function isGenericOfStaticMethod(
130+
variable: TSESLint.Scope.Variable,
131+
): boolean {
132+
if (!('isTypeVariable' in variable)) {
133+
// this shouldn't happen...
134+
return false;
135+
}
136+
137+
if (!variable.isTypeVariable) {
138+
return false;
139+
}
140+
141+
if (variable.identifiers.length === 0) {
142+
return false;
143+
}
144+
145+
const typeParameter = variable.identifiers[0].parent;
146+
if (typeParameter?.type !== AST_NODE_TYPES.TSTypeParameter) {
147+
return false;
148+
}
149+
const typeParameterDecl = typeParameter.parent;
150+
if (
151+
typeParameterDecl?.type !== AST_NODE_TYPES.TSTypeParameterDeclaration
152+
) {
153+
return false;
154+
}
155+
const functionExpr = typeParameterDecl.parent;
156+
if (
157+
!functionExpr ||
158+
(functionExpr.type !== AST_NODE_TYPES.FunctionExpression &&
159+
functionExpr.type !== AST_NODE_TYPES.TSEmptyBodyFunctionExpression)
160+
) {
161+
return false;
162+
}
163+
const methodDefinition = functionExpr.parent;
164+
if (methodDefinition?.type !== AST_NODE_TYPES.MethodDefinition) {
165+
return false;
166+
}
167+
return methodDefinition.static;
168+
}
169+
170+
function isGenericOfClassDecl(variable: TSESLint.Scope.Variable): boolean {
171+
if (!('isTypeVariable' in variable)) {
172+
// this shouldn't happen...
173+
return false;
174+
}
175+
176+
if (!variable.isTypeVariable) {
177+
return false;
178+
}
179+
180+
if (variable.identifiers.length === 0) {
181+
return false;
182+
}
183+
184+
const typeParameter = variable.identifiers[0].parent;
185+
if (typeParameter?.type !== AST_NODE_TYPES.TSTypeParameter) {
186+
return false;
187+
}
188+
const typeParameterDecl = typeParameter.parent;
189+
if (
190+
typeParameterDecl?.type !== AST_NODE_TYPES.TSTypeParameterDeclaration
191+
) {
192+
return false;
193+
}
194+
const classDecl = typeParameterDecl.parent;
195+
return classDecl?.type === AST_NODE_TYPES.ClassDeclaration;
196+
}
197+
198+
function isGenericOfAStaticMethodShadow(
199+
variable: TSESLint.Scope.Variable,
200+
shadowed: TSESLint.Scope.Variable,
201+
): boolean {
202+
return (
203+
isGenericOfStaticMethod(variable) && isGenericOfClassDecl(shadowed)
204+
);
205+
}
206+
129207
/**
130208
* Check if variable name is allowed.
131209
* @param variable The variable to check.
@@ -321,6 +399,13 @@ export default util.createRule<Options, MessageIds>({
321399
continue;
322400
}
323401

402+
// ignore static class method generic shadowing class generic
403+
// this is impossible for the scope analyser to understand
404+
// so we have to handle this manually in this rule
405+
if (isGenericOfAStaticMethodShadow(variable, shadowed)) {
406+
continue;
407+
}
408+
324409
const isESLintGlobal = 'writeable' in shadowed;
325410
if (
326411
(shadowed.identifiers.length > 0 ||

Diff for: packages/eslint-plugin/tests/rules/dot-notation.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ x['protected_prop'] = 123;
8787
`,
8888
options: [{ allowProtectedClassPropertyAccess: true }],
8989
},
90+
{
91+
code: `
92+
class X {
93+
prop: string;
94+
[key: string]: number;
95+
}
96+
97+
const x = new X();
98+
x['hello'] = 3;
99+
`,
100+
options: [{ allowIndexSignaturePropertyAccess: true }],
101+
},
90102
],
91103
invalid: [
92104
{
@@ -287,5 +299,27 @@ x.protected_prop = 123;
287299
`,
288300
errors: [{ messageId: 'useDot' }],
289301
},
302+
{
303+
code: `
304+
class X {
305+
prop: string;
306+
[key: string]: number;
307+
}
308+
309+
const x = new X();
310+
x['prop'] = 'hello';
311+
`,
312+
options: [{ allowIndexSignaturePropertyAccess: true }],
313+
errors: [{ messageId: 'useDot' }],
314+
output: `
315+
class X {
316+
prop: string;
317+
[key: string]: number;
318+
}
319+
320+
const x = new X();
321+
x.prop = 'hello';
322+
`,
323+
},
290324
],
291325
});

Diff for: packages/eslint-plugin/tests/rules/no-shadow.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,19 @@ type Fn = (Foo: string) => typeof Foo;
159159
`,
160160
options: [{ ignoreFunctionTypeParameterNameValueShadow: false }],
161161
},
162+
`
163+
export class Wrapper<Wrapped> {
164+
private constructor(private readonly wrapped: Wrapped) {}
165+
166+
unwrap(): Wrapped {
167+
return this.wrapped;
168+
}
169+
170+
static create<Wrapped>(wrapped: Wrapped) {
171+
return new Wrapper<Wrapped>(wrapped);
172+
}
173+
}
174+
`,
162175
],
163176
invalid: [
164177
{

Diff for: packages/eslint-plugin/typings/eslint-rules.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ declare module 'eslint/lib/rules/dot-notation' {
713713
allowPattern?: string;
714714
allowPrivateClassPropertyAccess?: boolean;
715715
allowProtectedClassPropertyAccess?: boolean;
716+
allowIndexSignaturePropertyAccess?: boolean;
716717
},
717718
],
718719
{

0 commit comments

Comments
 (0)