@mui/internal-code-infra 0.0.4-canary.1 → 0.0.4-canary.100

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.
Files changed (146) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +22 -8
  3. package/build/babel-config.d.mts +11 -3
  4. package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
  5. package/build/brokenLinksChecker/index.d.mts +192 -2
  6. package/build/changelog/fetchChangelogs.d.mts +3 -0
  7. package/build/changelog/types.d.ts +3 -1
  8. package/build/cli/cmdArgosPush.d.mts +8 -2
  9. package/build/cli/cmdBuild.d.mts +48 -2
  10. package/build/cli/cmdCopyFiles.d.mts +14 -2
  11. package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
  12. package/build/cli/cmdGenerateChangelog.d.mts +17 -2
  13. package/build/cli/cmdGithubAuth.d.mts +2 -2
  14. package/build/cli/cmdListWorkspaces.d.mts +16 -2
  15. package/build/cli/cmdNetlifyIgnore.d.mts +12 -2
  16. package/build/cli/cmdPublish.d.mts +24 -4
  17. package/build/cli/cmdPublishCanary.d.mts +21 -2
  18. package/build/cli/cmdPublishNewPackage.d.mts +10 -2
  19. package/build/cli/cmdRenameScope.d.mts +18 -0
  20. package/build/cli/cmdSetVersionOverrides.d.mts +5 -2
  21. package/build/cli/cmdVale.d.mts +46 -0
  22. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
  23. package/build/eslint/baseConfig.d.mts +3 -1
  24. package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
  25. package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
  26. package/build/eslint/mui/rules/no-floating-cleanup.d.mts +5 -0
  27. package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
  28. package/build/eslint/mui/rules/no-presentation-role.d.mts +5 -0
  29. package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +8 -2
  30. package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
  31. package/build/remark/config.d.mts +43 -0
  32. package/build/remark/createLintTester.d.mts +10 -0
  33. package/build/remark/firstBlockHeading.d.mts +4 -0
  34. package/build/remark/gitDiff.d.mts +2 -0
  35. package/build/remark/noSpaceInLinks.d.mts +2 -0
  36. package/build/remark/straightQuotes.d.mts +2 -0
  37. package/build/remark/tableAlignment.d.mts +2 -0
  38. package/build/remark/terminalLanguage.d.mts +2 -0
  39. package/build/utils/babel.d.mts +7 -1
  40. package/build/utils/build.d.mts +59 -37
  41. package/build/utils/extractErrorCodes.d.mts +9 -0
  42. package/build/utils/git.d.mts +16 -0
  43. package/build/utils/github.d.mts +13 -1
  44. package/build/utils/pnpm.d.mts +200 -7
  45. package/build/utils/testUtils.d.mts +28 -0
  46. package/build/utils/typescript.d.mts +6 -16
  47. package/package.json +83 -56
  48. package/src/babel-config.mjs +9 -3
  49. package/src/brokenLinksChecker/crawlWorker.mjs +240 -0
  50. package/src/brokenLinksChecker/index.mjs +263 -188
  51. package/src/build-env.d.ts +13 -0
  52. package/src/changelog/fetchChangelogs.mjs +8 -2
  53. package/src/changelog/renderChangelog.mjs +1 -1
  54. package/src/changelog/types.ts +3 -1
  55. package/src/cli/cmdBuild.mjs +51 -85
  56. package/src/cli/cmdListWorkspaces.mjs +16 -3
  57. package/src/cli/cmdNetlifyIgnore.mjs +34 -93
  58. package/src/cli/cmdPublish.mjs +105 -36
  59. package/src/cli/cmdPublishCanary.mjs +145 -109
  60. package/src/cli/cmdPublishNewPackage.mjs +31 -8
  61. package/src/cli/cmdRenameScope.mjs +76 -0
  62. package/src/cli/cmdSetVersionOverrides.mjs +8 -9
  63. package/src/cli/cmdVale.mjs +513 -0
  64. package/src/cli/index.mjs +4 -0
  65. package/src/cli/packageJson.d.ts +2 -3
  66. package/src/eslint/baseConfig.mjs +51 -19
  67. package/src/eslint/docsConfig.mjs +2 -1
  68. package/src/eslint/mui/config.mjs +24 -4
  69. package/src/eslint/mui/index.mjs +6 -0
  70. package/src/eslint/mui/rules/no-floating-cleanup.mjs +187 -0
  71. package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
  72. package/src/eslint/mui/rules/no-presentation-role.mjs +60 -0
  73. package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
  74. package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
  75. package/src/estree-typescript.d.ts +1 -1
  76. package/src/remark/config.mjs +157 -0
  77. package/src/remark/createLintTester.mjs +19 -0
  78. package/src/remark/firstBlockHeading.mjs +87 -0
  79. package/src/remark/gitDiff.mjs +43 -0
  80. package/src/remark/noSpaceInLinks.mjs +42 -0
  81. package/src/remark/straightQuotes.mjs +31 -0
  82. package/src/remark/tableAlignment.mjs +23 -0
  83. package/src/remark/terminalLanguage.mjs +19 -0
  84. package/src/stylelint/index.mjs +2 -2
  85. package/src/untyped-plugins.d.ts +14 -14
  86. package/src/utils/build.mjs +604 -270
  87. package/src/utils/git.mjs +27 -7
  88. package/src/utils/pnpm.mjs +549 -26
  89. package/src/utils/testUtils.mjs +53 -0
  90. package/src/utils/typescript.mjs +23 -42
  91. package/vale/.vale.ini +1 -0
  92. package/vale/styles/MUI/CorrectReferenceAllCases.yml +42 -0
  93. package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
  94. package/vale/styles/MUI/GoogleLatin.yml +11 -0
  95. package/vale/styles/MUI/MuiBrandName.yml +22 -0
  96. package/vale/styles/MUI/NoBritish.yml +112 -0
  97. package/vale/styles/MUI/NoCompanyName.yml +17 -0
  98. package/build/markdownlint/duplicate-h1.d.mts +0 -56
  99. package/build/markdownlint/git-diff.d.mts +0 -11
  100. package/build/markdownlint/index.d.mts +0 -52
  101. package/build/markdownlint/straight-quotes.d.mts +0 -11
  102. package/build/markdownlint/table-alignment.d.mts +0 -11
  103. package/build/markdownlint/terminal-language.d.mts +0 -11
  104. package/src/brokenLinksChecker/__fixtures__/static-site/broken-links.html +0 -20
  105. package/src/brokenLinksChecker/__fixtures__/static-site/broken-targets.html +0 -22
  106. package/src/brokenLinksChecker/__fixtures__/static-site/example.md +0 -20
  107. package/src/brokenLinksChecker/__fixtures__/static-site/external-links.html +0 -21
  108. package/src/brokenLinksChecker/__fixtures__/static-site/ignored-page.html +0 -17
  109. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +0 -28
  110. package/src/brokenLinksChecker/__fixtures__/static-site/known-targets.json +0 -5
  111. package/src/brokenLinksChecker/__fixtures__/static-site/nested/page.html +0 -21
  112. package/src/brokenLinksChecker/__fixtures__/static-site/orphaned-page.html +0 -20
  113. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-api-links.html +0 -20
  114. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-custom-targets.html +0 -24
  115. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-ignored-content.html +0 -28
  116. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-known-target-links.html +0 -19
  117. package/src/brokenLinksChecker/__fixtures__/static-site/unclosed-tags.html +0 -1
  118. package/src/brokenLinksChecker/__fixtures__/static-site/valid.html +0 -20
  119. package/src/brokenLinksChecker/__fixtures__/static-site/with-anchors.html +0 -31
  120. package/src/brokenLinksChecker/index.test.ts +0 -261
  121. package/src/changelog/categorizeCommits.test.ts +0 -319
  122. package/src/changelog/filterCommits.test.ts +0 -363
  123. package/src/changelog/parseCommitLabels.test.ts +0 -509
  124. package/src/changelog/renderChangelog.test.ts +0 -378
  125. package/src/changelog/sortSections.test.ts +0 -199
  126. package/src/eslint/mui/rules/add-undef-to-optional.test.mjs +0 -361
  127. package/src/eslint/mui/rules/consistent-production-guard.test.mjs +0 -162
  128. package/src/eslint/mui/rules/disallow-active-elements-as-key-event-target.test.mjs +0 -66
  129. package/src/eslint/mui/rules/disallow-react-api-in-server-components.test.mjs +0 -305
  130. package/src/eslint/mui/rules/docgen-ignore-before-comment.test.mjs +0 -52
  131. package/src/eslint/mui/rules/flatten-parentheses.test.mjs +0 -245
  132. package/src/eslint/mui/rules/mui-name-matches-component-name.test.mjs +0 -247
  133. package/src/eslint/mui/rules/no-empty-box.test.mjs +0 -40
  134. package/src/eslint/mui/rules/no-styled-box.test.mjs +0 -73
  135. package/src/eslint/mui/rules/require-dev-wrapper.test.mjs +0 -265
  136. package/src/eslint/mui/rules/rules-of-use-theme-variants.test.mjs +0 -149
  137. package/src/eslint/mui/rules/straight-quotes.test.mjs +0 -67
  138. package/src/markdownlint/duplicate-h1.mjs +0 -69
  139. package/src/markdownlint/git-diff.mjs +0 -31
  140. package/src/markdownlint/index.mjs +0 -62
  141. package/src/markdownlint/straight-quotes.mjs +0 -26
  142. package/src/markdownlint/table-alignment.mjs +0 -42
  143. package/src/markdownlint/terminal-language.mjs +0 -19
  144. package/src/utils/build.test.mjs +0 -705
  145. package/src/utils/template.test.mjs +0 -133
  146. package/src/utils/typescript.test.mjs +0 -380
