diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab1bc8583..c51654303c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,83 @@ # Change Log All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](http://semver.org/). -This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). +This project adheres to [Semantic Versioning](https://semver.org/). +This change log adheres to standards from [Keep a CHANGELOG](https://keepachangelog.com). ## Unreleased +## [7.29.0] - 2022.02.24 + +### Added +* add [`hook-use-state`] rule to enforce symmetric useState hook variable names ([#2921][] @duncanbeevers) +* [`jsx-no-target-blank`]: Improve fixer with option `allowReferrer` ([#3167][] @apepper) +* [`jsx-curly-brace-presence`]: add "propElementValues" config option ([#3191][] @ljharb) +* add [`iframe-missing-sandbox`] rule ([#2753][] @tosmolka @ljharb) +* [`no-did-mount-set-state`], [`no-did-update-set-state`]: no-op with react >= 16.3 ([#1754][] @ljharb) +* [`jsx-sort-props`]: support multiline prop groups ([#3198][] @duhamelgm) +* [`jsx-key`]: add `warnDuplicates` option to warn on duplicate jsx keys in an array ([#2614][] @ljharb) +* [`jsx-sort-props`]: add `locale` option ([#3002][] @ljharb) + +### Fixed +* [`prop-types`], `propTypes`: add support for exported type inference ([#3163][] @vedadeepta) +* [`no-invalid-html-attribute`]: allow 'shortcut icon' on `link` ([#3174][] @Primajin) +* [`prefer-exact-props`] improve performance for `Identifier` visitor ([#3190][] @meowtec) +* `propTypes`: Handle TSTypeReference in no-unused-prop-type ([#3195][] @niik) +* [`sort-prop-types`]: avoid repeated warnings of the same node/reason ([#519][] @ljharb) +* [`jsx-indent`]: Fix indent handling for closing parentheses ([#620][] @stefanbuck]) +* [`prop-types`/`propTypes`]: follow a returned identifier to see if it is JSX ([#1046][] @ljharb) +* [`no-unused-state`]: TS: support `getDerivedStateFromProps` as an arrow function ([#2061][] @ljharb) +* [`no-array-index-key`]: catch `.toString` and `String()` usage ([#2813][] @RedTn) +* [`function-component-definition`]: do not break on dollar signs ([#3207][] @ljharb) +* [`prefer-stateless-function`]: avoid a crash inside `doctrine` ([#2596][] @ljharb) +* [`prop-types`]: catch infinite loop ([#2861][] @ljharb) +* [`forbid-prop-types`]: properly report name in error message; check undestructured arguments ([#2945][] @ljharb) + +### Changed +* [readme] change [`jsx-runtime`] link from branch to sha ([#3160][] @tatsushitoji) +* [Docs] HTTP => HTTPS ([#3133][] @Schweinepriester) +* [readme] Some grammar fixes ([#3186][] @JJ) +* [Docs] [`jsx-no-target-blank`]: Improve readme ([#3169][] @apepper) +* [Docs] [`display-name`]: improve examples ([#3189][] @golopot) +* [Refactor] [`no-invalid-html-attribute`]: sort HTML_ELEMENTS and messages ([#3182][] @Primajin) +* [Docs] [`forbid-foreign-prop-types`]: document `allowInPropTypes` option ([#1815][] @ljharb) +* [Refactor] [`jsx-sort-default-props`]: remove unnecessary code ([#1817][] @ljharb) +* [Docs] [`jsx-no-target-blank`]: fix syntax highlighting ([#3199][] @shamrin) +* [Docs] [`jsx-key`]: improve example ([#3202][] @chnakamura) +* [Refactor] [`jsx-key`]: use more AST selectors (@ljharb) + +[7.29.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.28.0...v7.29.0 +[#3207]: https://github.com/yannickcr/eslint-plugin-react/issues/3207 +[#3202]: https://github.com/yannickcr/eslint-plugin-react/pull/3202 +[#3199]: https://github.com/yannickcr/eslint-plugin-react/pull/3199 +[#3198]: https://github.com/yannickcr/eslint-plugin-react/pull/3198 +[#3195]: https://github.com/yannickcr/eslint-plugin-react/pull/3195 +[#3191]: https://github.com/yannickcr/eslint-plugin-react/pull/3191 +[#3190]: https://github.com/yannickcr/eslint-plugin-react/pull/3190 +[#3189]: https://github.com/yannickcr/eslint-plugin-react/pull/3189 +[#3186]: https://github.com/yannickcr/eslint-plugin-react/pull/3186 +[#3182]: https://github.com/yannickcr/eslint-plugin-react/pull/3182 +[#3174]: https://github.com/yannickcr/eslint-plugin-react/pull/3174 +[#3169]: https://github.com/yannickcr/eslint-plugin-react/pull/3169 +[#3167]: https://github.com/yannickcr/eslint-plugin-react/pull/3167 +[#3163]: https://github.com/yannickcr/eslint-plugin-react/pull/3163 +[#3160]: https://github.com/yannickcr/eslint-plugin-react/pull/3160 +[#3133]: https://github.com/yannickcr/eslint-plugin-react/pull/3133 +[#3002]: https://github.com/yannickcr/eslint-plugin-react/issues/3002 +[#2945]: https://github.com/yannickcr/eslint-plugin-react/issues/2945 +[#2921]: https://github.com/yannickcr/eslint-plugin-react/pull/2921 +[#2861]: https://github.com/yannickcr/eslint-plugin-react/issues/2861 +[#2813]: https://github.com/yannickcr/eslint-plugin-react/pull/2813 +[#2753]: https://github.com/yannickcr/eslint-plugin-react/pull/2753 +[#2614]: https://github.com/yannickcr/eslint-plugin-react/issues/2614 +[#2596]: https://github.com/yannickcr/eslint-plugin-react/issues/2596 +[#2061]: https://github.com/yannickcr/eslint-plugin-react/issues/2061 +[#1817]: https://github.com/yannickcr/eslint-plugin-react/issues/1817 +[#1815]: https://github.com/yannickcr/eslint-plugin-react/issues/1815 +[#1754]: https://github.com/yannickcr/eslint-plugin-react/issues/1754 +[#1046]: https://github.com/yannickcr/eslint-plugin-react/issues/1046 +[#620]: https://github.com/yannickcr/eslint-plugin-react/pull/620 +[#519]: https://github.com/yannickcr/eslint-plugin-react/issues/519 + ## [7.28.0] - 2021.12.22 ### Added @@ -2176,7 +2249,7 @@ React ([#1073][] @jomasti) * Add support for `PureComponent` in [`prefer-stateless-function`][] ([#781][] @tiemevanveen) ### Fixed -* Fix [`jsx-uses-vars`][] to work better with [`prefer-const`](http://eslint.org/docs/rules/prefer-const). You'll need to upgrade to ESLint 3.4.0 to completely fix the compatibility issue ([#716][]) +* Fix [`jsx-uses-vars`][] to work better with [`prefer-const`](https://eslint.org/docs/rules/prefer-const). You'll need to upgrade to ESLint 3.4.0 to completely fix the compatibility issue ([#716][]) * Fix [`require-render-return`][] crash ([#784][]) * Fix related components detection in [`prop-types`][] ([#735][]) * Fix component detection to ignore functions expression without a parent component @@ -3498,6 +3571,8 @@ If you're still not using React 15 you can keep the old behavior by setting the [`forbid-foreign-prop-types`]: docs/rules/forbid-foreign-prop-types.md [`forbid-prop-types`]: docs/rules/forbid-prop-types.md [`function-component-definition`]: docs/rules/function-component-definition.md +[`hook-use-state`]: docs/rules/hook-use-state.md +[`iframe-missing-sandbox`]: docs/rules/iframe-missing-sandbox.md [`jsx-boolean-value`]: docs/rules/jsx-boolean-value.md [`jsx-child-element-spacing`]: docs/rules/jsx-child-element-spacing.md [`jsx-closing-bracket-location`]: docs/rules/jsx-closing-bracket-location.md @@ -3551,6 +3626,7 @@ If you're still not using React 15 you can keep the old behavior by setting the [`no-did-update-set-state`]: docs/rules/no-did-update-set-state.md [`no-direct-mutation-state`]: docs/rules/no-direct-mutation-state.md [`no-find-dom-node`]: docs/rules/no-find-dom-node.md +[`no-invalid-html-attribute`]: docs/rules/no-invalid-html-attribute.md [`no-is-mounted`]: docs/rules/no-is-mounted.md [`no-multi-comp`]: docs/rules/no-multi-comp.md [`no-namespace`]: docs/rules/no-namespace.md @@ -3586,4 +3662,3 @@ If you're still not using React 15 you can keep the old behavior by setting the [`style-prop-object`]: docs/rules/style-prop-object.md [`void-dom-elements-no-children`]: docs/rules/void-dom-elements-no-children.md [`wrap-multilines`]: docs/rules/jsx-wrap-multilines.md -[`no-invalid-html-attribute`]: docs/rules/no-invalid-html-attribute.md diff --git a/README.md b/README.md index b10df85fef..b1efbcd0de 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Use [our preset](#recommended) to get reasonable defaults: ] ``` -If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), extend [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/index.js#L163-L176) in your eslint config (add `"plugin:react/jsx-runtime"` to `"extends"`) to disable the relevant rules. +If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), extend [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/c8917b0885094b5e4cc2a6f613f7fb6f16fe932e/index.js#L163-L176) in your eslint config (add `"plugin:react/jsx-runtime"` to `"extends"`) to disable the relevant rules. You should also specify settings that will be shared across all the plugin rules. ([More about eslint shared settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings)) @@ -45,8 +45,7 @@ You should also specify settings that will be shared across all the plugin rules "fragment": "Fragment", // Fragment to use (may be a property of ), default to "Fragment" "version": "detect", // React version. "detect" automatically picks the version you have installed. // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. - // default to latest and warns if missing - // It will default to "detect" in the future + // It will default to "latest" and warn if missing, and to "detect" in the future "flowVersion": "0.53" // Flow version }, "propWrapperFunctions": [ @@ -132,6 +131,8 @@ Enable the rules that you would like to use. | | | [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md) | Forbid using another component's propTypes | | | | [react/forbid-prop-types](docs/rules/forbid-prop-types.md) | Forbid certain propTypes | | | đź”§ | [react/function-component-definition](docs/rules/function-component-definition.md) | Standardize the way function component get defined | +| | | [react/hook-use-state](docs/rules/hook-use-state.md) | Ensure symmetric naming of useState hook value and setter variables | +| | | [react/iframe-missing-sandbox](docs/rules/iframe-missing-sandbox.md) | Enforce sandbox attribute on iframe elements | | | | [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md) | Reports when this.state is accessed within setState | | | | [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md) | Prevent adjacent inline elements not separated by whitespace. | | | | [react/no-array-index-key](docs/rules/no-array-index-key.md) | Prevent usage of Array index in keys | @@ -261,11 +262,11 @@ This pairs well with the `eslint:all` rule. } ``` -**Note**: These configurations will import `eslint-plugin-react` and enable JSX in [parser options](http://eslint.org/docs/user-guide/configuring#specifying-parser-options). +**Note**: These configurations will import `eslint-plugin-react` and enable JSX in [parser options](https://eslint.org/docs/user-guide/configuring/language-options#specifying-parser-options). # License -`eslint-plugin-react` is licensed under the [MIT License](http://www.opensource.org/licenses/mit-license.php). +`eslint-plugin-react` is licensed under the [MIT License](https://opensource.org/licenses/mit-license.php). [npm-url]: https://npmjs.org/package/eslint-plugin-react diff --git a/docs/rules/boolean-prop-naming.md b/docs/rules/boolean-prop-naming.md index b7610b56bf..3c043ca9c0 100644 --- a/docs/rules/boolean-prop-naming.md +++ b/docs/rules/boolean-prop-naming.md @@ -118,5 +118,5 @@ This value is boolean. It tells if nested props should be validated as well. By ``` [PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html -[TypeScript]: http://www.typescriptlang.org/ +[TypeScript]: https://www.typescriptlang.org/ [Flow]: https://flow.org/ diff --git a/docs/rules/default-props-match-prop-types.md b/docs/rules/default-props-match-prop-types.md index 35974d6186..4e0b25bd6f 100644 --- a/docs/rules/default-props-match-prop-types.md +++ b/docs/rules/default-props-match-prop-types.md @@ -196,5 +196,5 @@ If you don't care about stray `defaultsProps` in your components, you can disabl - [Official React documentation on defaultProps](https://facebook.github.io/react/docs/typechecking-with-proptypes.html#default-prop-values) [PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html -[TypeScript]: http://www.typescriptlang.org/ +[TypeScript]: https://www.typescriptlang.org/ [Flow]: https://flow.org/ diff --git a/docs/rules/display-name.md b/docs/rules/display-name.md index 9c0d911623..cc895ffd7e 100644 --- a/docs/rules/display-name.md +++ b/docs/rules/display-name.md @@ -12,6 +12,14 @@ var Hello = createReactClass({ return
Hello {this.props.name}
; } }); + +const Hello = React.memo(({ a }) => { + return <>{a} +}) + +export default ({ a }) => { + return <>{a} +} ``` Examples of **correct** code for this rule: @@ -23,6 +31,10 @@ var Hello = createReactClass({ return
Hello {this.props.name}
; } }); + +const Hello = React.memo(function Hello({ a }) { + return <>{a} +}) ``` ## Rule Options @@ -37,7 +49,7 @@ var Hello = createReactClass({ When `true` the rule will ignore the name set by the transpiler and require a `displayName` property in this case. -Examples of **correct** code for this rule: +Examples of **correct** code for `{ ignoreTranspilerName: true }` option: ```jsx var Hello = createReactClass({ @@ -66,7 +78,7 @@ export default function Hello({ name }) { Hello.displayName = 'Hello'; ``` -Examples of **incorrect** code for this rule: +Examples of **incorrect** code for `{ ignoreTranspilerName: true }` option: ```jsx var Hello = createReactClass({ diff --git a/docs/rules/forbid-foreign-prop-types.md b/docs/rules/forbid-foreign-prop-types.md index ae8cb5ab0a..64b99f8fc5 100644 --- a/docs/rules/forbid-foreign-prop-types.md +++ b/docs/rules/forbid-foreign-prop-types.md @@ -2,7 +2,7 @@ This rule forbids using another component's prop types unless they are explicitly imported/exported. This allows people who want to use [babel-plugin-transform-react-remove-prop-types](https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types) to remove propTypes from their components in production builds, to do so safely. -In order to ensure that imports are explicitly exported it is recommended to use the ["named" rule in eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/named.md) in conjunction with this rule. +In order to ensure that imports are explicitly exported it is recommended to use the ["named" rule in eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import/blob/HEAD/docs/rules/named.md) in conjunction with this rule. ## Rule Details @@ -25,6 +25,18 @@ Examples of **correct** code for this rule: import SomeComponent, {propTypes as someComponentPropTypes} from './SomeComponent'; ``` +## Rule Options + +```js +... +"react/forbid-foreign-prop-types": [, { "allowInPropTypes": [] }] +... +``` + +### `allowInPropTypes` + +If `true`, the rule will not warn on foreign propTypes usage inside a propTypes declaration. + ## When Not To Use It This rule aims to make a certain production optimization, removing prop types, less prone to error. This rule may not be relevant to you if you do not wish to make use of this optimization. diff --git a/docs/rules/hook-use-state.md b/docs/rules/hook-use-state.md new file mode 100644 index 0000000000..0fb071762f --- /dev/null +++ b/docs/rules/hook-use-state.md @@ -0,0 +1,46 @@ +# Ensure destructuring and symmetric naming of useState hook value and setter variables (react/hook-use-state) + +## Rule Details + +This rule checks whether the value and setter variables destructured from a `React.useState()` call are named symmetrically. + +Examples of **incorrect** code for this rule: + +```js +import React from 'react'; +export default function useColor() { + // useState call is not destructured into value + setter pair + const useStateResult = React.useState(); + return useStateResult; +} +``` + +```js +import React from 'react'; +export default function useColor() { + // useState call is destructured into value + setter pair, but identifier + // names do not follow the [thing, setThing] naming convention + const [color, updateColor] = React.useState(); + return useStateResult; +} +``` + +Examples of **correct** code for this rule: + +```js +import React from 'react'; +export default function useColor() { + // useState call is destructured into value + setter pair whose identifiers + // follow the [thing, setThing] naming convention + const [color, setColor] = React.useState(); + return [color, setColor]; +} +``` + +```js +import React from 'react'; +export default function useColor() { + // useState result is directly returned + return React.useState(); +} +``` diff --git a/docs/rules/iframe-missing-sandbox.md b/docs/rules/iframe-missing-sandbox.md new file mode 100644 index 0000000000..f21f98ad9f --- /dev/null +++ b/docs/rules/iframe-missing-sandbox.md @@ -0,0 +1,40 @@ +# Enforce sandbox attribute on iframe elements (react/iframe-missing-sandbox) + +The sandbox attribute enables an extra set of restrictions for the content in the iframe. Using sandbox attribute is considered a good security practice. + +See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox + +## Rule Details + +This rule checks all React iframe elements and verifies that there is sandbox attribute and that it's value is valid. In addition to that it also reports cases where attribute contains `allow-scripts` and `allow-same-origin` at the same time as this combination allows the embedded document to remove the sandbox attribute and bypass the restrictions. + +The following patterns are considered warnings: + +```jsx +var React = require('react'); + +var Frame = () => ( +
+ + {React.createElement('iframe')} +
+); +``` + +The following patterns are **not** considered warnings: + +```jsx +var React = require('react'); + +var Frame = + {React.createElement('iframe', { sandbox: "allow-popups" })} + +); +``` + +## When not to use + +If you don't want to enforce sandbox attribute on iframe elements. diff --git a/docs/rules/jsx-curly-brace-presence.md b/docs/rules/jsx-curly-brace-presence.md index 645d6536f2..9236c54f9f 100644 --- a/docs/rules/jsx-curly-brace-presence.md +++ b/docs/rules/jsx-curly-brace-presence.md @@ -8,15 +8,17 @@ For situations where JSX expressions are unnecessary, please refer to [the React ## Rule Details -By default, this rule will check for and warn about unnecessary curly braces in both JSX props and children. +By default, this rule will check for and warn about unnecessary curly braces in both JSX props and children. For the sake of backwards compatibility, prop values that are JSX elements are not considered by default. -You can pass in options to enforce the presence of curly braces on JSX props or children or both. The same options are available for not allowing unnecessary curly braces as well as ignoring the check. +You can pass in options to enforce the presence of curly braces on JSX props, children, JSX prop values that are JSX elements, or any combination of the three. The same options are available for not allowing unnecessary curly braces as well as ignoring the check. + +**Note**: it is _highly recommended_ that you configure this rule with an object, and that you set "propElementValues" to "always". The ability to omit curly braces around prop values that are JSX elements is obscure, and intentionally undocumented, and should not be relied upon. ## Rule Options ```js ... -"react/jsx-curly-brace-presence": [, { "props": , "children": }] +"react/jsx-curly-brace-presence": [, { "props": , "children": , "propElementValues": }] ... ``` @@ -32,9 +34,9 @@ or alternatively They are `always`, `never` and `ignore` for checking on JSX props and children. -* `always`: always enforce curly braces inside JSX props or/and children -* `never`: never allow unnecessary curly braces inside JSX props or/and children -* `ignore`: ignore the rule for JSX props or/and children +* `always`: always enforce curly braces inside JSX props, children, and/or JSX prop values that are JSX Elements +* `never`: never allow unnecessary curly braces inside JSX props, children, and/or JSX prop values that are JSX Elements +* `ignore`: ignore the rule for JSX props, children, and/or JSX prop values that are JSX Elements If passed in the option to fix, this is how a style violation will get fixed @@ -73,9 +75,31 @@ They can be fixed to: ; ``` +Examples of **incorrect** code for this rule, when configured with `{ props: "always", children: "always", "propElementValues": "always" }`: +```jsx + />; +``` + +They can be fixed to: + +```jsx +} />; +``` + +Examples of **incorrect** code for this rule, when configured with `{ props: "never", children: "never", "propElementValues": "never" }`: +```jsx +} />; +``` + +They can be fixed to: + +```jsx + />; +``` + ### Alternative syntax -The options are also `always`, `never` and `ignore` for the same meanings. +The options are also `always`, `never`, and `ignore` for the same meanings. In this syntax, only a string is provided and the default will be set to that option for checking on both JSX props and children. diff --git a/docs/rules/jsx-curly-spacing.md b/docs/rules/jsx-curly-spacing.md index 2a533195e3..9f428f5d81 100644 --- a/docs/rules/jsx-curly-spacing.md +++ b/docs/rules/jsx-curly-spacing.md @@ -219,7 +219,7 @@ Examples of **correct** code for this rule, when configured with `"never"` and ` ; ``` -Please note that spacing of the object literal curly braces themselves is controlled by the built-in [`object-curly-spacing`](http://eslint.org/docs/rules/object-curly-spacing) rule. +Please note that spacing of the object literal curly braces themselves is controlled by the built-in [`object-curly-spacing`](https://eslint.org/docs/rules/object-curly-spacing) rule. ### Shorthand options diff --git a/docs/rules/jsx-key.md b/docs/rules/jsx-key.md index 9becf034e2..70c4160fb3 100644 --- a/docs/rules/jsx-key.md +++ b/docs/rules/jsx-key.md @@ -22,7 +22,7 @@ Examples of **correct** code for this rule: ```jsx [, , ]; -data.map((x, i) => {x}); +data.map((x) => {x}); ``` @@ -57,6 +57,19 @@ Examples of **incorrect** code for this rule: ; ``` +### `warnOnDuplicates` (default: `false`) + +When `true` the rule will check for any duplicate key prop values. + +Examples of **incorrect** code for this rule: + +```jsx +const spans = [ + , + , +]; +``` + ## When Not To Use It If you are not using JSX then you can disable this rule. diff --git a/docs/rules/jsx-no-target-blank.md b/docs/rules/jsx-no-target-blank.md index 40cdc4259e..23ac6fbe71 100644 --- a/docs/rules/jsx-no-target-blank.md +++ b/docs/rules/jsx-no-target-blank.md @@ -9,7 +9,7 @@ This rule aims to prevent user generated link hrefs and form actions from creati ## Rule Options -```json +```js ... "react/jsx-no-target-blank": [, { "allowReferrer": , @@ -37,7 +37,7 @@ This rule aims to prevent user generated link hrefs and form actions from creati Examples of **incorrect** code for this rule, when configured with `{ "enforceDynamicLinks": "always" }`: ```jsx -var Hello = +var Hello = var Hello = ``` @@ -45,8 +45,8 @@ Examples of **correct** code for this rule: ```jsx var Hello =

