@mui/internal-code-infra 0.1.1-canary.5 → 0.1.1-canary.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
export type PropertyDefinition = import('@typescript-eslint/utils').TSESTree.PropertyDefinition;
|
|
3
|
+
export type Node = import('@typescript-eslint/utils').TSESTree.Node;
|
|
4
|
+
/**
|
|
5
|
+
* ESLint rule that disallows assigning a class field in the constructor when it is also
|
|
6
|
+
* declared as a (non-`declare`) class field.
|
|
7
|
+
*
|
|
8
|
+
* Babel 8 only strips TypeScript fields marked `declare` (see mui/material-ui#49241), so a
|
|
9
|
+
* field without an initializer is emitted as a property definition that sets it to
|
|
10
|
+
* `undefined` before the constructor assigns it again. Either initialize the field at its
|
|
11
|
+
* declaration, or mark it `declare` so it only serves as a type annotation.
|
|
12
|
+
*
|
|
13
|
+
* `declare` fields are exempt from TypeScript's strict property initialization check
|
|
14
|
+
* (TS2564), so prefer initializing at the declaration where the value doesn't depend on
|
|
15
|
+
* constructor arguments. The `declare` suggestion is only offered when every path through the
|
|
16
|
+
* constructor assigns the field, i.e. for an unconditional top-level assignment.
|
|
17
|
+
*
|
|
18
|
+
* Only plain `=` assignments count: compound assignments read the field rather than
|
|
19
|
+
* re-initialize it. A field with an initializer is only reported for an unconditional
|
|
20
|
+
* top-level assignment, as a conditional one overrides a default. Private (`#`) and decorated
|
|
21
|
+
* fields are ignored: they are runtime declarations that `declare` can't replace.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Invalid
|
|
25
|
+
* class ModalManager {
|
|
26
|
+
* private modals: Modal[];
|
|
27
|
+
* constructor() {
|
|
28
|
+
* this.modals = [];
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Valid - initialized at the declaration
|
|
34
|
+
* class ModalManager {
|
|
35
|
+
* private modals: Modal[] = [];
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Valid - declared only, assigned in the constructor
|
|
40
|
+
* class ModalManager {
|
|
41
|
+
* declare private modals: Modal[];
|
|
42
|
+
* constructor(modals: Modal[]) {
|
|
43
|
+
* this.modals = modals;
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
declare const _default: ESLintUtils.RuleModule<"addDeclare" | "assignedInConstructor", [], unknown, ESLintUtils.RuleListener> & {
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
50
|
+
export default _default;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-code-infra",
|
|
3
|
-
"version": "0.1.1-canary.
|
|
3
|
+
"version": "0.1.1-canary.7",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "Infra scripts and configs to be used across MUI repos.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"code-infra": "./bin/code-infra.mjs"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@argos-ci/core": "6.8.
|
|
56
|
+
"@argos-ci/core": "6.8.6",
|
|
57
57
|
"@babel/cli": "^8.0.6",
|
|
58
58
|
"@babel/core": "^8.0.6",
|
|
59
59
|
"@babel/plugin-syntax-jsx": "^8.0.1",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"eslint-plugin-testing-library": "^7.16.2",
|
|
105
105
|
"execa": "^10.0.1",
|
|
106
106
|
"git-url-parse": "^16.1.0",
|
|
107
|
-
"globals": "^17.
|
|
107
|
+
"globals": "^17.13.0",
|
|
108
108
|
"globby": "^16.2.4",
|
|
109
109
|
"html-validate": "^11.16.1",
|
|
110
110
|
"minimatch": "^10.2.6",
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"publishConfig": {
|
|
188
188
|
"access": "public"
|
|
189
189
|
},
|
|
190
|
-
"gitSha": "
|
|
190
|
+
"gitSha": "d15ad292b4ac08fbfbb55aebffb354f659d71aea",
|
|
191
191
|
"scripts": {
|
|
192
192
|
"build": "tsc -p tsconfig.build.json",
|
|
193
193
|
"typescript": "tsc -noEmit",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineConfig } from 'eslint/config';
|
|
2
|
-
import { EXTENSION_DTS } from '../extensions.mjs';
|
|
2
|
+
import { EXTENSION_DTS, EXTENSION_TS_ONLY } from '../extensions.mjs';
|
|
3
3
|
|
|
4
4
|
const restrictedMethods = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
|
|
5
5
|
|
|
@@ -538,6 +538,13 @@ export function createCoreConfig(options = {}) {
|
|
|
538
538
|
'@typescript-eslint/return-await': 'off',
|
|
539
539
|
},
|
|
540
540
|
},
|
|
541
|
+
{
|
|
542
|
+
name: 'mui-base/ts',
|
|
543
|
+
files: [`**/*${EXTENSION_TS_ONLY}`],
|
|
544
|
+
rules: {
|
|
545
|
+
'mui/no-constructor-assigned-field': 'error',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
541
548
|
{
|
|
542
549
|
name: 'mui-base/dts',
|
|
543
550
|
files: [`**/*${EXTENSION_DTS}`],
|
package/src/eslint/mui/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import addUndefToOptional from './rules/add-undef-to-optional.mjs';
|
|
|
14
14
|
import flattenParentheses from './rules/flatten-parentheses.mjs';
|
|
15
15
|
import noPresentationRole from './rules/no-presentation-role.mjs';
|
|
16
16
|
import noFloatingCleanup from './rules/no-floating-cleanup.mjs';
|
|
17
|
+
import noConstructorAssignedField from './rules/no-constructor-assigned-field.mjs';
|
|
17
18
|
|
|
18
19
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
19
20
|
const muiPlugin = {
|
|
@@ -39,6 +40,7 @@ const muiPlugin = {
|
|
|
39
40
|
'flatten-parentheses': /** @type {any} */ (flattenParentheses),
|
|
40
41
|
'no-presentation-role': noPresentationRole,
|
|
41
42
|
'no-floating-cleanup': /** @type {any} */ (noFloatingCleanup),
|
|
43
|
+
'no-constructor-assigned-field': /** @type {any} */ (noConstructorAssignedField),
|
|
42
44
|
},
|
|
43
45
|
};
|
|
44
46
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { ASTUtils, ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@typescript-eslint/utils').TSESTree.PropertyDefinition} PropertyDefinition
|
|
5
|
+
* @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
9
|
+
(name) =>
|
|
10
|
+
`https://github.com/mui/mui-public/blob/master/packages/code-infra/src/eslint/mui/rules/${name}.mjs`,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the constructor whose own code contains `node`, or `null` when `node` is outside a
|
|
15
|
+
* constructor or inside a nested function or class.
|
|
16
|
+
* @param {Node} node
|
|
17
|
+
*/
|
|
18
|
+
function getEnclosingConstructor(node) {
|
|
19
|
+
let current = node.parent;
|
|
20
|
+
while (current && !ASTUtils.isFunction(current)) {
|
|
21
|
+
if (current.type === 'ClassBody') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
current = current.parent;
|
|
25
|
+
}
|
|
26
|
+
const method = current?.parent;
|
|
27
|
+
return method && ASTUtils.isConstructor(method) ? method : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the extra edits needed before `field` can be marked `declare`.
|
|
32
|
+
* @param {PropertyDefinition} field
|
|
33
|
+
*/
|
|
34
|
+
function getDeclareHint(field) {
|
|
35
|
+
const edits = [];
|
|
36
|
+
if (field.value) {
|
|
37
|
+
edits.push('drop its initializer');
|
|
38
|
+
}
|
|
39
|
+
if (field.definite) {
|
|
40
|
+
edits.push('drop the `!`');
|
|
41
|
+
}
|
|
42
|
+
if (field.override) {
|
|
43
|
+
edits.push('drop `override`');
|
|
44
|
+
}
|
|
45
|
+
return edits.length > 0 ? ` (${edits.join(', ')})` : '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ESLint rule that disallows assigning a class field in the constructor when it is also
|
|
50
|
+
* declared as a (non-`declare`) class field.
|
|
51
|
+
*
|
|
52
|
+
* Babel 8 only strips TypeScript fields marked `declare` (see mui/material-ui#49241), so a
|
|
53
|
+
* field without an initializer is emitted as a property definition that sets it to
|
|
54
|
+
* `undefined` before the constructor assigns it again. Either initialize the field at its
|
|
55
|
+
* declaration, or mark it `declare` so it only serves as a type annotation.
|
|
56
|
+
*
|
|
57
|
+
* `declare` fields are exempt from TypeScript's strict property initialization check
|
|
58
|
+
* (TS2564), so prefer initializing at the declaration where the value doesn't depend on
|
|
59
|
+
* constructor arguments. The `declare` suggestion is only offered when every path through the
|
|
60
|
+
* constructor assigns the field, i.e. for an unconditional top-level assignment.
|
|
61
|
+
*
|
|
62
|
+
* Only plain `=` assignments count: compound assignments read the field rather than
|
|
63
|
+
* re-initialize it. A field with an initializer is only reported for an unconditional
|
|
64
|
+
* top-level assignment, as a conditional one overrides a default. Private (`#`) and decorated
|
|
65
|
+
* fields are ignored: they are runtime declarations that `declare` can't replace.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Invalid
|
|
69
|
+
* class ModalManager {
|
|
70
|
+
* private modals: Modal[];
|
|
71
|
+
* constructor() {
|
|
72
|
+
* this.modals = [];
|
|
73
|
+
* }
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // Valid - initialized at the declaration
|
|
78
|
+
* class ModalManager {
|
|
79
|
+
* private modals: Modal[] = [];
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // Valid - declared only, assigned in the constructor
|
|
84
|
+
* class ModalManager {
|
|
85
|
+
* declare private modals: Modal[];
|
|
86
|
+
* constructor(modals: Modal[]) {
|
|
87
|
+
* this.modals = modals;
|
|
88
|
+
* }
|
|
89
|
+
* }
|
|
90
|
+
*/
|
|
91
|
+
export default createRule({
|
|
92
|
+
name: 'no-constructor-assigned-field',
|
|
93
|
+
meta: {
|
|
94
|
+
type: 'suggestion',
|
|
95
|
+
docs: {
|
|
96
|
+
description:
|
|
97
|
+
'Disallow assigning class fields in the constructor when they are also declared as class fields',
|
|
98
|
+
},
|
|
99
|
+
hasSuggestions: true,
|
|
100
|
+
messages: {
|
|
101
|
+
assignedInConstructor:
|
|
102
|
+
"Field '{{name}}' is declared as a class field and also assigned in the constructor. " +
|
|
103
|
+
'Either initialize it at the declaration and remove the constructor assignment, ' +
|
|
104
|
+
'or mark it `declare`{{declareHint}}.',
|
|
105
|
+
addDeclare: 'Mark the field `declare`.',
|
|
106
|
+
},
|
|
107
|
+
schema: [],
|
|
108
|
+
},
|
|
109
|
+
defaultOptions: [],
|
|
110
|
+
create(context) {
|
|
111
|
+
/**
|
|
112
|
+
* Fields assigned in a constructor, mapped to whether any of those assignments is an
|
|
113
|
+
* unconditional top-level statement.
|
|
114
|
+
* @type {Map<PropertyDefinition, boolean>}
|
|
115
|
+
*/
|
|
116
|
+
const assignedFields = new Map();
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
AssignmentExpression(node) {
|
|
120
|
+
if (
|
|
121
|
+
node.operator !== '=' ||
|
|
122
|
+
node.left.type !== 'MemberExpression' ||
|
|
123
|
+
node.left.object.type !== 'ThisExpression'
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const name = ASTUtils.getPropertyName(node.left);
|
|
128
|
+
if (name === null) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const constructor = getEnclosingConstructor(node);
|
|
132
|
+
if (!constructor) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const field = constructor.parent.body.find(
|
|
137
|
+
/** @returns {member is PropertyDefinition} */
|
|
138
|
+
(member) =>
|
|
139
|
+
member.type === 'PropertyDefinition' &&
|
|
140
|
+
!member.static &&
|
|
141
|
+
!member.declare &&
|
|
142
|
+
member.decorators.length === 0 &&
|
|
143
|
+
ASTUtils.getPropertyName(member) === name,
|
|
144
|
+
);
|
|
145
|
+
if (!field) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const statement = node.parent;
|
|
150
|
+
const unconditional =
|
|
151
|
+
statement.type === 'ExpressionStatement' && statement.parent === constructor.value.body;
|
|
152
|
+
assignedFields.set(field, Boolean(assignedFields.get(field)) || unconditional);
|
|
153
|
+
},
|
|
154
|
+
'ClassBody:exit'(classBody) {
|
|
155
|
+
for (const member of classBody.body) {
|
|
156
|
+
if (member.type !== 'PropertyDefinition' || !assignedFields.has(member)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const unconditional = Boolean(assignedFields.get(member));
|
|
160
|
+
if (member.value && !unconditional) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const declareHint = getDeclareHint(member);
|
|
165
|
+
context.report({
|
|
166
|
+
node: member,
|
|
167
|
+
messageId: 'assignedInConstructor',
|
|
168
|
+
data: { name: ASTUtils.getPropertyName(member), declareHint },
|
|
169
|
+
suggest:
|
|
170
|
+
unconditional && declareHint === ''
|
|
171
|
+
? [
|
|
172
|
+
{
|
|
173
|
+
messageId: 'addDeclare',
|
|
174
|
+
fix: (fixer) => fixer.insertTextBefore(member, 'declare '),
|
|
175
|
+
},
|
|
176
|
+
]
|
|
177
|
+
: [],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
});
|
|
@@ -63,6 +63,7 @@ export function createTestConfig() {
|
|
|
63
63
|
'import/named': 'off',
|
|
64
64
|
'mui/disallow-active-element-as-key-event-target': 'error',
|
|
65
65
|
'mui/consistent-production-guard': 'off',
|
|
66
|
+
'mui/no-constructor-assigned-field': 'off',
|
|
66
67
|
|
|
67
68
|
// disable eslint-plugin-jsx-a11y
|
|
68
69
|
// tests are not driven by assistive technology
|