package/src/cli/index.mjs CHANGED
@@ -14,7 +14,9 @@ import cmdNetlifyIgnore from './cmdNetlifyIgnore.mjs';
14
14
  import cmdPublish from './cmdPublish.mjs';
15
15
  import cmdPublishCanary from './cmdPublishCanary.mjs';
16
16
  import cmdPublishNewPackage from './cmdPublishNewPackage.mjs';
17
+ import cmdRenameScope from './cmdRenameScope.mjs';
17
18
  import cmdSetVersionOverrides from './cmdSetVersionOverrides.mjs';
19
+ import cmdVale from './cmdVale.mjs';
18
20
  import cmdValidateBuiltTypes from './cmdValidateBuiltTypes.mjs';
19
21
 
20
22
  const pkgJson = createRequire(import.meta.url)('../../package.json');
@@ -46,7 +48,9 @@ await yargs(hideBin(process.argv))
46
48
  .command(cmdPublish)
47
49
  .command(cmdPublishCanary)
48
50
  .command(cmdPublishNewPackage)
51
+ .command(cmdRenameScope)
49
52
  .command(cmdSetVersionOverrides)
53
+ .command(cmdVale)
50
54
  .command(cmdValidateBuiltTypes)
51
55
  .fail((msg, err, yargsInstance) => {
52
56
  if (msg) {
@@ -56,8 +56,7 @@ const pet: Pet2 = '';
56
56
  ```
57
57
  */
58
58
  type LiteralUnion<LiteralType, BaseType extends Primitive> =
59
- | LiteralType
60
- | (BaseType & Record<never, never>);
59
+ LiteralType | (BaseType & Record<never, never>);
61
60
 
62
61
  declare namespace PackageJson {
63
62
  /**
@@ -308,7 +307,7 @@ declare namespace PackageJson {
308
307
  browser?: string | Partial<Record<string, string | false>>;
309
308
 
310
309
  /**
311
- Denote which files in your project are "pure" and therefore safe for Webpack to prune if unused.
310
+ Denote which files in your project are "pure" and therefore safe for webpack to prune if unused.
312
311
 
313
312
  [Read more.](https://webpack.js.org/guides/tree-shaking/)
314
313
  */
@@ -6,6 +6,7 @@ import compatPlugin from 'eslint-plugin-compat';
6
6
  import importPlugin from 'eslint-plugin-import';
7
7
  import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
8
8
  import reactPlugin from 'eslint-plugin-react';
9
+ import * as mdx from 'eslint-plugin-mdx';
9
10
  import { configs as reactCompilerPluginConfigs } from 'eslint-plugin-react-compiler';
10
11
  import reactHooks from 'eslint-plugin-react-hooks';
11
12
  import globals from 'globals';
@@ -34,19 +35,60 @@ function includeIgnoreIfExists(filePath, description) {
34
35
  * @param {boolean} [params.consistentTypeImports] - Whether to enforce consistent type imports.
35
36
  * @param {boolean} [params.materialUi] - Whether to enable Material UI specific rules (mui/material-ui-*).
36
37
  * @param {string} [params.baseDirectory] - The base directory for the configuration.
38
+ * @param {boolean} [params.markdown] - @deprecated Markdown/MDX linting is enabled by default; this option no longer needs to be passed. To skip markdown linting, use eslint ignore patterns for the relevant files.
37
39
  * @returns {import('eslint').Linter.Config[]}
38
40
  */
39
41
  export function createBaseConfig({
40
42
  enableReactCompiler = false,
41
43
  consistentTypeImports = false,
42
44
  materialUi = false,
45
+ markdown = true,
43
46
  baseDirectory = process.cwd(),
44
47
  } = {}) {
45
48
  return defineConfig([
49
+ {
50
+ name: 'settings',
51
+ languageOptions: {
52
+ ecmaVersion: 7,
53
+ globals: {
54
+ ...globals.es2020,
55
+ ...globals.browser,
56
+ ...globals.node,
57
+ },
58
+ },
59
+ plugins: {
60
+ mui: muiPlugin,
61
+ },
62
+ settings: {
63
+ react: {
64
+ version: 'detect',
65
+ },
66
+ browserslistOpts: {
67
+ config: path.join(baseDirectory, '.browserslistrc'),
68
+ env: 'stable',
69
+ ignoreUnknownVersions: true,
70
+ },
71
+ },
72
+ },
46
73
  includeIgnoreIfExists(path.join(baseDirectory, '.gitignore'), `Ignore rules from .gitignore`),
47
74
  includeIgnoreIfExists(path.join(baseDirectory, '.lintignore'), `Ignore rules from .lintignore`),
48
75
  createJsonConfig(),
49
76
  prettier,
77
+ // Markdown + MDX linting via eslint-plugin-mdx. Severities for markdown
78
+ // quality checks live in the project's `.remarkrc` (see
79
+ // `@mui/internal-code-infra/remark`), not here.
80
+ markdown
81
+ ? [
82
+ {
83
+ ...mdx.flat,
84
+ rules: {
85
+ ...mdx.flat.rules,
86
+ 'mdx/remark': 'error',
87
+ },
88
+ },
89
+ mdx.flatCodeBlocks,
90
+ ]
91
+ : [],
50
92
  {
51
93
  name: 'Base config',
52
94
  files: [`**/*${EXTENSION_TS}`],
@@ -62,25 +104,7 @@ export function createBaseConfig({
62
104
  enableReactCompiler ? reactCompilerPluginConfigs.recommended : {},
63
105
  compatPlugin.configs['flat/recommended'],
64
106
  {
65
- name: 'typescript-eslint-parser',
66
- languageOptions: {
67
- ecmaVersion: 7,
68
- globals: {
69
- ...globals.es2020,
70
- ...globals.browser,
71
- ...globals.node,
72
- },
73
- },
74
- plugins: {
75
- mui: muiPlugin,
76
- },
77
- settings: {
78
- browserslistOpts: {
79
- config: path.join(baseDirectory, '.browserslistrc'),
80
- env: 'stable',
81
- ignoreUnknownVersions: true,
82
- },
83
- },
107
+ name: 'core',
84
108
  extends: createCoreConfig({ enableReactCompiler, consistentTypeImports, materialUi }),
85
109
  },
86
110
  // Lint rule to disallow usage of typescript namespaces.We've seen at least two problems with them:
@@ -116,6 +140,14 @@ export function createBaseConfig({
116
140
  'no-unassigned-vars': 'off',
117
141
  },
118
142
  },
143
+ // @TODO: Remove this once @typescript-eslint/no-shadow supports wrapped functions
144
+ // See https://github.com/eslint/eslint/pull/20982 (once merged also needs port to `typescript-eslint`)
145
+ {
146
+ name: 'Disabled tseslint-plugins',
147
+ rules: {
148
+ '@typescript-eslint/no-shadow': 'off',
149
+ },
150
+ },
119
151
  ]),
120
152
  },
121
153
  {
@@ -1,5 +1,6 @@
1
1
  import nextjs from '@next/eslint-plugin-next';
2
2
  import { defineConfig } from 'eslint/config';
3
+ import { EXTENSION_TS } from './extensions.mjs';
3
4
 
4
5
  /**
5
6
  * @returns {import('eslint').Linter.Config[]}
@@ -24,7 +25,7 @@ export function createDocsConfig() {
24
25
  rootDir: 'docs',
25
26
  },
26
27
  },
27
- files: ['**/*.js', '**/*.mjs', '**/*.jsx', '**/*.ts', '**/*.tsx'],
28
+ files: [`**/*${EXTENSION_TS}`],
28
29
  rules: {
29
30
  'compat/compat': 'off',
30
31
  'jsx-a11y/anchor-is-valid': 'off',
@@ -1,4 +1,5 @@
1
1
  import { defineConfig } from 'eslint/config';
2
+ import { EXTENSION_DTS } from '../extensions.mjs';
2
3
 
3
4
  const restrictedMethods = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
4
5
 
@@ -414,10 +415,12 @@ export function createCoreConfig(options = {}) {
414
415
  'mui/material-ui-no-styled-box': 'error',
415
416
  }
416
417
  : {}),
418
+ 'mui/no-guarded-throw': 'error',
417
419
  'mui/straight-quotes': 'off',
418
420
  'mui/consistent-production-guard': 'error',
419
421
  'mui/add-undef-to-optional': 'off',
420
422
  'mui/flatten-parentheses': 'warn',
423
+ 'mui/no-presentation-role': 'off',
421
424
 
422
425
  'react-hooks/exhaustive-deps': [
423
426
  'error',
@@ -460,9 +463,9 @@ export function createCoreConfig(options = {}) {
460
463
  'react/state-in-constructor': 'off',
461
464
  // stylistic opinion. For conditional assignment we want it outside, otherwise as static
462
465
  'react/static-property-placement': 'off',
463
- // noopener is enough
464
- // https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-target-blank.md#rule-options
465
- 'react/jsx-no-target-blank': ['error', { allowReferrer: true }],
466
+ // This rule is outdated, rel are no longer needed for security on target="_blank" links.
467
+ // See https://github.com/mui/material-ui/pull/40447 for more details.
468
+ 'react/jsx-no-target-blank': 'off',
466
469
 
467
470
  'no-restricted-syntax': [
468
471
  'error',
@@ -499,6 +502,15 @@ export function createCoreConfig(options = {}) {
499
502
  message: 'Do not call `Error(...)` without `new`. Use `new Error(...)` instead.',
500
503
  selector: "CallExpression[callee.name='Error']",
501
504
  },
505
+ {
506
+ // xmlns="http://www.w3.org/2000/svg" is only needed on standalone .svg files so the
507
+ // browser treats them as SVG instead of generic XML. Inside HTML the <svg> element is
508
+ // already recognised by the browser, so the attribute is dead weight.
509
+ // https://github.com/mui/mui-public/pull/1321
510
+ message:
511
+ 'Remove xmlns from inline <svg>. The attribute is redundant in HTML and adds unnecessary bytes.',
512
+ selector: 'JSXOpeningElement[name.name="svg"] > JSXAttribute[name.name="xmlns"]',
513
+ },
502
514
  ...restrictedSyntaxRules,
503
515
  ],
504
516
 
@@ -515,9 +527,10 @@ export function createCoreConfig(options = {}) {
515
527
  '@typescript-eslint/consistent-type-imports': [
516
528
  'error',
517
529
  {
518
- fixStyle: 'inline-type-imports',
530
+ fixStyle: 'separate-type-imports',
519
531
  },
520
532
  ],
533
+ 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
521
534
  }
522
535
  : {}),
523
536
  // Prevent the use of `e` as a shorthand for `event`, `error`, etc.
@@ -525,5 +538,12 @@ export function createCoreConfig(options = {}) {
525
538
  '@typescript-eslint/return-await': 'off',
526
539
  },
527
540
  },
541
+ {
542
+ name: 'mui-base/dts',
543
+ files: [`**/*${EXTENSION_DTS}`],
544
+ rules: {
545
+ '@typescript-eslint/consistent-type-imports': 'off',
546
+ },
547
+ },
528
548
  ]);
529
549
  }
