@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.5",
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.5",
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.12.0",
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": "c2a0ca83b8dd00ae2fa2b5714dbc6684d50926d9",
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}`],
@@ -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