@mui/internal-code-infra 0.0.4-canary.8 → 0.0.4-canary.81
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.
- package/LICENSE +1 -1
- package/README.md +19 -8
- package/build/babel-config.d.mts +5 -1
- package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
- package/build/brokenLinksChecker/index.d.mts +45 -2
- package/build/changelog/types.d.ts +1 -1
- package/build/cli/cmdArgosPush.d.mts +2 -2
- package/build/cli/cmdBuild.d.mts +3 -2
- package/build/cli/cmdCopyFiles.d.mts +2 -2
- package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
- package/build/cli/cmdGenerateChangelog.d.mts +2 -2
- package/build/cli/cmdGithubAuth.d.mts +2 -2
- package/build/cli/cmdListWorkspaces.d.mts +6 -4
- package/build/cli/cmdNetlifyIgnore.d.mts +3 -2
- package/build/cli/cmdPublish.d.mts +4 -2
- package/build/cli/cmdPublishCanary.d.mts +3 -3
- package/build/cli/cmdPublishNewPackage.d.mts +4 -2
- package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
- package/build/cli/cmdVale.d.mts +46 -0
- package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
- package/build/eslint/baseConfig.d.mts +3 -1
- package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
- package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
- package/build/eslint/mui/rules/no-floating-cleanup.d.mts +5 -0
- package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
- package/build/eslint/mui/rules/no-presentation-role.d.mts +5 -0
- package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
- package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
- package/build/remark/config.d.mts +43 -0
- package/build/remark/createLintTester.d.mts +10 -0
- package/build/remark/firstBlockHeading.d.mts +4 -0
- package/build/remark/gitDiff.d.mts +2 -0
- package/build/remark/noSpaceInLinks.d.mts +2 -0
- package/build/remark/straightQuotes.d.mts +2 -0
- package/build/remark/tableAlignment.d.mts +2 -0
- package/build/remark/terminalLanguage.d.mts +2 -0
- package/build/utils/babel.d.mts +1 -1
- package/build/utils/build.d.mts +56 -37
- package/build/utils/git.d.mts +7 -0
- package/build/utils/github.d.mts +1 -1
- package/build/utils/pnpm.d.mts +81 -2
- package/build/utils/testUtils.d.mts +7 -0
- package/build/utils/typescript.d.mts +6 -16
- package/package.json +66 -42
- package/src/babel-config.mjs +3 -1
- package/src/brokenLinksChecker/crawlWorker.mjs +240 -0
- package/src/brokenLinksChecker/index.mjs +263 -188
- package/src/changelog/fetchChangelogs.mjs +6 -2
- package/src/changelog/renderChangelog.mjs +1 -1
- package/src/changelog/types.ts +1 -1
- package/src/cli/cmdBuild.mjs +51 -85
- package/src/cli/cmdListWorkspaces.mjs +12 -27
- package/src/cli/cmdNetlifyIgnore.mjs +34 -93
- package/src/cli/cmdPublish.mjs +95 -20
- package/src/cli/cmdPublishCanary.mjs +128 -132
- package/src/cli/cmdPublishNewPackage.mjs +27 -6
- package/src/cli/cmdSetVersionOverrides.mjs +8 -9
- package/src/cli/cmdVale.mjs +513 -0
- package/src/cli/index.mjs +2 -0
- package/src/cli/packageJson.d.ts +1 -1
- package/src/eslint/baseConfig.mjs +51 -19
- package/src/eslint/docsConfig.mjs +2 -1
- package/src/eslint/mui/config.mjs +24 -4
- package/src/eslint/mui/index.mjs +6 -0
- package/src/eslint/mui/rules/no-floating-cleanup.mjs +187 -0
- package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
- package/src/eslint/mui/rules/no-presentation-role.mjs +60 -0
- package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
- package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
- package/src/estree-typescript.d.ts +1 -1
- package/src/remark/config.mjs +157 -0
- package/src/remark/createLintTester.mjs +19 -0
- package/src/remark/firstBlockHeading.mjs +87 -0
- package/src/remark/gitDiff.mjs +43 -0
- package/src/remark/noSpaceInLinks.mjs +42 -0
- package/src/remark/straightQuotes.mjs +31 -0
- package/src/remark/tableAlignment.mjs +23 -0
- package/src/remark/terminalLanguage.mjs +19 -0
- package/src/untyped-plugins.d.ts +11 -11
- package/src/utils/build.mjs +604 -270
- package/src/utils/git.mjs +27 -7
- package/src/utils/pnpm.mjs +277 -10
- package/src/utils/testUtils.mjs +18 -0
- package/src/utils/typescript.mjs +23 -42
- package/vale/.vale.ini +1 -0
- package/vale/styles/MUI/CorrectReferenceAllCases.yml +42 -0
- package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
- package/vale/styles/MUI/GoogleLatin.yml +11 -0
- package/vale/styles/MUI/MuiBrandName.yml +22 -0
- package/vale/styles/MUI/NoBritish.yml +112 -0
- package/vale/styles/MUI/NoCompanyName.yml +17 -0
- package/build/markdownlint/duplicate-h1.d.mts +0 -56
- package/build/markdownlint/git-diff.d.mts +0 -11
- package/build/markdownlint/index.d.mts +0 -52
- package/build/markdownlint/straight-quotes.d.mts +0 -11
- package/build/markdownlint/table-alignment.d.mts +0 -11
- package/build/markdownlint/terminal-language.d.mts +0 -11
- package/src/brokenLinksChecker/__fixtures__/static-site/broken-links.html +0 -20
- package/src/brokenLinksChecker/__fixtures__/static-site/broken-targets.html +0 -22
- package/src/brokenLinksChecker/__fixtures__/static-site/example.md +0 -20
- package/src/brokenLinksChecker/__fixtures__/static-site/external-links.html +0 -21
- package/src/brokenLinksChecker/__fixtures__/static-site/ignored-page.html +0 -17
- package/src/brokenLinksChecker/__fixtures__/static-site/index.html +0 -28
- package/src/brokenLinksChecker/__fixtures__/static-site/known-targets.json +0 -5
- package/src/brokenLinksChecker/__fixtures__/static-site/nested/page.html +0 -21
- package/src/brokenLinksChecker/__fixtures__/static-site/orphaned-page.html +0 -20
- package/src/brokenLinksChecker/__fixtures__/static-site/page-with-api-links.html +0 -20
- package/src/brokenLinksChecker/__fixtures__/static-site/page-with-custom-targets.html +0 -24
- package/src/brokenLinksChecker/__fixtures__/static-site/page-with-ignored-content.html +0 -28
- package/src/brokenLinksChecker/__fixtures__/static-site/page-with-known-target-links.html +0 -19
- package/src/brokenLinksChecker/__fixtures__/static-site/unclosed-tags.html +0 -1
- package/src/brokenLinksChecker/__fixtures__/static-site/valid.html +0 -20
- package/src/brokenLinksChecker/__fixtures__/static-site/with-anchors.html +0 -31
- package/src/brokenLinksChecker/index.test.ts +0 -261
- package/src/changelog/categorizeCommits.test.ts +0 -319
- package/src/changelog/filterCommits.test.ts +0 -363
- package/src/changelog/parseCommitLabels.test.ts +0 -509
- package/src/changelog/renderChangelog.test.ts +0 -378
- package/src/changelog/sortSections.test.ts +0 -199
- package/src/eslint/mui/rules/add-undef-to-optional.test.mjs +0 -361
- package/src/eslint/mui/rules/consistent-production-guard.test.mjs +0 -162
- package/src/eslint/mui/rules/disallow-active-elements-as-key-event-target.test.mjs +0 -66
- package/src/eslint/mui/rules/disallow-react-api-in-server-components.test.mjs +0 -305
- package/src/eslint/mui/rules/docgen-ignore-before-comment.test.mjs +0 -52
- package/src/eslint/mui/rules/flatten-parentheses.test.mjs +0 -245
- package/src/eslint/mui/rules/mui-name-matches-component-name.test.mjs +0 -247
- package/src/eslint/mui/rules/no-empty-box.test.mjs +0 -40
- package/src/eslint/mui/rules/no-styled-box.test.mjs +0 -73
- package/src/eslint/mui/rules/require-dev-wrapper.test.mjs +0 -265
- package/src/eslint/mui/rules/rules-of-use-theme-variants.test.mjs +0 -149
- package/src/eslint/mui/rules/straight-quotes.test.mjs +0 -67
- package/src/markdownlint/duplicate-h1.mjs +0 -69
- package/src/markdownlint/git-diff.mjs +0 -31
- package/src/markdownlint/index.mjs +0 -62
- package/src/markdownlint/straight-quotes.mjs +0 -26
- package/src/markdownlint/table-alignment.mjs +0 -42
- package/src/markdownlint/terminal-language.mjs +0 -19
- package/src/utils/build.test.mjs +0 -705
- package/src/utils/template.test.mjs +0 -133
- package/src/utils/typescript.test.mjs +0 -380
|
@@ -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
|
-
//
|
|
464
|
-
// https://github.com/
|
|
465
|
-
'react/jsx-no-target-blank':
|
|
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: '
|
|
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
|
}
|
package/src/eslint/mui/index.mjs
CHANGED
|
@@ -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;
|
|
@@ -39,3 +39,55 @@ export function isLiteralEq(node, value) {
|
|
|
39
39
|
export function isLiteralNeq(node, value) {
|
|
40
40
|
return node.type === 'Literal' && node.value !== value;
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if a BinaryExpression compares process.env.NODE_ENV with === or !==
|
|
45
|
+
* @param {import('estree').Node} node
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function isNodeEnvBinaryComparison(node) {
|
|
49
|
+
return (
|
|
50
|
+
node.type === 'BinaryExpression' &&
|
|
51
|
+
(node.operator === '===' || node.operator === '!==') &&
|
|
52
|
+
(isProcessEnvNodeEnv(node.left) || isProcessEnvNodeEnv(node.right))
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Walks up the parent chain and checks if the node is inside an IfStatement
|
|
58
|
+
* whose test is a NODE_ENV binary comparison.
|
|
59
|
+
* If a callback is provided, it is called with the IfStatement and the direct
|
|
60
|
+
* child that leads to the node. The function returns true only when the callback
|
|
61
|
+
* returns true. Without a callback the function returns true when the node is
|
|
62
|
+
* inside any branch (consequent or alternate) of such an IfStatement.
|
|
63
|
+
* @param {import('eslint').Rule.Node} node
|
|
64
|
+
* @param {(ifStatement: import('estree').IfStatement & import('eslint').Rule.NodeParentExtension, child: import('eslint').Rule.Node) => boolean} [callback]
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
export function isInsideNodeEnvCheck(node, callback) {
|
|
68
|
+
/** @type {import('eslint').Rule.Node | null} */
|
|
69
|
+
let current = node.parent;
|
|
70
|
+
/** @type {import('eslint').Rule.Node} */
|
|
71
|
+
let currentChild = node;
|
|
72
|
+
|
|
73
|
+
while (current) {
|
|
74
|
+
if (current.type === 'IfStatement' && isNodeEnvBinaryComparison(current.test)) {
|
|
75
|
+
if (callback) {
|
|
76
|
+
if (callback(current, currentChild)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
const isInConsequent = current.consequent === currentChild;
|
|
81
|
+
const isInAlternate = current.alternate === currentChild;
|
|
82
|
+
if (isInConsequent || isInAlternate) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
currentChild = current;
|
|
89
|
+
current = current.parent;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isProcessEnvNodeEnv,
|
|
3
|
+
isLiteralEq,
|
|
4
|
+
isLiteralNeq,
|
|
5
|
+
isInsideNodeEnvCheck,
|
|
6
|
+
} from './nodeEnvUtils.mjs';
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* ESLint rule that enforces certain function calls to be wrapped with
|
|
@@ -58,18 +63,21 @@ const rule = {
|
|
|
58
63
|
const functionNames = options.functionNames || ['warnOnce', 'warn', 'checkSlot'];
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
|
-
* Checks if
|
|
62
|
-
* @param {import('estree').
|
|
66
|
+
* Checks if an expression is comparing process.env.NODE_ENV appropriately
|
|
67
|
+
* @param {import('estree').Expression} expression - The expression to check
|
|
63
68
|
* @param {string} operator - The expected comparison operator (===, !==, etc.)
|
|
64
69
|
* @param {string} value - The value to compare with
|
|
65
70
|
* @returns {boolean}
|
|
66
71
|
*/
|
|
67
|
-
function isNodeEnvComparison(
|
|
68
|
-
|
|
72
|
+
function isNodeEnvComparison(expression, operator, value) {
|
|
73
|
+
if (expression.type !== 'BinaryExpression') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const { left, right } = expression;
|
|
69
77
|
|
|
70
78
|
// Check for exact match with the specified value
|
|
71
79
|
if (
|
|
72
|
-
|
|
80
|
+
expression.operator === operator &&
|
|
73
81
|
((isProcessEnvNodeEnv(left) && isLiteralEq(right, value)) ||
|
|
74
82
|
(isProcessEnvNodeEnv(right) && isLiteralEq(left, value)))
|
|
75
83
|
) {
|
|
@@ -79,7 +87,7 @@ const rule = {
|
|
|
79
87
|
// For !== operator also allow === with any literal value that's NOT 'production'
|
|
80
88
|
if (
|
|
81
89
|
operator === '!==' &&
|
|
82
|
-
|
|
90
|
+
expression.operator === '===' &&
|
|
83
91
|
((isProcessEnvNodeEnv(left) && isLiteralNeq(right, value)) ||
|
|
84
92
|
(isProcessEnvNodeEnv(right) && isLiteralNeq(left, value)))
|
|
85
93
|
) {
|
|
@@ -95,41 +103,18 @@ const rule = {
|
|
|
95
103
|
* @returns {boolean}
|
|
96
104
|
*/
|
|
97
105
|
function isWrappedInProductionCheck(node) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let currentChild = node;
|
|
102
|
-
|
|
103
|
-
while (current) {
|
|
104
|
-
// Check if we're inside an if statement
|
|
105
|
-
if (current.type === 'IfStatement') {
|
|
106
|
-
// Determine which branch we're in
|
|
107
|
-
const isInConsequent = current.consequent === currentChild;
|
|
108
|
-
const isInAlternate = current.alternate === currentChild;
|
|
109
|
-
|
|
110
|
-
// Skip if not in a branch
|
|
111
|
-
if (isInConsequent || isInAlternate) {
|
|
112
|
-
const test = current.test;
|
|
106
|
+
return isInsideNodeEnvCheck(node, (ifStatement, child) => {
|
|
107
|
+
const isInConsequent = ifStatement.consequent === child;
|
|
108
|
+
const isInAlternate = ifStatement.alternate === child;
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
test.type === 'BinaryExpression' &&
|
|
121
|
-
isNodeEnvComparison(test, operator, 'production')
|
|
122
|
-
) {
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
110
|
+
if (isInConsequent || isInAlternate) {
|
|
111
|
+
// If we're in the consequent, we need !==
|
|
112
|
+
// If we're in the alternate (else), we need ===
|
|
113
|
+
const operator = isInConsequent ? '!==' : '===';
|
|
114
|
+
return isNodeEnvComparison(ifStatement.test, operator, 'production');
|
|
126
115
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
current = current.parent;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return false;
|
|
116
|
+
return false;
|
|
117
|
+
});
|
|
133
118
|
}
|
|
134
119
|
|
|
135
120
|
return {
|