@@ -4,6 +4,7 @@ import disallowReactApiInServerComponents from './rules/disallow-react-api-in-se
4
4
  import docgenIgnoreBeforeComment from './rules/docgen-ignore-before-comment.mjs';
5
5
  import muiNameMatchesComponentName from './rules/mui-name-matches-component-name.mjs';
6
6
  import noEmptyBox from './rules/no-empty-box.mjs';
7
+ import noGuardedThrow from './rules/no-guarded-throw.mjs';
7
8
  import noRestrictedResolvedImports from './rules/no-restricted-resolved-imports.mjs';
8
9
  import noStyledBox from './rules/no-styled-box.mjs';
9
10
  import requireDevWrapper from './rules/require-dev-wrapper.mjs';
@@ -11,6 +12,8 @@ import rulesOfUseThemeVariants from './rules/rules-of-use-theme-variants.mjs';
11
12
  import straightQuotes from './rules/straight-quotes.mjs';
12
13
  import addUndefToOptional from './rules/add-undef-to-optional.mjs';
13
14
  import flattenParentheses from './rules/flatten-parentheses.mjs';
15
+ import noPresentationRole from './rules/no-presentation-role.mjs';
16
+ import noFloatingCleanup from './rules/no-floating-cleanup.mjs';
14
17
 