-var Hello = -var Hello = +var Hello = +var Hello = var Hello = var Hello = var Hello = @@ -68,7 +68,7 @@ Spread attributes are a handy way of passing programmatically-generated props to ```jsx const unsafeProps = { - href: "http://example.com", + href: "https://example.com", target: "_blank", }; @@ -88,13 +88,13 @@ Defaults to false. If false, this rule will ignore all spread attributes. If tru When option `forms` is set to `true`, the following is considered an error: ```jsx -var Hello =
; +var Hello =
; ``` When option `links` is set to `true`, the following is considered an error: ```jsx -var Hello = +var Hello = ``` ### Custom link components @@ -104,14 +104,14 @@ This rule supports the ability to use custom components for links, such as ` +var Hello = var Hello = ``` Examples of **correct** code for this rule: ```jsx -var Hello = +var Hello = var Hello = var Hello = var Hello = @@ -123,6 +123,8 @@ This rule supports the ability to use custom components for forms. To enable thi ## When To Override It +Modern browsers (Chrome ≥ 88, Edge ≥ 88, Firefox ≥ 79 and Safari ≥ 12.2) automatically imply `rel="noopener"`. Therefore this rule is no longer needed, if legacy browsers are not supported. See https://web.dev/external-anchors-use-rel-noopener/ and https://caniuse.com/mdn-html_elements_a_implicit_noopener for more details. + For links to a trusted host (e.g. internal links to your own site, or links to a another host you control, where you can be certain this security vulnerability does not exist), you may want to keep the HTTP Referer header for analytics purposes. If you do not support Internet Explorer (any version), Chrome < 49, Opera < 36, Firefox < 52, desktop Safari < 10.1 or iOS Safari < 10.3, you may set `allowReferrer` to `true`, keep the HTTP Referer header and only add `rel="noopener"` to your links. diff --git a/docs/rules/jsx-sort-props.md b/docs/rules/jsx-sort-props.md index 49c876ed7e..acafa87b5e 100644 --- a/docs/rules/jsx-sort-props.md +++ b/docs/rules/jsx-sort-props.md @@ -29,9 +29,11 @@ Examples of **correct** code for this rule: "callbacksLast": , "shorthandFirst": , "shorthandLast": , + "multiline": "ignore" | "first" | "last", "ignoreCase": , "noSortAlphabetically": , "reservedFirst": |>, + "locale": "auto" | "any valid locale" }] ... ``` @@ -70,6 +72,42 @@ When `true`, short hand props must be listed after all other props (unless `call ``` +### `multiline` + +Enforced sorting for multiline props + +* `ignore`: Multiline props will not be taken in consideration for sorting. + +* `first`: Multiline props must be listed before all other props (unless `shorthandFirst` is set), but still respecting the alphabetical order. + +* `last`: Multiline props must be listed after all other props (unless either `callbacksLast` or `shorthandLast` are set), but still respecting the alphabetical order. + +Defaults to `ignore`. + +```jsx +// 'jsx-sort-props': [1, { multiline: 'first' }] + + +// 'jsx-sort-props': [1, { multiline: 'last' }] + +``` + ### `noSortAlphabetically` When `true`, alphabetical order is **not** enforced: @@ -98,6 +136,12 @@ With `reservedFirst: ["key"]`, the following will **not** warn: ``` +### `locale` + +Defaults to `"auto"`, meaning, the locale of the current environment. + +Any other string provided here may be passed to `String.prototype.localeCompare` - note that an unknown or invalid locale may throw an exception and crash. + ## When Not To Use It This rule is a formatting preference and not following it won't negatively affect the quality of your code. If alphabetizing props isn't a part of your coding standards, then you can leave this rule off. diff --git a/docs/rules/jsx-uses-vars.md b/docs/rules/jsx-uses-vars.md index a0238f6ff1..4a5e5f1766 100644 --- a/docs/rules/jsx-uses-vars.md +++ b/docs/rules/jsx-uses-vars.md @@ -1,6 +1,6 @@ # Prevent variables used in JSX to be incorrectly marked as unused (react/jsx-uses-vars) -Since 0.17.0 the `eslint` `no-unused-vars` rule does not detect variables used in JSX ([see details](http://eslint.org/blog/2015/03/eslint-0.17.0-released#changes-to-jsxreact-handling)). This rule will find variables used in JSX and mark them as used. +Since 0.17.0 the `eslint` `no-unused-vars` rule does not detect variables used in JSX ([see details](https://eslint.org/blog/2015/03/eslint-0.17.0-released#changes-to-jsxreact-handling)). This rule will find variables used in JSX and mark them as used. This rule only has an effect when the `no-unused-vars` rule is enabled. diff --git a/docs/rules/no-render-return-value.md b/docs/rules/no-render-return-value.md index b528251d14..319bfd142b 100644 --- a/docs/rules/no-render-return-value.md +++ b/docs/rules/no-render-return-value.md @@ -1,6 +1,6 @@ # Prevent usage of the return value of ReactDOM.render (react/no-render-return-value) -> `ReactDOM.render()` currently returns a reference to the root `ReactComponent` instance. However, using this return value is legacy and should be avoided because future versions of React may render components asynchronously in some cases. If you need a reference to the root `ReactComponent` instance, the preferred solution is to attach a [callback ref](http://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute) to the root element. +> `ReactDOM.render()` currently returns a reference to the root `ReactComponent` instance. However, using this return value is legacy and should be avoided because future versions of React may render components asynchronously in some cases. If you need a reference to the root `ReactComponent` instance, the preferred solution is to attach a [callback ref](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) to the root element. Source: [ReactDOM documentation](https://facebook.github.io/react/docs/react-dom.html#render) diff --git a/docs/rules/no-unused-prop-types.md b/docs/rules/no-unused-prop-types.md index c9950dbfc5..6ca79d46cc 100644 --- a/docs/rules/no-unused-prop-types.md +++ b/docs/rules/no-unused-prop-types.md @@ -140,5 +140,5 @@ AComponent.propTypes = { ``` [PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html -[TypeScript]: http://www.typescriptlang.org/ +[TypeScript]: https://www.typescriptlang.org/ [Flow]: https://flow.org/ diff --git a/docs/rules/prefer-stateless-function.md b/docs/rules/prefer-stateless-function.md index 8c2ede9275..9e283c2652 100644 --- a/docs/rules/prefer-stateless-function.md +++ b/docs/rules/prefer-stateless-function.md @@ -6,7 +6,7 @@ Stateless functional components are simpler than class based components and will This rule will check your class based React components for -* methods/properties other than `displayName`, `propTypes`, `contextTypes`, `defaultProps`, `render` and useless constructor (same detection as `eslint` [no-useless-constructor rule](http://eslint.org/docs/rules/no-useless-constructor)) +* methods/properties other than `displayName`, `propTypes`, `contextTypes`, `defaultProps`, `render` and useless constructor (same detection as `eslint` [no-useless-constructor rule](https://eslint.org/docs/rules/no-useless-constructor)) * instance property other than `this.props` and `this.context` * extension of `React.PureComponent` (if the `ignorePureComponents` flag is true) * presence of `ref` attribute in JSX diff --git a/docs/rules/prop-types.md b/docs/rules/prop-types.md index ce0b1e8f32..2eb2a7fd87 100644 --- a/docs/rules/prop-types.md +++ b/docs/rules/prop-types.md @@ -176,5 +176,5 @@ For now we should detect components created with: * an ES6 class that inherit from `React.Component` or `Component` [PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html -[TypeScript]: http://www.typescriptlang.org/ +[TypeScript]: https://www.typescriptlang.org/ [Flow]: https://flow.org/ diff --git a/docs/rules/require-default-props.md b/docs/rules/require-default-props.md index 8feaaf477d..7857710c71 100644 --- a/docs/rules/require-default-props.md +++ b/docs/rules/require-default-props.md @@ -348,5 +348,5 @@ If you don't care about using `defaultProps` for your component's props that are - [Official React documentation on defaultProps](https://facebook.github.io/react/docs/typechecking-with-proptypes.html#default-prop-values) [PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html -[TypeScript]: http://www.typescriptlang.org/ +[TypeScript]: https://www.typescriptlang.org/ [Flow]: https://flow.org/ diff --git a/index.js b/index.js index 6b8fbcb8f9..baabf87df4 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,8 @@ const allRules = { 'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'), 'forbid-prop-types': require('./lib/rules/forbid-prop-types'), 'function-component-definition': require('./lib/rules/function-component-definition'), + 'hook-use-state': require('./lib/rules/hook-use-state'), + 'iframe-missing-sandbox': require('./lib/rules/iframe-missing-sandbox'), 'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'), 'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'), 'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'), diff --git a/lib/rules/forbid-prop-types.js b/lib/rules/forbid-prop-types.js index cd297eb199..0eea6a78d2 100644 --- a/lib/rules/forbid-prop-types.js +++ b/lib/rules/forbid-prop-types.js @@ -114,7 +114,8 @@ module.exports = { } if (value.type === 'CallExpression') { value.arguments.forEach((arg) => { - reportIfForbidden(arg.name, declaration, target); + const name = arg.type === 'MemberExpression' ? arg.property.name : arg.name; + reportIfForbidden(name, declaration, name); }); value = value.callee; } diff --git a/lib/rules/function-component-definition.js b/lib/rules/function-component-definition.js index a20e1e3134..8f20e63366 100644 --- a/lib/rules/function-component-definition.js +++ b/lib/rules/function-component-definition.js @@ -16,7 +16,7 @@ const reportC = require('../util/report'); function buildFunction(template, parts) { return Object.keys(parts) - .reduce((acc, key) => acc.replace(`{${key}}`, parts[key] || ''), template); + .reduce((acc, key) => acc.replace(`{${key}}`, () => (parts[key] || '')), template); } const NAMED_FUNCTION_TEMPLATES = { diff --git a/lib/rules/hook-use-state.js b/lib/rules/hook-use-state.js new file mode 100644 index 0000000000..6bfb604ae8 --- /dev/null +++ b/lib/rules/hook-use-state.js @@ -0,0 +1,152 @@ +/** + * @fileoverview Ensure symmetric naming of useState hook value and setter variables + * @author Duncan Beevers + */ + +'use strict'; + +const Components = require('../util/Components'); +const docsUrl = require('../util/docsUrl'); +const report = require('../util/report'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const messages = { + useStateErrorMessage: 'useState call is not destructured into value + setter pair', +}; + +module.exports = { + meta: { + docs: { + description: 'Ensure symmetric naming of useState hook value and setter variables', + category: 'Best Practices', + recommended: false, + url: docsUrl('hook-use-state'), + }, + messages, + schema: [], + type: 'suggestion', + hasSuggestions: true, + }, + + create: Components.detect((context, components, util) => ({ + CallExpression(node) { + const isImmediateReturn = node.parent + && node.parent.type === 'ReturnStatement'; + + if (isImmediateReturn || !util.isReactHookCall(node, ['useState'])) { + return; + } + + const isDestructuringDeclarator = node.parent + && node.parent.type === 'VariableDeclarator' + && node.parent.id.type === 'ArrayPattern'; + + if (!isDestructuringDeclarator) { + report( + context, + messages.useStateErrorMessage, + 'useStateErrorMessage', + { node } + ); + return; + } + + const variableNodes = node.parent.id.elements; + const valueVariable = variableNodes[0]; + const setterVariable = variableNodes[1]; + + const valueVariableName = valueVariable + ? valueVariable.name + : undefined; + + const setterVariableName = setterVariable + ? setterVariable.name + : undefined; + + const expectedSetterVariableName = valueVariableName ? ( + `set${valueVariableName.charAt(0).toUpperCase()}${valueVariableName.slice(1)}` + ) : undefined; + + const isSymmetricGetterSetterPair = valueVariable + && setterVariable + && setterVariableName === expectedSetterVariableName + && variableNodes.length === 2; + + if (!isSymmetricGetterSetterPair) { + const suggestions = [ + { + desc: 'Destructure useState call into value + setter pair', + fix: (fixer) => { + const fix = fixer.replaceTextRange( + node.parent.id.range, + `[${valueVariableName}, ${expectedSetterVariableName}]` + ); + + return fix; + }, + }, + ]; + + const defaultReactImports = components.getDefaultReactImports(); + const defaultReactImportSpecifier = defaultReactImports + ? defaultReactImports[0] + : undefined; + + const defaultReactImportName = defaultReactImportSpecifier + ? defaultReactImportSpecifier.local.name + : undefined; + + const namedReactImports = components.getNamedReactImports(); + const useStateReactImportSpecifier = namedReactImports + ? namedReactImports.find((specifier) => specifier.imported.name === 'useState') + : undefined; + + const isSingleGetter = valueVariable && variableNodes.length === 1; + const isUseStateCalledWithSingleArgument = node.arguments.length === 1; + if (isSingleGetter && isUseStateCalledWithSingleArgument) { + const useMemoReactImportSpecifier = namedReactImports + && namedReactImports.find((specifier) => specifier.imported.name === 'useMemo'); + + let useMemoCode; + if (useMemoReactImportSpecifier) { + useMemoCode = useMemoReactImportSpecifier.local.name; + } else if (defaultReactImportName) { + useMemoCode = `${defaultReactImportName}.useMemo`; + } else { + useMemoCode = 'useMemo'; + } + + suggestions.unshift({ + desc: 'Replace useState call with useMemo', + fix: (fixer) => [ + // Add useMemo import, if necessary + useStateReactImportSpecifier + && (!useMemoReactImportSpecifier || defaultReactImportName) + && fixer.insertTextAfter(useStateReactImportSpecifier, ', useMemo'), + // Convert single-value destructure to simple assignment + fixer.replaceTextRange(node.parent.id.range, valueVariableName), + // Convert useState call to useMemo + arrow function + dependency array + fixer.replaceTextRange( + node.range, + `${useMemoCode}(() => ${context.getSourceCode().getText(node.arguments[0])}, [])` + ), + ].filter(Boolean), + }); + } + + report( + context, + messages.useStateErrorMessage, + 'useStateErrorMessage', + { + node: node.parent.id, + suggest: suggestions, + } + ); + } + }, + })), +}; diff --git a/lib/rules/iframe-missing-sandbox.js b/lib/rules/iframe-missing-sandbox.js new file mode 100644 index 0000000000..9a8bd4774d --- /dev/null +++ b/lib/rules/iframe-missing-sandbox.js @@ -0,0 +1,142 @@ +/** + * @fileoverview TBD + */ + +'use strict'; + +const docsUrl = require('../util/docsUrl'); +const isCreateElement = require('../util/isCreateElement'); +const report = require('../util/report'); + +const messages = { + attributeMissing: 'An iframe element is missing a sandbox attribute', + invalidValue: 'An iframe element defines a sandbox attribute with invalid value "{{ value }}"', + invalidCombination: 'An iframe element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid', +}; + +const ALLOWED_VALUES = [ + // From https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox + '', + 'allow-downloads-without-user-activation', + 'allow-downloads', + 'allow-forms', + 'allow-modals', + 'allow-orientation-lock', + 'allow-pointer-lock', + 'allow-popups', + 'allow-popups-to-escape-sandbox', + 'allow-presentation', + 'allow-same-origin', + 'allow-scripts', + 'allow-storage-access-by-user-activation', + 'allow-top-navigation', + 'allow-top-navigation-by-user-activation', +]; + +function validateSandboxAttribute(context, node, attribute) { + if (typeof attribute !== 'string') { + // Only string literals are supported for now + return; + } + const values = attribute.split(' '); + let allowScripts = false; + let allowSameOrigin = false; + values.forEach((attributeValue) => { + const trimmedAttributeValue = attributeValue.trim(); + if (ALLOWED_VALUES.indexOf(trimmedAttributeValue) === -1) { + report(context, messages.invalidValue, 'invalidValue', { + node, + data: { + value: trimmedAttributeValue, + }, + }); + } + if (trimmedAttributeValue === 'allow-scripts') { + allowScripts = true; + } + if (trimmedAttributeValue === 'allow-same-origin') { + allowSameOrigin = true; + } + }); + if (allowScripts && allowSameOrigin) { + report(context, messages.invalidCombination, 'invalidCombination', { + node, + }); + } +} + +function checkAttributes(context, node) { + let sandboxAttributeFound = false; + node.attributes.forEach((attribute) => { + if (attribute.type === 'JSXAttribute' + && attribute.name + && attribute.name.type === 'JSXIdentifier' + && attribute.name.name === 'sandbox' + ) { + sandboxAttributeFound = true; + if ( + attribute.value + && attribute.value.type === 'Literal' + && attribute.value.value + ) { + validateSandboxAttribute(context, node, attribute.value.value); + } + } + }); + if (!sandboxAttributeFound) { + report(context, messages.attributeMissing, 'attributeMissing', { + node, + }); + } +} + +function checkProps(context, node) { + let sandboxAttributeFound = false; + if (node.arguments.length > 1) { + const props = node.arguments[1]; + const sandboxProp = props.properties && props.properties.find((x) => x.type === 'Property' && x.key.name === 'sandbox'); + if (sandboxProp) { + sandboxAttributeFound = true; + if (sandboxProp.value && sandboxProp.value.type === 'Literal' && sandboxProp.value.value) { + validateSandboxAttribute(context, node, sandboxProp.value.value); + } + } + } + if (!sandboxAttributeFound) { + report(context, messages.attributeMissing, 'attributeMissing', { + node, + }); + } +} + +module.exports = { + meta: { + docs: { + description: 'Enforce sandbox attribute on iframe elements', + category: 'Best Practices', + recommended: false, + url: docsUrl('iframe-missing-sandbox'), + }, + + schema: [], + + messages, + }, + + create(context) { + return { + 'JSXOpeningElement[name.name="iframe"]'(node) { + checkAttributes(context, node); + }, + + CallExpression(node) { + if (isCreateElement(node, context) && node.arguments && node.arguments.length > 0) { + const tag = node.arguments[0]; + if (tag.type === 'Literal' && tag.value === 'iframe') { + checkProps(context, node); + } + } + }, + }; + }, +}; diff --git a/lib/rules/jsx-curly-brace-presence.js b/lib/rules/jsx-curly-brace-presence.js index 8817a8bae3..5b2ae5127f 100755 --- a/lib/rules/jsx-curly-brace-presence.js +++ b/lib/rules/jsx-curly-brace-presence.js @@ -25,7 +25,7 @@ const OPTION_VALUES = [ OPTION_NEVER, OPTION_IGNORE, ]; -const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER }; +const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER, propElementValues: OPTION_IGNORE }; // ------------------------------------------------------------------------------ // Rule Definition @@ -58,6 +58,7 @@ module.exports = { properties: { props: { enum: OPTION_VALUES }, children: { enum: OPTION_VALUES }, + propElementValues: { enum: OPTION_VALUES }, }, additionalProperties: false, }, @@ -73,7 +74,7 @@ module.exports = { const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g; const ruleOptions = context.options[0]; const userConfig = typeof ruleOptions === 'string' - ? { props: ruleOptions, children: ruleOptions } + ? { props: ruleOptions, children: ruleOptions, propElementValues: ruleOptions } : Object.assign({}, DEFAULT_CONFIG, ruleOptions); function containsLineTerminators(rawStringValue) { @@ -173,22 +174,28 @@ module.exports = { node: JSXExpressionNode, fix(fixer) { const expression = JSXExpressionNode.expression; - const expressionType = expression.type; - const parentType = JSXExpressionNode.parent.type; let textToReplace; - if (parentType === 'JSXAttribute') { - textToReplace = `"${expressionType === 'TemplateLiteral' - ? expression.quasis[0].value.raw - : expression.raw.substring(1, expression.raw.length - 1) - }"`; - } else if (jsxUtil.isJSX(expression)) { + if (jsxUtil.isJSX(expression)) { const sourceCode = context.getSourceCode(); - textToReplace = sourceCode.getText(expression); } else { - textToReplace = expressionType === 'TemplateLiteral' - ? expression.quasis[0].value.cooked : expression.value; + const expressionType = expression && expression.type; + const parentType = JSXExpressionNode.parent.type; + + if (parentType === 'JSXAttribute') { + textToReplace = `"${expressionType === 'TemplateLiteral' + ? expression.quasis[0].value.raw + : expression.raw.substring(1, expression.raw.length - 1) + }"`; + } else if (jsxUtil.isJSX(expression)) { + const sourceCode = context.getSourceCode(); + + textToReplace = sourceCode.getText(expression); + } else { + textToReplace = expressionType === 'TemplateLiteral' + ? expression.quasis[0].value.cooked : expression.value; + } } return fixer.replaceText(JSXExpressionNode, textToReplace); @@ -200,6 +207,10 @@ module.exports = { report(context, messages.missingCurly, 'missingCurly', { node: literalNode, fix(fixer) { + if (jsxUtil.isJSX(literalNode)) { + return fixer.replaceText(literalNode, `{${context.getSourceCode().getText(literalNode)}}`); + } + // If a HTML entity name is found, bail out because it can be fixed // by either using the real character or the unicode equivalent. // If it contains any line terminator character, bail out as well. @@ -323,7 +334,8 @@ module.exports = { return adjSiblings.some((x) => x.type && arrayIncludes(['JSXExpressionContainer', 'JSXElement'], x.type)); } - function shouldCheckForUnnecessaryCurly(parent, node, config) { + function shouldCheckForUnnecessaryCurly(node, config) { + const parent = node.parent; // Bail out if the parent is a JSXAttribute & its contents aren't // StringLiteral or TemplateLiteral since e.g // } prop2={...} /> @@ -358,6 +370,9 @@ module.exports = { } function shouldCheckForMissingCurly(node, config) { + if (jsxUtil.isJSX(node)) { + return config.propElementValues !== OPTION_IGNORE; + } if ( isLineBreak(node.raw) || containsOnlyHtmlEntities(node.raw) @@ -381,13 +396,19 @@ module.exports = { // -------------------------------------------------------------------------- return { - JSXExpressionContainer: (node) => { - if (shouldCheckForUnnecessaryCurly(node.parent, node, userConfig)) { + 'JSXAttribute > JSXExpressionContainer > JSXElement'(node) { + if (userConfig.propElementValues === OPTION_NEVER) { + reportUnnecessaryCurly(node.parent); + } + }, + + JSXExpressionContainer(node) { + if (shouldCheckForUnnecessaryCurly(node, userConfig)) { lintUnnecessaryCurly(node); } }, - 'Literal, JSXText': (node) => { + 'JSXAttribute > JSXElement, Literal, JSXText'(node) { if (shouldCheckForMissingCurly(node, userConfig)) { reportMissingCurly(node); } diff --git a/lib/rules/jsx-fragments.js b/lib/rules/jsx-fragments.js index 7354eecc39..38b4dd8b4b 100644 --- a/lib/rules/jsx-fragments.js +++ b/lib/rules/jsx-fragments.js @@ -8,7 +8,7 @@ const elementType = require('jsx-ast-utils/elementType'); const pragmaUtil = require('../util/pragma'); const variableUtil = require('../util/variable'); -const versionUtil = require('../util/version'); +const testReactVersion = require('../util/version').testReactVersion; const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -54,7 +54,7 @@ module.exports = { const closeFragLong = ``; function reportOnReactVersion(node) { - if (!versionUtil.testReactVersion(context, '16.2.0')) { + if (!testReactVersion(context, '>= 16.2.0')) { report(context, messages.fragmentsNotSupported, 'fragmentsNotSupported', { node, }); diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js index dca534229d..9f8692119e 100644 --- a/lib/rules/jsx-indent.js +++ b/lib/rules/jsx-indent.js @@ -104,13 +104,32 @@ module.exports = { * @private */ function getFixerFunction(node, needed) { - return function fix(fixer) { - const indent = Array(needed + 1).join(indentChar); - if (node.type === 'JSXText' || node.type === 'Literal') { + const indent = Array(needed + 1).join(indentChar); + + if (node.type === 'JSXText' || node.type === 'Literal') { + return function fix(fixer) { const regExp = /\n[\t ]*(\S)/g; const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`); return fixer.replaceText(node, fixedText); + }; + } + + if (node.type === 'ReturnStatement') { + const raw = context.getSourceCode().getText(node); + const lines = raw.split('\n'); + if (lines.length > 1) { + return function fix(fixer) { + const lastLineStart = raw.lastIndexOf('\n'); + const lastLine = raw.slice(lastLineStart).replace(/^\n[\t ]*(\S)/, (match, p1) => `\n${indent}${p1}`); + return fixer.replaceTextRange( + [node.range[0] + lastLineStart, node.range[1]], + lastLine + ); + }; } + } + + return function fix(fixer) { return fixer.replaceTextRange( [node.range[0] - node.loc.start.column, node.range[0]], indent @@ -392,6 +411,19 @@ module.exports = { }, Literal: handleLiteral, JSXText: handleLiteral, + + ReturnStatement(node) { + if (!node.parent) { + return; + } + + const openingIndent = getNodeIndent(node); + const closingIndent = getNodeIndent(node, true); + + if (openingIndent !== closingIndent) { + report(node, openingIndent, closingIndent); + } + }, }; }, }; diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js index 99b8ec19cd..4e813fb6a6 100644 --- a/lib/rules/jsx-key.js +++ b/lib/rules/jsx-key.js @@ -7,6 +7,7 @@ const hasProp = require('jsx-ast-utils/hasProp'); const propName = require('jsx-ast-utils/propName'); +const values = require('object.values'); const docsUrl = require('../util/docsUrl'); const pragmaUtil = require('../util/pragma'); const report = require('../util/report'); @@ -18,6 +19,7 @@ const report = require('../util/report'); const defaultOptions = { checkFragmentShorthand: false, checkKeyMustBeforeSpread: false, + warnOnDuplicates: false, }; const messages = { @@ -26,6 +28,7 @@ const messages = { missingArrayKey: 'Missing "key" prop for element in array', missingArrayKeyUsePrag: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead', keyBeforeSpread: '`key` prop must be placed before any `{...spread}, to avoid conflicting with React’s new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html`', + nonUniqueKeys: '`key` prop must be unique', }; module.exports = { @@ -50,6 +53,10 @@ module.exports = { type: 'boolean', default: defaultOptions.checkKeyMustBeforeSpread, }, + warnOnDuplicates: { + type: 'boolean', + default: defaultOptions.warnOnDuplicates, + }, }, additionalProperties: false, }], @@ -59,6 +66,7 @@ module.exports = { const options = Object.assign({}, defaultOptions, context.options[0]); const checkFragmentShorthand = options.checkFragmentShorthand; const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread; + const warnOnDuplicates = options.warnOnDuplicates; const reactPragma = pragmaUtil.getFromContext(context); const fragmentPragma = pragmaUtil.getFragmentFromContext(context); @@ -96,20 +104,49 @@ module.exports = { }); } + const seen = new WeakSet(); + return { - JSXElement(node) { - if (hasProp(node.openingElement.attributes, 'key')) { - if (checkKeyMustBeforeSpread && isKeyAfterSpread(node.openingElement.attributes)) { - report(context, messages.keyBeforeSpread, 'keyBeforeSpread', { - node, - }); - } + 'ArrayExpression, JSXElement > JSXElement'(node) { + const jsx = (node.type === 'ArrayExpression' ? node.elements : node.parent.children).filter((x) => x.type === 'JSXElement'); + if (jsx.length === 0) { return; } - if (node.parent.type === 'ArrayExpression') { - report(context, messages.missingArrayKey, 'missingArrayKey', { - node, + const map = {}; + jsx.forEach((element) => { + const attrs = element.openingElement.attributes; + const keys = attrs.filter((x) => x.name && x.name.name === 'key'); + + if (keys.length === 0) { + report(context, messages.missingArrayKey, 'missingArrayKey', { + node: element, + }); + } else { + keys.forEach((attr) => { + const value = context.getSourceCode().getText(attr.value); + if (!map[value]) { map[value] = []; } + map[value].push(attr); + + if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) { + report(context, messages.keyBeforeSpread, 'keyBeforeSpread', { + node: node.type === 'ArrayExpression' ? node : node.parent, + }); + } + }); + } + }); + + if (warnOnDuplicates) { + values(map).filter((v) => v.length > 1).forEach((v) => { + v.forEach((n) => { + if (!seen.has(n)) { + seen.add(n); + report(context, messages.nonUniqueKeys, 'nonUniqueKeys', { + node: n, + }); + } + }); }); } }, @@ -131,19 +168,19 @@ module.exports = { }, // Array.prototype.map - 'CallExpression, OptionalCallExpression'(node) { - if (node.callee && node.callee.type !== 'MemberExpression' && node.callee.type !== 'OptionalMemberExpression') { - return; - } - - if (node.callee && node.callee.property && node.callee.property.name !== 'map') { - return; - } - + // eslint-disable-next-line no-multi-str + 'CallExpression[callee.type="MemberExpression"][callee.property.name="map"],\ + CallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"],\ + OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\ + OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) { const fn = node.arguments[0]; const isFn = fn && fn.type === 'FunctionExpression'; const isArrFn = fn && fn.type === 'ArrowFunctionExpression'; + if (!fn && !isFn && !isArrFn) { + return; + } + if (isArrFn && (fn.body.type === 'JSXElement' || fn.body.type === 'JSXFragment')) { checkIteratorElement(fn.body); } diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js index dbece120c5..1f6edc9e07 100644 --- a/lib/rules/jsx-no-target-blank.js +++ b/lib/rules/jsx-no-target-blank.js @@ -175,6 +175,7 @@ module.exports = { || (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttribute)); if (hasDangerousLink && !hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex)) { const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer'; + const relValue = allowReferrer ? 'noopener' : 'noreferrer'; report(context, messages[messageId], messageId, { node, fix(fixer) { @@ -188,11 +189,11 @@ module.exports = { } if (!relAttribute) { - return fixer.insertTextAfter(nodeWithAttrs.attributes.slice(-1)[0], ' rel="noreferrer"'); + return fixer.insertTextAfter(nodeWithAttrs.attributes.slice(-1)[0], ` rel="${relValue}"`); } if (!relAttribute.value) { - return fixer.insertTextAfter(relAttribute, '="noreferrer"'); + return fixer.insertTextAfter(relAttribute, `="${relValue}"`); } if (relAttribute.value.type === 'Literal') { diff --git a/lib/rules/jsx-sort-default-props.js b/lib/rules/jsx-sort-default-props.js index 36ad64cad3..d291066d45 100644 --- a/lib/rules/jsx-sort-default-props.js +++ b/lib/rules/jsx-sort-default-props.js @@ -7,8 +7,6 @@ const variableUtil = require('../util/variable'); const docsUrl = require('../util/docsUrl'); -const propWrapperUtil = require('../util/propWrapper'); -// const propTypesSortUtil = require('../util/propTypesSort'); const report = require('../util/report'); // ------------------------------------------------------------------------------ @@ -137,26 +135,16 @@ module.exports = { } function checkNode(node) { - switch (node && node.type) { - case 'ObjectExpression': - checkSorted(node.properties); - break; - case 'Identifier': { - const propTypesObject = findVariableByName(node.name); - if (propTypesObject && propTypesObject.properties) { - checkSorted(propTypesObject.properties); - } - break; - } - case 'CallExpression': { - const innerNode = node.arguments && node.arguments[0]; - if (propWrapperUtil.isPropWrapperFunction(context, node.callee.name) && innerNode) { - checkNode(innerNode); - } - break; + if (!node) { + return; + } + if (node.type === 'ObjectExpression') { + checkSorted(node.properties); + } else if (node.type === 'Identifier') { + const propTypesObject = findVariableByName(node.name); + if (propTypesObject && propTypesObject.properties) { + checkSorted(propTypesObject.properties); } - default: - break; } } diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js index 6c864bdea1..8daf981908 100644 --- a/lib/rules/jsx-sort-props.js +++ b/lib/rules/jsx-sort-props.js @@ -6,6 +6,7 @@ 'use strict'; const propName = require('jsx-ast-utils/propName'); +const includes = require('array-includes'); const docsUrl = require('../util/docsUrl'); const jsxUtil = require('../util/jsx'); const report = require('../util/report'); @@ -18,6 +19,10 @@ function isCallbackPropName(name) { return /^on[A-Z]/.test(name); } +function isMultilineProp(node) { + return node.loc.start.line !== node.loc.end.line; +} + const messages = { noUnreservedProps: 'A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}', listIsEmpty: 'A customized reserved first list must not be empty', @@ -25,6 +30,8 @@ const messages = { listCallbacksLast: 'Callbacks must be listed after all other props', listShorthandFirst: 'Shorthand props must be listed before all other props', listShorthandLast: 'Shorthand props must be listed after all other props', + listMultilineFirst: 'Multiline props must be listed before all other props', + listMultilineLast: 'Multiline props must be listed after all other props', sortPropsByAlpha: 'Props should be sorted alphabetically', }; @@ -75,19 +82,36 @@ function contextCompare(a, b, options) { } } + if (options.multiline !== 'ignore') { + const multilineSign = options.multiline === 'first' ? -1 : 1; + const aIsMultiline = isMultilineProp(a); + const bIsMultiline = isMultilineProp(b); + if (aIsMultiline && !bIsMultiline) { + return multilineSign; + } + if (!aIsMultiline && bIsMultiline) { + return -multilineSign; + } + } + if (options.noSortAlphabetically) { return 0; } + const actualLocale = options.locale === 'auto' ? undefined : options.locale; + if (options.ignoreCase) { aProp = aProp.toLowerCase(); bProp = bProp.toLowerCase(); - return aProp.localeCompare(bProp); + return aProp.localeCompare(bProp, actualLocale); } if (aProp === bProp) { return 0; } - return aProp < bProp ? -1 : 1; + if (options.locale === 'auto') { + return aProp < bProp ? -1 : 1; + } + return aProp.localeCompare(bProp, actualLocale); } /** @@ -127,8 +151,10 @@ const generateFixerFunction = (node, context, reservedList) => { const callbacksLast = configuration.callbacksLast || false; const shorthandFirst = configuration.shorthandFirst || false; const shorthandLast = configuration.shorthandLast || false; + const multiline = configuration.multiline || 'ignore'; const noSortAlphabetically = configuration.noSortAlphabetically || false; const reservedFirst = configuration.reservedFirst || false; + const locale = configuration.locale || 'auto'; // Sort props according to the context. Only supports ignoreCase. // Since we cannot safely move JSXSpreadAttribute (due to potential variable overrides), @@ -138,9 +164,11 @@ const generateFixerFunction = (node, context, reservedList) => { callbacksLast, shorthandFirst, shorthandLast, + multiline, noSortAlphabetically, reservedFirst, reservedList, + locale, }; const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes); const sortedAttributeGroups = sortableAttributeGroups @@ -213,6 +241,34 @@ function validateReservedFirstConfig(context, reservedFirst) { } } +const reportedNodeAttributes = new WeakMap(); +/** + * Check if the current node attribute has already been reported with the same error type + * if that's the case then we don't report a new error + * otherwise we report the error + * @param {Object} nodeAttribute The node attribute to be reported + * @param {string} errorType The error type to be reported + * @param {Object} node The parent node for the node attribute + * @param {Object} context The context of the rule + * @param {Array} reservedList The list of reserved props + */ +function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) { + const errors = reportedNodeAttributes.get(nodeAttribute) || []; + + if (includes(errors, errorType)) { + return; + } + + errors.push(errorType); + + reportedNodeAttributes.set(nodeAttribute, errors); + + report(context, messages[errorType], errorType, { + node: nodeAttribute.name, + fix: generateFixerFunction(node, context, reservedList), + }); +} + module.exports = { meta: { docs: { @@ -241,6 +297,11 @@ module.exports = { shorthandLast: { type: 'boolean', }, + // Whether multiline properties should be listed first or last + multiline: { + enum: ['ignore', 'first', 'last'], + default: 'ignore', + }, ignoreCase: { type: 'boolean', }, @@ -251,6 +312,10 @@ module.exports = { reservedFirst: { type: ['array', 'boolean'], }, + locale: { + type: 'string', + default: 'auto', + }, }, additionalProperties: false, }], @@ -262,10 +327,12 @@ module.exports = { const callbacksLast = configuration.callbacksLast || false; const shorthandFirst = configuration.shorthandFirst || false; const shorthandLast = configuration.shorthandLast || false; + const multiline = configuration.multiline || 'ignore'; const noSortAlphabetically = configuration.noSortAlphabetically || false; const reservedFirst = configuration.reservedFirst || false; const reservedFirstError = validateReservedFirstConfig(context, reservedFirst); let reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST; + const locale = configuration.locale || 'auto'; return { JSXOpeningElement(node) { @@ -285,6 +352,8 @@ module.exports = { const currentValue = decl.value; const previousIsCallback = isCallbackPropName(previousPropName); const currentIsCallback = isCallbackPropName(currentPropName); + const previousIsMultiline = isMultilineProp(memo); + const currentIsMultiline = isMultilineProp(decl); if (ignoreCase) { previousPropName = previousPropName.toLowerCase(); @@ -304,10 +373,8 @@ module.exports = { return decl; } if (!previousIsReserved && currentIsReserved) { - report(context, messages.listReservedPropsFirst, 'listReservedPropsFirst', { - node: decl.name, - fix: generateFixerFunction(node, context, reservedList), - }); + reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, reservedList); + return memo; } } @@ -319,10 +386,8 @@ module.exports = { } if (previousIsCallback && !currentIsCallback) { // Encountered a non-callback prop after a callback prop - report(context, messages.listCallbacksLast, 'listCallbacksLast', { - node: memo.name, - fix: generateFixerFunction(node, context, reservedList), - }); + reportNodeAttribute(memo, 'listCallbacksLast', node, context, reservedList); + return memo; } } @@ -332,10 +397,8 @@ module.exports = { return decl; } if (!currentValue && previousValue) { - report(context, messages.listShorthandFirst, 'listShorthandFirst', { - node: memo.name, - fix: generateFixerFunction(node, context, reservedList), - }); + reportNodeAttribute(decl, 'listShorthandFirst', node, context, reservedList); + return memo; } } @@ -345,10 +408,34 @@ module.exports = { return decl; } if (currentValue && !previousValue) { - report(context, messages.listShorthandLast, 'listShorthandLast', { - node: memo.name, - fix: generateFixerFunction(node, context, reservedList), - }); + reportNodeAttribute(memo, 'listShorthandLast', node, context, reservedList); + + return memo; + } + } + + if (multiline === 'first') { + if (previousIsMultiline && !currentIsMultiline) { + // Exiting the multiline prop section + return decl; + } + if (!previousIsMultiline && currentIsMultiline) { + // Encountered a non-multiline prop before a multiline prop + reportNodeAttribute(decl, 'listMultilineFirst', node, context, reservedList); + + return memo; + } + } + + if (multiline === 'last') { + if (!previousIsMultiline && currentIsMultiline) { + // Entering the multiline prop section + return decl; + } + if (previousIsMultiline && !currentIsMultiline) { + // Encountered a non-multiline prop after a multiline prop + reportNodeAttribute(memo, 'listMultilineLast', node, context, reservedList); + return memo; } } @@ -356,15 +443,13 @@ module.exports = { if ( !noSortAlphabetically && ( - ignoreCase - ? previousPropName.localeCompare(currentPropName) > 0 + (ignoreCase || locale !== 'auto') + ? previousPropName.localeCompare(currentPropName, locale === 'auto' ? undefined : locale) > 0 : previousPropName > currentPropName ) ) { - report(context, messages.sortPropsByAlpha, 'sortPropsByAlpha', { - node: decl.name, - fix: generateFixerFunction(node, context, reservedList), - }); + reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, reservedList); + return memo; } diff --git a/lib/rules/no-array-index-key.js b/lib/rules/no-array-index-key.js index 2e9f3db22c..27f75e9a41 100644 --- a/lib/rules/no-array-index-key.js +++ b/lib/rules/no-array-index-key.js @@ -159,6 +159,38 @@ module.exports = { node, }); }); + + return; + } + + if (node.type === 'CallExpression' + && node.callee + && node.callee.type === 'MemberExpression' + && node.callee.object + && isArrayIndex(node.callee.object) + && node.callee.property + && node.callee.property.type === 'Identifier' + && node.callee.property.name === 'toString' + ) { + // key={bar.toString()} + report(context, messages.noArrayIndex, 'noArrayIndex', { + node, + }); + return; + } + + if (node.type === 'CallExpression' + && node.callee + && node.callee.type === 'Identifier' + && node.callee.name === 'String' + && Array.isArray(node.arguments) + && node.arguments.length > 0 + && isArrayIndex(node.arguments[0]) + ) { + // key={String(bar)} + report(context, messages.noArrayIndex, 'noArrayIndex', { + node: node.arguments[0], + }); } } diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js index f050a8234d..08f8cebce6 100644 --- a/lib/rules/no-deprecated.js +++ b/lib/rules/no-deprecated.js @@ -13,7 +13,7 @@ const Components = require('../util/Components'); const astUtil = require('../util/ast'); const docsUrl = require('../util/docsUrl'); const pragmaUtil = require('../util/pragma'); -const versionUtil = require('../util/version'); +const testReactVersion = require('../util/version').testReactVersion; const report = require('../util/report'); // ------------------------------------------------------------------------------ @@ -114,7 +114,7 @@ module.exports = { deprecated && deprecated[method] && deprecated[method][0] - && versionUtil.testReactVersion(context, deprecated[method][0]) + && testReactVersion(context, `>= ${deprecated[method][0]}`) ); } diff --git a/lib/rules/no-invalid-html-attribute.js b/lib/rules/no-invalid-html-attribute.js index 1a5b255f4e..274f2586da 100644 --- a/lib/rules/no-invalid-html-attribute.js +++ b/lib/rules/no-invalid-html-attribute.js @@ -39,10 +39,16 @@ const rel = new Map([ ['prerender', new Set(['link'])], ['prev', new Set(['link', 'area', 'a', 'form'])], ['search', new Set(['link', 'area', 'a', 'form'])], + ['shortcut', new Set(['link'])], // generally allowed but needs pair with "icon" + ['shortcut\u0020icon', new Set(['link'])], ['stylesheet', new Set(['link'])], ['tag', new Set(['area', 'a'])], ]); +const pairs = new Map([ + ['shortcut', new Set(['icon'])], +]); + /** * Map between attributes and a mapping between valid values and a set of tags they are valid on * @type {Map>>} @@ -51,152 +57,160 @@ const VALID_VALUES = new Map([ ['rel', rel], ]); +/** + * Map between attributes and a mapping between pair-values and a set of values they are valid with + * @type {Map>>} + */ +const VALID_PAIR_VALUES = new Map([ + ['rel', pairs], +]); + /** * The set of all possible HTML elements. Used for skipping custom types * @type {Set} */ const HTML_ELEMENTS = new Set([ - 'html', - 'base', - 'head', - 'link', - 'meta', - 'style', - 'title', - 'body', + 'a', + 'abbr', + 'acronym', 'address', + 'applet', + 'area', 'article', 'aside', - 'footer', - 'header', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'main', - 'nav', - 'section', - 'blockquote', - 'dd', - 'div', - 'dl', - 'dt', - 'figcaption', - 'figure', - 'hr', - 'li', - 'ol', - 'p', - 'pre', - 'ul', - 'a', - 'abbr', + 'audio', 'b', + 'base', + 'basefont', 'bdi', 'bdo', + 'bgsound', + 'big', + 'blink', + 'blockquote', + 'body', 'br', + 'button', + 'canvas', + 'caption', + 'center', 'cite', 'code', + 'col', + 'colgroup', + 'content', 'data', + 'datalist', + 'dd', + 'del', + 'details', 'dfn', + 'dialog', + 'dir', + 'div', + 'dl', + 'dt', 'em', + 'embed', + 'fieldset', + 'figcaption', + 'figure', + 'font', + 'footer', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', 'i', + 'iframe', + 'image', + 'img', + 'input', + 'ins', 'kbd', + 'keygen', + 'label', + 'legend', + 'li', + 'link', + 'main', + 'map', 'mark', + 'marquee', + 'math', + 'menu', + 'menuitem', + 'meta', + 'meter', + 'nav', + 'nobr', + 'noembed', + 'noframes', + 'noscript', + 'object', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'param', + 'picture', + 'plaintext', + 'portal', + 'pre', + 'progress', 'q', + 'rb', 'rp', 'rt', + 'rtc', 'ruby', 's', 'samp', + 'script', + 'section', + 'select', + 'shadow', + 'slot', 'small', + 'source', + 'spacer', 'span', + 'strike', 'strong', + 'style', 'sub', + 'summary', 'sup', - 'time', - 'u', - 'var', - 'wbr', - 'area', - 'audio', - 'img', - 'map', - 'track', - 'video', - 'embed', - 'iframe', - 'object', - 'param', - 'picture', - 'portal', - 'source', 'svg', - 'math', - 'canvas', - 'noscript', - 'script', - 'del', - 'ins', - 'caption', - 'col', - 'colgroup', 'table', 'tbody', 'td', + 'template', + 'textarea', 'tfoot', 'th', 'thead', + 'time', + 'title', 'tr', - 'button', - 'datalist', - 'fieldset', - 'form', - 'input', - 'label', - 'legend', - 'meter', - 'optgroup', - 'option', - 'output', - 'progress', - 'select', - 'textarea', - 'details', - 'dialog', - 'menu', - 'summary', - 'slot', - 'template', - 'acronym', - 'applet', - 'basefont', - 'bgsound', - 'big', - 'blink', - 'center', - 'content', - 'dir', - 'font', - 'frame', - 'frameset', - 'hgroup', - 'image', - 'keygen', - 'marquee', - 'menuitem', - 'nobr', - 'noembed', - 'noframes', - 'plaintext', - 'rb', - 'rtc', - 'shadow', - 'spacer', - 'strike', + 'track', 'tt', + 'u', + 'ul', + 'var', + 'video', + 'wbr', 'xmp', ]); @@ -208,14 +222,16 @@ const COMPONENT_ATTRIBUTE_MAP = new Map(); COMPONENT_ATTRIBUTE_MAP.set('rel', new Set(['link', 'a', 'area', 'form'])); const messages = { - onlyStrings: '“{{attributeName}}” attribute only supports strings.', - noEmpty: 'An empty “{{attributeName}}” attribute is meaningless.', + emptyIsMeaningless: 'An empty “{{attributeName}}” attribute is meaningless.', neverValid: '“{{reportingValue}}” is never a valid “{{attributeName}}” attribute value.', - notValidFor: '“{{reportingValue}}” is not a valid “{{attributeName}}” attribute value for <{{elementName}}>.', - spaceDelimited: '”{{attributeName}}“ attribute values should be space delimited.', + noEmpty: 'An empty “{{attributeName}}” attribute is meaningless.', noMethod: 'The ”{{attributeName}}“ attribute cannot be a method.', + notAlone: '“{{reportingValue}}” must be directly followed by “{{missingValue}}”.', + notPaired: '“{{reportingValue}}” can not be directly followed by “{{secondValue}}” without “{{missingValue}}”.', + notValidFor: '“{{reportingValue}}” is not a valid “{{attributeName}}” attribute value for <{{elementName}}>.', onlyMeaningfulFor: 'The ”{{attributeName}}“ attribute only has meaning on the tags: {{tagNames}}', - emptyIsMeaningless: 'An empty “{{attributeName}}” attribute is meaningless.', + onlyStrings: '“{{attributeName}}” attribute only supports strings.', + spaceDelimited: '”{{attributeName}}“ attribute values should be space delimited.', }; function splitIntoRangedParts(node, regex) { @@ -256,10 +272,10 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN return; } - const parts = splitIntoRangedParts(node, /([^\s]+)/g); - for (const part of parts) { - const allowedTags = VALID_VALUES.get(attributeName).get(part.value); - const reportingValue = part.reportingValue; + const singleAttributeParts = splitIntoRangedParts(node, /(\S+)/g); + for (const singlePart of singleAttributeParts) { + const allowedTags = VALID_VALUES.get(attributeName).get(singlePart.value); + const reportingValue = singlePart.reportingValue; if (!allowedTags) { report(context, messages.neverValid, 'neverValid', { node, @@ -268,7 +284,7 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN reportingValue, }, fix(fixer) { - return fixer.removeRange(part.range); + return fixer.removeRange(singlePart.range); }, }); } else if (!allowedTags.has(parentNodeName)) { @@ -280,15 +296,44 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN elementName: parentNodeName, }, fix(fixer) { - return fixer.removeRange(part.range); + return fixer.removeRange(singlePart.range); }, }); } } + const allowedPairsForAttribute = VALID_PAIR_VALUES.get(attributeName); + if (allowedPairsForAttribute) { + const pairAttributeParts = splitIntoRangedParts(node, /(?=(\b\S+\s*\S+))/g); + for (const pairPart of pairAttributeParts) { + for (const allowedPair of allowedPairsForAttribute) { + const pairing = allowedPair[0]; + const siblings = allowedPair[1]; + const attributes = pairPart.reportingValue.split('\u0020'); + const firstValue = attributes[0]; + const secondValue = attributes[1]; + if (firstValue === pairing) { + const lastValue = attributes[attributes.length - 1]; // in case of multiple white spaces + if (!siblings.has(lastValue)) { + const message = secondValue ? messages.notPaired : messages.notAlone; + const messageId = secondValue ? 'notPaired' : 'notAlone'; + report(context, message, messageId, { + node, + data: { + reportingValue: firstValue, + secondValue, + missingValue: Array.from(siblings).join(', '), + }, + }); + } + } + } + } + } + const whitespaceParts = splitIntoRangedParts(node, /(\s+)/g); for (const whitespacePart of whitespaceParts) { - if (whitespacePart.value !== ' ' || whitespacePart.range[0] === (node.range[0] + 1) || whitespacePart.range[1] === (node.range[1] - 1)) { + if (whitespacePart.range[0] === (node.range[0] + 1) || whitespacePart.range[1] === (node.range[1] - 1)) { report(context, messages.spaceDelimited, 'spaceDelimited', { node, data: { attributeName }, @@ -296,6 +341,14 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN return fixer.removeRange(whitespacePart.range); }, }); + } else if (whitespacePart.value !== '\u0020') { + report(context, messages.spaceDelimited, 'spaceDelimited', { + node, + data: { attributeName }, + fix(fixer) { + return fixer.replaceTextRange(whitespacePart.range, '\u0020'); + }, + }); } } } diff --git a/lib/rules/no-render-return-value.js b/lib/rules/no-render-return-value.js index 3813739a99..c243f30759 100644 --- a/lib/rules/no-render-return-value.js +++ b/lib/rules/no-render-return-value.js @@ -5,7 +5,7 @@ 'use strict'; -const versionUtil = require('../util/version'); +const testReactVersion = require('../util/version').testReactVersion; const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -37,16 +37,15 @@ module.exports = { // -------------------------------------------------------------------------- let calleeObjectName = /^ReactDOM$/; - if (versionUtil.testReactVersion(context, '15.0.0')) { + if (testReactVersion(context, '>= 15.0.0')) { calleeObjectName = /^ReactDOM$/; - } else if (versionUtil.testReactVersion(context, '0.14.0')) { + } else if (testReactVersion(context, '^0.14.0')) { calleeObjectName = /^React(DOM)?$/; - } else if (versionUtil.testReactVersion(context, '0.13.0')) { + } else if (testReactVersion(context, '^0.13.0')) { calleeObjectName = /^React$/; } return { - CallExpression(node) { const callee = node.callee; const parent = node.parent; diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js index 400b7e15b4..a1a155216c 100644 --- a/lib/rules/no-unknown-property.js +++ b/lib/rules/no-unknown-property.js @@ -7,7 +7,7 @@ const has = require('object.hasown/polyfill')(); const docsUrl = require('../util/docsUrl'); -const versionUtil = require('../util/version'); +const testReactVersion = require('../util/version').testReactVersion; const report = require('../util/report'); // ------------------------------------------------------------------------------ @@ -135,7 +135,7 @@ const DOM_PROPERTY_NAMES = [ ]; function getDOMPropertyNames(context) { // this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823 - if (!versionUtil.testReactVersion(context, '16.1.0')) { + if (!testReactVersion(context, '>= 16.1.0')) { return ['allowTransparency'].concat(DOM_PROPERTY_NAMES); } return DOM_PROPERTY_NAMES; @@ -154,7 +154,7 @@ function getDOMPropertyNames(context) { const tagConvention = /^[a-z][^-]*$/; function isTagName(node) { if (tagConvention.test(node.parent.name.name)) { - // http://www.w3.org/TR/custom-elements/#type-extension-semantics + // https://www.w3.org/TR/custom-elements/#type-extension-semantics return !node.parent.attributes.some((attrNode) => ( attrNode.type === 'JSXAttribute' && attrNode.name.type === 'JSXIdentifier' diff --git a/lib/rules/no-unsafe.js b/lib/rules/no-unsafe.js index f532a53e06..fed9830469 100644 --- a/lib/rules/no-unsafe.js +++ b/lib/rules/no-unsafe.js @@ -8,7 +8,7 @@ const Components = require('../util/Components'); const astUtil = require('../util/ast'); const docsUrl = require('../util/docsUrl'); -const versionUtil = require('../util/version'); +const testReactVersion = require('../util/version').testReactVersion; const report = require('../util/report'); // ------------------------------------------------------------------------------ @@ -48,7 +48,7 @@ module.exports = { const config = context.options[0] || {}; const checkAliases = config.checkAliases || false; - const isApplicable = versionUtil.testReactVersion(context, '16.3.0'); + const isApplicable = testReactVersion(context, '>= 16.3.0'); if (!isApplicable) { return {}; } diff --git a/lib/rules/no-unused-state.js b/lib/rules/no-unused-state.js index aa1bdee346..0854d0740f 100644 --- a/lib/rules/no-unused-state.js +++ b/lib/rules/no-unused-state.js @@ -243,6 +243,18 @@ module.exports = { classInfo = null; } + function isGDSFP(node) { + const name = getName(node.key); + if ( + !node.static + || name !== 'getDerivedStateFromProps' + || node.value.params.length < 2 // no `state` argument + ) { + return false; + } + return true; + } + return { ClassDeclaration: handleES6ComponentEnter, @@ -314,8 +326,9 @@ module.exports = { // expression, record all the fields of that object as state fields. const unwrappedValueNode = ast.unwrapTSAsExpression(node.value); + const name = getName(node.key); if ( - getName(node.key) === 'state' + name === 'state' && !node.static && unwrappedValueNode && unwrappedValueNode.type === 'ObjectExpression' @@ -345,12 +358,39 @@ module.exports = { } }, + 'PropertyDefinition, ClassProperty'(node) { + if (!isGDSFP(node)) { + return; + } + + const childScope = context.getScope().childScopes.find((x) => x.block === node.value); + if (!childScope) { + return; + } + const scope = childScope.variableScope.childScopes.find((x) => x.block === node.value); + const stateArg = node.value.params[1]; // probably "state" + if (!scope.variables) { + return; + } + const argVar = scope.variables.find((x) => x.name === stateArg.name); + + const stateRefs = argVar.references; + + stateRefs.forEach((ref) => { + const identifier = ref.identifier; + if (identifier && identifier.parent && identifier.parent.type === 'MemberExpression') { + addUsedStateField(identifier.parent.property); + } + }); + }, + 'PropertyDefinition:exit'(node) { if ( classInfo && !node.static && node.value && node.value.type === 'ArrowFunctionExpression' + && !isGDSFP(node) ) { // Forget our set of local aliases. classInfo.aliases = null; diff --git a/lib/rules/no-will-update-set-state.js b/lib/rules/no-will-update-set-state.js index 4283fa036f..4f84ee6724 100644 --- a/lib/rules/no-will-update-set-state.js +++ b/lib/rules/no-will-update-set-state.js @@ -6,9 +6,9 @@ 'use strict'; const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule'); -const versionUtil = require('../util/version'); +const testReactVersion = require('../util/version').testReactVersion; module.exports = makeNoMethodSetStateRule( 'componentWillUpdate', - (context) => versionUtil.testReactVersion(context, '16.3.0') + (context) => testReactVersion(context, '>= 16.3.0') ); diff --git a/lib/rules/prefer-exact-props.js b/lib/rules/prefer-exact-props.js index fb50f1e31f..037fa29914 100644 --- a/lib/rules/prefer-exact-props.js +++ b/lib/rules/prefer-exact-props.js @@ -120,7 +120,7 @@ module.exports = { }, Identifier(node) { - if (!utils.getParentStatelessComponent(node)) { + if (!utils.getStatelessComponent(node.parent)) { return; } diff --git a/lib/rules/prefer-stateless-function.js b/lib/rules/prefer-stateless-function.js index 00132c6864..3ade763a4d 100644 --- a/lib/rules/prefer-stateless-function.js +++ b/lib/rules/prefer-stateless-function.js @@ -8,7 +8,7 @@ 'use strict'; const Components = require('../util/Components'); -const versionUtil = require('../util/version'); +const testReactVersion = require('../util/version').testReactVersion; const astUtil = require('../util/ast'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -347,7 +347,7 @@ module.exports = { scope = scope.upper; } const isRender = blockNode && blockNode.key && blockNode.key.name === 'render'; - const allowNull = versionUtil.testReactVersion(context, '15.0.0'); // Stateless components can return null since React 15 + const allowNull = testReactVersion(context, '>= 15.0.0'); // Stateless components can return null since React 15 const isReturningJSX = utils.isReturningJSX(node, !allowNull); const isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false); if ( diff --git a/lib/rules/sort-prop-types.js b/lib/rules/sort-prop-types.js index 8b1a379414..977f246432 100644 --- a/lib/rules/sort-prop-types.js +++ b/lib/rules/sort-prop-types.js @@ -118,6 +118,10 @@ module.exports = { // ); // } + const callbackPropsLastSeen = new WeakSet(); + const requiredPropsFirstSeen = new WeakSet(); + const propsNotSortedSeen = new WeakSet(); + declarations.reduce((prev, curr, idx, decls) => { if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') { return decls[idx + 1]; @@ -142,10 +146,13 @@ module.exports = { } if (!previousIsRequired && currentIsRequired) { // Encountered a non-required prop after a required prop - report(context, messages.requiredPropsFirst, 'requiredPropsFirst', { - node: curr, - // fix - }); + if (!requiredPropsFirstSeen.has(curr)) { + requiredPropsFirstSeen.add(curr); + report(context, messages.requiredPropsFirst, 'requiredPropsFirst', { + node: curr, + // fix + }); + } return curr; } } @@ -157,19 +164,25 @@ module.exports = { } if (previousIsCallback && !currentIsCallback) { // Encountered a non-callback prop after a callback prop - report(context, messages.callbackPropsLast, 'callbackPropsLast', { - node: prev, - // fix - }); + if (!callbackPropsLastSeen.has(prev)) { + callbackPropsLastSeen.add(prev); + report(context, messages.callbackPropsLast, 'callbackPropsLast', { + node: prev, + // fix + }); + } return prev; } } if (!noSortAlphabetically && currentPropName < prevPropName) { - report(context, messages.propsNotSorted, 'propsNotSorted', { - node: curr, + if (!propsNotSortedSeen.has(curr)) { + propsNotSortedSeen.add(curr); + report(context, messages.propsNotSorted, 'propsNotSorted', { + node: curr, // fix - }); + }); + } return prev; } diff --git a/lib/util/Components.js b/lib/util/Components.js index 3d9221d13b..1d89953a20 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -310,10 +310,16 @@ function componentRule(rule, context) { return false; } - const commentAst = doctrine.parse(comment.value, { - unwrap: true, - tags: ['extends', 'augments'], - }); + let commentAst; + try { + commentAst = doctrine.parse(comment.value, { + unwrap: true, + tags: ['extends', 'augments'], + }); + } catch (e) { + // handle a bug in the archived `doctrine`, see #2596 + return false; + } const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent'); diff --git a/lib/util/ast.js b/lib/util/ast.js index cea5682247..d8172289d8 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -320,13 +320,31 @@ function isTSInterfaceHeritage(node) { function isTSInterfaceDeclaration(node) { if (!node) return false; - const nodeType = node.type; + let nodeType = node.type; + if (node.type === 'ExportNamedDeclaration' && node.declaration) { + nodeType = node.declaration.type; + } return nodeType === 'TSInterfaceDeclaration'; } +function isTSTypeDeclaration(node) { + if (!node) return false; + let nodeType = node.type; + let nodeKind = node.kind; + if (node.type === 'ExportNamedDeclaration' && node.declaration) { + nodeType = node.declaration.type; + nodeKind = node.declaration.kind; + } + return nodeType === 'VariableDeclaration' && nodeKind === 'type'; +} + function isTSTypeAliasDeclaration(node) { if (!node) return false; - const nodeType = node.type; + let nodeType = node.type; + if (node.type === 'ExportNamedDeclaration' && node.declaration) { + nodeType = node.declaration.type; + return nodeType === 'TSTypeAliasDeclaration' && node.exportKind === 'type'; + } return nodeType === 'TSTypeAliasDeclaration'; } @@ -380,4 +398,5 @@ module.exports = { isTSFunctionType, isTSTypeQuery, isTSTypeParameterInstantiation, + isTSTypeDeclaration, }; diff --git a/lib/util/jsx.js b/lib/util/jsx.js index 8e84b05d43..e7ae2357d7 100644 --- a/lib/util/jsx.js +++ b/lib/util/jsx.js @@ -8,6 +8,7 @@ const estraverse = require('estraverse'); const elementType = require('jsx-ast-utils/elementType'); const astUtil = require('./ast'); +const variableUtil = require('./variable'); // See https://github.com/babel/babel/blob/ce420ba51c68591e057696ef43e028f41c6e04cd/packages/babel-types/src/validators/react/isCompatTag.js // for why we only test for the first character @@ -125,7 +126,8 @@ function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) { break; case 'JSXElement': case 'JSXFragment': - setFound(); break; + setFound(); + break; case 'CallExpression': if (isCreateElement(childNode)) { setFound(); @@ -137,6 +139,13 @@ function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) { setFound(); } break; + case 'Identifier': { + const variable = variableUtil.findVariableByName(context, childNode.name); + if (isJSX(variable)) { + setFound(); + } + break; + } default: } }, diff --git a/lib/util/makeNoMethodSetStateRule.js b/lib/util/makeNoMethodSetStateRule.js index 800b2ca34d..8f93762c60 100644 --- a/lib/util/makeNoMethodSetStateRule.js +++ b/lib/util/makeNoMethodSetStateRule.js @@ -7,6 +7,7 @@ const docsUrl = require('./docsUrl'); const report = require('./report'); +const testReactVersion = require('./version').testReactVersion; // ------------------------------------------------------------------------------ // Rule Definition @@ -29,7 +30,18 @@ const messages = { noSetState: 'Do not use setState in {{name}}', }; -function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { +const methodNoopsAsOf = { + componentDidMount: '>= 16.3.0', + componentDidUpdate: '>= 16.3.0', +}; + +function shouldBeNoop(context, methodName) { + return methodName in methodNoopsAsOf + && testReactVersion(context, methodNoopsAsOf[methodName]) + && !testReactVersion(context, '999.999.999'); // for when the version is not specified +} + +module.exports = function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { return { meta: { docs: { @@ -66,8 +78,11 @@ function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { // -------------------------------------------------------------------------- return { - CallExpression(node) { + if (shouldBeNoop(context, methodName)) { + return; + } + const callee = node.callee; if ( callee.type !== 'MemberExpression' @@ -101,6 +116,4 @@ function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { }; }, }; -} - -module.exports = makeNoMethodSetStateRule; +}; diff --git a/lib/util/pragma.js b/lib/util/pragma.js index a1cf557362..62c983c3d3 100644 --- a/lib/util/pragma.js +++ b/lib/util/pragma.js @@ -11,7 +11,7 @@ const JS_IDENTIFIER_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/; function getCreateClassFromContext(context) { let pragma = 'createReactClass'; - // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) + // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings) if (context.settings.react && context.settings.react.createClass) { pragma = context.settings.react.createClass; } @@ -23,7 +23,7 @@ function getCreateClassFromContext(context) { function getFragmentFromContext(context) { let pragma = 'Fragment'; - // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) + // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings) if (context.settings.react && context.settings.react.fragment) { pragma = context.settings.react.fragment; } @@ -42,7 +42,7 @@ function getFromContext(context) { if (pragmaNode) { const matches = JSX_ANNOTATION_REGEX.exec(pragmaNode.value); pragma = matches[1].split('.')[0]; - // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) + // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings) } else if (context.settings.react && context.settings.react.pragma) { pragma = context.settings.react.pragma; } diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 7cd4e2ab7b..8da9fb9f15 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -9,7 +9,7 @@ const flatMap = require('array.prototype.flatmap'); const annotations = require('./annotations'); const propsUtil = require('./props'); const variableUtil = require('./variable'); -const versionUtil = require('./version'); +const testFlowVersion = require('./version').testFlowVersion; const propWrapperUtil = require('./propWrapper'); const astUtil = require('./ast'); const isFirstLetterCapitalized = require('./isFirstLetterCapitalized'); @@ -547,6 +547,30 @@ module.exports = function propTypesInstructions(context, components, utils) { if (node.right) return getRightMostTypeName(node.right); } + /** + * Returns true if the node is either a interface or type alias declaration + * @param {ASTNode} node + * @return {boolean} + */ + function filterInterfaceOrTypeAlias(node) { + return ( + astUtil.isTSInterfaceDeclaration(node) || astUtil.isTSTypeAliasDeclaration(node) + ); + } + + /** + * Returns true if the interface or type alias declaration node name matches the type-name str + * @param {ASTNode} node + * @param {string} typeName + * @return {boolean} + */ + function filterInterfaceOrAliasByName(node, typeName) { + return ( + (node.id && node.id.name === typeName) + || (node.declaration && node.declaration.id && node.declaration.id.name === typeName) + ); + } + class DeclarePropTypesForTSTypeAnnotation { constructor(propTypes, declaredPropTypes) { this.propTypes = propTypes; @@ -644,19 +668,22 @@ module.exports = function propTypesInstructions(context, components, utils) { * From line 577 to line 581, and line 588 to line 590 are trying to handle typescript-eslint-parser * Need to be deprecated after remove typescript-eslint-parser support. */ - const candidateTypes = this.sourceCode.ast.body.filter((item) => item.type === 'VariableDeclaration' && item.kind === 'type'); - const declarations = flatMap(candidateTypes, (type) => type.declarations); + const candidateTypes = this.sourceCode.ast.body.filter((item) => astUtil.isTSTypeDeclaration(item)); + + const declarations = flatMap( + candidateTypes, + (type) => type.declarations || (type.declaration && type.declaration.declarations) || type.declaration); // we tried to find either an interface or a type with the TypeReference name const typeDeclaration = declarations.filter((dec) => dec.id.name === typeName); const interfaceDeclarations = this.sourceCode.ast.body - .filter( - (item) => (astUtil.isTSInterfaceDeclaration(item) - || astUtil.isTSTypeAliasDeclaration(item)) - && item.id.name === typeName); + .filter(filterInterfaceOrTypeAlias) + .filter((item) => filterInterfaceOrAliasByName(item, typeName)) + .map((item) => (item.declaration || item)); + if (typeDeclaration.length !== 0) { - typeDeclaration.map((t) => t.init).forEach(this.visitTSNode, this); + typeDeclaration.map((t) => t.init || t.typeAnnotation).forEach(this.visitTSNode, this); } else if (interfaceDeclarations.length !== 0) { interfaceDeclarations.forEach(this.traverseDeclaredInterfaceOrTypeAlias, this); } else { @@ -940,6 +967,7 @@ module.exports = function propTypesInstructions(context, components, utils) { ignorePropsValidation = true; } break; + case 'TSTypeReference': case 'TSTypeAnnotation': { const tsTypeAnnotation = new DeclarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes); ignorePropsValidation = tsTypeAnnotation.shouldIgnorePropTypes; @@ -1052,7 +1080,7 @@ module.exports = function propTypesInstructions(context, components, utils) { try { // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props. // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props. - propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1; + propsParameterPosition = testFlowVersion(context, '>= 0.53.0') ? 0 : 1; } catch (e) { // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52 propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1; @@ -1159,7 +1187,11 @@ module.exports = function propTypesInstructions(context, components, utils) { if (!component) { return; } - markPropTypesAsDeclared(component.node, node.parent.right || node.parent); + try { + markPropTypesAsDeclared(component.node, node.parent.right || node.parent); + } catch (e) { + if (e.constructor !== RangeError) { throw e; } + } } }, diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js index 66e6759e72..3f38445756 100644 --- a/lib/util/usedPropTypes.js +++ b/lib/util/usedPropTypes.js @@ -5,7 +5,7 @@ 'use strict'; const astUtil = require('./ast'); -const versionUtil = require('./version'); +const testReactVersion = require('./version').testReactVersion; const ast = require('./ast'); // ------------------------------------------------------------------------------ @@ -282,7 +282,7 @@ function getPropertyName(node, context, utils, checkAsyncSafeLifeCycles) { } module.exports = function usedPropTypesInstructions(context, components, utils) { - const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0'); + const checkAsyncSafeLifeCycles = testReactVersion(context, '>= 16.3.0'); const propVariables = createPropVariables(); const pushScope = propVariables.pushScope; diff --git a/lib/util/version.js b/lib/util/version.js index 3089ab4665..c762e02bd0 100644 --- a/lib/util/version.js +++ b/lib/util/version.js @@ -8,6 +8,7 @@ const fs = require('fs'); const resolve = require('resolve'); const path = require('path'); +const semver = require('semver'); const error = require('./error'); let warnedForMissingVersion = false; @@ -71,7 +72,7 @@ function detectReactVersion(context) { function getReactVersionFromContext(context) { let confVer = '999.999.999'; - // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) + // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings) if (context.settings && context.settings.react && context.settings.react.version) { let settingsVersion = context.settings.react.version; if (settingsVersion === 'detect') { @@ -88,7 +89,7 @@ function getReactVersionFromContext(context) { warnedForMissingVersion = true; } confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer; - return confVer.split('.').map((part) => Number(part)); + return semver.coerce(confVer.split('.').map((part) => Number(part)).join('.')).version; } // TODO, semver-major: remove context fallback @@ -111,7 +112,7 @@ function detectFlowVersion(context) { function getFlowVersionFromContext(context) { let confVer = '999.999.999'; - // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) + // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings) if (context.settings.react && context.settings.react.flowVersion) { let flowVersion = context.settings.react.flowVersion; if (flowVersion === 'detect') { @@ -126,31 +127,19 @@ function getFlowVersionFromContext(context) { throw 'Could not retrieve flowVersion from settings'; // eslint-disable-line no-throw-literal } confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer; - return confVer.split('.').map((part) => Number(part)); + return semver.coerce(confVer.split('.').map((part) => Number(part)).join('.')).version; } -function normalizeParts(parts) { - return Array.from({ length: 3 }, (_, i) => (parts[i] || 0)); +function test(semverRange, confVer) { + return semver.satisfies(confVer, semverRange); } -function test(context, methodVer, confVer) { - const methodVers = normalizeParts(String(methodVer || '').split('.').map((part) => Number(part))); - const confVers = normalizeParts(confVer); - const higherMajor = methodVers[0] < confVers[0]; - const higherMinor = methodVers[0] === confVers[0] && methodVers[1] < confVers[1]; - const higherOrEqualPatch = methodVers[0] === confVers[0] - && methodVers[1] === confVers[1] - && methodVers[2] <= confVers[2]; - - return higherMajor || higherMinor || higherOrEqualPatch; -} - -function testReactVersion(context, methodVer) { - return test(context, methodVer, getReactVersionFromContext(context)); +function testReactVersion(context, semverRange) { + return test(semverRange, getReactVersionFromContext(context)); } -function testFlowVersion(context, methodVer) { - return test(context, methodVer, getFlowVersionFromContext(context)); +function testFlowVersion(context, semverRange) { + return test(semverRange, getFlowVersionFromContext(context)); } module.exports = { diff --git a/package.json b/package.json index 85094c9cbb..5aaaa611ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react", - "version": "7.28.0", + "version": "7.29.0", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", @@ -33,40 +33,40 @@ "doctrine": "^2.1.0", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "object.entries": "^1.1.5", "object.fromentries": "^2.0.5", "object.hasown": "^1.1.0", "object.values": "^1.1.5", - "prop-types": "^15.7.2", + "prop-types": "^15.8.1", "resolve": "^2.0.0-next.3", "semver": "^6.3.0", "string.prototype.matchall": "^4.0.6" }, "devDependencies": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@babel/plugin-syntax-decorators": "^7.16.0", - "@babel/plugin-syntax-do-expressions": "^7.16.0", - "@babel/plugin-syntax-function-bind": "^7.16.0", - "@babel/preset-react": "^7.16.0", + "@babel/core": "^7.17.5", + "@babel/eslint-parser": "^7.17.0", + "@babel/plugin-syntax-decorators": "^7.17.0", + "@babel/plugin-syntax-do-expressions": "^7.16.7", + "@babel/plugin-syntax-function-bind": "^7.16.7", + "@babel/preset-react": "^7.16.7", "@types/eslint": "=7.2.10", "@types/estree": "^0.0.50", - "@types/node": "^16.11.7", + "@types/node": "^16.11.26", "@typescript-eslint/parser": "^2.34.0 || ^3.10.1 || ^4.0.0 || ^5.0.0", - "aud": "^1.1.5", + "aud": "^2.0.0", "babel-eslint": "^8 || ^9 || ^10.1.0", "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-eslint-plugin": "^2.3.0 || ^3.5.3 || ^4.0.1", - "eslint-plugin-import": "^2.25.3", - "eslint-remote-tester": "^2.0.1", - "eslint-remote-tester-repositories": "^0.0.3", + "eslint-plugin-import": "^2.25.4", + "eslint-remote-tester": "^2.1.1", + "eslint-remote-tester-repositories": "^0.0.4", "eslint-scope": "^3.7.3", "espree": "^3.5.4", "istanbul": "^0.4.5", - "ls-engines": "^0.6.1", - "markdown-magic": "^2.5.2", + "ls-engines": "^0.6.5", + "markdown-magic": "^2.6.0", "mocha": "^5.2.0", "sinon": "^7.5.0", "typescript": "^3.9.9", diff --git a/test/eslint-remote-tester.config.js b/test/eslint-remote-tester.config.js index fd5e2c2802..69ed6abc69 100644 --- a/test/eslint-remote-tester.config.js +++ b/test/eslint-remote-tester.config.js @@ -21,7 +21,12 @@ module.exports = { env: { es6: true, }, - parser: '@typescript-eslint/parser', + overrides: [ + { + files: ['*.ts', '*.tsx', '*.mts', '*.cts'], + parser: '@typescript-eslint/parser', + }, + ], parserOptions: { ecmaVersion: 2020, sourceType: 'module', diff --git a/tests/helpers/parsers.js b/tests/helpers/parsers.js index cc6a29f21f..597bd78441 100644 --- a/tests/helpers/parsers.js +++ b/tests/helpers/parsers.js @@ -64,7 +64,8 @@ const parsers = { `features: [${Array.from(features).join(',')}]`, `parser: ${parser}`, testObject.parserOptions ? `parserOptions: ${JSON.stringify(testObject.parserOptions)}` : [], - testObject.options ? `options: ${JSON.stringify(testObject.options)}` : [] + testObject.options ? `options: ${JSON.stringify(testObject.options)}` : [], + testObject.settings ? `settings: ${JSON.stringify(testObject.settings)}` : [] ); const extraComment = `\n// ${extras.join(', ')}`; diff --git a/tests/lib/rules/default-props-match-prop-types.js b/tests/lib/rules/default-props-match-prop-types.js index ef037ca2f6..f61b5197b1 100644 --- a/tests/lib/rules/default-props-match-prop-types.js +++ b/tests/lib/rules/default-props-match-prop-types.js @@ -778,6 +778,26 @@ ruleTester.run('default-props-match-prop-types', rule, { `, features: ['types'], }, + { + code: ` + import type { FieldProps } from 'redux-form'; + + type Props = FieldProps & { + name: string, + type: string, + label?: string, + placeholder?: string, + disabled?: boolean, + }; + + TextField.defaultProps = { + label: '', + placeholder: '', + disabled: false, + }; + `, + features: ['types'], + }, ]), invalid: parsers.all([ @@ -1736,5 +1756,32 @@ ruleTester.run('default-props-match-prop-types', rule, { }, ], }, + { + code: ` + export type SharedProps = {| + disabled: boolean, + |}; + + type Props = {| + ...SharedProps, + focused?: boolean, + |}; + + class Foo extends React.Component { + static defaultProps = { + disabled: false + }; + }; + `, + features: ['flow'], + errors: [ + { + messageId: 'requiredHasDefault', + data: { + name: 'disabled', + }, + }, + ], + }, ]), }); diff --git a/tests/lib/rules/display-name.js b/tests/lib/rules/display-name.js index 4ce226b27f..662f8e273c 100644 --- a/tests/lib/rules/display-name.js +++ b/tests/lib/rules/display-name.js @@ -535,6 +535,50 @@ ruleTester.run('display-name', rule, { `, features: ['ts', 'no-babel'], }, + { + code: ` + function Test() { + const data = [ + { + name: 'Bob', + }, + ]; + + const columns = [ + { + Header: 'Name', + accessor: 'name', + Cell: ({ value }) =>
{value}
, + }, + ]; + + return ; + } + `, + }, + { + code: ` + class Test { + render() { + const data = [ + { + name: 'Bob', + }, + ]; + + const columns = [ + { + Header: 'Name', + accessor: 'name', + Cell: ({ value }) =>
{value}
, + }, + ]; + + return ; + } + } + `, + }, ]), invalid: parsers.all([ diff --git a/tests/lib/rules/forbid-prop-types.js b/tests/lib/rules/forbid-prop-types.js index d97415a88e..ee3a635df7 100644 --- a/tests/lib/rules/forbid-prop-types.js +++ b/tests/lib/rules/forbid-prop-types.js @@ -1719,5 +1719,55 @@ ruleTester.run('forbid-prop-types', rule, { `, errors: 1, }, + { + code: ` + import React from './React'; + + import { arrayOf, object } from 'prop-types'; + + const App = ({ foo }) => ( +
+ Hello world {foo} +
+ ); + + App.propTypes = { + foo: arrayOf(object) + } + + export default App; + `, + errors: [ + { + messageId: 'forbiddenPropType', + data: { target: 'object' }, + }, + ], + }, + { + code: ` + import React from './React'; + + import PropTypes, { arrayOf } from 'prop-types'; + + const App = ({ foo }) => ( +
+ Hello world {foo} +
+ ); + + App.propTypes = { + foo: arrayOf(PropTypes.object) + } + + export default App; + `, + errors: [ + { + messageId: 'forbiddenPropType', + data: { target: 'object' }, + }, + ], + }, ]), }); diff --git a/tests/lib/rules/function-component-definition.js b/tests/lib/rules/function-component-definition.js index b5d02dfd33..2da6396a99 100644 --- a/tests/lib/rules/function-component-definition.js +++ b/tests/lib/rules/function-component-definition.js @@ -969,5 +969,35 @@ ruleTester.run('function-component-definition', rule, { options: [{ namedComponents: ['function-expression', 'function-declaration'] }], errors: [{ messageId: 'function-expression' }], }, + { + code: ` + const genX = (symbol) => \`the symbol is \${symbol}\`; + + const IndexPage = () => { + return ( +
+ Hello World.{genX('$')} +
+ ) + } + + export default IndexPage; + `, + output: ` + const genX = (symbol) => \`the symbol is \${symbol}\`; + + function IndexPage() { + return ( +
+ Hello World.{genX('$')} +
+ ) + } + + export default IndexPage; + `, + options: [{ namedComponents: ['function-declaration'] }], + errors: [{ messageId: 'function-declaration' }], + }, ]), }); diff --git a/tests/lib/rules/hook-use-state.js b/tests/lib/rules/hook-use-state.js new file mode 100644 index 0000000000..a68f952eb8 --- /dev/null +++ b/tests/lib/rules/hook-use-state.js @@ -0,0 +1,549 @@ +/** + * @fileoverview Ensure symmetric naming of setState hook value and setter variables + * @author Duncan Beevers + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/hook-use-state'); +const parsers = require('../../helpers/parsers'); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, +}); + +const tests = { + valid: parsers.all([ + { + code: ` + import { useState } from 'react' + function useColor() { + const [color, setColor] = useState() + return [color, setColor] + } + `, + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [color, setColor] = useState('#ffffff') + return [color, setColor] + } + `, + }, + { + code: ` + import React from 'react' + function useColor() { + const [color, setColor] = React.useState() + return [color, setColor] + } + `, + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [color1, setColor1] = useState() + return [color1, setColor1] + } + `, + }, + { + code: 'useState()', + }, + { + code: 'const result = useState()', + }, + { + code: 'const [color, setFlavor] = useState()', + }, + { + code: ` + import React from 'react' + import useState from 'someOtherUseState' + const [color, setFlavor] = useState() + `, + }, + { + code: ` + import { useRef } from 'react' + const result = useState() + `, + }, + { + code: ` + import { useState as useStateAlternativeName } from 'react' + function useColor() { + const [color, setColor] = useStateAlternativeName() + return [color, setColor] + } + `, + }, + { + code: 'const result = React.useState()', + }, + { + code: ` + import { useState } from 'react' + function useColor() { + return useState() + } + `, + }, + { + code: ` + import { useState } from 'react' + function useColor() { + function useState() { // shadows React's useState + return null + } + + const result = useState() + } + `, + }, + { + code: ` + import React from 'react' + function useColor() { + const React = { + useState: () => { + return null + } + } + + const result = React.useState() + } + `, + }, + { + code: ` + import { useState } from 'react'; + function useColor() { + const [color, setColor] = useState() + return [color, setColor] + } + `, + features: ['ts', 'no-babel-old'], + }, + { + code: ` + import { useState } from 'react'; + function useColor() { + const [color, setColor] = useState('#ffffff') + return [color, setColor] + } + `, + features: ['ts'], + }, + ]), + invalid: parsers.all([ + { + code: ` + import { useState } from 'react'; + const result = useState() + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState } from 'react'; + function useColor() { + const result = useState() + return result + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState as useStateAlternativeName } from 'react' + function useColor() { + const result = useStateAlternativeName() + return result + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import React from 'react' + function useColor() { + const result = React.useState() + return result + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import ReactAlternative from 'react' + function useColor() { + const result = ReactAlternative.useState() + return result + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const result = useState() + return result + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [, , extra1] = useState() + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [, setColor] = useState() + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const { color } = useState() + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [] = useState() + } + `, + errors: [{ + message: 'useState call is not destructured into value + setter pair', + }], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [, , , ,] = useState() + } + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [color] = useState() + } + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + output: ` + import { useState } from 'react' + function useColor() { + const [color, setColor] = useState() + } + `, + }, + ], + }, + ], + }, + { + code: ` + import { useState } from 'react' + function useColor(initialColor) { + const [color] = useState(initialColor) + } + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + desc: 'Replace useState call with useMemo', + output: ` + import { useState, useMemo } from 'react' + function useColor(initialColor) { + const color = useMemo(() => initialColor, []) + } + `, + }, + { + desc: 'Destructure useState call into value + setter pair', + output: ` + import { useState } from 'react' + function useColor(initialColor) { + const [color, setColor] = useState(initialColor) + } + `, + }, + ], + }, + ], + }, + { + code: ` + import { useState, useMemo as useMemoAlternative } from 'react' + function useColor(initialColor) { + const [color] = useState(initialColor) + } + `, + errors: [{ + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + desc: 'Replace useState call with useMemo', + output: ` + import { useState, useMemo as useMemoAlternative } from 'react' + function useColor(initialColor) { + const color = useMemoAlternative(() => initialColor, []) + } + `, + }, + { + desc: 'Destructure useState call into value + setter pair', + output: ` + import { useState, useMemo as useMemoAlternative } from 'react' + function useColor(initialColor) { + const [color, setColor] = useState(initialColor) + } + `, + }, + ], + }], + }, + { + code: ` + import React from 'react' + function useColor(initialColor) { + const [color] = React.useState(initialColor) + } + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + desc: 'Replace useState call with useMemo', + output: ` + import React from 'react' + function useColor(initialColor) { + const color = React.useMemo(() => initialColor, []) + } + `, + }, + { + desc: 'Destructure useState call into value + setter pair', + output: ` + import React from 'react' + function useColor(initialColor) { + const [color, setColor] = React.useState(initialColor) + } + `, + }, + ], + }, + ], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [color, , extra1] = useState() + return [color] + } + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + output: ` + import { useState } from 'react' + function useColor() { + const [color, setColor] = useState() + return [color] + } + `, + }, + ], + }, + ], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [color, setColor, extra1, extra2, extra3] = useState() + return [color, setColor] + } + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + output: ` + import { useState } from 'react' + function useColor() { + const [color, setColor] = useState() + return [color, setColor] + } + `, + }, + ], + }, + ], + }, + { + code: ` + import { useState } from 'react' + const [, makeColor] = useState() + `, + errors: [{ message: 'useState call is not destructured into value + setter pair' }], + }, + { + code: ` + import { useState } from 'react' + const [color, setFlavor, extraneous] = useState() + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + output: ` + import { useState } from 'react' + const [color, setColor] = useState() + `, + }, + ], + }, + ], + }, + { + code: ` + import { useState } from 'react' + const [color, setFlavor] = useState() + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + output: ` + import { useState } from 'react' + const [color, setColor] = useState() + `, + }, + ], + }, + ], + }, + { + code: ` + import { useState } from 'react' + const [color, setFlavor] = useState() + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + output: ` + import { useState } from 'react' + const [color, setColor] = useState() + `, + }, + ], + }, + ], + features: ['ts', 'no-babel-old'], + }, + { + code: ` + import { useState } from 'react' + function useColor() { + const [color, setFlavor] = useState('#ffffff') + return [color, setFlavor] + } + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + output: ` + import { useState } from 'react' + function useColor() { + const [color, setColor] = useState('#ffffff') + return [color, setFlavor] + } + `, + }, + ], + }, + ], + features: ['ts', 'no-babel-old'], + }, + { + code: ` + import React from 'react' + function useColor() { + const [color, setFlavor] = React.useState('#ffffff') + return [color, setFlavor] + } + `, + errors: [ + { + message: 'useState call is not destructured into value + setter pair', + suggestions: [ + { + output: ` + import React from 'react' + function useColor() { + const [color, setColor] = React.useState('#ffffff') + return [color, setFlavor] + } + `, + }, + ], + }, + ], + features: ['ts', 'no-babel-old'], + }, + ]), +}; + +ruleTester.run('hook-set-state-names', rule, tests); diff --git a/tests/lib/rules/iframe-missing-sandbox.js b/tests/lib/rules/iframe-missing-sandbox.js new file mode 100644 index 0000000000..82c4304e14 --- /dev/null +++ b/tests/lib/rules/iframe-missing-sandbox.js @@ -0,0 +1,124 @@ +/** + * @fileoverview TBD + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/iframe-missing-sandbox'); + +const parsers = require('../../helpers/parsers'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, +}; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions }); +ruleTester.run('iframe-missing-sandbox', rule, { + valid: parsers.all([ + { code: '
;' }, + + { code: '' }, + { code: 'React.createElement("iframe", { src: "foo.htm", sandbox: true })' }, + + { code: '' }, + + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: '' }, + { code: 'React.createElement("iframe", { sandbox: "allow-forms" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-modals" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-orientation-lock" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-pointer-lock" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-popups" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-popups-to-escape-sandbox" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-presentation" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-same-origin" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-scripts" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-top-navigation" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-top-navigation-by-user-activation" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-forms allow-modals" })' }, + { code: 'React.createElement("iframe", { sandbox: "allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation" })' }, + ]), + invalid: parsers.all([ + { + code: ';', + errors: [{ messageId: 'attributeMissing' }], + }, + { + code: '', + errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }], + }, + { + code: 'React.createElement("iframe", { sandbox: "__unknown__" })', + errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }], + }, + + { + code: ';', + errors: [{ messageId: 'invalidCombination' }], + }, + { + code: '