diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2a3004d9ec..b0f714ffe1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -63,7 +63,7 @@ Before submitting your contribution though, please make sure to take a moment an - `packages/plugins/eslint-plugin-react-naming-convention`: ESLint plugin for React naming conventions. - `packages/plugins/eslint-plugin-react-debug`: ESLint plugin for debugging ESLint React rules. - `packages/plugins/eslint-plugin`: The main ESLint plugin of ESLint React. Contains all the rules from the above plugins. -- `packages/utilities/eff`: A subset of effect to produce a more lightweight version of the library. +- `packages/utilities/eff`: JavaScript and TypeScript utilities (previously some re-exports of the `effect` library). - `packages/utilities/ast`: TSESTree AST utility module. - `packages/utilities/var`: TSESTree AST utility module for static analysis of variables - `packages/utilities/jsx`: TSESTree AST utility module for static analysis of JSX. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d1b97c7475..057b341d0c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ # These are supported funding model platforms -ko_fi: rEl1cx +ko_fi: Rel1cx # open_collective: eslint-react diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 81c36e9abe..6117141dbb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,7 @@ body: attributes: value: | ## First of all - 1. Please search for [existing issues](https://github.com/rEl1cx/eslint-react/issues?q=is%3Aissue) about this problem first. + 1. Please search for [existing issues](https://github.com/Rel1cx/eslint-react/issues?q=is%3Aissue) about this problem first. 2. Make sure `eslint`, `typescript` and all relevant @eslint-react packages are up to date. 3. Make sure it's an issue with @eslint-react and not something else you are using. 4. Remember to follow our community guidelines and be friendly. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ac5f4b1b7d..e2044cea5d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ ### What kind of change does this PR introduce? diff --git a/CHANGELOG.md b/CHANGELOG.md index c13ec4ebf4..f7db8cbdbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,26 +6,26 @@ No notable changes in this release. ### 🐞 Fixes -- fix(plugins/x): fixed false positives in `no-unstable-context-value` and `no-unstable-default-props` by @rEl1cx in +- fix(plugins/x): fixed false positives in `no-unstable-context-value` and `no-unstable-default-props` by @Rel1cx in ## v1.23.0 (2024-12-31) ### πŸͺ„ Improvements -- refactor: JSX fragments related rules no longer rely on `jsxPragma` and `jsxPragmaFrag` settings to perform their checks by @rEl1cx in +- refactor: JSX fragments related rules no longer rely on `jsxPragma` and `jsxPragmaFrag` settings to perform their checks by @Rel1cx in - refactor: improve applicability of the `no-useless-fragment` and `prefer-shorthand-fragment` rules - refactor: deprecate `settings["react-x"].jsxPragma` and `settings["react-x"].jsxPragmaFrag` as they are no longer needed by any rules -- refactor: replace `short-unique-id` w/ `uid` by @SukkaW in +- refactor: replace `short-unique-id` w/ `uid` by @SukkaW in ### 🐞 Fixes -- fix(plugins/hooks-extra): fix `call` and `new` expression related false positives in `no-unnecessary-use-memo` and `no-unnecessary-use-callback` by @rEl1cx in +- fix(plugins/hooks-extra): fix `call` and `new` expression related false positives in `no-unnecessary-use-memo` and `no-unnecessary-use-callback` by @Rel1cx in ## v1.22.2 (2024-12-30) ### πŸͺ„ Improvements -- perf: re-implement `no-duplicate-key` rule to improve its performance @rEl1cx in and [33ab3cc](https://github.com/rEl1cx/eslint-react/commit/33ab3cc6ca11bf8412e07efa35f640dfbad77f6e) +- perf: re-implement `no-duplicate-key` rule to improve its performance @Rel1cx in and [33ab3cc](https://github.com/Rel1cx/eslint-react/commit/33ab3cc6ca11bf8412e07efa35f640dfbad77f6e) - refactor: prevent potential interference from TypeScript's `as`, `satisfies`, and non-null assertion operator in various rules ## v1.22.1 (2024-12-24) @@ -66,7 +66,7 @@ The new rule names are aligned with the same rules in the [biomejs/rules-sources ### ✨ New -- feat(plugins/hooks-extra): add `no-useless-custom-hooks` rule by @rEl1cx +- feat(plugins/hooks-extra): add `no-useless-custom-hooks` rule by @Rel1cx ### πŸͺ„ Improvements @@ -89,56 +89,56 @@ function useAuth() { ### πŸͺ„ Improvements -- refactor(shared): replace `local-pkg` package with node built-in API by @rEl1cx in +- refactor(shared): replace `local-pkg` package with node built-in API by @Rel1cx in ## v1.20.0 (2024-12-16) ### ✨ New -- feat(plugins/x): add codemod-autofix to `no-component-will-*` by @rEl1cx in +- feat(plugins/x): add codemod-autofix to `no-component-will-*` by @Rel1cx in ### πŸͺ„ Improvements -- refactor: use default settings when no settings are provided in `settings["react-x"]` by @rEl1cx in -- docs: update `no-context-provider.mdx` by @danielrentz in -- docs: add 'Min. React' column to rules overview page by @rEl1cx in -- docs: add features section to rules overview page by @rEl1cx +- refactor: use default settings when no settings are provided in `settings["react-x"]` by @Rel1cx in +- docs: update `no-context-provider.mdx` by @danielrentz in +- docs: add 'Min. React' column to rules overview page by @Rel1cx in +- docs: add features section to rules overview page by @Rel1cx ## New Contributors -- @danielrentz made their first contribution in +- @danielrentz made their first contribution in ## v1.19.0 (2024-12-10) ### ✨ New -- feat(plugins/x): add `no-context-provider` rule by @rEl1cx -- feat(plugins/x): add autofix for `no-forward-ref` rule by @rEl1cx in -- feat(plugins/eslint-plugin): add `no-forward-ref` and `no-context-provider` to recommended presets by @rEl1cx +- feat(plugins/x): add `no-context-provider` rule by @Rel1cx +- feat(plugins/x): add autofix for `no-forward-ref` rule by @Rel1cx in +- feat(plugins/eslint-plugin): add `no-forward-ref` and `no-context-provider` to recommended presets by @Rel1cx ### πŸͺ„ Improvements -- refactor(plugins/eslint-plugin): remove `prefer-read-only-props` from `recommended-type-checked` preset by @rEl1cx in -- refactor(plugins/eslint-plugin): hide `avoid-shorthand-boolean` and `avoid-shorthand-fragment` from presets and docs by @rEl1cx in +- refactor(plugins/eslint-plugin): remove `prefer-read-only-props` from `recommended-type-checked` preset by @Rel1cx in +- refactor(plugins/eslint-plugin): hide `avoid-shorthand-boolean` and `avoid-shorthand-fragment` from presets and docs by @Rel1cx in - Update `@typescript-eslint`'s packages to `^8.18.0` ## v1.18.0 (2024-12-08) ### ✨ New -- feat(plugins/x): add `no-forward-ref` rule by @rEl1cx in +- feat(plugins/x): add `no-forward-ref` rule by @Rel1cx in ### πŸͺ„ Improvements -- perf(plugins/dom): improve performance of `no-void-elements-with-children` by @rEl1cx +- perf(plugins/dom): improve performance of `no-void-elements-with-children` by @Rel1cx ## v1.17.3 (2024-12-03) ### 🐞 Fixes -- fix(plugins/web-api): add 'forEach' support to 'no-leaked-event-listener', closes #842 by @rEl1cx in -- fix(plugins/web-api): add 'for of' support to 'no-leaked-event-listenner', closes #842 by @rEl1cx in -- fix(plugins/x): 'no-array-index-key' mistaking 'foo.bar.map' for 'Rea… by @rEl1cx in +- fix(plugins/web-api): add 'forEach' support to 'no-leaked-event-listener', closes #842 by @Rel1cx in +- fix(plugins/web-api): add 'for of' support to 'no-leaked-event-listenner', closes #842 by @Rel1cx in +- fix(plugins/x): 'no-array-index-key' mistaking 'foo.bar.map' for 'Rea… by @Rel1cx in ### πŸͺ„ Improvements @@ -160,9 +160,9 @@ function useAuth() { ### 🐞 Fixes -- fix(plugins/x): 'no-leaked-conditional-rendering' should also warn 'anyStringVar' when react version is lower than 18, closes #853 by @rEl1cx in -- fix(plugins/dom): add popover api props to 'no-unknown-property', closes #855 by @rEl1cx in -- fix(plugins/debug): 'is-from-react' use correct settings when calling 'isInitializedFromReact', by @rEl1cx +- fix(plugins/x): 'no-leaked-conditional-rendering' should also warn 'anyStringVar' when react version is lower than 18, closes #853 by @Rel1cx in +- fix(plugins/dom): add popover api props to 'no-unknown-property', closes #855 by @Rel1cx in +- fix(plugins/debug): 'is-from-react' use correct settings when calling 'isInitializedFromReact', by @Rel1cx ## v1.17.0 (2024-11-21) @@ -180,29 +180,29 @@ function useAuth() { ### 🐞 Fixes -- fix(plugins/x): 'no-leaked-conditional-rendering' report empty string, closes #853 by @rEl1cx in +- fix(plugins/x): 'no-leaked-conditional-rendering' report empty string, closes #853 by @Rel1cx in ### πŸͺ„ Improvements -- refactor: update the default behavior of import check, closes #858 by @rEl1cx in +- refactor: update the default behavior of import check, closes #858 by @Rel1cx in ## v1.16.1 (2024-11-10) ### ✨ New -- feat(plugins/x): add `jsx-no-duplicate-props` by @rEl1cx in +- feat(plugins/x): add `jsx-no-duplicate-props` by @Rel1cx in ### πŸͺ„ Improvements -- docs: use correct link for `prefer-react-namespace-import` in rule list by @rakleed in +- docs: use correct link for `prefer-react-namespace-import` in rule list by @rakleed in ## v1.16.0 (2024-11-01) ### ✨ New -- feat(plugins/react-x): add `jsx-uses-vars`, closes #834 by @rEl1cx in -- feat(plugins/react-dom): add `no-unknown-property`, closes #846 by @rEl1cx -- feat: add `recommended-typescript` and `recommended-typescript-legacy` presets by @rEl1cx +- feat(plugins/react-x): add `jsx-uses-vars`, closes #834 by @Rel1cx in +- feat(plugins/react-dom): add `no-unknown-property`, closes #846 by @Rel1cx +- feat: add `recommended-typescript` and `recommended-typescript-legacy` presets by @Rel1cx ### πŸͺ„ Improvements @@ -218,20 +218,20 @@ function useAuth() { ### ✨ New -- feat: added code fixer to `react-x/avoid-shorthand-boolean` and `react-x/prefer-shorthand-fragment` by @rEl1cx +- feat: added code fixer to `react-x/avoid-shorthand-boolean` and `react-x/prefer-shorthand-fragment` by @Rel1cx ### 🐞 Fixes -- fix(plugins/react-x): respect semicolon by @hyoban in -- fix(utilities/ast): added missing ts `as` and `satisfies` expressions handling to `getFunctionIdentifier` by @rEl1cx , closes +- fix(plugins/react-x): respect semicolon by @hyoban in +- fix(utilities/ast): added missing ts `as` and `satisfies` expressions handling to `getFunctionIdentifier` by @Rel1cx , closes ## v1.15.0 (2024-10-12) ### ✨ New -- feat: add support for constructors in `hooks-extra/prefer-use-state-lazy-initialization` by @imjordanxd in -- feat: add `prefer-react-namespace-import`, closes #803 by @imjordanxd in -- feat: add support for `allowExpressions` in `no-useless-fragment` by @imjordanxd in +- feat: add support for constructors in `hooks-extra/prefer-use-state-lazy-initialization` by @imjordanxd in +- feat: add `prefer-react-namespace-import`, closes #803 by @imjordanxd in +- feat: add support for `allowExpressions` in `no-useless-fragment` by @imjordanxd in ### 🐞 Fixes @@ -239,16 +239,16 @@ function useAuth() { ### πŸͺ„ Improvements -- docs: Update `hooks-extra-no-direct-set-state-in-use-effect.mdx` by @neovov in -- docs: use a standard mono-width font for the docs, closes #835 by @rEl1cx in -- Undeprecate `hooks-extra-no-direct-set-state-in-use-layout-effect` and remove it from recommended presets, closes #839 by @rEl1cx in +- docs: Update `hooks-extra-no-direct-set-state-in-use-effect.mdx` by @neovov in +- docs: use a standard mono-width font for the docs, closes #835 by @Rel1cx in +- Undeprecate `hooks-extra-no-direct-set-state-in-use-layout-effect` and remove it from recommended presets, closes #839 by @Rel1cx in ## New Contributors -- @imjordanxd made their first contribution in -- @neovov made their first contribution in +- @imjordanxd made their first contribution in +- @neovov made their first contribution in -**Full Changelog**: +**Full Changelog**: ## v1.14.3 (2024-09-29) diff --git a/README.md b/README.md index 863bcf7fc2..d09d612c56 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ A set of composable ESLint rules for libraries and frameworks that use React as ### Modular plugins -- [`eslint-plugin-react-x`](https://github.com/rEl1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) - Core rules (renderer-agnostic, compatible with x-platform). -- [`eslint-plugin-react-dom`](https://github.com/rEl1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) - DOM specific rules for React DOM. -- [`eslint-plugin-react-web-api`](https://github.com/rEl1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-web-api) - Rules for interacting with Web APIs. -- [`eslint-plugin-react-hooks-extra`](https://github.com/rEl1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra) - Extra React Hooks rules. -- [`eslint-plugin-react-naming-convention`](https://github.com/rEl1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-naming-convention) - Naming convention rules. +- [`eslint-plugin-react-x`](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) - Core rules (renderer-agnostic, compatible with x-platform). +- [`eslint-plugin-react-dom`](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) - DOM specific rules for React DOM. +- [`eslint-plugin-react-web-api`](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-web-api) - Rules for interacting with Web APIs. +- [`eslint-plugin-react-hooks-extra`](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra) - Extra React Hooks rules. +- [`eslint-plugin-react-naming-convention`](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-naming-convention) - Naming convention rules. ## Installation @@ -118,9 +118,9 @@ export default tseslint.config({ - [`eslint-config-sheriff`](https://github.com/AndreaPontrandolfo/sheriff) - A comprehensive and opinionated Typescript-first ESLint configuration. - [`eslint-config-sukka`](https://github.com/SukkaW/eslint-config-sukka) - Sukka's ESLint config preset. -_Data collected from GitHub dependents network, if there are any mismatch or outdated information, feel free to [open issue](https://github.com/rEl1cx/eslint-react/issues/new?assignees=&labels=type%3A+documentation&projects=&template=docs_report.md&title=%5Bdocs%5D+) or pull request._ +_Data collected from GitHub dependents network, if there are any mismatch or outdated information, feel free to [open issue](https://github.com/Rel1cx/eslint-react/issues/new?assignees=&labels=type%3A+documentation&projects=&template=docs_report.md&title=%5Bdocs%5D+) or pull request._ -Find more on [GitHub Dependents](https://github.com/rEl1cx/eslint-react/network/dependents). +Find more on [GitHub Dependents](https://github.com/Rel1cx/eslint-react/network/dependents). ## Contributing diff --git a/eslint.config.ts b/eslint.config.ts index 53169ec27e..a6b9f3ae34 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -82,8 +82,8 @@ const enableTypeCheckedRules = { allowAny: false, allowNullableBoolean: true, allowNullableEnum: false, - allowNullableNumber: true, - allowNullableObject: false, + allowNullableNumber: false, + allowNullableObject: true, allowNullableString: false, allowNumber: true, allowString: false, @@ -151,12 +151,11 @@ export default tseslint.config( { files: [...GLOB_JS, ...GLOB_TS], rules: { - curly: "warn", eqeqeq: ["error", "smart"], "no-console": "error", "no-else-return": "error", "no-fallthrough": ["error", { commentPattern: ".*intentional fallthrough.*" }], - "no-implicit-coercion": "error", + "no-implicit-coercion": ["error", { allow: ["!!"] }], "no-mixed-operators": "warn", "no-process-exit": "error", "no-undef": "off", @@ -187,14 +186,7 @@ export default tseslint.config( "@typescript-eslint/no-empty-object-type": "off", "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-unnecessary-parameter-property-assignment": "warn", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - argsIgnorePattern: "^_", - caughtErrors: "all", - varsIgnorePattern: "^_", - }, - ], + "@typescript-eslint/no-unused-vars": ["warn", { caughtErrors: "all" }], ...enableTypeCheckedRules, // Part: jsdoc rules "jsdoc/check-param-names": "warn", @@ -206,13 +198,12 @@ export default tseslint.config( "jsdoc/require-param-description": "warn", "jsdoc/require-returns": "off", "jsdoc/require-yields": "warn", - "jsdoc/tag-lines": "warn", + "jsdoc/tag-lines": "off", // Part: simple-import-sort rules "simple-import-sort/exports": "warn", "simple-import-sort/imports": "warn", // Part: stylistic rules "@stylistic/arrow-parens": ["warn", "always"], - "@stylistic/curly-newline": ["warn", "always"], "@stylistic/no-multi-spaces": ["warn"], "@stylistic/operator-linebreak": [ "warn", @@ -249,7 +240,7 @@ export default tseslint.config( }, ], "perfectionist/sort-switch-case": "off", - "perfectionist/sort-union-types": "warn", + "perfectionist/sort-union-types": "off", // Part: unicorn rules "unicorn/template-indent": [ "warn", diff --git a/package.json b/package.json index e9f5a584ec..198c004d9a 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,13 @@ "eslint-plugin-react-hooks-extra", "eslint-plugin-react-naming-convention" ], - "homepage": "https://github.com/rEl1cx/eslint-react", + "homepage": "https://github.com/Rel1cx/eslint-react", "bugs": { - "url": "https://github.com/rEl1cx/eslint-react/issues" + "url": "https://github.com/Rel1cx/eslint-react/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/rEl1cx/eslint-react.git" + "url": "git+https://github.com/Rel1cx/eslint-react.git" }, "license": "MIT", "author": "Eva1ent", @@ -74,7 +74,6 @@ "cspell": "^8.17.1", "dedent": "^1.5.3", "dprint": "^0.48.0", - "effect": "^3.12.1", "esbuild": "^0.24.2", "eslint": "^9.17.0", "eslint-config-flat-gitignore": "^1.0.0", diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md index b218a7608e..01014660e2 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -41,7 +41,7 @@ ## Functions - [getComponentNameFromIdentifier](functions/getComponentNameFromIdentifier.md) -- [getElementRepresentName](functions/getElementRepresentName.md) +- [getElementNameAndRepresentName](functions/getElementNameAndRepresentName.md) - [getFunctionComponentIdentifier](functions/getFunctionComponentIdentifier.md) - [getId](functions/getId.md) - [hasNoneOrValidComponentName](functions/hasNoneOrValidComponentName.md) diff --git a/packages/core/docs/functions/getElementNameAndRepresentName.md b/packages/core/docs/functions/getElementNameAndRepresentName.md new file mode 100644 index 0000000000..9d3a3d6ae2 --- /dev/null +++ b/packages/core/docs/functions/getElementNameAndRepresentName.md @@ -0,0 +1,31 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / getElementNameAndRepresentName + +# Function: getElementNameAndRepresentName() + +> **getElementNameAndRepresentName**(`node`, `context`, `polymorphicPropName`?, `additionalComponents`?): \[`string`, `string`\] + +## Parameters + +### node + +`JSXOpeningElement` + +### context + +`Readonly`\<`RuleContext`\<`string`, readonly `unknown`[]\>\> + +### polymorphicPropName? + +`string` + +### additionalComponents? + +`object`[] = `[]` + +## Returns + +\[`string`, `string`\] diff --git a/packages/core/docs/functions/getElementRepresentName.md b/packages/core/docs/functions/getElementRepresentName.md deleted file mode 100644 index 04f8db6fac..0000000000 --- a/packages/core/docs/functions/getElementRepresentName.md +++ /dev/null @@ -1,23 +0,0 @@ -[**@eslint-react/core**](../README.md) - -*** - -[@eslint-react/core](../README.md) / getElementRepresentName - -# Function: getElementRepresentName() - -> **getElementRepresentName**(`node`, `context`): `string` - -## Parameters - -### node - -`JSXOpeningElement` - -### context - -`Readonly`\<`RuleContext`\<`string`, readonly `unknown`[]\>\> - -## Returns - -`string` diff --git a/packages/core/docs/functions/getFunctionComponentIdentifier.md b/packages/core/docs/functions/getFunctionComponentIdentifier.md index 8370aa1690..ccbd61abcb 100644 --- a/packages/core/docs/functions/getFunctionComponentIdentifier.md +++ b/packages/core/docs/functions/getFunctionComponentIdentifier.md @@ -6,7 +6,7 @@ # Function: getFunctionComponentIdentifier() -> **getFunctionComponentIdentifier**(`node`, `context`): `O.Option`\<`TSESTree.Identifier` \| `TSESTree.Identifier`[]\> +> **getFunctionComponentIdentifier**(`node`, `context`): `TSESTree.Identifier` \| `TSESTree.Identifier`[] \| `_` ## Parameters @@ -20,4 +20,4 @@ ## Returns -`O.Option`\<`TSESTree.Identifier` \| `TSESTree.Identifier`[]\> +`TSESTree.Identifier` \| `TSESTree.Identifier`[] \| `_` diff --git a/packages/core/docs/functions/isCleanupFunction.md b/packages/core/docs/functions/isCleanupFunction.md index e94a26b400..1335d50f58 100644 --- a/packages/core/docs/functions/isCleanupFunction.md +++ b/packages/core/docs/functions/isCleanupFunction.md @@ -6,7 +6,7 @@ # Function: isCleanupFunction() -> **isCleanupFunction**(`node`): `boolean` +> **isCleanupFunction**(`node`): `undefined` \| `boolean` ## Parameters @@ -16,4 +16,4 @@ ## Returns -`boolean` +`undefined` \| `boolean` diff --git a/packages/core/docs/functions/isInsideReactHook.md b/packages/core/docs/functions/isInsideReactHook.md index fb32e8ebca..6e1abea02e 100644 --- a/packages/core/docs/functions/isInsideReactHook.md +++ b/packages/core/docs/functions/isInsideReactHook.md @@ -6,7 +6,7 @@ # Function: isInsideReactHook() -> **isInsideReactHook**(`node`): `boolean` +> **isInsideReactHook**(`node`): `undefined` \| `boolean` ## Parameters @@ -16,4 +16,4 @@ ## Returns -`boolean` +`undefined` \| `boolean` diff --git a/packages/core/docs/functions/isReactHook.md b/packages/core/docs/functions/isReactHook.md index b4c6e669ac..b49faf3ff6 100644 --- a/packages/core/docs/functions/isReactHook.md +++ b/packages/core/docs/functions/isReactHook.md @@ -6,14 +6,14 @@ # Function: isReactHook() -> **isReactHook**(`node`): `boolean` +> **isReactHook**(`node`): `undefined` \| `boolean` ## Parameters ### node -`TSESTreeFunction` +`undefined` | `TSESTreeFunction` ## Returns -`boolean` +`undefined` \| `boolean` diff --git a/packages/core/docs/functions/isReactHookCall.md b/packages/core/docs/functions/isReactHookCall.md index 50efd9da6b..dc5ab88fa8 100644 --- a/packages/core/docs/functions/isReactHookCall.md +++ b/packages/core/docs/functions/isReactHookCall.md @@ -14,10 +14,10 @@ Check if the given node is a React Hook call by its name. ### node -`Node` - The node to check. +`undefined` | `Node` + ## Returns `boolean` diff --git a/packages/core/docs/functions/isReactHookCallWithName.md b/packages/core/docs/functions/isReactHookCallWithName.md index 197130f9ee..fdd4b22166 100644 --- a/packages/core/docs/functions/isReactHookCallWithName.md +++ b/packages/core/docs/functions/isReactHookCallWithName.md @@ -12,7 +12,7 @@ ### node -`CallExpression` +`undefined` | `CallExpression` ### context diff --git a/packages/core/docs/functions/isReactHookCallWithNameLoose.md b/packages/core/docs/functions/isReactHookCallWithNameLoose.md index ef7e17a223..69450980cf 100644 --- a/packages/core/docs/functions/isReactHookCallWithNameLoose.md +++ b/packages/core/docs/functions/isReactHookCallWithNameLoose.md @@ -12,7 +12,7 @@ ### node -`CallExpression` +`undefined` | `CallExpression` ## Returns diff --git a/packages/core/docs/functions/isSetupFunction.md b/packages/core/docs/functions/isSetupFunction.md index cfb6f76aba..5238957750 100644 --- a/packages/core/docs/functions/isSetupFunction.md +++ b/packages/core/docs/functions/isSetupFunction.md @@ -6,14 +6,14 @@ # Function: isSetupFunction() -> **isSetupFunction**(`node`): `boolean` +> **isSetupFunction**(`node`): `undefined` \| `boolean` ## Parameters ### node -`Node` +`undefined` | `Node` ## Returns -`boolean` +`undefined` \| `boolean` diff --git a/packages/core/docs/functions/isUseEffectCallLoose.md b/packages/core/docs/functions/isUseEffectCallLoose.md index be8064b00d..c7c7bbd769 100644 --- a/packages/core/docs/functions/isUseEffectCallLoose.md +++ b/packages/core/docs/functions/isUseEffectCallLoose.md @@ -12,7 +12,7 @@ ### node -`Node` +`undefined` | `Node` ## Returns diff --git a/packages/core/docs/functions/useComponentCollector.md b/packages/core/docs/functions/useComponentCollector.md index f86edeccca..ff7e360198 100644 --- a/packages/core/docs/functions/useComponentCollector.md +++ b/packages/core/docs/functions/useComponentCollector.md @@ -26,19 +26,19 @@ > **ctx**: `object` -#### ctx.getCurrentFunction() +#### ctx.getCurrentEntry() -> **getCurrentFunction**: () => `Option`\<\{ `hookCalls`: `CallExpression`[]; `isComponent`: `boolean`; `key`: `string`; `node`: `TSESTreeFunction`; \}\> +> **getCurrentEntry**: () => `undefined` \| \{ `hookCalls`: `CallExpression`[]; `isComponent`: `boolean`; `key`: `string`; `node`: `TSESTreeFunction`; \} ##### Returns -`Option`\<\{ `hookCalls`: `CallExpression`[]; `isComponent`: `boolean`; `key`: `string`; `node`: `TSESTreeFunction`; \}\> +`undefined` \| \{ `hookCalls`: `CallExpression`[]; `isComponent`: `boolean`; `key`: `string`; `node`: `TSESTreeFunction`; \} #### ctx.getAllComponents() ##### Parameters -###### \_ +###### node `Program` @@ -46,7 +46,7 @@ `Map`\<`string`, [`ERFunctionComponent`](../interfaces/ERFunctionComponent.md)\> -#### ctx.getCurrentFunctionStack() +#### ctx.getCurrentEntries() ##### Returns diff --git a/packages/core/docs/functions/useComponentCollectorLegacy.md b/packages/core/docs/functions/useComponentCollectorLegacy.md index 5cecdcb9c6..090e7eb739 100644 --- a/packages/core/docs/functions/useComponentCollectorLegacy.md +++ b/packages/core/docs/functions/useComponentCollectorLegacy.md @@ -20,7 +20,7 @@ ##### Parameters -###### \_ +###### node `Program` diff --git a/packages/core/docs/functions/useHookCollector.md b/packages/core/docs/functions/useHookCollector.md index 055d56e614..f618b382d8 100644 --- a/packages/core/docs/functions/useHookCollector.md +++ b/packages/core/docs/functions/useHookCollector.md @@ -20,7 +20,7 @@ ##### Parameters -###### \_ +###### node `Program` diff --git a/packages/core/docs/interfaces/ERClassComponent.md b/packages/core/docs/interfaces/ERClassComponent.md index 7bcdc88ed1..fc67bcdac6 100644 --- a/packages/core/docs/interfaces/ERClassComponent.md +++ b/packages/core/docs/interfaces/ERClassComponent.md @@ -24,7 +24,7 @@ ### displayName -> **displayName**: `Option`\<`Expression`\> +> **displayName**: `undefined` \| `Expression` *** @@ -50,7 +50,7 @@ ### id -> **id**: `Option`\<`Identifier`\> +> **id**: `undefined` \| `Identifier` #### Overrides @@ -76,7 +76,7 @@ ### name -> **name**: `Option`\<`string`\> +> **name**: `undefined` \| `string` #### Inherited from diff --git a/packages/core/docs/interfaces/ERFunctionComponent.md b/packages/core/docs/interfaces/ERFunctionComponent.md index 803266dd5e..c7a1f75153 100644 --- a/packages/core/docs/interfaces/ERFunctionComponent.md +++ b/packages/core/docs/interfaces/ERFunctionComponent.md @@ -24,7 +24,7 @@ ### displayName -> **displayName**: `Option`\<`Expression`\> +> **displayName**: `undefined` \| `Expression` *** @@ -56,7 +56,7 @@ ### id -> **id**: `Option`\<`Identifier` \| `Identifier`[]\> +> **id**: `undefined` \| `Identifier` \| `Identifier`[] #### Overrides @@ -66,7 +66,7 @@ ### initPath -> **initPath**: `Option`\<`FunctionInitPath`\> +> **initPath**: `undefined` \| `FunctionInitPath` *** @@ -82,7 +82,7 @@ ### name -> **name**: `Option`\<`string`\> +> **name**: `undefined` \| `string` #### Inherited from diff --git a/packages/core/docs/interfaces/ERHook.md b/packages/core/docs/interfaces/ERHook.md index 769677d38a..c705505a9b 100644 --- a/packages/core/docs/interfaces/ERHook.md +++ b/packages/core/docs/interfaces/ERHook.md @@ -50,7 +50,7 @@ ### id -> **id**: `Some`\<`Identifier`\> +> **id**: `undefined` \| `Identifier` #### Overrides @@ -70,7 +70,7 @@ ### name -> **name**: `Some`\<`string`\> +> **name**: `string` #### Overrides diff --git a/packages/core/docs/interfaces/ERSemanticNode.md b/packages/core/docs/interfaces/ERSemanticNode.md index 18559a81f8..1e3237abd6 100644 --- a/packages/core/docs/interfaces/ERSemanticNode.md +++ b/packages/core/docs/interfaces/ERSemanticNode.md @@ -34,7 +34,7 @@ ### id -> **id**: `Option`\<`Identifier` \| `Identifier`[]\> +> **id**: `undefined` \| `Identifier` \| `Identifier`[] *** @@ -46,7 +46,7 @@ ### name -> **name**: `Option`\<`string`\> +> **name**: `undefined` \| `string` *** diff --git a/packages/core/package.json b/packages/core/package.json index 7e69a1ccff..4969139405 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,13 +2,13 @@ "name": "@eslint-react/core", "version": "1.23.3-next.8", "description": "ESLint React's ESLint utility module for static analysis of React core APIs and Patterns.", - "homepage": "https://github.com/rEl1cx/eslint-react", + "homepage": "https://github.com/Rel1cx/eslint-react", "bugs": { - "url": "https://github.com/rEl1cx/eslint-react/issues" + "url": "https://github.com/Rel1cx/eslint-react/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/rEl1cx/eslint-react.git", + "url": "git+https://github.com/Rel1cx/eslint-react.git", "directory": "packages/core" }, "license": "MIT", diff --git a/packages/core/src/component/component-collector-legacy.ts b/packages/core/src/component/component-collector-legacy.ts index 657cf26d3c..556f7d4e5c 100644 --- a/packages/core/src/component/component-collector-legacy.ts +++ b/packages/core/src/component/component-collector-legacy.ts @@ -1,5 +1,5 @@ import * as AST from "@eslint-react/ast"; -import { O } from "@eslint-react/eff"; +import { _ } from "@eslint-react/eff"; import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; import { getId } from "../utils"; @@ -11,7 +11,8 @@ export function useComponentCollectorLegacy() { const components = new Map(); const ctx = { - getAllComponents(_: TSESTree.Program): typeof components { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAllComponents(node: TSESTree.Program): typeof components { return components; }, } as const; @@ -31,10 +32,10 @@ export function useComponentCollectorLegacy() { _: key, id, kind: "class", - name: O.flatMapNullable(id, (n) => n.name), + name: id?.name, node, // TODO: Get displayName of class component - displayName: O.none(), + displayName: _, flag, hint: 0n, // TODO: Get methods of class component diff --git a/packages/core/src/component/component-collector.ts b/packages/core/src/component/component-collector.ts index e8591a1a8f..5b007db8f9 100644 --- a/packages/core/src/component/component-collector.ts +++ b/packages/core/src/component/component-collector.ts @@ -1,11 +1,10 @@ import * as AST from "@eslint-react/ast"; -import { O } from "@eslint-react/eff"; +import { _ } from "@eslint-react/eff"; import * as JSX from "@eslint-react/jsx"; import type { RuleContext } from "@eslint-react/types"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { ESLintUtils } from "@typescript-eslint/utils"; -import { match } from "ts-pattern"; import { isChildrenOfCreateElement } from "../element"; import { isReactHookCall } from "../hook"; @@ -34,27 +33,25 @@ function hasValidHierarchy(node: AST.TSESTreeFunction, context: RuleContext, hin if (hint & ERComponentHint.SkipClassProperty && AST.isFunctionOfClassProperty(node.parent)) { return false; } - return !O.exists( - AST.findParentNode( - node, - AST.isOneOf([ - T.JSXExpressionContainer, - T.ArrowFunctionExpression, - T.FunctionExpression, - T.Property, - T.ClassBody, - ]), - ), - AST.is(T.JSXExpressionContainer), + const boundaryNode = AST.findParentNode( + node, + AST.isOneOf([ + T.JSXExpressionContainer, + T.ArrowFunctionExpression, + T.FunctionExpression, + T.Property, + T.ClassBody, + ]), ); + return !boundaryNode || boundaryNode.type !== T.JSXExpressionContainer; } function getComponentFlag(initPath: ERFunctionComponent["initPath"]) { let flag = ERFunctionComponentFlag.None; - if (AST.hasCallInFunctionInitPath("memo")(initPath)) { + if (initPath && AST.hasCallInFunctionInitPath("memo")(initPath)) { flag |= ERFunctionComponentFlag.Memo; } - if (AST.hasCallInFunctionInitPath("forwardRef")(initPath)) { + if (initPath && AST.hasCallInFunctionInitPath("forwardRef")(initPath)) { flag |= ERFunctionComponentFlag.ForwardRef; } return flag; @@ -72,14 +69,15 @@ export function useComponentCollector( hookCalls: TSESTree.CallExpression[]; isComponent: boolean; }[] = []; - const getCurrentFunction = () => O.fromNullable(functionEntries.at(-1)); + const getCurrentEntry = () => functionEntries.at(-1); const onFunctionEnter = (node: AST.TSESTreeFunction) => { const key = getId(); functionEntries.push({ key, node, hookCalls: [], isComponent: false }); }; const onFunctionExit = () => { const entry = functionEntries.at(-1); - if (!entry?.isComponent) { + if (!entry) return; + if (!entry.isComponent) { return functionEntries.pop(); } const shouldDrop = AST.getNestedReturnStatements(entry.node.body) @@ -87,7 +85,7 @@ export function useComponentCollector( .reverse() .some((r) => { return context.sourceCode.getScope(r).block === entry.node - && r.argument != null + && !!r.argument && !JSX.isJSXValue(r.argument, jsxCtx, hint); }); if (shouldDrop) { @@ -97,24 +95,24 @@ export function useComponentCollector( }; const ctx = { - getAllComponents(_: TSESTree.Program): typeof components { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAllComponents(node: TSESTree.Program): typeof components { return components; }, - getCurrentFunction, - getCurrentFunctionStack() { + getCurrentEntries() { return [...functionEntries]; }, + getCurrentEntry, } as const; const listeners = { ":function[type]": onFunctionEnter, ":function[type]:exit": onFunctionExit, "ArrowFunctionExpression[type][body.type!='BlockStatement']"() { - const mbEntry = getCurrentFunction(); - if (O.isNone(mbEntry)) { + const entry = getCurrentEntry(); + if (!entry) { return; } - const entry = mbEntry.value; const { body } = entry.node; const isComponent = hasNoneOrValidComponentName(entry.node, context) && JSX.isJSXValue(body, jsxCtx, hint) @@ -124,7 +122,7 @@ export function useComponentCollector( } const initPath = AST.getFunctionInitPath(entry.node); const id = getFunctionComponentIdentifier(entry.node, context); - const name = O.flatMapNullable(id, getComponentNameFromIdentifier); + const name = id && getComponentNameFromIdentifier(id); const key = getId(); components.set(key, { _: key, @@ -132,7 +130,7 @@ export function useComponentCollector( kind: "function", name, node: entry.node, - displayName: O.none(), + displayName: _, flag: getComponentFlag(initPath), hint, hookCalls: entry.hookCalls, @@ -143,42 +141,35 @@ export function useComponentCollector( node: TSESTree.AssignmentExpression & { left: TSESTree.MemberExpression }, ) { const { left, right } = node; - const mbComponentName = match(left.object) - .with({ type: T.Identifier }, (n) => O.some(n.name)) - .otherwise(O.none); - if (O.isNone(mbComponentName)) { - return; - } - const componentName = mbComponentName.value; - const component = Array - .from(components.values()) - .findLast(({ name }) => O.exists(name, (n) => n === componentName)); - if (component == null) { + const componentName = left.object.type === T.Identifier + ? left.object.name + : _; + const component = [...components.values()] + .findLast(({ name }) => name != null && name === componentName); + if (!component) { return; } components.set(component._, { ...component, - displayName: O.some(right), + displayName: right, }); }, "CallExpression[type]:exit"(node: TSESTree.CallExpression) { if (!isReactHookCall(node)) { return; } - const mbEntry = getCurrentFunction(); - if (O.isNone(mbEntry)) { + const entry = getCurrentEntry(); + if (!entry) { return; } - const entry = mbEntry.value; functionEntries.pop(); functionEntries.push({ ...entry, hookCalls: [...entry.hookCalls, node] }); }, "ReturnStatement[type]"(node: TSESTree.ReturnStatement) { - const mbEntry = getCurrentFunction(); - if (O.isNone(mbEntry)) { + const entry = getCurrentEntry(); + if (!entry) { return; } - const entry = mbEntry.value; const isComponent = hasNoneOrValidComponentName(entry.node, context) && JSX.isJSXValue(node.argument, jsxCtx, hint) && hasValidHierarchy(entry.node, context, hint); @@ -189,14 +180,14 @@ export function useComponentCollector( functionEntries.push({ ...entry, isComponent }); const initPath = AST.getFunctionInitPath(entry.node); const id = getFunctionComponentIdentifier(entry.node, context); - const name = O.flatMapNullable(id, getComponentNameFromIdentifier); + const name = id && getComponentNameFromIdentifier(id); components.set(entry.key, { _: entry.key, id, kind: "function", name, node: entry.node, - displayName: O.none(), + displayName: _, flag: getComponentFlag(initPath), hint, hookCalls: entry.hookCalls, diff --git a/packages/core/src/component/component-id.ts b/packages/core/src/component/component-id.ts index 4a2fbd3bb6..e9f92c87f4 100644 --- a/packages/core/src/component/component-id.ts +++ b/packages/core/src/component/component-id.ts @@ -1,5 +1,5 @@ import * as AST from "@eslint-react/ast"; -import { O } from "@eslint-react/eff"; +import { _ } from "@eslint-react/eff"; import type { RuleContext } from "@eslint-react/types"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; @@ -19,9 +19,9 @@ function isComponentWrapperCall(node: TSESTree.Node, context: RuleContext) { export function getFunctionComponentIdentifier( node: AST.TSESTreeFunction, context: RuleContext, -): O.Option { +): TSESTree.Identifier | TSESTree.Identifier[] | _ { const functionId = AST.getFunctionIdentifier(node); - if (O.isSome(functionId)) { + if (functionId) { return functionId; } const { parent } = node; @@ -32,7 +32,7 @@ export function getFunctionComponentIdentifier( && parent.parent.type === T.VariableDeclarator && parent.parent.id.type === T.Identifier ) { - return O.some(parent.parent.id); + return parent.parent.id; } // Get function component identifier from `const Component = memo(forwardRef(() => {}));` if ( @@ -43,7 +43,7 @@ export function getFunctionComponentIdentifier( && parent.parent.parent.type === T.VariableDeclarator && parent.parent.parent.id.type === T.Identifier ) { - return O.some(parent.parent.parent.id); + return parent.parent.parent.id; } - return O.none(); + return _; } diff --git a/packages/core/src/component/component-render-method.ts b/packages/core/src/component/component-render-method.ts index 01f9e6d6f0..08f317efd8 100644 --- a/packages/core/src/component/component-render-method.ts +++ b/packages/core/src/component/component-render-method.ts @@ -1,5 +1,4 @@ import * as AST from "@eslint-react/ast"; -import { O } from "@eslint-react/eff"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { TSESTree } from "@typescript-eslint/utils"; @@ -39,7 +38,7 @@ export function isFunctionOfRenderMethod(node: AST.TSESTreeFunction) { * @returns `true` if node is inside class component's render block, `false` if not */ export function isInsideRenderMethod(node: TSESTree.Node) { - return O.isSome(AST.findParentNode(node, (node) => + return !!AST.findParentNode(node, (node) => isRenderMethodLike(node) - && isClassComponent(node.parent.parent))); + && isClassComponent(node.parent.parent)); } diff --git a/packages/core/src/component/component.ts b/packages/core/src/component/component.ts index f1fb555e57..b963b24fc3 100644 --- a/packages/core/src/component/component.ts +++ b/packages/core/src/component/component.ts @@ -1,5 +1,5 @@ import type * as AST from "@eslint-react/ast"; -import type { O } from "@eslint-react/eff"; +import type { _ } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; import type { ERSemanticNode } from "../semantic-node"; @@ -8,24 +8,38 @@ import type { ERClassComponentFlag, ERFunctionComponentFlag } from "./component- /* eslint-disable perfectionist/sort-interfaces */ export interface ERFunctionComponent extends ERSemanticNode { - id: O.Option; + id: + | _ + | TSESTree.Identifier + | TSESTree.Identifier[]; kind: "function"; node: AST.TSESTreeFunction; flag: ERFunctionComponentFlag; hint: ERComponentHint; - initPath: O.Option; + initPath: + | _ + | AST.FunctionInitPath; hookCalls: TSESTree.CallExpression[]; - displayName: O.Option; + displayName: + | _ + | TSESTree.Expression; } export interface ERClassComponent extends ERSemanticNode { - id: O.Option; + id: + | _ + | TSESTree.Identifier; kind: "class"; node: AST.TSESTreeClass; flag: ERClassComponentFlag; hint: ERComponentHint; - methods: (TSESTree.MethodDefinition | TSESTree.PropertyDefinition)[]; - displayName: O.Option; + methods: Array< + | TSESTree.MethodDefinition + | TSESTree.PropertyDefinition + >; + displayName: + | _ + | TSESTree.Expression; } /* eslint-enable perfectionist/sort-interfaces */ diff --git a/packages/core/src/component/is.ts b/packages/core/src/component/is.ts index c7d9ffb452..e430ea41cd 100644 --- a/packages/core/src/component/is.ts +++ b/packages/core/src/component/is.ts @@ -8,7 +8,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; * @returns `true` if the node is a class component, `false` otherwise */ export function isClassComponent(node: TSESTree.Node): node is AST.TSESTreeClass { - if ("superClass" in node && node.superClass != null) { + if ("superClass" in node && node.superClass) { const re = /^(?:Pure)?Component$/u; switch (true) { case node.superClass.type === T.Identifier: @@ -27,7 +27,7 @@ export function isClassComponent(node: TSESTree.Node): node is AST.TSESTreeClass * @returns `true` if the node is a pure component, `false` otherwise */ export function isPureComponent(node: TSESTree.Node) { - if ("superClass" in node && node.superClass != null) { + if ("superClass" in node && node.superClass) { const re = /^PureComponent$/u; switch (true) { case node.superClass.type === T.Identifier: diff --git a/packages/core/src/component/misc.ts b/packages/core/src/component/misc.ts index 64d859b61b..e4c36d2986 100644 --- a/packages/core/src/component/misc.ts +++ b/packages/core/src/component/misc.ts @@ -1,16 +1,12 @@ import type * as AST from "@eslint-react/ast"; -import { O } from "@eslint-react/eff"; import type { RuleContext } from "@eslint-react/types"; import { getFunctionComponentIdentifier } from "./component-id"; import { isComponentName } from "./component-name"; export function hasNoneOrValidComponentName(node: AST.TSESTreeFunction, context: RuleContext) { - const mbIdentifier = getFunctionComponentIdentifier(node, context); - if (O.isNone(mbIdentifier)) { - return true; - } - const id = mbIdentifier.value; + const id = getFunctionComponentIdentifier(node, context); + if (!id) return true; const name = Array.isArray(id) ? id.at(-1)?.name : id.name; diff --git a/packages/core/src/effect/is.ts b/packages/core/src/effect/is.ts index a2dd610da6..2ff574750e 100644 --- a/packages/core/src/effect/is.ts +++ b/packages/core/src/effect/is.ts @@ -1,11 +1,12 @@ import * as AST from "@eslint-react/ast"; -import { F, O } from "@eslint-react/eff"; +import { _ } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { isUseEffectCallLoose } from "../hook"; -export function isSetupFunction(node: TSESTree.Node) { +export function isSetupFunction(node: TSESTree.Node | _) { + if (node === _) return _; return node.parent?.type === T.CallExpression && node.parent.callee !== node && node.parent.callee.type === T.Identifier @@ -14,14 +15,8 @@ export function isSetupFunction(node: TSESTree.Node) { } export function isCleanupFunction(node: TSESTree.Node) { - return F.pipe( - O.Do, - O.bind("nearReturn", () => AST.findParentNodeGuard(node, AST.is(T.ReturnStatement))), - O.bind("nearFunction", () => AST.findParentNodeGuard(node, AST.isFunction)), - O.bind("nearFunctionOfReturn", ({ nearReturn }) => AST.findParentNodeGuard(nearReturn, AST.isFunction)), - O.exists(({ nearFunction, nearFunctionOfReturn }) => - nearFunction === nearFunctionOfReturn - && isSetupFunction(nearFunction) - ), - ); + const nearReturn = AST.findParentNodeGuard(node, AST.is(T.ReturnStatement)); + const nearFunction = AST.findParentNodeGuard(node, AST.isFunction); + const nearFunctionOfReturn = AST.findParentNodeGuard(nearReturn, AST.isFunction); + return nearFunction === nearFunctionOfReturn && isSetupFunction(nearFunction); } diff --git a/packages/core/src/element/element-name.ts b/packages/core/src/element/element-name.ts new file mode 100644 index 0000000000..1370a893a4 --- /dev/null +++ b/packages/core/src/element/element-name.ts @@ -0,0 +1,35 @@ +import { _ } from "@eslint-react/eff"; +import * as JSX from "@eslint-react/jsx"; +import type { CustomComponentNormalized } from "@eslint-react/shared"; +import type { RuleContext } from "@eslint-react/types"; +import * as VAR from "@eslint-react/var"; +import type { TSESTree } from "@typescript-eslint/types"; + +export function getElementNameAndRepresentName( + node: TSESTree.JSXOpeningElement, + context: RuleContext, + polymorphicPropName?: string, + additionalComponents: CustomComponentNormalized[] = [], +): [string, string] { + const name = JSX.getElementName(node); + // Skip JsxIntrinsicElements + if (name === name.toLowerCase()) return [name, name]; + // Get the component name using the `settings["react-x"].additionalComponents` setting + const component = additionalComponents + .findLast((c) => c.name === name || c.re.test(name)); + if (component !== _) return [name, component.as]; + if (polymorphicPropName == null) return [name, name]; + // Get the component name using the `settings["react-x"].polymorphicPropName` setting + const initialScope = context.sourceCode.getScope(node); + const polymorphicProp = JSX.findPropInAttributes( + polymorphicPropName, + initialScope, + node.attributes, + ); + if (polymorphicProp === _) return [name, name]; + const polymorphicName = VAR.toResolved(JSX.getPropValue(polymorphicProp, initialScope)).value; + if (typeof polymorphicName === "string") { + return [name, polymorphicName]; + } + return [name, name]; +} diff --git a/packages/core/src/element/get-element-represent-name.ts b/packages/core/src/element/get-element-represent-name.ts deleted file mode 100644 index 1762d3c258..0000000000 --- a/packages/core/src/element/get-element-represent-name.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { F, isString, O } from "@eslint-react/eff"; -import * as JSX from "@eslint-react/jsx"; -import { getSettingsFromContext } from "@eslint-react/shared"; -import type { RuleContext } from "@eslint-react/types"; -import type { TSESTree } from "@typescript-eslint/types"; - -export function getElementRepresentName(node: TSESTree.JSXOpeningElement, context: RuleContext) { - const rawElementName = JSX.getElementName(node); - if (rawElementName === rawElementName.toLowerCase()) { - return rawElementName; - } - const { components, polymorphicPropName } = getSettingsFromContext(context); - const asElementName = components.get(rawElementName); - if (isString(asElementName)) { - return asElementName; - } - return F.pipe( - O.fromNullable(polymorphicPropName), - O.flatMap(JSX.findPropInAttributes(node.attributes, context.sourceCode.getScope(node))), - O.flatMap((attr) => JSX.getPropValue(attr, context.sourceCode.getScope(attr))), - O.filter(isString), - O.getOrElse(() => rawElementName), - ); -} diff --git a/packages/core/src/element/hierarchy.ts b/packages/core/src/element/hierarchy.ts index c859d1b874..86a7e57692 100644 --- a/packages/core/src/element/hierarchy.ts +++ b/packages/core/src/element/hierarchy.ts @@ -1,5 +1,4 @@ import * as AST from "@eslint-react/ast"; -import { F, O } from "@eslint-react/eff"; import type { RuleContext } from "@eslint-react/types"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; @@ -16,13 +15,11 @@ export function isInsideCreateElementProps( node: TSESTree.Node, context: RuleContext, ) { - return F.pipe( - O.Do, - O.bind("call", () => AST.findParentNodeGuard(node, isCreateElementCall(context))), - O.bind("prop", () => AST.findParentNodeGuard(node, AST.is(T.ObjectExpression))), - O.bind("arg1", ({ call }) => O.fromNullable(call.arguments[1])), - O.exists(({ arg1, prop }) => prop === arg1), - ); + const call = AST.findParentNodeGuard(node, isCreateElementCall(context)); + if (!call) return false; + const prop = AST.findParentNodeGuard(node, AST.is(T.ObjectExpression)); + if (!prop) return false; + return prop === call.arguments[1]; } /** @@ -35,14 +32,10 @@ export function isChildrenOfCreateElement( node: TSESTree.Node, context: RuleContext, ) { - return F.pipe( - O.fromNullable(node.parent), - O.filter(AST.is(T.CallExpression)), - O.filter(isCreateElementCall(context)), - O.exists((n) => - n.arguments - .slice(2) - .some((arg) => arg === node) - ), - ); + const parent = node.parent; + if (!parent || parent.type !== T.CallExpression) return false; + if (!isCreateElementCall(parent, context)) return false; + return parent.arguments + .slice(2) + .some((arg) => arg === node); } diff --git a/packages/core/src/element/index.ts b/packages/core/src/element/index.ts index 960d186b6d..15aa700ded 100644 --- a/packages/core/src/element/index.ts +++ b/packages/core/src/element/index.ts @@ -1,2 +1,2 @@ -export * from "./get-element-represent-name"; +export * from "./element-name"; export * from "./hierarchy"; diff --git a/packages/core/src/hook/hierarchy.ts b/packages/core/src/hook/hierarchy.ts index a2c183eef2..1eb1efc41f 100644 --- a/packages/core/src/hook/hierarchy.ts +++ b/packages/core/src/hook/hierarchy.ts @@ -1,9 +1,8 @@ import * as AST from "@eslint-react/ast"; -import { O } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; import { isReactHook } from "./is"; export function isInsideReactHook(node: TSESTree.Node) { - return O.exists(AST.findParentNodeGuard(node, AST.isFunction), isReactHook); + return isReactHook(AST.findParentNodeGuard(node, AST.isFunction)); } diff --git a/packages/core/src/hook/hook-collector.ts b/packages/core/src/hook/hook-collector.ts index fe566aeecf..0a26bb5db3 100644 --- a/packages/core/src/hook/hook-collector.ts +++ b/packages/core/src/hook/hook-collector.ts @@ -1,5 +1,5 @@ import * as AST from "@eslint-react/ast"; -import { F, O } from "@eslint-react/eff"; +import { _ } from "@eslint-react/eff"; import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; import { getId } from "../utils"; @@ -9,13 +9,13 @@ import { isReactHookCall } from "./is"; export function useHookCollector() { const hooks = new Map(); - const fStack: [node: AST.TSESTreeFunction, id: O.Option][] = []; + const fEntries: { key: string | _; node: AST.TSESTreeFunction }[] = []; const onFunctionEnter = (node: AST.TSESTreeFunction) => { const id = AST.getFunctionIdentifier(node); - const name = O.flatMapNullable(id, (id) => id.name); - if (O.isSome(id) && O.isSome(name) && isReactHookName(name.value)) { + const name = id?.name; + if (name !== _ && isReactHookName(name)) { const key = getId(); - fStack.push([node, O.some(key)]); + fEntries.push({ key, node }); hooks.set(key, { _: key, id, @@ -28,13 +28,14 @@ export function useHookCollector() { }); return; } - fStack.push([node, O.none()]); + fEntries.push({ key: _, node }); }; const onFunctionExit = () => { - fStack.pop(); + fEntries.pop(); }; const ctx = { - getAllHooks(_: TSESTree.Program): typeof hooks { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAllHooks(node: TSESTree.Program): typeof hooks { return hooks; }, getCurrentHooks() { @@ -48,24 +49,21 @@ export function useHookCollector() { if (!isReactHookCall(node)) { return; } - const [fNode, hookId] = fStack.at(-1) ?? []; - if (fNode == null || hookId == null) { + const fEntry = fEntries.at(-1); + if (fEntry?.key === _) { return; } - F.pipe( - O.Do, - O.bind("id", () => hookId), - O.bind("hook", ({ id }) => O.fromNullable(hooks.get(id))), - O.map(({ id, hook }) => { - hooks.set(id, { - ...hook, - hookCalls: [ - ...hook.hookCalls, - node, - ], - }); - }), - ); + const hook = hooks.get(fEntry.key); + if (hook === _) { + return; + } + hooks.set(hook._, { + ...hook, + hookCalls: [ + ...hook.hookCalls, + node, + ], + }); }, } as const satisfies ESLintUtils.RuleListener; return { ctx, listeners } as const; diff --git a/packages/core/src/hook/hook.ts b/packages/core/src/hook/hook.ts index f99b4a57bc..1ce39b915b 100644 --- a/packages/core/src/hook/hook.ts +++ b/packages/core/src/hook/hook.ts @@ -1,5 +1,5 @@ import type * as AST from "@eslint-react/ast"; -import type { O } from "@eslint-react/eff"; +import type { _ } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; import type { ERSemanticNode } from "../semantic-node"; @@ -7,11 +7,11 @@ import type { ERSemanticNode } from "../semantic-node"; /* eslint-disable perfectionist/sort-interfaces */ export interface ERHook extends ERSemanticNode { // The identifier of the hook - id: O.Some; + id: TSESTree.Identifier | _; // The AST node of the hook node: AST.TSESTreeFunction; // The name of the hook - name: O.Some; + name: string; // The `HookFlag` of the hook, reserved for future use // flag: bigint; // The type of the hook, reserved for future use diff --git a/packages/core/src/hook/is.ts b/packages/core/src/hook/is.ts index 7f958c9ceb..644b87ea0c 100644 --- a/packages/core/src/hook/is.ts +++ b/packages/core/src/hook/is.ts @@ -1,5 +1,5 @@ import * as AST from "@eslint-react/ast"; -import { F, O } from "@eslint-react/eff"; +import { _, flip, returnFalse } from "@eslint-react/eff"; import { unsafeDecodeSettings } from "@eslint-react/shared"; import type { RuleContext } from "@eslint-react/types"; import type { TSESTree } from "@typescript-eslint/types"; @@ -8,12 +8,10 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { isInitializedFromReact } from "../utils"; import { isReactHookName } from "./hook-name"; -export function isReactHook(node: AST.TSESTreeFunction) { - return F.pipe( - AST.getFunctionIdentifier(node), - O.flatMapNullable((id) => id.name), - O.exists(isReactHookName), - ); +export function isReactHook(node: AST.TSESTreeFunction | _) { + if (node === _) return _; + const id = AST.getFunctionIdentifier(node); + return id?.name !== _ && isReactHookName(id.name); } /** @@ -21,7 +19,8 @@ export function isReactHook(node: AST.TSESTreeFunction) { * @param node The node to check. * @returns `true` if the node is a React Hook call, `false` otherwise. */ -export function isReactHookCall(node: TSESTree.Node) { +export function isReactHookCall(node: TSESTree.Node | _) { + if (node === _) return false; if (node.type !== T.CallExpression) { return false; } @@ -34,7 +33,8 @@ export function isReactHookCall(node: TSESTree.Node) { return false; } -export function isReactHookCallWithName(node: TSESTree.CallExpression, context: RuleContext) { +export function isReactHookCallWithName(node: TSESTree.CallExpression | _, context: RuleContext) { + if (node === _) return returnFalse; const settings = unsafeDecodeSettings(context.settings); return (name: string) => { const initialScope = context.sourceCode.getScope(node); @@ -55,7 +55,8 @@ export function isReactHookCallWithName(node: TSESTree.CallExpression, context: }; } -export function isReactHookCallWithNameLoose(node: TSESTree.CallExpression) { +export function isReactHookCallWithNameLoose(node: TSESTree.CallExpression | _) { + if (node === _) return returnFalse; return (name: string) => { switch (node.callee.type) { case T.Identifier: @@ -89,7 +90,8 @@ export function isReactHookCallWithNameAlias(name: string, context: RuleContext, }; } -export function isUseEffectCallLoose(node: TSESTree.Node) { +export function isUseEffectCallLoose(node: TSESTree.Node | _) { + if (node === _) return false; if (node.type !== T.CallExpression) { return false; } @@ -104,18 +106,18 @@ export function isUseEffectCallLoose(node: TSESTree.Node) { } } -export const isUseCallbackCall = F.flip(isReactHookCallWithName)("useCallback"); -export const isUseContextCall = F.flip(isReactHookCallWithName)("useContext"); -export const isUseDebugValueCall = F.flip(isReactHookCallWithName)("useDebugValue"); -export const isUseDeferredValueCall = F.flip(isReactHookCallWithName)("useDeferredValue"); -export const isUseEffectCall = F.flip(isReactHookCallWithName)("useEffect"); -export const isUseIdCall = F.flip(isReactHookCallWithName)("useId"); -export const isUseImperativeHandleCall = F.flip(isReactHookCallWithName)("useImperativeHandle"); -export const isUseInsertionEffectCall = F.flip(isReactHookCallWithName)("useInsertionEffect"); -export const isUseLayoutEffectCall = F.flip(isReactHookCallWithName)("useLayoutEffect"); -export const isUseMemoCall = F.flip(isReactHookCallWithName)("useMemo"); -export const isUseReducerCall = F.flip(isReactHookCallWithName)("useReducer"); -export const isUseRefCall = F.flip(isReactHookCallWithName)("useRef"); -export const isUseStateCall = F.flip(isReactHookCallWithName)("useState"); -export const isUseSyncExternalStoreCall = F.flip(isReactHookCallWithName)("useSyncExternalStore"); -export const isUseTransitionCall = F.flip(isReactHookCallWithName)("useTransition"); +export const isUseCallbackCall = flip(isReactHookCallWithName)("useCallback"); +export const isUseContextCall = flip(isReactHookCallWithName)("useContext"); +export const isUseDebugValueCall = flip(isReactHookCallWithName)("useDebugValue"); +export const isUseDeferredValueCall = flip(isReactHookCallWithName)("useDeferredValue"); +export const isUseEffectCall = flip(isReactHookCallWithName)("useEffect"); +export const isUseIdCall = flip(isReactHookCallWithName)("useId"); +export const isUseImperativeHandleCall = flip(isReactHookCallWithName)("useImperativeHandle"); +export const isUseInsertionEffectCall = flip(isReactHookCallWithName)("useInsertionEffect"); +export const isUseLayoutEffectCall = flip(isReactHookCallWithName)("useLayoutEffect"); +export const isUseMemoCall = flip(isReactHookCallWithName)("useMemo"); +export const isUseReducerCall = flip(isReactHookCallWithName)("useReducer"); +export const isUseRefCall = flip(isReactHookCallWithName)("useRef"); +export const isUseStateCall = flip(isReactHookCallWithName)("useState"); +export const isUseSyncExternalStoreCall = flip(isReactHookCallWithName)("useSyncExternalStore"); +export const isUseTransitionCall = flip(isReactHookCallWithName)("useTransition"); diff --git a/packages/core/src/phase/constants.ts b/packages/core/src/phase/constants.ts index c983084e3b..b1895cd7d4 100644 --- a/packages/core/src/phase/constants.ts +++ b/packages/core/src/phase/constants.ts @@ -1,4 +1,4 @@ -import { birecord } from "@eslint-react/types"; +import { birecord } from "@eslint-react/eff"; export const ERPhaseRelevance = birecord({ mount: "unmount", diff --git a/packages/core/src/phase/is-inverse-phase.ts b/packages/core/src/phase/is-inverse-phase.ts index 6d71a41783..215b49c745 100644 --- a/packages/core/src/phase/is-inverse-phase.ts +++ b/packages/core/src/phase/is-inverse-phase.ts @@ -1,4 +1,4 @@ -import { F } from "@eslint-react/eff"; +import { dual } from "@eslint-react/eff"; import { ERPhaseRelevance } from "./constants"; import type { ERPhaseKind } from "./phase"; @@ -6,4 +6,4 @@ import type { ERPhaseKind } from "./phase"; export const isInversePhase: { (a: ERPhaseKind): (b: ERPhaseKind) => boolean; (a: ERPhaseKind, b: ERPhaseKind): boolean; -} = F.dual(2, (a: ERPhaseKind, b: ERPhaseKind) => ERPhaseRelevance.get(a) === b); +} = dual(2, (a: ERPhaseKind, b: ERPhaseKind) => ERPhaseRelevance.get(a) === b); diff --git a/packages/core/src/render-prop/hierarchy.ts b/packages/core/src/render-prop/hierarchy.ts index 220d934b7e..f34ddda126 100644 --- a/packages/core/src/render-prop/hierarchy.ts +++ b/packages/core/src/render-prop/hierarchy.ts @@ -1,5 +1,4 @@ import * as AST from "@eslint-react/ast"; -import { F, O } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; @@ -21,7 +20,7 @@ export function isDirectValueOfRenderPropertyLoose(node: TSESTree.Node) { && node.key.type === T.Identifier && node.key.name.startsWith("render"); }; - return matching(node) || (node.parent != null && matching(node.parent)); + return matching(node) || (!!node.parent && matching(node.parent)); } /** @@ -39,15 +38,9 @@ export function isDeclaredInRenderPropLoose(node: TSESTree.Node) { if (isDirectValueOfRenderPropertyLoose(node)) { return true; } - - return F.pipe( - AST.findParentNodeGuard(node, AST.is(T.JSXExpressionContainer)), - O.flatMapNullable((c) => c.parent), - O.filter(AST.is(T.JSXAttribute)), - O.flatMapNullable((a) => a.name), - O.exists((n) => - n.type === T.JSXIdentifier - && n.name.startsWith("render") - ), - ); + const parent = AST.findParentNodeGuard(node, AST.is(T.JSXExpressionContainer))?.parent; + if (parent?.type !== T.JSXAttribute) { + return false; + } + return parent.name.type === T.JSXIdentifier && parent.name.name.startsWith("render"); } diff --git a/packages/core/src/render-prop/is.ts b/packages/core/src/render-prop/is.ts index 49585ab546..20f03f447c 100644 --- a/packages/core/src/render-prop/is.ts +++ b/packages/core/src/render-prop/is.ts @@ -1,5 +1,4 @@ import * as AST from "@eslint-react/ast"; -import { O } from "@eslint-react/eff"; import * as JSX from "@eslint-react/jsx"; import type { RuleContext } from "@eslint-react/types"; import type { TSESTree } from "@typescript-eslint/types"; @@ -19,7 +18,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; */ export function isRenderFunctionLoose(node: AST.TSESTreeFunction, context: RuleContext) { const { body, parent } = node; - if (O.exists(AST.getFunctionIdentifier(node), (id) => id.name.startsWith("render"))) { + if (AST.getFunctionIdentifier(node)?.name.startsWith("render")) { return parent.type === T.JSXExpressionContainer && parent.parent.type === T.JSXAttribute && parent.parent.name.type === T.JSXIdentifier diff --git a/packages/core/src/semantic-node.ts b/packages/core/src/semantic-node.ts index 884c1af20a..a4ff96b3f6 100644 --- a/packages/core/src/semantic-node.ts +++ b/packages/core/src/semantic-node.ts @@ -1,11 +1,16 @@ -import type { O } from "@eslint-react/eff"; +import type { _ } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; export interface ERSemanticNode { _: string; - id: O.Option; + id: + | _ + | TSESTree.Identifier + | TSESTree.Identifier[]; kind: string; - name: O.Option; + name: + | _ + | string; node: TSESTree.Node; flag: bigint; hint: bigint; diff --git a/packages/core/src/utils/is-from-react.ts b/packages/core/src/utils/is-from-react.ts index ea0e18d542..56eab14ed7 100644 --- a/packages/core/src/utils/is-from-react.ts +++ b/packages/core/src/utils/is-from-react.ts @@ -1,5 +1,5 @@ import * as AST from "@eslint-react/ast"; -import { F } from "@eslint-react/eff"; +import { dual } from "@eslint-react/eff"; import { unsafeDecodeSettings } from "@eslint-react/shared"; import type { RuleContext } from "@eslint-react/types"; import type { TSESTree } from "@typescript-eslint/types"; @@ -96,7 +96,7 @@ type IsCallFromReact = { }; export function isCallFromReact(name: string): IsCallFromReact { - return F.dual(2, (node: TSESTree.Node, context: RuleContext): node is TSESTree.CallExpression => { + return dual(2, (node: TSESTree.Node, context: RuleContext): node is TSESTree.CallExpression => { if (node.type !== T.CallExpression) { return false; } @@ -123,7 +123,7 @@ export function isCallFromReactMember( pragmaMemberName: string, name: string, ): IsCallFromReactMember { - return F.dual(2, ( + return dual(2, ( node: TSESTree.Node, context: RuleContext, ): node is diff --git a/packages/core/src/utils/is-react-api.ts b/packages/core/src/utils/is-react-api.ts index 3cc5f464fc..fd2b820d3f 100644 --- a/packages/core/src/utils/is-react-api.ts +++ b/packages/core/src/utils/is-react-api.ts @@ -3,17 +3,17 @@ import { isCallFromReact, isCallFromReactMember, isFromReact, isFromReactMember export function isReactAPIWithName(name: string): ReturnType; export function isReactAPIWithName(name: string, member: string): ReturnType; export function isReactAPIWithName(name: string, member?: string) { - return member == null - ? isFromReact(name) - : isFromReactMember(name, member); + return member != null + ? isFromReactMember(name, member) + : isFromReact(name); } export function isReactAPICallWithName(name: string): ReturnType; export function isReactAPICallWithName(name: string, member: string): ReturnType; export function isReactAPICallWithName(name: string, member?: string) { - return member == null - ? isCallFromReact(name) - : isCallFromReactMember(name, member); + return member != null + ? isCallFromReactMember(name, member) + : isCallFromReact(name); } export const isChildrenCount = isReactAPIWithName("Children", "count"); diff --git a/packages/plugins/eslint-plugin-react-debug/package.json b/packages/plugins/eslint-plugin-react-debug/package.json index 1c2c40c5ca..8ff4e4fee4 100644 --- a/packages/plugins/eslint-plugin-react-debug/package.json +++ b/packages/plugins/eslint-plugin-react-debug/package.json @@ -10,13 +10,13 @@ "eslint-plugin", "eslint-plugin-react-debug" ], - "homepage": "https://github.com/rEl1cx/eslint-react", + "homepage": "https://github.com/Rel1cx/eslint-react", "bugs": { - "url": "https://github.com/rEl1cx/eslint-react/issues" + "url": "https://github.com/Rel1cx/eslint-react/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/rEl1cx/eslint-react.git", + "url": "git+https://github.com/Rel1cx/eslint-react.git", "directory": "packages/plugins/eslint-plugin-react-debug" }, "license": "MIT", diff --git a/packages/plugins/eslint-plugin-react-debug/src/rules/class-component.ts b/packages/plugins/eslint-plugin-react-debug/src/rules/class-component.ts index 3e81de9c9d..f38a3e3515 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/rules/class-component.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/rules/class-component.ts @@ -1,5 +1,4 @@ import { useComponentCollectorLegacy } from "@eslint-react/core"; -import { F, O } from "@eslint-react/eff"; import type { RuleFeature } from "@eslint-react/types"; import type { CamelCase } from "string-ts"; @@ -33,12 +32,12 @@ export default createRule<[], MessageID>({ ...listeners, "Program:exit"(node) { const components = ctx.getAllComponents(node); - for (const { name, node: component } of components.values()) { + for (const { name = "anonymous", node: component } of components.values()) { context.report({ messageId: "classComponent", node: component, data: { - name: O.getOrElse(F.constant("anonymous"))(name), + name, }, }); } diff --git a/packages/plugins/eslint-plugin-react-debug/src/rules/function-component.ts b/packages/plugins/eslint-plugin-react-debug/src/rules/function-component.ts index 12e5b59619..6d8b70246b 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/rules/function-component.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/rules/function-component.ts @@ -1,5 +1,4 @@ import { ERFunctionComponentFlag, useComponentCollector } from "@eslint-react/core"; -import { F, O } from "@eslint-react/eff"; import type { RuleFeature } from "@eslint-react/types"; import type { CamelCase } from "string-ts"; @@ -34,12 +33,12 @@ export default createRule<[], MessageID>({ ...listeners, "Program:exit"(node) { const components = ctx.getAllComponents(node); - for (const { name, node, flag, hookCalls } of components.values()) { + for (const { name = "anonymous", node, flag, hookCalls } of components.values()) { context.report({ messageId: "functionComponent", node, data: { - name: O.getOrElse(name, F.constant("anonymous")), + name, forwardRef: (flag & ERFunctionComponentFlag.ForwardRef) > 0n, hookCalls: hookCalls.length, memo: (flag & ERFunctionComponentFlag.Memo) > 0n, diff --git a/packages/plugins/eslint-plugin-react-debug/src/rules/hook.ts b/packages/plugins/eslint-plugin-react-debug/src/rules/hook.ts index c876c537ae..89db24a7e3 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/rules/hook.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/rules/hook.ts @@ -39,7 +39,7 @@ export default createRule<[], MessageID>({ messageId: "hook", node, data: { - name: name.value, + name, hookCalls: hookCalls.length, }, }); diff --git a/packages/plugins/eslint-plugin-react-debug/src/rules/is-from-react.ts b/packages/plugins/eslint-plugin-react-debug/src/rules/is-from-react.ts index 4e7045b98a..dd68205c66 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/rules/is-from-react.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/rules/is-from-react.ts @@ -1,11 +1,9 @@ import { isInitializedFromReact } from "@eslint-react/core"; -import { F, O } from "@eslint-react/eff"; import { getSettingsFromContext } from "@eslint-react/shared"; import type { RuleFeature } from "@eslint-react/types"; import type { Scope } from "@typescript-eslint/scope-manager"; import type { TSESTree } from "@typescript-eslint/utils"; import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; -import type { ReportDescriptor } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; import { createRule } from "../utils"; @@ -56,24 +54,23 @@ export default createRule<[], MessageID>({ return isInitializedFromReact(name, initialScope, settings.importSource); } } - function getReportDescriptor( - node: TSESTree.Identifier | TSESTree.JSXIdentifier, - ): O.Option> { + function visitorFunction(node: TSESTree.Identifier | TSESTree.JSXIdentifier) { const shouldSkipDuplicate = node.parent.type === T.ImportSpecifier && node.parent.imported === node && node.parent.imported.name === node.parent.local.name; if (shouldSkipDuplicate) { - return O.none(); + return; } const name = node.name; const initialScope = context.sourceCode.getScope(node); if (!isFromReact(node, initialScope)) { - return O.none(); + return; } - return O.some({ + context.report({ messageId: "isFromReact", node, data: { + // eslint-disable-next-line eslint-plugin/no-unused-placeholders type: node.type, name, importSource: settings.importSource, @@ -81,8 +78,8 @@ export default createRule<[], MessageID>({ }); } return { - Identifier: F.flow(getReportDescriptor, O.map(context.report)), - JSXIdentifier: F.flow(getReportDescriptor, O.map(context.report)), + Identifier: visitorFunction, + JSXIdentifier: visitorFunction, }; }, defaultOptions: [], diff --git a/packages/plugins/eslint-plugin-react-dom/package.json b/packages/plugins/eslint-plugin-react-dom/package.json index fa9f4fa5b3..05f29366ce 100644 --- a/packages/plugins/eslint-plugin-react-dom/package.json +++ b/packages/plugins/eslint-plugin-react-dom/package.json @@ -10,13 +10,13 @@ "eslint-plugin", "eslint-plugin-react-dom" ], - "homepage": "https://github.com/rEl1cx/eslint-react", + "homepage": "https://github.com/Rel1cx/eslint-react", "bugs": { - "url": "https://github.com/rEl1cx/eslint-react/issues" + "url": "https://github.com/Rel1cx/eslint-react/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/rEl1cx/eslint-react.git", + "url": "git+https://github.com/Rel1cx/eslint-react.git", "directory": "packages/plugins/eslint-plugin-react-dom" }, "license": "MIT", diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts index e066973474..5c0c713918 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts @@ -1,4 +1,3 @@ -import { isNullable } from "@eslint-react/eff"; import * as JSX from "@eslint-react/jsx"; import type { RuleFeature } from "@eslint-react/types"; import type { TSESTree } from "@typescript-eslint/types"; @@ -14,13 +13,6 @@ export const RULE_FEATURES = [ export type MessageID = CamelCase; -function firstChildIsText(node: TSESTree.JSXElement) { - const [firstChild] = node.children; - return node.children.length > 0 - && !isNullable(firstChild) - && !JSX.isLineBreak(firstChild); -} - // TODO: Use the information in `settings["react-x"].additionalComponents` to add support for user-defined components that use different properties to receive HTML and set them internally. export default createRule<[], MessageID>({ meta: { @@ -39,20 +31,24 @@ export default createRule<[], MessageID>({ create(context) { return { JSXElement(node) { + const attributes = node.openingElement.attributes; const initialScope = context.sourceCode.getScope(node); - const hasChildrenWithIn = () => node.children.length > 0 && firstChildIsText(node); - const hasChildrenProp = () => JSX.hasProp(node.openingElement.attributes, "children", initialScope); - // dprint-ignore - const hasDanger = () => JSX.hasProp(node.openingElement.attributes, "dangerouslySetInnerHTML", initialScope); - if (!(hasChildrenWithIn() || hasChildrenProp()) || !hasDanger()) { - return; + const hasChildren = hasChildrenWithin(node) || JSX.hasProp("children", initialScope, attributes); + const hasDangerouslySetInnerHTML = JSX.hasProp("dangerouslySetInnerHTML", initialScope, attributes); + if (hasChildren && hasDangerouslySetInnerHTML) { + context.report({ + messageId: "noDangerouslySetInnerhtmlWithChildren", + node, + }); } - context.report({ - messageId: "noDangerouslySetInnerhtmlWithChildren", - node, - }); }, }; }, defaultOptions: [], }); + +function hasChildrenWithin(node: TSESTree.JSXElement): boolean { + return node.children.length > 0 + && node.children[0] != null + && !JSX.isLineBreak(node.children[0]); +} diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts index 2d2039efe6..035eb66b62 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts @@ -1,4 +1,4 @@ -import { F, O } from "@eslint-react/eff"; +import { _ } from "@eslint-react/eff"; import * as JSX from "@eslint-react/jsx"; import type { RuleFeature } from "@eslint-react/types"; import type { CamelCase } from "string-ts"; @@ -30,12 +30,13 @@ export default createRule<[], MessageID>({ create(context) { return { JSXElement(node) { - F.pipe( - O.some("dangerouslySetInnerHTML"), - O.flatMap(JSX.findPropInAttributes(node.openingElement.attributes, context.sourceCode.getScope(node))), - O.map((prop) => ({ messageId: "noDangerouslySetInnerhtml", node: prop } as const)), - O.map(context.report), - ); + const attributes = node.openingElement.attributes; + const prop = JSX.getProp("dangerouslySetInnerHTML", context.sourceCode.getScope(node), attributes); + if (prop === _) return; + context.report({ + messageId: "noDangerouslySetInnerhtml", + node: prop, + }); }, }; }, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts index 0f9999c41c..3ac6425951 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts @@ -1,10 +1,9 @@ -import { getElementRepresentName } from "@eslint-react/core"; -import { O } from "@eslint-react/eff"; -import * as JSX from "@eslint-react/jsx"; +import { getElementNameAndRepresentName } from "@eslint-react/core"; +import { getSettingsFromContext } from "@eslint-react/shared"; import type { RuleFeature } from "@eslint-react/types"; import type { CamelCase } from "string-ts"; -import { createRule } from "../utils"; +import { createRule, getAdditionalAttributes, getAttributeStringValue } from "../utils"; export const RULE_NAME = "no-missing-button-type"; @@ -29,22 +28,32 @@ export default createRule<[], MessageID>({ }, name: RULE_NAME, create(context) { + const settings = getSettingsFromContext(context); + const additionalComponents = settings.additionalComponents.filter((c) => c.as === "button"); return { JSXElement(node) { - const elementName = getElementRepresentName(node.openingElement, context); - if (elementName !== "button") { - return; + const [name, representName] = getElementNameAndRepresentName( + node.openingElement, + context, + settings.polymorphicPropName, + settings.additionalComponents, + ); + if (representName !== "button") return; + const getPropValue = (propName: string) => { + return getAttributeStringValue( + propName, + node, + context, + getAdditionalAttributes(name, additionalComponents), + ); + }; + const prop = getPropValue("type"); + if (typeof prop !== "string") { + context.report({ + messageId: "noMissingButtonType", + node, + }); } - const { attributes } = node.openingElement; - const initialScope = context.sourceCode.getScope(node); - const mbProp = JSX.findPropInAttributes(attributes, initialScope)("type"); - if (O.isSome(mbProp)) { - return; - } - context.report({ - messageId: "noMissingButtonType", - node, - }); }, }; }, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.spec.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.spec.ts index dec15f5aa6..8418bef783 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.spec.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.spec.ts @@ -19,23 +19,6 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, - // has sandbox attribute but not explicitly set to iframe element - { - code: /* tsx */ ` - const props = { - sandbox: "allow-downloads", - }; - - function App() { - return