@mui/internal-code-infra 0.0.4-canary.5 → 0.0.4-canary.51
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 +11 -3
- 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 +2 -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 +4 -2
- 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-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/markdownlint/duplicate-h1.d.mts +1 -1
- package/build/markdownlint/git-diff.d.mts +1 -1
- package/build/markdownlint/index.d.mts +1 -1
- package/build/markdownlint/straight-quotes.d.mts +1 -1
- package/build/markdownlint/table-alignment.d.mts +1 -1
- package/build/markdownlint/terminal-language.d.mts +1 -1
- 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 +4 -4
- package/build/utils/github.d.mts +1 -1
- package/build/utils/pnpm.d.mts +68 -2
- package/build/utils/testUtils.d.mts +7 -0
- package/build/utils/typescript.d.mts +2 -2
- package/package.json +62 -35
- package/src/babel-config.mjs +9 -3
- package/src/brokenLinksChecker/__fixtures__/static-site/index.html +1 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +15 -0
- package/src/brokenLinksChecker/crawlWorker.mjs +217 -0
- package/src/brokenLinksChecker/index.mjs +217 -164
- package/src/brokenLinksChecker/index.test.ts +50 -13
- package/src/changelog/categorizeCommits.test.ts +5 -5
- package/src/changelog/fetchChangelogs.mjs +6 -2
- package/src/changelog/parseCommitLabels.test.ts +5 -5
- package/src/changelog/renderChangelog.mjs +1 -1
- package/src/changelog/types.ts +1 -1
- package/src/cli/cmdListWorkspaces.mjs +9 -2
- package/src/cli/cmdNetlifyIgnore.mjs +35 -93
- package/src/cli/cmdPublish.mjs +51 -14
- package/src/cli/cmdPublishCanary.mjs +128 -132
- package/src/cli/cmdPublishNewPackage.mjs +27 -6
- package/src/cli/cmdVale.mjs +513 -0
- package/src/cli/cmdVale.test.mjs +644 -0
- package/src/cli/index.mjs +2 -0
- package/src/cli/packageJson.d.ts +1 -1
- package/src/eslint/baseConfig.mjs +45 -20
- package/src/eslint/docsConfig.mjs +2 -1
- package/src/eslint/jsonConfig.mjs +2 -1
- package/src/eslint/mui/config.mjs +21 -1
- package/src/eslint/mui/index.mjs +4 -0
- package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
- package/src/eslint/mui/rules/no-guarded-throw.test.mjs +206 -0
- package/src/eslint/mui/rules/no-presentation-role.mjs +60 -0
- package/src/eslint/mui/rules/no-presentation-role.test.mjs +33 -0
- package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
- package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
- package/src/eslint/testConfig.mjs +2 -1
- 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/firstBlockHeading.test.mjs +107 -0
- package/src/remark/gitDiff.mjs +43 -0
- package/src/remark/gitDiff.test.mjs +45 -0
- package/src/remark/noSpaceInLinks.mjs +42 -0
- package/src/remark/noSpaceInLinks.test.mjs +22 -0
- package/src/remark/straightQuotes.mjs +31 -0
- package/src/remark/straightQuotes.test.mjs +25 -0
- package/src/remark/tableAlignment.mjs +23 -0
- package/src/remark/tableAlignment.test.mjs +28 -0
- package/src/remark/terminalLanguage.mjs +19 -0
- package/src/remark/terminalLanguage.test.mjs +17 -0
- package/src/untyped-plugins.d.ts +11 -11
- package/src/utils/build.mjs +18 -1
- package/src/utils/build.test.mjs +585 -575
- package/src/utils/pnpm.mjs +192 -3
- package/src/utils/pnpm.test.mjs +580 -0
- package/src/utils/testUtils.mjs +18 -0
- package/src/utils/typescript.test.mjs +249 -272
- package/vale/.vale.ini +1 -0
- package/vale/styles/MUI/CorrectReferenceAllCases.yml +43 -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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { includeIgnoreFile, fixupConfigRules } from '@eslint/compat';
|
|
2
2
|
import eslintJs from '@eslint/js';
|
|
3
|
-
|
|
3
|
+
// TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
|
|
4
|
+
import { defineConfig } from '@eslint/config-helpers';
|
|
4
5
|
import prettier from 'eslint-config-prettier/flat';
|
|
5
6
|
import compatPlugin from 'eslint-plugin-compat';
|
|
6
7
|
import importPlugin from 'eslint-plugin-import';
|
|
7
8
|
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
|
|
8
9
|
import reactPlugin from 'eslint-plugin-react';
|
|
10
|
+
import * as mdx from 'eslint-plugin-mdx';
|
|
9
11
|
import { configs as reactCompilerPluginConfigs } from 'eslint-plugin-react-compiler';
|
|
10
12
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
11
13
|
import globals from 'globals';
|
|
@@ -33,6 +35,7 @@ function includeIgnoreIfExists(filePath, description) {
|
|
|
33
35
|
* @param {boolean} [params.enableReactCompiler] - Whether to enable React Compiler.
|
|
34
36
|
* @param {boolean} [params.consistentTypeImports] - Whether to enforce consistent type imports.
|
|
35
37
|
* @param {boolean} [params.materialUi] - Whether to enable Material UI specific rules (mui/material-ui-*).
|
|
38
|
+
* @param {boolean} [params.markdown] - Whether to enable markdown/MDX linting via `eslint-plugin-mdx`. Opt-in so dependents can adopt on their own schedule.
|
|
36
39
|
* @param {string} [params.baseDirectory] - The base directory for the configuration.
|
|
37
40
|
* @returns {import('eslint').Linter.Config[]}
|
|
38
41
|
*/
|
|
@@ -40,13 +43,53 @@ export function createBaseConfig({
|
|
|
40
43
|
enableReactCompiler = false,
|
|
41
44
|
consistentTypeImports = false,
|
|
42
45
|
materialUi = false,
|
|
46
|
+
markdown = false,
|
|
43
47
|
baseDirectory = process.cwd(),
|
|
44
48
|
} = {}) {
|
|
45
49
|
return defineConfig([
|
|
50
|
+
{
|
|
51
|
+
name: 'settings',
|
|
52
|
+
languageOptions: {
|
|
53
|
+
ecmaVersion: 7,
|
|
54
|
+
globals: {
|
|
55
|
+
...globals.es2020,
|
|
56
|
+
...globals.browser,
|
|
57
|
+
...globals.node,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
plugins: {
|
|
61
|
+
mui: muiPlugin,
|
|
62
|
+
},
|
|
63
|
+
settings: {
|
|
64
|
+
react: {
|
|
65
|
+
version: 'detect',
|
|
66
|
+
},
|
|
67
|
+
browserslistOpts: {
|
|
68
|
+
config: path.join(baseDirectory, '.browserslistrc'),
|
|
69
|
+
env: 'stable',
|
|
70
|
+
ignoreUnknownVersions: true,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
46
74
|
includeIgnoreIfExists(path.join(baseDirectory, '.gitignore'), `Ignore rules from .gitignore`),
|
|
47
75
|
includeIgnoreIfExists(path.join(baseDirectory, '.lintignore'), `Ignore rules from .lintignore`),
|
|
48
76
|
createJsonConfig(),
|
|
49
77
|
prettier,
|
|
78
|
+
// Markdown + MDX linting via eslint-plugin-mdx. Severities for markdown
|
|
79
|
+
// quality checks live in the project's `.remarkrc` (see
|
|
80
|
+
// `@mui/internal-code-infra/remark`), not here.
|
|
81
|
+
markdown
|
|
82
|
+
? [
|
|
83
|
+
{
|
|
84
|
+
...mdx.flat,
|
|
85
|
+
rules: {
|
|
86
|
+
...mdx.flat.rules,
|
|
87
|
+
'mdx/remark': 'error',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
mdx.flatCodeBlocks,
|
|
91
|
+
]
|
|
92
|
+
: [],
|
|
50
93
|
{
|
|
51
94
|
name: 'Base config',
|
|
52
95
|
files: [`**/*${EXTENSION_TS}`],
|
|
@@ -62,25 +105,7 @@ export function createBaseConfig({
|
|
|
62
105
|
enableReactCompiler ? reactCompilerPluginConfigs.recommended : {},
|
|
63
106
|
compatPlugin.configs['flat/recommended'],
|
|
64
107
|
{
|
|
65
|
-
name: '
|
|
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
|
-
},
|
|
108
|
+
name: 'core',
|
|
84
109
|
extends: createCoreConfig({ enableReactCompiler, consistentTypeImports, materialUi }),
|
|
85
110
|
},
|
|
86
111
|
// Lint rule to disallow usage of typescript namespaces.We've seen at least two problems with them:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import nextjs from '@next/eslint-plugin-next';
|
|
2
|
-
|
|
2
|
+
// TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
|
|
3
|
+
import { defineConfig } from '@eslint/config-helpers';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @returns {import('eslint').Linter.Config[]}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
|
|
2
|
+
import { defineConfig } from '@eslint/config-helpers';
|
|
3
|
+
import { EXTENSION_DTS } from '../extensions.mjs';
|
|
2
4
|
|
|
3
5
|
const restrictedMethods = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
|
|
4
6
|
|
|
@@ -414,10 +416,12 @@ export function createCoreConfig(options = {}) {
|
|
|
414
416
|
'mui/material-ui-no-styled-box': 'error',
|
|
415
417
|
}
|
|
416
418
|
: {}),
|
|
419
|
+
'mui/no-guarded-throw': 'error',
|
|
417
420
|
'mui/straight-quotes': 'off',
|
|
418
421
|
'mui/consistent-production-guard': 'error',
|
|
419
422
|
'mui/add-undef-to-optional': 'off',
|
|
420
423
|
'mui/flatten-parentheses': 'warn',
|
|
424
|
+
'mui/no-presentation-role': 'off',
|
|
421
425
|
|
|
422
426
|
'react-hooks/exhaustive-deps': [
|
|
423
427
|
'error',
|
|
@@ -499,6 +503,15 @@ export function createCoreConfig(options = {}) {
|
|
|
499
503
|
message: 'Do not call `Error(...)` without `new`. Use `new Error(...)` instead.',
|
|
500
504
|
selector: "CallExpression[callee.name='Error']",
|
|
501
505
|
},
|
|
506
|
+
{
|
|
507
|
+
// xmlns="http://www.w3.org/2000/svg" is only needed on standalone .svg files so the
|
|
508
|
+
// browser treats them as SVG instead of generic XML. Inside HTML the <svg> element is
|
|
509
|
+
// already recognised by the browser, so the attribute is dead weight.
|
|
510
|
+
// https://github.com/mui/mui-public/pull/1321
|
|
511
|
+
message:
|
|
512
|
+
'Remove xmlns from inline <svg>. The attribute is redundant in HTML and adds unnecessary bytes.',
|
|
513
|
+
selector: 'JSXOpeningElement[name.name="svg"] > JSXAttribute[name.name="xmlns"]',
|
|
514
|
+
},
|
|
502
515
|
...restrictedSyntaxRules,
|
|
503
516
|
],
|
|
504
517
|
|
|
@@ -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,7 @@ 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';
|
|
14
16
|
|
|
15
17
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
16
18
|
const muiPlugin = {
|
|
@@ -22,6 +24,7 @@ const muiPlugin = {
|
|
|
22
24
|
'consistent-production-guard': consistentProductionGuard,
|
|
23
25
|
'disallow-active-element-as-key-event-target': disallowActiveElementAsKeyEventTarget,
|
|
24
26
|
'docgen-ignore-before-comment': docgenIgnoreBeforeComment,
|
|
27
|
+
'no-guarded-throw': noGuardedThrow,
|
|
25
28
|
'material-ui-name-matches-component-name': muiNameMatchesComponentName,
|
|
26
29
|
'material-ui-rules-of-use-theme-variants': rulesOfUseThemeVariants,
|
|
27
30
|
'material-ui-no-empty-box': noEmptyBox,
|
|
@@ -33,6 +36,7 @@ const muiPlugin = {
|
|
|
33
36
|
// Some discrepancies between TypeScript and ESLint types - casting to any
|
|
34
37
|
'add-undef-to-optional': /** @type {any} */ (addUndefToOptional),
|
|
35
38
|
'flatten-parentheses': /** @type {any} */ (flattenParentheses),
|
|
39
|
+
'no-presentation-role': noPresentationRole,
|
|
36
40
|
},
|
|
37
41
|
};
|
|
38
42
|
|
|
@@ -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,206 @@
|
|
|
1
|
+
import eslint from 'eslint';
|
|
2
|
+
import parser from '@typescript-eslint/parser';
|
|
3
|
+
import rule from './no-guarded-throw.mjs';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new eslint.RuleTester({
|
|
6
|
+
languageOptions: {
|
|
7
|
+
parser,
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
ruleTester.run('no-guarded-throw', rule, {
|
|
12
|
+
valid: [
|
|
13
|
+
// Should pass: Unconditional throw
|
|
14
|
+
{
|
|
15
|
+
code: `
|
|
16
|
+
throw new Error('Something went wrong');
|
|
17
|
+
`,
|
|
18
|
+
},
|
|
19
|
+
// Should pass: Throw inside a non-NODE_ENV conditional
|
|
20
|
+
{
|
|
21
|
+
code: `
|
|
22
|
+
if (value == null) {
|
|
23
|
+
throw new TypeError('value is required');
|
|
24
|
+
}
|
|
25
|
+
`,
|
|
26
|
+
},
|
|
27
|
+
// Should pass: Throw inside a catch block (no NODE_ENV guard)
|
|
28
|
+
{
|
|
29
|
+
code: `
|
|
30
|
+
try {
|
|
31
|
+
doSomething();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error('Failed');
|
|
34
|
+
}
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
// Should pass: Throw inside arrow function inside NODE_ENV guard
|
|
38
|
+
{
|
|
39
|
+
code: `
|
|
40
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
41
|
+
const fn = () => {
|
|
42
|
+
throw new Error('inside arrow function');
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
// Should pass: Throw inside function expression inside NODE_ENV guard
|
|
48
|
+
{
|
|
49
|
+
code: `
|
|
50
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
51
|
+
const fn = function () {
|
|
52
|
+
throw new Error('inside function expression');
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
`,
|
|
56
|
+
},
|
|
57
|
+
// Should pass: Throw inside function declaration inside NODE_ENV guard
|
|
58
|
+
{
|
|
59
|
+
code: `
|
|
60
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
61
|
+
function validate() {
|
|
62
|
+
throw new Error('inside function declaration');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
invalid: [
|
|
69
|
+
// Should fail: Throw inside !== 'production' guard
|
|
70
|
+
{
|
|
71
|
+
code: `
|
|
72
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
73
|
+
throw new Error('Dev-only error');
|
|
74
|
+
}
|
|
75
|
+
`,
|
|
76
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
77
|
+
},
|
|
78
|
+
// Should fail: Throw inside === 'production' guard
|
|
79
|
+
{
|
|
80
|
+
code: `
|
|
81
|
+
if (process.env.NODE_ENV === 'production') {
|
|
82
|
+
throw new Error('Prod-only error');
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
86
|
+
},
|
|
87
|
+
// Should fail: Throw in else block of === 'production' check
|
|
88
|
+
{
|
|
89
|
+
code: `
|
|
90
|
+
if (process.env.NODE_ENV === 'production') {
|
|
91
|
+
// production path
|
|
92
|
+
} else {
|
|
93
|
+
throw new Error('Non-production error');
|
|
94
|
+
}
|
|
95
|
+
`,
|
|
96
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
97
|
+
},
|
|
98
|
+
// Should fail: Throw nested inside NODE_ENV guard
|
|
99
|
+
{
|
|
100
|
+
code: `
|
|
101
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
102
|
+
if (value == null) {
|
|
103
|
+
throw new TypeError('value is required');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`,
|
|
107
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
108
|
+
},
|
|
109
|
+
// Should fail: Reversed comparison (literal on left)
|
|
110
|
+
{
|
|
111
|
+
code: `
|
|
112
|
+
if ('production' !== process.env.NODE_ENV) {
|
|
113
|
+
throw new Error('Dev-only error');
|
|
114
|
+
}
|
|
115
|
+
`,
|
|
116
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
117
|
+
},
|
|
118
|
+
// Should fail: Throw in loop inside NODE_ENV guard
|
|
119
|
+
{
|
|
120
|
+
code: `
|
|
121
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
122
|
+
for (const item of items) {
|
|
123
|
+
throw new Error('Invalid item');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`,
|
|
127
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
128
|
+
},
|
|
129
|
+
// Should fail: NODE_ENV combined with other conditions using &&
|
|
130
|
+
{
|
|
131
|
+
code: `
|
|
132
|
+
if (process.env.NODE_ENV !== 'production' && value == null) {
|
|
133
|
+
throw new TypeError('value is required');
|
|
134
|
+
}
|
|
135
|
+
`,
|
|
136
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
137
|
+
},
|
|
138
|
+
// Should fail: NODE_ENV combined with other conditions using ||
|
|
139
|
+
{
|
|
140
|
+
code: `
|
|
141
|
+
if (condition || process.env.NODE_ENV === 'test') {
|
|
142
|
+
throw new Error('Test or condition error');
|
|
143
|
+
}
|
|
144
|
+
`,
|
|
145
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
146
|
+
},
|
|
147
|
+
// Should fail: Unary not on process.env.NODE_ENV
|
|
148
|
+
{
|
|
149
|
+
code: `
|
|
150
|
+
if (!process.env.NODE_ENV) {
|
|
151
|
+
throw new Error('NODE_ENV not set');
|
|
152
|
+
}
|
|
153
|
+
`,
|
|
154
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
155
|
+
},
|
|
156
|
+
// Should fail: NODE_ENV passed to a function
|
|
157
|
+
{
|
|
158
|
+
code: `
|
|
159
|
+
if (fn(process.env.NODE_ENV)) {
|
|
160
|
+
throw new Error('Function check failed');
|
|
161
|
+
}
|
|
162
|
+
`,
|
|
163
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
164
|
+
},
|
|
165
|
+
// Should fail: Throw inside try/catch inside NODE_ENV guard
|
|
166
|
+
{
|
|
167
|
+
code: `
|
|
168
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
169
|
+
try {
|
|
170
|
+
doSomething();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new Error('caught inside guard');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
`,
|
|
176
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
177
|
+
},
|
|
178
|
+
// Should fail: Throw deeply nested in control flow inside NODE_ENV guard
|
|
179
|
+
{
|
|
180
|
+
code: `
|
|
181
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
182
|
+
if (value == null) {
|
|
183
|
+
for (const item of items) {
|
|
184
|
+
if (!item.valid) {
|
|
185
|
+
throw new Error('invalid item');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
`,
|
|
191
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
192
|
+
},
|
|
193
|
+
// Should fail: Throw inside switch/case inside NODE_ENV guard
|
|
194
|
+
{
|
|
195
|
+
code: `
|
|
196
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
197
|
+
switch (type) {
|
|
198
|
+
case 'a':
|
|
199
|
+
throw new Error('invalid type a');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`,
|
|
203
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
@@ -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;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import eslint from 'eslint';
|
|
2
|
+
import parser from '@typescript-eslint/parser';
|
|
3
|
+
import rule from './no-presentation-role.mjs';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new eslint.RuleTester({
|
|
6
|
+
languageOptions: {
|
|
7
|
+
parser,
|
|
8
|
+
parserOptions: {
|
|
9
|
+
ecmaFeatures: { jsx: true },
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
ruleTester.run('no-presentation-role', rule, {
|
|
15
|
+
valid: ['<div role="none" />', '<div role="button" />', '<div />', '<div role={presentation} />'],
|
|
16
|
+
invalid: [
|
|
17
|
+
{
|
|
18
|
+
code: '<div role="presentation" />',
|
|
19
|
+
errors: [{ messageId: 'noPresentation' }],
|
|
20
|
+
output: '<div role="none" />',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
code: "<div role={'presentation'} />",
|
|
24
|
+
errors: [{ messageId: 'noPresentation' }],
|
|
25
|
+
output: '<div role="none" />',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: '<table role="presentation"><tr><td /></tr></table>',
|
|
29
|
+
errors: [{ messageId: 'noPresentation' }],
|
|
30
|
+
output: '<table role="none"><tr><td /></tr></table>',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
@@ -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
|
+
}
|