15
18
  /** @type {import('eslint').ESLint.Plugin} */
16
19
  const muiPlugin = {
@@ -22,6 +25,7 @@ const muiPlugin = {
22
25
  'consistent-production-guard': consistentProductionGuard,
23
26
  'disallow-active-element-as-key-event-target': disallowActiveElementAsKeyEventTarget,
24
27
  'docgen-ignore-before-comment': docgenIgnoreBeforeComment,
28
+ 'no-guarded-throw': noGuardedThrow,
25
29
  'material-ui-name-matches-component-name': muiNameMatchesComponentName,
26
30
  'material-ui-rules-of-use-theme-variants': rulesOfUseThemeVariants,
27
31
  'material-ui-no-empty-box': noEmptyBox,
@@ -33,6 +37,8 @@ const muiPlugin = {
33
37
  // Some discrepancies between TypeScript and ESLint types - casting to any
34
38
  'add-undef-to-optional': /** @type {any} */ (addUndefToOptional),
35
39
  'flatten-parentheses': /** @type {any} */ (flattenParentheses),
40
+ 'no-presentation-role': noPresentationRole,
41
+ 'no-floating-cleanup': /** @type {any} */ (noFloatingCleanup),
36
42
  },
37
43
  };
38
44
 
@@ -0,0 +1,187 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import ts from 'typescript';
3
+
4
+ /**
5
+ * no-floating-cleanup
6
+ *
7
+ * Like `@typescript-eslint/no-floating-promises`, but for functions: reports
8
+ * call expressions used as statements whose return value is a function
9
+ * (e.g. an `unsubscribe` / cleanup callback). Ignoring such a value usually
10
+ * means the subscription can never be torn down — a leak.
11
+ *
12
+ * Requires type information (parserOptions.project / projectService).
13
+ *
14
+ * Calls whose resolved signature declares a `this` return type are never
15
+ * reported: fluent / builder APIs (e.g. d3 scales) chain by returning `this`,
16
+ * so the discarded result is callable but not a leaked cleanup function.
17
+ *
18
+ * Opt-out at a call site, same convention as no-floating-promises:
19
+ * void subscribe(cb);
20
+ */
21
+
22
+ const RULE_NAME = 'no-floating-cleanup';
23
+
24
+ const createRule = ESLintUtils.RuleCreator(
25
+ (name) =>
26
+ `https://github.com/mui/mui-public/blob/master/packages/code-infra/src/eslint/mui/rules/${name}.mjs`,
27
+ );
28
+
29
+ export default createRule({
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description:
34
+ 'Disallow ignoring returned cleanup/unsubscribe functions, which leaks subscriptions.',
35
+ },
36
+ messages: {
37
+ floatingCleanup:
38
+ "Return value of type '{{typeName}}' is ignored. This likely leaks a subscription — " +
39
+ 'store the cleanup function, or discard it explicitly with the `void` operator.',
40
+ },
41
+ schema: [],
42
+ },
43
+ name: RULE_NAME,
44
+ defaultOptions: [],
45
+ create(context) {
46
+ const services = ESLintUtils.getParserServices(context);
47
+ const checker = services.program.getTypeChecker();
48
+
49
+ /**
50
+ * Does this type (or any union member) look like a cleanup function?
51
+ * @param {import('typescript').Type} type
52
+ * @returns {boolean}
53
+ */
54
+ function isCleanupType(type) {
55
+ if (type.isUnion()) {
56
+ return type.types.some(isCleanupType);
57
+ }
58
+ // Never fire on any/unknown/never — too noisy and not provable.
59
+ // eslint-disable-next-line no-bitwise -- TypeScript type flags are a bitmask
60
+ if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) {
61
+ return false;
62
+ }
63
+
64
+ return checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0;
65
+ }
66
+
67
+ /**
68
+ * @param {import('typescript').Type} type
69
+ * @returns {string}
70
+ */
71
+ function describeType(type) {
72
+ return (type.aliasSymbol && type.aliasSymbol.getName()) || checker.typeToString(type);
73
+ }
74
+
75
+ /**
76
+ * Does this call resolve to a signature that returns `this`? Fluent /
77
+ * builder APIs (e.g. d3 scales) chain by returning `this`, so the
78
+ * discarded result is callable but never a leaked cleanup function —
79
+ * those calls should not be reported. Covers both an explicit `: this`
80
+ * annotation and an inferred `this` return (no annotation).
81
+ * @param {import('@typescript-eslint/utils').TSESTree.Node} estreeCall
82
+ * @returns {boolean}
83
+ */
84
+ function returnsThis(estreeCall) {
85
+ /** @type {import('typescript').Node | undefined} */
86
+ let tsNode = services.esTreeNodeToTSNodeMap.get(estreeCall);
87
+ // Unwrap `await (...)` / parens down to the underlying call.
88
+ while (tsNode && (ts.isAwaitExpression(tsNode) || ts.isParenthesizedExpression(tsNode))) {
89
+ tsNode = tsNode.expression;
90
+ }
91
+ if (!tsNode || !(ts.isCallExpression(tsNode) || ts.isNewExpression(tsNode))) {
92
+ return false;
93
+ }
94
+ const declaration = checker.getResolvedSignature(tsNode)?.getDeclaration();
95
+ if (!declaration) {
96
+ return false;
97
+ }
98
+ // Explicit `: this` annotation.
99
+ if (declaration.type?.kind === ts.SyntaxKind.ThisType) {
100
+ return true;
101
+ }
102
+ // Inferred `this` return: the *declared* (uninstantiated) signature's
103
+ // return type is the polymorphic `this` type, which prints as `this`.
104
+ // (The resolved signature would have it bound to the receiver type.)
105
+ const declaredSignature = checker.getSignatureFromDeclaration(declaration);
106
+ return (
107
+ !!declaredSignature &&
108
+ checker.typeToString(checker.getReturnTypeOfSignature(declaredSignature)) === 'this'
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Unwrap an expression-statement expression down to the calls whose
114
+ * results are being discarded. Mirrors the cases no-floating-promises
115
+ * handles: plain calls, optional chains, `await`, comma sequences,
116
+ * and the operand(s) of unary / logical / conditional expressions.
117
+ * A top-level `void` operator is the explicit opt-out, so we stop there.
118
+ * @param {import('@typescript-eslint/utils').TSESTree.Node} expr
119
+ * @param {import('@typescript-eslint/utils').TSESTree.Node[]} [out]
120
+ * @returns {import('@typescript-eslint/utils').TSESTree.Node[]}
121
+ */
122
+ function findDiscardedCalls(expr, out = []) {
123
+ switch (expr.type) {
124
+ case 'CallExpression':
125
+ case 'NewExpression':
126
+ out.push(expr);
127
+ break;
128
+ case 'ChainExpression':
129
+ findDiscardedCalls(expr.expression, out);
130
+ break;
131
+ case 'AwaitExpression':
132
+ // type of the AwaitExpression node is already the awaited type
133
+ if (expr.argument.type === 'CallExpression' || expr.argument.type === 'ChainExpression') {
134
+ out.push(expr);
135
+ }
136
+ break;
137
+ case 'UnaryExpression':
138
+ // `void expr` is the explicit opt-out; any other unary operator
139
+ // (e.g. `!subscribe()`) still discards the operand's result.
140
+ if (expr.operator !== 'void') {
141
+ findDiscardedCalls(expr.argument, out);
142
+ }
143
+ break;
144
+ case 'LogicalExpression':
145
+ // `a && subscribe()`, `a || subscribe()`, `a ?? subscribe()`:
146
+ // either side may be the discarded value.
147
+ findDiscardedCalls(expr.left, out);
148
+ findDiscardedCalls(expr.right, out);
149
+ break;
150
+ case 'ConditionalExpression':
151
+ // `cond ? subscribe() : other()`: the branches are discarded,
152
+ // not the test.
153
+ findDiscardedCalls(expr.consequent, out);
154
+ findDiscardedCalls(expr.alternate, out);
155
+ break;
156
+ case 'SequenceExpression':
157
+ // every value except the last is discarded; the last one is the
158
+ // statement's value, which is also discarded here
159
+ expr.expressions.forEach((subExpr) => findDiscardedCalls(subExpr, out));
160
+ break;
161
+ default:
162
+ break;
163
+ }
164
+ return out;
165
+ }
166
+
167
+ return {
168
+ /** @param {import('@typescript-eslint/utils').TSESTree.ExpressionStatement} node */
169
+ ExpressionStatement(node) {
170
+ findDiscardedCalls(node.expression).forEach((call) => {
171
+ // Fluent/builder calls return `this` for chaining — not a leak.
172
+ if (returnsThis(call)) {
173
+ return;
174
+ }
175
+ const type = services.getTypeAtLocation(call);
176
+ if (isCleanupType(type)) {
177
+ context.report({
178
+ node: call,
179
+ messageId: 'floatingCleanup',
180
+ data: { typeName: describeType(type) },
181
+ });
182
+ }
183
+ });
184
+ },
185
+ };
186
+ },
187
+ });
@@ -0,0 +1,115 @@
1
+ import { isProcessEnvNodeEnv } from './nodeEnvUtils.mjs';
2
+
3
+ /**
4
+ * Recursively checks if process.env.NODE_ENV appears anywhere in the node tree
5
+ * @param {import('estree').Node | null | undefined} node
6
+ * @returns {boolean}
7
+ */
8
+ function containsProcessEnvNodeEnv(node) {
9
+ if (!node || typeof node !== 'object') {
10
+ return false;
11
+ }
12
+
13
+ if (isProcessEnvNodeEnv(node)) {
14
+ return true;
15
+ }
16
+
17
+ // Traverse all child nodes, skipping parent references to avoid circular traversal
18
+ for (const key of Object.keys(node)) {
19
+ if (key === 'parent') {
20
+ continue;
21
+ }
22
+ const child = /** @type {unknown} */ (/** @type {any} */ (node)[key]);
23
+ if (Array.isArray(child)) {
24
+ if (child.some(containsProcessEnvNodeEnv)) {
25
+ return true;
26
+ }
27
+ } else if (
28
+ child &&
29
+ typeof child === 'object' &&
30
+ /** @type {import('estree').Node} */ (child).type
31
+ ) {
32
+ if (containsProcessEnvNodeEnv(/** @type {import('estree').Node} */ (child))) {
33
+ return true;
34
+ }
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /**
41
+ * ESLint rule that disallows throw statements guarded by process.env.NODE_ENV checks.
42
+ *
43
+ * NODE_ENV guards cause throw statements to only execute in certain environments,
44
+ * leading to inconsistent control flow between development and production. Whether
45
+ * the guard excludes production (tree-shaking the throw away) or targets production
46
+ * specifically, the result is environment-dependent behavior that should be avoided.
47
+ *
48
+ * The rule stops at function boundaries, so throws inside functions defined within
49
+ * a NODE_ENV guard are not flagged, as the function may be called from other contexts.
50
+ *
51
+ * @example
52
+ * // Invalid - throw only in development, removed in production
53
+ * if (process.env.NODE_ENV !== 'production') {
54
+ * throw new Error('Missing required prop');
55
+ * }
56
+ *
57
+ * @example
58
+ * // Invalid - throw only in production
59
+ * if (process.env.NODE_ENV === 'production') {
60
+ * throw new Error('Production-only error');
61
+ * }
62
+ *
63
+ * @example
64
+ * // Valid - unconditional throw
65
+ * throw new Error('Something went wrong');
66
+ *
67
+ * @type {import('eslint').Rule.RuleModule}
68
+ */
69
+ const rule = {
70
+ meta: {
71
+ type: 'problem',
72
+ docs: {
73
+ description:
74
+ 'Disallow throw statements guarded by process.env.NODE_ENV checks, as they cause environment-dependent control flow',
75
+ },
76
+ messages: {
77
+ guardedThrow:
78
+ 'Do not guard `throw` statements with `process.env.NODE_ENV` checks. Guarded throws execute only in certain environments, causing inconsistent control flow between development and production.',
79
+ },
80
+ schema: [],
81
+ },
82
+ create(context) {
83
+ return {
84
+ ThrowStatement(node) {
85
+ /** @type {import('eslint').Rule.Node | null} */
86
+ let current = node.parent;
87
+ /** @type {import('eslint').Rule.Node} */
88
+ let currentChild = node;
89
+
90
+ while (current) {
91
+ if (
92
+ current.type === 'FunctionDeclaration' ||
93
+ current.type === 'FunctionExpression' ||
94
+ current.type === 'ArrowFunctionExpression'
95
+ ) {
96
+ break;
97
+ }
98
+ if (current.type === 'IfStatement') {
99
+ const isInConsequent = current.consequent === currentChild;
100
+ const isInAlternate = current.alternate === currentChild;
101
+
102
+ if ((isInConsequent || isInAlternate) && containsProcessEnvNodeEnv(current.test)) {
103
+ context.report({ node, messageId: 'guardedThrow' });
104
+ return;
105
+ }
106
+ }
107
+ currentChild = current;
108
+ current = current.parent;
109
+ }
110
+ },
111
+ };
112
+ },
113
+ };
114
+
115
+ export default rule;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @type {import('eslint').Rule.RuleModule}
3
+ */
4
+ const rule = {
5
+ meta: {
6
+ docs: {
7
+ description:
8
+ 'Disallow role="presentation" in favor of role="none". Both are equivalent, but role="none" is clearer and shorter.',
9
+ },
10
+ messages: {
11
+ noPresentation:
12
+ 'Use role="none" instead of role="presentation". They are equivalent, but role="none" is preferred.',
13
+ },
14
+ fixable: 'code',
15
+ type: 'suggestion',
16
+ schema: [],
17
+ },
18
+ create(context) {
19
+ return {
20
+ /** @param {import('estree-jsx').JSXAttribute} node */
21
+ JSXAttribute(node) {
22
+ if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'role') {
23
+ return;
24
+ }
25
+
26
+ const { value } = node;
27
+
28
+ // role="presentation"
29
+ if (value !== null && value.type === 'Literal' && value.value === 'presentation') {
30
+ context.report({
31
+ node,
32
+ messageId: 'noPresentation',
33
+ fix(fixer) {
34
+ return fixer.replaceText(value, '"none"');
35
+ },
36
+ });
37
+ return;
38
+ }
39
+
40
+ // role={'presentation'}
41
+ if (
42
+ value !== null &&
43
+ value.type === 'JSXExpressionContainer' &&
44
+ value.expression.type === 'Literal' &&
45
+ value.expression.value === 'presentation'
46
+ ) {
47
+ context.report({
48
+ node,
49
+ messageId: 'noPresentation',
50
+ fix(fixer) {
51
+ return fixer.replaceText(value, '"none"');
52
+ },
53
+ });
54
+ }
55
+ },
56
+ };
57
+ },
58
+ };
59
+
60
+ export default rule;