diff --git a/.eslintrc b/.eslintrc index c910dcddbe..d1c050d9a7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -50,5 +50,11 @@ "no-template-curly-in-string": 1, }, }, + { + "files": "markdown.config.js", + "rules": { + "no-console": 0, + }, + }, ], } diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index 7409fe9583..0683aaf178 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -60,7 +60,7 @@ jobs: - uses: ljharb/actions/node/run@main name: 'npm install && npm run tests-only' with: - after_install: npm install --no-save "eslint@${{ matrix.eslint }}" + after_install: NPM_CONFIG_LEGACY_PEER_DEPS=true npm install --no-save "eslint@${{ matrix.eslint }}" node-version: ${{ matrix.node-version }} command: 'unit-test' after_success: 'npm run coveralls' diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d96130870..56d653dbe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,65 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +## [7.23.1] - 2021.03.23 + +### Fixed +* version detection: support processor virtual filename ([#2949][] @JounQin) + +[7.23.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.23.0...v7.23.1 +[#2949]: https://github.com/yannickcr/eslint-plugin-react/pull/2949 + +## [7.23.0] - 2021.03.22 + +### Added +* [`jsx-no-target-blank`]: add fixer ([#2862][] @Nokel81) +* [`jsx-pascal-case`]: support minimatch `ignore` option ([#2906][] @bcherny) +* [`jsx-pascal-case`]: support `allowNamespace` option ([#2917][] @kev-y-huang) +* [`jsx-newline`]: Add prevent option ([#2935][] @jsphstls) +* [`no-unstable-nested-components`]: Prevent creating unstable components inside components ([#2750][] @AriPerkkio) +* added `jsx-runtime` config, for the modern JSX runtime transform (@ljharb) + +### Fixed +* [`jsx-no-constructed-context-values`]: avoid a crash with `as X` TS code ([#2894][] @ljharb) +* [`jsx-no-constructed-context-values`]: avoid a crash with boolean shorthand ([#2895][] @ljharb) +* [`static-property-placement`]: do not report non-components ([#2893][] @golopot) +* [`no-array-index-key`]: support optional chaining ([#2897][] @SyMind) +* [`no-typos`]: avoid a crash on bindingless `prop-types` import; add warning ([#2899][] @ljharb) +* [`jsx-curly-brace-presence`]: ignore containers with comments ([#2900][] @golopot) +* [`destructuring-assignment`]: fix a false positive for local prop named `context` in SFC ([#2929][] @SyMind) +* [`jsx-no-target-blank`]: Allow rel="noreferrer" when `allowReferrer` is true ([#2925][] @edemaine) +* [`boolean-prop-naming`]: add check for typescript "boolean" type ([#2930][] @vedadeepta) +* version detection: Add tests that verify versioning works for sibling and child projects ([#2943][] @jcrosetto) +* [`jsx-curly-newline`]: Update error messages ([#2933][] @jbrower2) + +### Changed +* [Docs] [`jsx-no-constructed-context-values`][]: fix invalid example syntax ([#2910][] @kud) +* [readme] Replace lists of rules with tables in readme ([#2908][] @motato1) +* [Docs] added missing curly braces ([#2923][] @Muditxofficial) + +[7.23.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.22.0...v7.23.0 +[#2943]: https://github.com/yannickcr/eslint-plugin-react/pull/2943 +[#2935]: https://github.com/yannickcr/eslint-plugin-react/pull/2935 +[#2933]: https://github.com/yannickcr/eslint-plugin-react/pull/2933 +[#2930]: https://github.com/yannickcr/eslint-plugin-react/pull/2930 +[#2929]: https://github.com/yannickcr/eslint-plugin-react/pull/2929 +[#2925]: https://github.com/yannickcr/eslint-plugin-react/pull/2925 +[#2923]: https://github.com/yannickcr/eslint-plugin-react/pull/2923 +[#2917]: https://github.com/yannickcr/eslint-plugin-react/pull/2917 +[#2910]: https://github.com/yannickcr/eslint-plugin-react/pull/2910 +[#2908]: https://github.com/yannickcr/eslint-plugin-react/pull/2908 +[#2906]: https://github.com/yannickcr/eslint-plugin-react/pull/2906 +[#2900]: https://github.com/yannickcr/eslint-plugin-react/pull/2900 +[#2899]: https://github.com/yannickcr/eslint-plugin-react/issues/2899 +[#2897]: https://github.com/yannickcr/eslint-plugin-react/pull/2897 +[#2895]: https://github.com/yannickcr/eslint-plugin-react/issues/2895 +[#2894]: https://github.com/yannickcr/eslint-plugin-react/issues/2894 +[#2893]: https://github.com/yannickcr/eslint-plugin-react/pull/2893 +[#2862]: https://github.com/yannickcr/eslint-plugin-react/pull/2862 +[#2750]: https://github.com/yannickcr/eslint-plugin-react/pull/2750 + +## [7.22.0] - 2020.12.29 + ### Added * [`jsx-key`]: added `checkKeyMustBeforeSpread` option for new jsx transform ([#2835][] @morlay) * [`jsx-newline`]: add new rule ([#2693][] @jzabala) @@ -31,6 +90,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`no-unused-prop-types`]: Add new example to rule ([#2852][] @thehereward) * [`prop-types`]: fix example ([#2881][] @technote-space) +[7.22.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.21.5...v7.22.0 [#2891]: https://github.com/yannickcr/eslint-plugin-react/pull/2891 [#2883]: https://github.com/yannickcr/eslint-plugin-react/pull/2883 [#2882]: https://github.com/yannickcr/eslint-plugin-react/issues/2882 @@ -3260,3 +3320,4 @@ If you're still not using React 15 you can keep the old behavior by setting the [`function-component-definition`]: docs/rules/function-component-definition.md [`jsx-newline`]: docs/rules/jsx-newline.md [`jsx-no-constructed-context-values`]: docs/rules/jsx-no-constructed-context-values.md +[`no-unstable-nested-components`]: docs/rules/no-unstable-nested-components.md \ No newline at end of file diff --git a/README.md b/README.md index 68461e0ca9..929a8d5aee 100644 --- a/README.md +++ b/README.md @@ -98,101 +98,109 @@ Enable the rules that you would like to use. # List of supported rules +✔: Enabled in the [`recommended`](#recommended) configuration.\ +🔧: Fixable with [`eslint --fix`](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems). + -* [react/boolean-prop-naming](docs/rules/boolean-prop-naming.md): Enforces consistent naming for boolean props -* [react/button-has-type](docs/rules/button-has-type.md): Forbid "button" element without an explicit "type" attribute -* [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md): Enforce all defaultProps are defined and not "required" in propTypes. -* [react/destructuring-assignment](docs/rules/destructuring-assignment.md): Enforce consistent usage of destructuring assignment of props, state, and context -* [react/display-name](docs/rules/display-name.md): Prevent missing displayName in a React component definition -* [react/forbid-component-props](docs/rules/forbid-component-props.md): Forbid certain props on components -* [react/forbid-dom-props](docs/rules/forbid-dom-props.md): Forbid certain props on DOM Nodes -* [react/forbid-elements](docs/rules/forbid-elements.md): Forbid certain elements -* [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 (fixable) -* [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 -* [react/no-children-prop](docs/rules/no-children-prop.md): Prevent passing of children as props. -* [react/no-danger](docs/rules/no-danger.md): Prevent usage of dangerous JSX props -* [react/no-danger-with-children](docs/rules/no-danger-with-children.md): Report when a DOM element is using both children and dangerouslySetInnerHTML -* [react/no-deprecated](docs/rules/no-deprecated.md): Prevent usage of deprecated methods -* [react/no-did-mount-set-state](docs/rules/no-did-mount-set-state.md): Prevent usage of setState in componentDidMount -* [react/no-did-update-set-state](docs/rules/no-did-update-set-state.md): Prevent usage of setState in componentDidUpdate -* [react/no-direct-mutation-state](docs/rules/no-direct-mutation-state.md): Prevent direct mutation of this.state -* [react/no-find-dom-node](docs/rules/no-find-dom-node.md): Prevent usage of findDOMNode -* [react/no-is-mounted](docs/rules/no-is-mounted.md): Prevent usage of isMounted -* [react/no-multi-comp](docs/rules/no-multi-comp.md): Prevent multiple component definition per file -* [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md): Flag shouldComponentUpdate when extending PureComponent -* [react/no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of React.render -* [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of setState -* [react/no-string-refs](docs/rules/no-string-refs.md): Prevent string definitions for references and prevent referencing this.refs -* [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md): Report "this" being used in stateless components -* [react/no-typos](docs/rules/no-typos.md): Prevent common typos -* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Detect unescaped HTML entities, which might represent malformed tags -* [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable) -* [react/no-unsafe](docs/rules/no-unsafe.md): Prevent usage of unsafe lifecycle methods -* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types -* [react/no-unused-state](docs/rules/no-unused-state.md): Prevent definition of unused state fields -* [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md): Prevent usage of setState in componentWillUpdate -* [react/prefer-es6-class](docs/rules/prefer-es6-class.md): Enforce ES5 or ES6 class for React Components -* [react/prefer-read-only-props](docs/rules/prefer-read-only-props.md): Require read-only props. (fixable) -* [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless components to be written as a pure function -* [react/prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition -* [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing React when using JSX -* [react/require-default-props](docs/rules/require-default-props.md): Enforce a defaultProps definition for every prop that is not a required prop. -* [react/require-optimization](docs/rules/require-optimization.md): Enforce React components to have a shouldComponentUpdate method -* [react/require-render-return](docs/rules/require-render-return.md): Enforce ES5 or ES6 class for returning value in render function -* [react/self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children (fixable) -* [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order -* [react/sort-prop-types](docs/rules/sort-prop-types.md): Enforce propTypes declarations alphabetical sorting -* [react/state-in-constructor](docs/rules/state-in-constructor.md): State initialization in an ES6 class component should be in a constructor -* [react/static-property-placement](docs/rules/static-property-placement.md): Defines where React component static properties should be positioned. -* [react/style-prop-object](docs/rules/style-prop-object.md): Enforce style prop value is an object -* [react/void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md): Prevent passing of children to void DOM elements (e.g. `
`). +| ✔ | 🔧 | Rule | Description | +| :---: | :---: | :--- | :--- | +| | | [react/boolean-prop-naming](docs/rules/boolean-prop-naming.md) | Enforces consistent naming for boolean props | +| | | [react/button-has-type](docs/rules/button-has-type.md) | Forbid "button" element without an explicit "type" attribute | +| | | [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md) | Enforce all defaultProps are defined and not "required" in propTypes. | +| | | [react/destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context | +| ✔ | | [react/display-name](docs/rules/display-name.md) | Prevent missing displayName in a React component definition | +| | | [react/forbid-component-props](docs/rules/forbid-component-props.md) | Forbid certain props on components | +| | | [react/forbid-dom-props](docs/rules/forbid-dom-props.md) | Forbid certain props on DOM Nodes | +| | | [react/forbid-elements](docs/rules/forbid-elements.md) | Forbid certain elements | +| | | [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/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 | +| ✔ | | [react/no-children-prop](docs/rules/no-children-prop.md) | Prevent passing of children as props. | +| | | [react/no-danger](docs/rules/no-danger.md) | Prevent usage of dangerous JSX props | +| ✔ | | [react/no-danger-with-children](docs/rules/no-danger-with-children.md) | Report when a DOM element is using both children and dangerouslySetInnerHTML | +| ✔ | | [react/no-deprecated](docs/rules/no-deprecated.md) | Prevent usage of deprecated methods | +| | | [react/no-did-mount-set-state](docs/rules/no-did-mount-set-state.md) | Prevent usage of setState in componentDidMount | +| | | [react/no-did-update-set-state](docs/rules/no-did-update-set-state.md) | Prevent usage of setState in componentDidUpdate | +| ✔ | | [react/no-direct-mutation-state](docs/rules/no-direct-mutation-state.md) | Prevent direct mutation of this.state | +| ✔ | | [react/no-find-dom-node](docs/rules/no-find-dom-node.md) | Prevent usage of findDOMNode | +| ✔ | | [react/no-is-mounted](docs/rules/no-is-mounted.md) | Prevent usage of isMounted | +| | | [react/no-multi-comp](docs/rules/no-multi-comp.md) | Prevent multiple component definition per file | +| | | [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md) | Flag shouldComponentUpdate when extending PureComponent | +| ✔ | | [react/no-render-return-value](docs/rules/no-render-return-value.md) | Prevent usage of the return value of React.render | +| | | [react/no-set-state](docs/rules/no-set-state.md) | Prevent usage of setState | +| ✔ | | [react/no-string-refs](docs/rules/no-string-refs.md) | Prevent string definitions for references and prevent referencing this.refs | +| | | [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md) | Report "this" being used in stateless components | +| | | [react/no-typos](docs/rules/no-typos.md) | Prevent common typos | +| ✔ | | [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md) | Detect unescaped HTML entities, which might represent malformed tags | +| ✔ | 🔧 | [react/no-unknown-property](docs/rules/no-unknown-property.md) | Prevent usage of unknown DOM property | +| | | [react/no-unsafe](docs/rules/no-unsafe.md) | Prevent usage of unsafe lifecycle methods | +| | | [react/no-unstable-nested-components](docs/rules/no-unstable-nested-components.md) | Prevent creating unstable components inside components | +| | | [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md) | Prevent definitions of unused prop types | +| | | [react/no-unused-state](docs/rules/no-unused-state.md) | Prevent definition of unused state fields | +| | | [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md) | Prevent usage of setState in componentWillUpdate | +| | | [react/prefer-es6-class](docs/rules/prefer-es6-class.md) | Enforce ES5 or ES6 class for React Components | +| | 🔧 | [react/prefer-read-only-props](docs/rules/prefer-read-only-props.md) | Require read-only props. | +| | | [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md) | Enforce stateless components to be written as a pure function | +| ✔ | | [react/prop-types](docs/rules/prop-types.md) | Prevent missing props validation in a React component definition | +| ✔ | | [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md) | Prevent missing React when using JSX | +| | | [react/require-default-props](docs/rules/require-default-props.md) | Enforce a defaultProps definition for every prop that is not a required prop. | +| | | [react/require-optimization](docs/rules/require-optimization.md) | Enforce React components to have a shouldComponentUpdate method | +| ✔ | | [react/require-render-return](docs/rules/require-render-return.md) | Enforce ES5 or ES6 class for returning value in render function | +| | 🔧 | [react/self-closing-comp](docs/rules/self-closing-comp.md) | Prevent extra closing tags for components without children | +| | | [react/sort-comp](docs/rules/sort-comp.md) | Enforce component methods order | +| | | [react/sort-prop-types](docs/rules/sort-prop-types.md) | Enforce propTypes declarations alphabetical sorting | +| | | [react/state-in-constructor](docs/rules/state-in-constructor.md) | State initialization in an ES6 class component should be in a constructor | +| | | [react/static-property-placement](docs/rules/static-property-placement.md) | Defines where React component static properties should be positioned. | +| | | [react/style-prop-object](docs/rules/style-prop-object.md) | Enforce style prop value is an object | +| | | [react/void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md) | Prevent passing of children to void DOM elements (e.g. `
`). | ## JSX-specific rules -* [react/jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX (fixable) -* [react/jsx-child-element-spacing](docs/rules/jsx-child-element-spacing.md): Ensures inline tags are not rendered without spaces between them -* [react/jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX (fixable) -* [react/jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md): Validate closing tag location for multiline JSX (fixable) -* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Disallow unnecessary JSX expressions when literals alone are sufficient or enfore JSX expressions on literals in JSX children or attributes (fixable) -* [react/jsx-curly-newline](docs/rules/jsx-curly-newline.md): Enforce consistent line breaks inside jsx curly (fixable) -* [react/jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes (fixable) -* [react/jsx-equals-spacing](docs/rules/jsx-equals-spacing.md): Disallow or enforce spaces around equal signs in JSX attributes (fixable) -* [react/jsx-filename-extension](docs/rules/jsx-filename-extension.md): Restrict file extensions that may contain JSX -* [react/jsx-first-prop-new-line](docs/rules/jsx-first-prop-new-line.md): Ensure proper position of the first property in JSX (fixable) -* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments (fixable) -* [react/jsx-handler-names](docs/rules/jsx-handler-names.md): Enforce event handler naming conventions in JSX -* [react/jsx-indent](docs/rules/jsx-indent.md): Validate JSX indentation (fixable) -* [react/jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX (fixable) -* [react/jsx-key](docs/rules/jsx-key.md): Report missing `key` props in iterators/collection literals -* [react/jsx-max-depth](docs/rules/jsx-max-depth.md): Validate JSX maximum depth -* [react/jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md): Limit maximum of props on a single line in JSX (fixable) -* [react/jsx-newline](docs/rules/jsx-newline.md): Enforce a new line after jsx elements and expressions (fixable) -* [react/jsx-no-bind](docs/rules/jsx-no-bind.md): Prevents usage of Function.prototype.bind and arrow functions in React component props -* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md): Comments inside children section of tag should be placed inside braces -* [react/jsx-no-constructed-context-values](docs/rules/jsx-no-constructed-context-values.md): Prevents JSX context provider values from taking values that will cause needless rerenders. -* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Enforce no duplicate props -* [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent using string literals in React component definition -* [react/jsx-no-script-url](docs/rules/jsx-no-script-url.md): Forbid `javascript:` URLs -* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Forbid `target="_blank"` attribute without `rel="noreferrer"` -* [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX -* [react/jsx-no-useless-fragment](docs/rules/jsx-no-useless-fragment.md): Disallow unnecessary fragments (fixable) -* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX (fixable) -* [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components -* [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md): Disallow multiple spaces between inline JSX props (fixable) -* [react/jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md): Prevent JSX prop spreading -* [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md): Enforce default props alphabetical sorting -* [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable) -* [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable) -* [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md): Validate whitespace in and around the JSX opening and closing brackets (fixable) -* [react/jsx-uses-react](docs/rules/jsx-uses-react.md): Prevent React to be marked as unused -* [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md): Prevent variables used in JSX to be marked as unused -* [react/jsx-wrap-multilines](docs/rules/jsx-wrap-multilines.md): Prevent missing parentheses around multilines JSX (fixable) +| ✔ | 🔧 | Rule | Description | +| :---: | :---: | :--- | :--- | +| | 🔧 | [react/jsx-boolean-value](docs/rules/jsx-boolean-value.md) | Enforce boolean attributes notation in JSX | +| | | [react/jsx-child-element-spacing](docs/rules/jsx-child-element-spacing.md) | Ensures inline tags are not rendered without spaces between them | +| | 🔧 | [react/jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md) | Validate closing bracket location in JSX | +| | 🔧 | [react/jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md) | Validate closing tag location for multiline JSX | +| | 🔧 | [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md) | Disallow unnecessary JSX expressions when literals alone are sufficient or enfore JSX expressions on literals in JSX children or attributes | +| | 🔧 | [react/jsx-curly-newline](docs/rules/jsx-curly-newline.md) | Enforce consistent line breaks inside jsx curly | +| | 🔧 | [react/jsx-curly-spacing](docs/rules/jsx-curly-spacing.md) | Enforce or disallow spaces inside of curly braces in JSX attributes | +| | 🔧 | [react/jsx-equals-spacing](docs/rules/jsx-equals-spacing.md) | Disallow or enforce spaces around equal signs in JSX attributes | +| | | [react/jsx-filename-extension](docs/rules/jsx-filename-extension.md) | Restrict file extensions that may contain JSX | +| | 🔧 | [react/jsx-first-prop-new-line](docs/rules/jsx-first-prop-new-line.md) | Ensure proper position of the first property in JSX | +| | 🔧 | [react/jsx-fragments](docs/rules/jsx-fragments.md) | Enforce shorthand or standard form for React fragments | +| | | [react/jsx-handler-names](docs/rules/jsx-handler-names.md) | Enforce event handler naming conventions in JSX | +| | 🔧 | [react/jsx-indent](docs/rules/jsx-indent.md) | Validate JSX indentation | +| | 🔧 | [react/jsx-indent-props](docs/rules/jsx-indent-props.md) | Validate props indentation in JSX | +| ✔ | | [react/jsx-key](docs/rules/jsx-key.md) | Report missing `key` props in iterators/collection literals | +| | | [react/jsx-max-depth](docs/rules/jsx-max-depth.md) | Validate JSX maximum depth | +| | 🔧 | [react/jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md) | Limit maximum of props on a single line in JSX | +| | 🔧 | [react/jsx-newline](docs/rules/jsx-newline.md) | Require or prevent a new line after jsx elements and expressions. | +| | | [react/jsx-no-bind](docs/rules/jsx-no-bind.md) | Prevents usage of Function.prototype.bind and arrow functions in React component props | +| ✔ | | [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md) | Comments inside children section of tag should be placed inside braces | +| | | [react/jsx-no-constructed-context-values](docs/rules/jsx-no-constructed-context-values.md) | Prevents JSX context provider values from taking values that will cause needless rerenders. | +| ✔ | | [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md) | Enforce no duplicate props | +| | | [react/jsx-no-literals](docs/rules/jsx-no-literals.md) | Prevent using string literals in React component definition | +| | | [react/jsx-no-script-url](docs/rules/jsx-no-script-url.md) | Forbid `javascript:` URLs | +| ✔ | 🔧 | [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md) | Forbid `target="_blank"` attribute without `rel="noreferrer"` | +| ✔ | | [react/jsx-no-undef](docs/rules/jsx-no-undef.md) | Disallow undeclared variables in JSX | +| | 🔧 | [react/jsx-no-useless-fragment](docs/rules/jsx-no-useless-fragment.md) | Disallow unnecessary fragments | +| | 🔧 | [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md) | Limit to one expression per line in JSX | +| | | [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components | +| | 🔧 | [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props | +| | | [react/jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Prevent JSX prop spreading | +| | | [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce default props alphabetical sorting | +| | 🔧 | [react/jsx-sort-props](docs/rules/jsx-sort-props.md) | Enforce props alphabetical sorting | +| | 🔧 | [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md) | Validate spacing before closing bracket in JSX | +| | 🔧 | [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md) | Validate whitespace in and around the JSX opening and closing brackets | +| ✔ | | [react/jsx-uses-react](docs/rules/jsx-uses-react.md) | Prevent React to be marked as unused | +| ✔ | | [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md) | Prevent variables used in JSX to be marked as unused | +| | 🔧 | [react/jsx-wrap-multilines](docs/rules/jsx-wrap-multilines.md) | Prevent missing parentheses around multilines JSX | ## Other useful plugins @@ -217,30 +225,6 @@ To enable this configuration use the `extends` property in your `.eslintrc` conf See [ESLint documentation](http://eslint.org/docs/user-guide/configuring#extending-configuration-files) for more information about extending configuration files. -The rules enabled in this configuration are: - -* [react/display-name](docs/rules/display-name.md) -* [react/jsx-key](docs/rules/jsx-key.md) -* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md) -* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md) -* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md) -* [react/jsx-no-undef](docs/rules/jsx-no-undef.md) -* [react/jsx-uses-react](docs/rules/jsx-uses-react.md) -* [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md) -* [react/no-children-prop](docs/rules/no-children-prop.md) -* [react/no-danger-with-children](docs/rules/no-danger-with-children.md) -* [react/no-deprecated](docs/rules/no-deprecated.md) -* [react/no-direct-mutation-state](docs/rules/no-direct-mutation-state.md) -* [react/no-find-dom-node](docs/rules/no-find-dom-node.md) -* [react/no-is-mounted](docs/rules/no-is-mounted.md) -* [react/no-render-return-value](docs/rules/no-render-return-value.md) -* [react/no-string-refs](docs/rules/no-string-refs.md) -* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md) -* [react/no-unknown-property](docs/rules/no-unknown-property.md) -* [react/prop-types](docs/rules/prop-types.md) -* [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md) -* [react/require-render-return](docs/rules/require-render-return.md) - ## All This plugin also exports an `all` configuration that includes every available rule. diff --git a/docs/rules/jsx-curly-brace-presence.md b/docs/rules/jsx-curly-brace-presence.md index a2c4f180b9..ee8fae8149 100644 --- a/docs/rules/jsx-curly-brace-presence.md +++ b/docs/rules/jsx-curly-brace-presence.md @@ -151,6 +151,7 @@ Examples of **correct** code for this rule, even when configured with `"never"`: */ {' '} {' '} +{/* comment */ } // the comment makes the container necessary ``` ## When Not To Use It diff --git a/docs/rules/jsx-indent.md b/docs/rules/jsx-indent.md index 18573a6dab..064417052c 100644 --- a/docs/rules/jsx-indent.md +++ b/docs/rules/jsx-indent.md @@ -60,6 +60,7 @@ Examples of **incorrect** code for this rule: (bar) =>
hi
} /> + }> // [2, 2, {indentLogicalExpressions: true}] @@ -98,6 +99,7 @@ Examples of **correct** code for this rule: (bar) =>
hi
} /> + }> // [2, 2, {indentLogicalExpressions: true}] diff --git a/docs/rules/jsx-newline.md b/docs/rules/jsx-newline.md index a622c7ae3a..4096c00229 100644 --- a/docs/rules/jsx-newline.md +++ b/docs/rules/jsx-newline.md @@ -1,12 +1,24 @@ -# Enforce a new line after jsx elements and expressions (react/jsx-newline) +# Require or prevent a new line after jsx elements and expressions. (react/jsx-newline) **Fixable:** This rule is automatically fixable using the `--fix` flag on the command line. ## Rule Details -This is a stylistic rule intended to make JSX code more readable by enforcing spaces between adjacent JSX elements and expressions. +This is a stylistic rule intended to make JSX code more readable by requiring or preventing lines between adjacent JSX elements and expressions. -Examples of **incorrect** code for this rule: +## Rule Options +```json +... +"react/jsx-new-line": [, { "prevent": }] +... +``` + +* enabled: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. +* prevent: optional boolean. If `true` prevents empty lines between adjacent JSX elements and expressions. Defaults to `false`. + +## Examples + +Examples of **incorrect** code for this rule, when configured with `{ "prevent": false }`: ```jsx
@@ -33,7 +45,7 @@ Examples of **incorrect** code for this rule:
``` -Examples of **correct** code for this rule: +Examples of **correct** code for this rule, when configured with `{ "prevent": false }`: ```jsx
@@ -60,6 +72,61 @@ Examples of **correct** code for this rule:
``` +Examples of **incorrect** code for this rule, when configured with `{ "prevent": true }`: + + +```jsx +
+ + + + + + + {showSomething === true && } + + + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+``` + +Examples of **correct** code for this rule, when configured with `{ "prevent": true }`: + +```jsx +
+ + +
+``` + +```jsx +
+ + {showSomething === true && } +
+``` + +```jsx +
+ {showSomething === true && } + {showSomethingElse === true ? ( + + ) : ( + + )} +
+``` + ## When Not To Use It You can turn this rule off if you are not concerned with spacing between your JSX elements and expressions. \ No newline at end of file diff --git a/docs/rules/jsx-no-constructed-context-values.md b/docs/rules/jsx-no-constructed-context-values.md index c0b79de23e..6816b98b59 100644 --- a/docs/rules/jsx-no-constructed-context-values.md +++ b/docs/rules/jsx-no-constructed-context-values.md @@ -23,7 +23,7 @@ return ( Examples of **correct** code for this rule: ``` -const foo = useMemo(() => {foo: 'bar'}, []); +const foo = useMemo(() => ({foo: 'bar'}), []); return ( ... @@ -34,4 +34,4 @@ return ( ## Legitimate Uses React Context, and all its child nodes and Consumers are rerendered whenever the value prop changes. Because each Javascript object carries its own *identity*, things like object expressions (`{foo: 'bar'}`) or function expressions get a new identity on every run through the component. This makes the context think it has gotten a new object and can cause needless rerenders and unintended consequences. -This can be a pretty large performance hit because not only will it cause the context providers and consumers to rerender with all the elements in its subtree, the processing for the tree scan react does to render the provider and find consumers is also wasted. \ No newline at end of file +This can be a pretty large performance hit because not only will it cause the context providers and consumers to rerender with all the elements in its subtree, the processing for the tree scan react does to render the provider and find consumers is also wasted. diff --git a/docs/rules/jsx-pascal-case.md b/docs/rules/jsx-pascal-case.md index 92430bfc27..27de599a97 100644 --- a/docs/rules/jsx-pascal-case.md +++ b/docs/rules/jsx-pascal-case.md @@ -40,13 +40,14 @@ Examples of **correct** code for this rule: ```js ... -"react/jsx-pascal-case": [, { allowAllCaps: , ignore: }] +"react/jsx-pascal-case": [, { allowAllCaps: , allowNamespace: , ignore: }] ... ``` * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. * `allowAllCaps`: optional boolean set to `true` to allow components name in all caps (default to `false`). -* `ignore`: optional string-array of component names to ignore during validation. +* `allowNamespace`: optional boolean set to `true` to ignore namespaced components (default to `false`). +* `ignore`: optional string-array of component names to ignore during validation (supports [minimatch](https://github.com/isaacs/minimatch)-style globs). ### `allowAllCaps` @@ -57,6 +58,15 @@ Examples of **correct** code for this rule, when `allowAllCaps` is `true`: ``` +### `allowNamespace` + +Examples of **correct** code for this rule, when `allowNamespace` is `true`: + +```jsx + + +``` + ## When Not To Use It -If you are not using JSX. \ No newline at end of file +If you are not using JSX. diff --git a/docs/rules/no-unstable-nested-components.md b/docs/rules/no-unstable-nested-components.md new file mode 100644 index 0000000000..507d83d56e --- /dev/null +++ b/docs/rules/no-unstable-nested-components.md @@ -0,0 +1,142 @@ +# Prevent creating unstable components inside components (react/no-unstable-nested-components) + +Creating components inside components without memoization leads to unstable components. The nested component and all its children are recreated during each re-render. Given stateful children of the nested component will lose their state on each re-render. + +React reconcilation performs element type comparison with [reference equality](https://github.com/facebook/react/blob/v16.13.1/packages/react-reconciler/src/ReactChildFiber.js#L407). The reference to the same element changes on each re-render when defining components inside the render block. This leads to complete recreation of the current node and all its children. As a result the virtual DOM has to do extra unnecessary work and [possible bugs are introduced](https://codepen.io/ariperkkio/pen/vYLodLB). + +## Rule Details + +The following patterns are considered warnings: + +```jsx +function Component() { + function UnstableNestedComponent() { + return
; + } + + return ( +
+ +
+ ); +} +``` + +```jsx +function SomeComponent({ footer: Footer }) { + return ( +
+
+
+ ); +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +```jsx +class Component extends React.Component { + render() { + function UnstableNestedComponent() { + return
; + } + + return ( +
+ +
+ ); + } +} +``` + +The following patterns are **not** considered warnings: + +```jsx +function OutsideDefinedComponent(props) { + return
; +} + +function Component() { + return ( +
+ +
+ ); +} +``` + +```jsx +function Component() { + const MemoizedNestedComponent = React.useCallback(() =>
, []); + + return ( +
+ +
+ ); +} +``` + +```jsx +function Component() { + return ( + } /> + ) +} +``` + +By default component creation is allowed inside component props only if prop name starts with `render`. See `allowAsProps` option for disabling this limitation completely. + +```jsx +function SomeComponent(props) { + return
{props.renderFooter()}
; +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +## Rule Options + +```js +... +"react/no-unstable-nested-components": [ + "off" | "warn" | "error", + { "allowAsProps": true | false } +] +... +``` + +You can allow component creation inside component props by setting `allowAsProps` option to true. When using this option make sure you are **calling** the props in the receiving component and not using them as elements. + +The following patterns are **not** considered warnings: + +```jsx +function SomeComponent(props) { + return
{props.footer()}
; +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +## When Not To Use It + +If you are not interested in preventing bugs related to re-creation of the nested components or do not care about optimization of virtual DOM. diff --git a/index.js b/index.js index 8edb1177f1..8a0adf7f32 100644 --- a/index.js +++ b/index.js @@ -76,6 +76,7 @@ const allRules = { 'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'), 'no-unknown-property': require('./lib/rules/no-unknown-property'), 'no-unsafe': require('./lib/rules/no-unsafe'), + 'no-unstable-nested-components': require('./lib/rules/no-unstable-nested-components'), 'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'), 'no-unused-state': require('./lib/rules/no-unused-state'), 'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'), @@ -158,6 +159,20 @@ module.exports = { } }, rules: activeRulesConfig + }, + 'jsx-runtime': { + plugins: [ + 'react' + ], + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + rules: { + 'react/react-in-jsx-scope': 0, + 'react/jsx-uses-react': 0 + } } } }; diff --git a/lib/rules/boolean-prop-naming.js b/lib/rules/boolean-prop-naming.js index e61415b969..2f7171820b 100644 --- a/lib/rules/boolean-prop-naming.js +++ b/lib/rules/boolean-prop-naming.js @@ -14,6 +14,10 @@ const propWrapperUtil = require('../util/propWrapper'); // Rule Definition // ------------------------------------------------------------------------------ +// Predefine message for use in context.report conditional. +// messageId will still be usable in tests. +const PATTERN_MISMATCH_MSG = 'Prop name ({{propName}}) doesn\'t match rule ({{pattern}})'; + module.exports = { meta: { docs: { @@ -23,6 +27,10 @@ module.exports = { url: docsUrl('boolean-prop-naming') }, + messages: { + patternMismatch: PATTERN_MISMATCH_MSG + }, + schema: [{ additionalProperties: false, properties: { @@ -78,7 +86,7 @@ module.exports = { if (node.type === 'ExperimentalSpreadProperty' || node.type === 'SpreadElement') { return null; } - if (node.value.property) { + if (node.value && node.value.property) { const name = node.value.property.name; if (name === 'isRequired') { if (node.value.object && node.value.object.property) { @@ -88,7 +96,7 @@ module.exports = { } return name; } - if (node.value.type === 'Identifier') { + if (node.value && node.value.type === 'Identifier') { return node.value.name; } return null; @@ -137,6 +145,16 @@ module.exports = { ); } + function tsCheck(prop) { + if (prop.type !== 'TSPropertySignature') return false; + const typeAnnotation = (prop.typeAnnotation || {}).typeAnnotation; + return ( + typeAnnotation + && typeAnnotation.type === 'TSBooleanKeyword' + && rule.test(getPropName(prop)) === false + ); + } + /** * Checks if prop is nested * @param {Object} prop Property object, single prop type declaration @@ -162,7 +180,7 @@ module.exports = { runCheck(prop.value.arguments[0].properties, addInvalidProp); return; } - if (flowCheck(prop) || regularCheck(prop)) { + if (flowCheck(prop) || regularCheck(prop) || tsCheck(prop)) { addInvalidProp(prop); } }); @@ -193,15 +211,14 @@ module.exports = { function reportInvalidNaming(component) { component.invalidProps.forEach((propNode) => { const propName = getPropName(propNode); - context.report({ + context.report(Object.assign({ node: propNode, - message: config.message || 'Prop name ({{ propName }}) doesn\'t match rule ({{ pattern }})', data: { component: propName, propName, pattern: config.rule } - }); + }, config.message ? {message: config.message} : {messageId: 'patternMismatch'})); }); } @@ -282,6 +299,12 @@ module.exports = { } }, + TSTypeAliasDeclaration(node) { + if (node.typeAnnotation.type === 'TSTypeLiteral') { + objectTypeAnnotations.set(node.id.name, node.typeAnnotation); + } + }, + // eslint-disable-next-line object-shorthand 'Program:exit'() { if (!rule) { @@ -292,22 +315,30 @@ module.exports = { Object.keys(list).forEach((component) => { // If this is a functional component that uses a global type, check it if ( - list[component].node.type === 'FunctionDeclaration' + ( + list[component].node.type === 'FunctionDeclaration' + || list[component].node.type === 'ArrowFunctionExpression' + ) && list[component].node.params && list[component].node.params.length && list[component].node.params[0].typeAnnotation ) { const typeNode = list[component].node.params[0].typeAnnotation; const annotation = typeNode.typeAnnotation; - let propType; if (annotation.type === 'GenericTypeAnnotation') { propType = objectTypeAnnotations.get(annotation.id.name); } else if (annotation.type === 'ObjectTypeAnnotation') { propType = annotation; + } else if (annotation.type === 'TSTypeReference') { + propType = objectTypeAnnotations.get(annotation.typeName.name); } + if (propType) { - validatePropNaming(list[component].node, propType.properties); + validatePropNaming( + list[component].node, + propType.properties || propType.members + ); } } diff --git a/lib/rules/button-has-type.js b/lib/rules/button-has-type.js index 2dde70896f..ddab18bcb4 100644 --- a/lib/rules/button-has-type.js +++ b/lib/rules/button-has-type.js @@ -42,6 +42,14 @@ module.exports = { recommended: false, url: docsUrl('button-has-type') }, + + messages: { + missingType: 'Missing an explicit type attribute for button', + complexType: 'The button type attribute must be specified by a static string or a trivial ternary expression', + invalidValue: '"{{value}}" is an invalid value for button type attribute', + forbiddenValue: '"{{value}}" is an invalid value for button type attribute' + }, + schema: [{ type: 'object', properties: { @@ -68,28 +76,33 @@ module.exports = { function reportMissing(node) { context.report({ node, - message: 'Missing an explicit type attribute for button' + messageId: 'missingType' }); } function reportComplex(node) { context.report({ node, - message: 'The button type attribute must be specified by a static string or a trivial ternary expression' + messageId: 'complexType' }); } function checkValue(node, value) { - const q = (x) => `"${x}"`; if (!(value in configuration)) { context.report({ node, - message: `${q(value)} is an invalid value for button type attribute` + messageId: 'invalidValue', + data: { + value + } }); } else if (!configuration[value]) { context.report({ node, - message: `${q(value)} is a forbidden value for button type attribute` + messageId: 'forbiddenValue', + data: { + value + } }); } } diff --git a/lib/rules/default-props-match-prop-types.js b/lib/rules/default-props-match-prop-types.js index 4d096416d8..aef97fee6c 100644 --- a/lib/rules/default-props-match-prop-types.js +++ b/lib/rules/default-props-match-prop-types.js @@ -21,6 +21,11 @@ module.exports = { url: docsUrl('default-props-match-prop-types') }, + messages: { + requiredHasDefault: 'defaultProp "{{name}}" defined for isRequired propType.', + defaultHasNoType: 'defaultProp "{{name}}" has no corresponding propTypes declaration.' + }, + schema: [{ type: 'object', properties: { @@ -62,14 +67,18 @@ module.exports = { if (prop) { context.report({ node: defaultProp.node, - message: 'defaultProp "{{name}}" defined for isRequired propType.', - data: {name: defaultPropName} + messageId: 'requiredHasDefault', + data: { + name: defaultPropName + } }); } else { context.report({ node: defaultProp.node, - message: 'defaultProp "{{name}}" has no corresponding propTypes declaration.', - data: {name: defaultPropName} + messageId: 'defaultHasNoType', + data: { + name: defaultPropName + } }); } }); diff --git a/lib/rules/destructuring-assignment.js b/lib/rules/destructuring-assignment.js index dc3589ad77..58b45e86f5 100644 --- a/lib/rules/destructuring-assignment.js +++ b/lib/rules/destructuring-assignment.js @@ -10,6 +10,40 @@ const isAssignmentLHS = require('../util/ast').isAssignmentLHS; const DEFAULT_OPTION = 'always'; +function createSFCParams() { + const queue = []; + + return { + push(params) { + queue.unshift(params); + }, + pop() { + queue.shift(); + }, + propsName() { + const found = queue.find((params) => { + const props = params[0]; + return props && !props.destructuring && props.name; + }); + return found && found[0] && found[0].name; + }, + contextName() { + const found = queue.find((params) => { + const context = params[1]; + return context && !context.destructuring && context.name; + }); + return found && found[1] && found.name; + } + }; +} + +function evalParams(params) { + return params.map((param) => ({ + destructuring: param.type === 'ObjectPattern', + name: param.type === 'Identifier' && param.name + })); +} + module.exports = { meta: { docs: { @@ -18,6 +52,14 @@ module.exports = { recommended: false, url: docsUrl('destructuring-assignment') }, + + messages: { + noDestructPropsInSFCArg: 'Must never use destructuring props assignment in SFC argument', + noDestructContextInSFCArg: 'Must never use destructuring context assignment in SFC argument', + noDestructAssignment: 'Must never use destructuring {{type}} assignment', + useDestructAssignment: 'Must use destructuring {{type}} assignment' + }, + schema: [{ type: 'string', enum: [ @@ -38,35 +80,57 @@ module.exports = { create: Components.detect((context, components, utils) => { const configuration = context.options[0] || DEFAULT_OPTION; const ignoreClassFields = (context.options[1] && (context.options[1].ignoreClassFields === true)) || false; + const sfcParams = createSFCParams(); /** * @param {ASTNode} node We expect either an ArrowFunctionExpression, * FunctionDeclaration, or FunctionExpression */ function handleStatelessComponent(node) { - const destructuringProps = node.params && node.params[0] && node.params[0].type === 'ObjectPattern'; - const destructuringContext = node.params && node.params[1] && node.params[1].type === 'ObjectPattern'; + const params = evalParams(node.params); - if (destructuringProps && components.get(node) && configuration === 'never') { + const SFCComponent = components.get(context.getScope(node).block); + if (!SFCComponent) { + return; + } + sfcParams.push(params); + + if (params[0] && params[0].destructuring && components.get(node) && configuration === 'never') { context.report({ node, - message: 'Must never use destructuring props assignment in SFC argument' + messageId: 'noDestructPropsInSFCArg' }); - } else if (destructuringContext && components.get(node) && configuration === 'never') { + } else if (params[1] && params[1].destructuring && components.get(node) && configuration === 'never') { context.report({ node, - message: 'Must never use destructuring context assignment in SFC argument' + messageId: 'noDestructContextInSFCArg' }); } } + function handleStatelessComponentExit(node) { + const SFCComponent = components.get(context.getScope(node).block); + if (SFCComponent) { + sfcParams.pop(); + } + } + function handleSFCUsage(node) { + const propsName = sfcParams.propsName(); + const contextName = sfcParams.contextName(); // props.aProp || context.aProp - const isPropUsed = (node.object.name === 'props' || node.object.name === 'context') && !isAssignmentLHS(node); + const isPropUsed = ( + (propsName && node.object.name === propsName) + || (contextName && node.object.name === contextName) + ) + && !isAssignmentLHS(node); if (isPropUsed && configuration === 'always') { context.report({ node, - message: `Must use destructuring ${node.object.name} assignment` + messageId: 'useDestructAssignment', + data: { + type: node.object.name + } }); } } @@ -96,7 +160,10 @@ module.exports = { ) { context.report({ node, - message: `Must use destructuring ${node.object.property.name} assignment` + messageId: 'useDestructAssignment', + data: { + type: node.object.property.name + } }); } } @@ -109,6 +176,12 @@ module.exports = { FunctionExpression: handleStatelessComponent, + 'FunctionDeclaration:exit': handleStatelessComponentExit, + + 'ArrowFunctionExpression:exit': handleStatelessComponentExit, + + 'FunctionExpression:exit': handleStatelessComponentExit, + MemberExpression(node) { const SFCComponent = components.get(context.getScope(node).block); const classComponent = utils.getParentComponent(node); @@ -135,7 +208,10 @@ module.exports = { if (SFCComponent && destructuringSFC && configuration === 'never') { context.report({ node, - message: `Must never use destructuring ${node.init.name} assignment` + messageId: 'noDestructAssignment', + data: { + type: node.init.name + } }); } @@ -145,7 +221,10 @@ module.exports = { ) { context.report({ node, - message: `Must never use destructuring ${node.init.property.name} assignment` + messageId: 'noDestructAssignment', + data: { + type: node.init.property.name + } }); } } diff --git a/lib/rules/display-name.js b/lib/rules/display-name.js index 55ecd9a389..59591606d5 100644 --- a/lib/rules/display-name.js +++ b/lib/rules/display-name.js @@ -23,6 +23,10 @@ module.exports = { url: docsUrl('display-name') }, + messages: { + noDisplayName: 'Component definition is missing display name' + }, + schema: [{ type: 'object', properties: { @@ -38,8 +42,6 @@ module.exports = { const config = context.options[0] || {}; const ignoreTranspilerName = config.ignoreTranspilerName || false; - const MISSING_MESSAGE = 'Component definition is missing display name'; - /** * Mark a prop type as declared * @param {ASTNode} node The AST node being checked. @@ -57,10 +59,7 @@ module.exports = { function reportMissingDisplayName(component) { context.report({ node: component.node, - message: MISSING_MESSAGE, - data: { - component: component.name - } + messageId: 'noDisplayName' }); } diff --git a/lib/rules/forbid-component-props.js b/lib/rules/forbid-component-props.js index f5d2a9fce5..5aafe099fd 100644 --- a/lib/rules/forbid-component-props.js +++ b/lib/rules/forbid-component-props.js @@ -26,6 +26,10 @@ module.exports = { url: docsUrl('forbid-component-props') }, + messages: { + propIsForbidden: 'Prop "{{prop}}" is forbidden on Components' + }, + schema: [{ type: 'object', properties: { @@ -94,12 +98,13 @@ module.exports = { } const customMessage = forbid.get(prop).message; - const errorMessage = customMessage || `Prop \`${prop}\` is forbidden on Components`; - context.report({ + context.report(Object.assign({ node, - message: errorMessage - }); + data: { + prop + } + }, customMessage ? {message: customMessage} : {messageId: 'propIsForbidden'})); } }; } diff --git a/lib/rules/forbid-dom-props.js b/lib/rules/forbid-dom-props.js index 18a04d823e..af4b0c2516 100644 --- a/lib/rules/forbid-dom-props.js +++ b/lib/rules/forbid-dom-props.js @@ -26,6 +26,10 @@ module.exports = { url: docsUrl('forbid-dom-props') }, + messages: { + propIsForbidden: 'Prop "{{prop}}" is forbidden on DOM Nodes' + }, + schema: [{ type: 'object', properties: { @@ -83,12 +87,13 @@ module.exports = { } const customMessage = forbid.get(prop).message; - const errorMessage = customMessage || `Prop \`${prop}\` is forbidden on DOM Nodes`; - context.report({ + context.report(Object.assign({ node, - message: errorMessage - }); + data: { + prop + } + }, customMessage ? {message: customMessage} : {messageId: 'propIsForbidden'})); } }; } diff --git a/lib/rules/forbid-elements.js b/lib/rules/forbid-elements.js index ad333d2bbc..aec6560a0d 100644 --- a/lib/rules/forbid-elements.js +++ b/lib/rules/forbid-elements.js @@ -21,6 +21,11 @@ module.exports = { url: docsUrl('forbid-elements') }, + messages: { + forbiddenElement: '<{{element}}> is forbidden', + forbiddenElement_message: '<{{element}}> is forbidden, {{message}}' + }, + schema: [{ type: 'object', properties: { @@ -60,17 +65,6 @@ module.exports = { } }); - function errorMessageForElement(name) { - const message = `<${name}> is forbidden`; - const additionalMessage = indexedForbidConfigs[name].message; - - if (additionalMessage) { - return `${message}, ${additionalMessage}`; - } - - return message; - } - function isValidCreateElement(node) { return node.callee && node.callee.type === 'MemberExpression' @@ -81,9 +75,15 @@ module.exports = { function reportIfForbidden(element, node) { if (has(indexedForbidConfigs, element)) { + const message = indexedForbidConfigs[element].message; + context.report({ node, - message: errorMessageForElement(element) + messageId: message ? 'forbiddenElement_message' : 'forbiddenElement', + data: { + element, + message + } }); } } diff --git a/lib/rules/forbid-foreign-prop-types.js b/lib/rules/forbid-foreign-prop-types.js index a13aa2b839..039382142f 100644 --- a/lib/rules/forbid-foreign-prop-types.js +++ b/lib/rules/forbid-foreign-prop-types.js @@ -17,6 +17,10 @@ module.exports = { url: docsUrl('forbid-foreign-prop-types') }, + messages: { + forbiddenPropType: 'Using propTypes from another component is not safe because they may be removed in production builds' + }, + schema: [ { type: 'object', @@ -109,7 +113,7 @@ module.exports = { ) { context.report({ node: node.property, - message: 'Using propTypes from another component is not safe because they may be removed in production builds' + messageId: 'forbiddenPropType' }); } }, @@ -120,7 +124,7 @@ module.exports = { if (propTypesNode) { context.report({ node: propTypesNode, - message: 'Using propTypes from another component is not safe because they may be removed in production builds' + messageId: 'forbiddenPropType' }); } } diff --git a/lib/rules/forbid-prop-types.js b/lib/rules/forbid-prop-types.js index 5045bf04b5..8c25145f59 100644 --- a/lib/rules/forbid-prop-types.js +++ b/lib/rules/forbid-prop-types.js @@ -29,6 +29,10 @@ module.exports = { url: docsUrl('forbid-prop-types') }, + messages: { + forbiddenPropType: 'Prop type "{{target}}" is forbidden' + }, + schema: [{ type: 'object', properties: { @@ -63,7 +67,10 @@ module.exports = { if (isForbidden(type)) { context.report({ node: declaration, - message: `Prop type \`${target}\` is forbidden` + messageId: 'forbiddenPropType', + data: { + target + } }); } } diff --git a/lib/rules/function-component-definition.js b/lib/rules/function-component-definition.js index a8fd95e511..9918d1ce26 100644 --- a/lib/rules/function-component-definition.js +++ b/lib/rules/function-component-definition.js @@ -28,12 +28,6 @@ const UNNAMED_FUNCTION_TEMPLATES = { 'arrow-function': '{typeParams}({params}){returnType} => {body}' }; -const ERROR_MESSAGES = { - 'function-declaration': 'Function component is not a function declaration', - 'function-expression': 'Function component is not a function expression', - 'arrow-function': 'Function component is not an arrow function' -}; - function hasOneUnconstrainedTypeParam(node) { if (node.typeParameters) { return node.typeParameters.params.length === 1 && !node.typeParameters.params[0].constraint; @@ -106,6 +100,12 @@ module.exports = { }, fixable: 'code', + messages: { + 'function-declaration': 'Function component is not a function declaration', + 'function-expression': 'Function component is not a function expression', + 'arrow-function': 'Function component is not an arrow function' + }, + schema: [{ type: 'object', properties: { @@ -149,7 +149,7 @@ module.exports = { function report(node, options) { context.report({ node, - message: options.message, + messageId: options.messageId, fix: getFixer(node, options.fixerOptions) }); } @@ -161,7 +161,7 @@ module.exports = { if (hasName(node) && namedConfig !== functionType) { report(node, { - message: ERROR_MESSAGES[namedConfig], + messageId: namedConfig, fixerOptions: { type: namedConfig, template: NAMED_FUNCTION_TEMPLATES[namedConfig], @@ -173,7 +173,7 @@ module.exports = { } if (!hasName(node) && unnamedConfig !== functionType) { report(node, { - message: ERROR_MESSAGES[unnamedConfig], + messageId: unnamedConfig, fixerOptions: { type: unnamedConfig, template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig], diff --git a/lib/rules/jsx-boolean-value.js b/lib/rules/jsx-boolean-value.js index be3cb57d5a..1fa74b40e4 100644 --- a/lib/rules/jsx-boolean-value.js +++ b/lib/rules/jsx-boolean-value.js @@ -56,6 +56,13 @@ module.exports = { }, fixable: 'code', + messages: { + omitBoolean: 'Value must be omitted for boolean attributes{{exceptionsMessage}}', + omitBoolean_noMessage: 'Value must be omitted for boolean attributes', + setBoolean: 'Value must be set for boolean attributes{{exceptionsMessage}}', + setBoolean_noMessage: 'Value must be set for boolean attributes' + }, + schema: { anyOf: [{ type: 'array', @@ -94,9 +101,6 @@ module.exports = { const configObject = context.options[1] || {}; const exceptions = new Set((configuration === ALWAYS ? configObject[NEVER] : configObject[ALWAYS]) || []); - const NEVER_MESSAGE = 'Value must be omitted for boolean attributes{{exceptionsMessage}}'; - const ALWAYS_MESSAGE = 'Value must be set for boolean attributes{{exceptionsMessage}}'; - return { JSXAttribute(node) { const propName = node.name && node.name.name; @@ -106,7 +110,7 @@ module.exports = { const data = getErrorData(exceptions); context.report({ node, - message: ALWAYS_MESSAGE, + messageId: data.exceptionsMessage ? 'setBoolean' : 'setBoolean_noMessage', data, fix(fixer) { return fixer.insertTextAfter(node, '={true}'); @@ -117,7 +121,7 @@ module.exports = { const data = getErrorData(exceptions); context.report({ node, - message: NEVER_MESSAGE, + messageId: data.exceptionsMessage ? 'omitBoolean' : 'omitBoolean_noMessage', data, fix(fixer) { return fixer.removeRange([node.name.range[1], value.range[1]]); diff --git a/lib/rules/jsx-child-element-spacing.js b/lib/rules/jsx-child-element-spacing.js index fcde13bc74..f2bfa60483 100644 --- a/lib/rules/jsx-child-element-spacing.js +++ b/lib/rules/jsx-child-element-spacing.js @@ -46,6 +46,12 @@ module.exports = { url: docsUrl('jsx-child-element-spacing') }, fixable: null, + + messages: { + spacingAfterPrev: 'Ambiguous spacing after previous element {{element}}', + spacingBeforeNext: 'Ambiguous spacing before next element {{element}}' + }, + schema: [ { type: 'object', @@ -86,13 +92,19 @@ module.exports = { context.report({ node: lastChild, loc: lastChild.loc.end, - message: `Ambiguous spacing after previous element ${elementName(lastChild)}` + messageId: 'spacingAfterPrev', + data: { + element: elementName(lastChild) + } }); } else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) { context.report({ node: nextChild, loc: nextChild.loc.start, - message: `Ambiguous spacing before next element ${elementName(nextChild)}` + messageId: 'spacingBeforeNext', + data: { + element: elementName(nextChild) + } }); } } diff --git a/lib/rules/jsx-closing-bracket-location.js b/lib/rules/jsx-closing-bracket-location.js index 77bab9131a..d86af7b737 100644 --- a/lib/rules/jsx-closing-bracket-location.js +++ b/lib/rules/jsx-closing-bracket-location.js @@ -21,6 +21,10 @@ module.exports = { }, fixable: 'code', + messages: { + bracketLocation: 'The closing bracket must be {{location}}{{details}}' + }, + schema: [{ oneOf: [ { @@ -51,7 +55,6 @@ module.exports = { }, create(context) { - const MESSAGE = 'The closing bracket must be {{location}}{{details}}'; const MESSAGE_LOCATION = { 'after-props': 'placed after the last prop', 'after-tag': 'placed after the opening tag', @@ -259,7 +262,7 @@ module.exports = { return; } - const data = {location: MESSAGE_LOCATION[expectedLocation], details: ''}; + const data = {location: MESSAGE_LOCATION[expectedLocation]}; const correctColumn = getCorrectColumn(tokens, expectedLocation); if (correctColumn !== null) { @@ -271,7 +274,7 @@ module.exports = { context.report({ node, loc: tokens.closing, - message: MESSAGE, + messageId: 'bracketLocation', data, fix(fixer) { const closingTag = tokens.selfClosing ? '/>' : '>'; diff --git a/lib/rules/jsx-closing-tag-location.js b/lib/rules/jsx-closing-tag-location.js index d4bf10e1a7..58435d88d6 100644 --- a/lib/rules/jsx-closing-tag-location.js +++ b/lib/rules/jsx-closing-tag-location.js @@ -11,6 +11,7 @@ const docsUrl = require('../util/docsUrl'); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ + module.exports = { meta: { docs: { @@ -19,7 +20,12 @@ module.exports = { recommended: false, url: docsUrl('jsx-closing-tag-location') }, - fixable: 'whitespace' + fixable: 'whitespace', + + messages: { + onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.', + matchIndent: 'Expected closing tag to match indentation of opening.' + } }, create(context) { @@ -37,17 +43,12 @@ module.exports = { return; } - let message; - if (!astUtil.isNodeFirstInLine(context, node)) { - message = 'Closing tag of a multiline JSX expression must be on its own line.'; - } else { - message = 'Expected closing tag to match indentation of opening.'; - } - context.report({ node, loc: node.loc, - message, + messageId: astUtil.isNodeFirstInLine(context, node) + ? 'matchIndent' + : 'onOwnLine', fix(fixer) { const indent = Array(opening.loc.start.column + 1).join(' '); if (astUtil.isNodeFirstInLine(context, node)) { diff --git a/lib/rules/jsx-curly-brace-presence.js b/lib/rules/jsx-curly-brace-presence.js index 2161c60cb2..6f8e7c7d58 100755 --- a/lib/rules/jsx-curly-brace-presence.js +++ b/lib/rules/jsx-curly-brace-presence.js @@ -42,6 +42,11 @@ module.exports = { }, fixable: 'code', + messages: { + unnecessaryCurly: 'Curly braces are unnecessary here.', + missingCurly: 'Need to wrap this literal in a JSX expression.' + }, + schema: [ { oneOf: [ @@ -163,7 +168,7 @@ module.exports = { function reportUnnecessaryCurly(JSXExpressionNode) { context.report({ node: JSXExpressionNode, - message: 'Curly braces are unnecessary here.', + messageId: 'unnecessaryCurly', fix(fixer) { const expression = JSXExpressionNode.expression; const expressionType = expression.type; @@ -192,7 +197,7 @@ module.exports = { function reportMissingCurly(literalNode) { context.report({ node: literalNode, - message: 'Need to wrap this literal in a JSX expression.', + messageId: 'missingCurly', fix(fixer) { // If a HTML entity name is found, bail out because it can be fixed // by either using the real character or the unicode equivalent. @@ -235,6 +240,11 @@ module.exports = { const expression = JSXExpressionNode.expression; const expressionType = expression.type; + // Curly braces containing comments are necessary + if (context.getSourceCode().getCommentsInside(JSXExpressionNode).length > 0) { + return; + } + if ( (expressionType === 'Literal' || expressionType === 'JSXText') && typeof expression.value === 'string' diff --git a/lib/rules/jsx-curly-newline.js b/lib/rules/jsx-curly-newline.js index ca7dfa89a0..784b0f47a7 100644 --- a/lib/rules/jsx-curly-newline.js +++ b/lib/rules/jsx-curly-newline.js @@ -67,8 +67,8 @@ module.exports = { messages: { expectedBefore: 'Expected newline before \'}\'.', expectedAfter: 'Expected newline after \'{\'.', - unexpectedBefore: 'Unexpected newline before \'{\'.', - unexpectedAfter: 'Unexpected newline after \'}\'.' + unexpectedBefore: 'Unexpected newline before \'}\'.', + unexpectedAfter: 'Unexpected newline after \'{\'.' } }, diff --git a/lib/rules/jsx-curly-spacing.js b/lib/rules/jsx-curly-spacing.js index b587d2fe52..2477028142 100644 --- a/lib/rules/jsx-curly-spacing.js +++ b/lib/rules/jsx-curly-spacing.js @@ -34,6 +34,15 @@ module.exports = { }, fixable: 'code', + messages: { + noNewlineAfter: 'There should be no newline after \'{{token}}\'', + noNewlineBefore: 'There should be no newline before \'{{token}}\'', + noSpaceAfter: 'There should be no space after \'{{token}}\'', + noSpaceBefore: 'There should be no space before \'{{token}}\'', + spaceNeededAfter: 'A space is required after \'{{token}}\'', + spaceNeededBefore: 'A space is required before \'{{token}}\'' + }, + schema: { definitions: { basicConfig: { @@ -190,7 +199,10 @@ module.exports = { context.report({ node, loc: token.loc.start, - message: `There should be no newline after '${token.value}'`, + messageId: 'noNewlineAfter', + data: { + token: token.value + }, fix(fixer) { const nextToken = context.getSourceCode().getTokenAfter(token); return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start', spacing); @@ -209,7 +221,10 @@ module.exports = { context.report({ node, loc: token.loc.start, - message: `There should be no newline before '${token.value}'`, + messageId: 'noNewlineBefore', + data: { + token: token.value + }, fix(fixer) { const previousToken = context.getSourceCode().getTokenBefore(token); return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end', spacing); @@ -227,7 +242,10 @@ module.exports = { context.report({ node, loc: token.loc.start, - message: `There should be no space after '${token.value}'`, + messageId: 'noSpaceAfter', + data: { + token: token.value + }, fix(fixer) { const sourceCode = context.getSourceCode(); const nextToken = sourceCode.getTokenAfter(token); @@ -262,7 +280,10 @@ module.exports = { context.report({ node, loc: token.loc.start, - message: `There should be no space before '${token.value}'`, + messageId: 'noSpaceBefore', + data: { + token: token.value + }, fix(fixer) { const sourceCode = context.getSourceCode(); const previousToken = sourceCode.getTokenBefore(token); @@ -297,7 +318,10 @@ module.exports = { context.report({ node, loc: token.loc.start, - message: `A space is required after '${token.value}'`, + messageId: 'spaceNeededAfter', + data: { + token: token.value + }, fix(fixer) { return fixer.insertTextAfter(token, ' '); } @@ -314,7 +338,10 @@ module.exports = { context.report({ node, loc: token.loc.start, - message: `A space is required before '${token.value}'`, + messageId: 'spaceNeededBefore', + data: { + token: token.value + }, fix(fixer) { return fixer.insertTextBefore(token, ' '); } diff --git a/lib/rules/jsx-equals-spacing.js b/lib/rules/jsx-equals-spacing.js index 27f0bec9a5..dcf812725a 100644 --- a/lib/rules/jsx-equals-spacing.js +++ b/lib/rules/jsx-equals-spacing.js @@ -21,6 +21,13 @@ module.exports = { }, fixable: 'code', + messages: { + noSpaceBefore: 'There should be no space before \'=\'', + noSpaceAfter: 'There should be no space after \'=\'', + needSpaceBefore: 'A space is required before \'=\'', + needSpaceAfter: 'A space is required after \'=\'' + }, + schema: [{ enum: ['always', 'never'] }] @@ -61,7 +68,7 @@ module.exports = { context.report({ node: attrNode, loc: equalToken.loc.start, - message: 'There should be no space before \'=\'', + messageId: 'noSpaceBefore', fix(fixer) { return fixer.removeRange([attrNode.name.range[1], equalToken.range[0]]); } @@ -71,7 +78,7 @@ module.exports = { context.report({ node: attrNode, loc: equalToken.loc.start, - message: 'There should be no space after \'=\'', + messageId: 'noSpaceAfter', fix(fixer) { return fixer.removeRange([equalToken.range[1], attrNode.value.range[0]]); } @@ -83,7 +90,7 @@ module.exports = { context.report({ node: attrNode, loc: equalToken.loc.start, - message: 'A space is required before \'=\'', + messageId: 'needSpaceBefore', fix(fixer) { return fixer.insertTextBefore(equalToken, ' '); } @@ -93,7 +100,7 @@ module.exports = { context.report({ node: attrNode, loc: equalToken.loc.start, - message: 'A space is required after \'=\'', + messageId: 'needSpaceAfter', fix(fixer) { return fixer.insertTextAfter(equalToken, ' '); } diff --git a/lib/rules/jsx-filename-extension.js b/lib/rules/jsx-filename-extension.js index b07bc5518e..ed2e669fb9 100644 --- a/lib/rules/jsx-filename-extension.js +++ b/lib/rules/jsx-filename-extension.js @@ -30,6 +30,11 @@ module.exports = { url: docsUrl('jsx-filename-extension') }, + messages: { + noJSXWithExtension: 'JSX not allowed in files with extension \'{{ext}}\'', + extensionOnlyForJSX: 'Only files containing JSX may use the extension \'{{ext}}\'' + }, + schema: [{ type: 'object', properties: { @@ -80,7 +85,10 @@ module.exports = { if (!isAllowedExtension) { context.report({ node: jsxNode, - message: `JSX not allowed in files with extension '${path.extname(filename)}'` + messageId: 'noJSXWithExtension', + data: { + ext: path.extname(filename) + } }); } return; @@ -89,7 +97,10 @@ module.exports = { if (isAllowedExtension && allow === 'as-needed') { context.report({ node, - message: `Only files containing JSX may use the extension '${path.extname(filename)}'` + messageId: 'extensionOnlyForJSX', + data: { + ext: path.extname(filename) + } }); } } diff --git a/lib/rules/jsx-first-prop-new-line.js b/lib/rules/jsx-first-prop-new-line.js index 339d761025..1a1d110bd2 100644 --- a/lib/rules/jsx-first-prop-new-line.js +++ b/lib/rules/jsx-first-prop-new-line.js @@ -21,6 +21,11 @@ module.exports = { }, fixable: 'code', + messages: { + propOnNewLine: 'Property should be placed on a new line', + propOnSameLine: 'Property should be placed on the same line as the component declaration' + }, + schema: [{ enum: ['always', 'never', 'multiline', 'multiline-multiprop'] }] @@ -44,7 +49,7 @@ module.exports = { if (decl.loc.start.line === node.loc.start.line) { context.report({ node: decl, - message: 'Property should be placed on a new line', + messageId: 'propOnNewLine', fix(fixer) { return fixer.replaceTextRange([node.name.range[1], decl.range[0]], '\n'); } @@ -57,7 +62,7 @@ module.exports = { if (node.loc.start.line < firstNode.loc.start.line) { context.report({ node: firstNode, - message: 'Property should be placed on the same line as the component declaration', + messageId: 'propOnSameLine', fix(fixer) { return fixer.replaceTextRange([node.name.range[1], firstNode.range[0]], ' '); } diff --git a/lib/rules/jsx-fragments.js b/lib/rules/jsx-fragments.js index 020b079624..c100630744 100644 --- a/lib/rules/jsx-fragments.js +++ b/lib/rules/jsx-fragments.js @@ -29,6 +29,13 @@ module.exports = { }, fixable: 'code', + messages: { + fragmentsNotSupported: 'Fragments are only supported starting from React v16.2. ' + + 'Please disable the `react/jsx-fragments` rule in ESLint settings or upgrade your version of React.', + preferPragma: 'Prefer {{react}}.{{fragment}} over fragment shorthand', + preferFragment: 'Prefer fragment shorthand over {{react}}.{{fragment}}' + }, + schema: [{ enum: ['syntax', 'element'] }] @@ -47,8 +54,7 @@ module.exports = { if (!versionUtil.testReactVersion(context, '16.2.0')) { context.report({ node, - message: 'Fragments are only supported starting from React v16.2. ' - + 'Please disable the `react/jsx-fragments` rule in ESLint settings or upgrade your version of React.' + messageId: 'fragmentsNotSupported' }); return true; } @@ -146,7 +152,11 @@ module.exports = { if (configuration === 'element') { context.report({ node, - message: `Prefer ${reactPragma}.${fragmentPragma} over fragment shorthand`, + messageId: 'preferPragma', + data: { + react: reactPragma, + fragment: fragmentPragma + }, fix: getFixerToLong(node) }); } @@ -178,7 +188,11 @@ module.exports = { if (configuration === 'syntax' && !(attrs && attrs.length > 0)) { context.report({ node, - message: `Prefer fragment shorthand over ${reactPragma}.${fragmentPragma}`, + messageId: 'preferFragment', + data: { + react: reactPragma, + fragment: fragmentPragma + }, fix: getFixerToShort(node) }); } diff --git a/lib/rules/jsx-handler-names.js b/lib/rules/jsx-handler-names.js index ec181a2104..c792bc040c 100644 --- a/lib/rules/jsx-handler-names.js +++ b/lib/rules/jsx-handler-names.js @@ -20,6 +20,11 @@ module.exports = { url: docsUrl('jsx-handler-names') }, + messages: { + badHandlerName: 'Handler function for {{propKey}} prop key must be a camelCase name beginning with \'{{handlerPrefix}}\' only', + badPropKey: 'Prop key for {{propValue}} must begin with \'{{handlerPropPrefix}\'' + }, + schema: [{ anyOf: [ { @@ -139,7 +144,11 @@ module.exports = { ) { context.report({ node, - message: `Handler function for ${propKey} prop key must be a camelCase name beginning with '${eventHandlerPrefix}' only` + messageId: 'badHandlerName', + data: { + propKey, + handlerPrefix: eventHandlerPrefix + } }); } else if ( propFnIsNamedCorrectly @@ -148,7 +157,11 @@ module.exports = { ) { context.report({ node, - message: `Prop key for ${propValue} must begin with '${eventHandlerPropPrefix}'` + messageId: 'badPropKey', + data: { + propValue, + handlerPropPrefix: eventHandlerPropPrefix + } }); } } diff --git a/lib/rules/jsx-indent-props.js b/lib/rules/jsx-indent-props.js index 63f8b2455d..883f54503f 100644 --- a/lib/rules/jsx-indent-props.js +++ b/lib/rules/jsx-indent-props.js @@ -46,6 +46,10 @@ module.exports = { }, fixable: 'code', + messages: { + wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.' + }, + schema: [{ oneOf: [{ enum: ['tab', 'first'] @@ -70,8 +74,6 @@ module.exports = { }, create(context) { - const MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.'; - const extraColumnStart = 0; let indentType = 'space'; /** @type {number|'first'} */ @@ -120,7 +122,7 @@ module.exports = { context.report({ node, - message: MESSAGE, + messageId: 'wrongIndent', data: msgContext, fix(fixer) { return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]], diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js index d8a4f5f734..01fd6f0c3e 100644 --- a/lib/rules/jsx-indent.js +++ b/lib/rules/jsx-indent.js @@ -47,6 +47,11 @@ module.exports = { url: docsUrl('jsx-indent') }, fixable: 'whitespace', + + messages: { + wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.' + }, + schema: [{ oneOf: [{ enum: ['tab'] @@ -68,8 +73,6 @@ module.exports = { }, create(context) { - const MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.'; - const extraColumnStart = 0; let indentType = 'space'; let indentSize = 4; @@ -126,22 +129,12 @@ module.exports = { gotten }; - if (loc) { - context.report({ - node, - loc, - message: MESSAGE, - data: msgContext, - fix: getFixerFunction(node, needed) - }); - } else { - context.report({ - node, - message: MESSAGE, - data: msgContext, - fix: getFixerFunction(node, needed) - }); - } + context.report(Object.assign({ + node, + messageId: 'wrongIndent', + data: msgContext, + fix: getFixerFunction(node, needed) + }, loc && {loc})); } /** diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js index 2e4abe14bb..5586d589ab 100644 --- a/lib/rules/jsx-key.js +++ b/lib/rules/jsx-key.js @@ -27,6 +27,15 @@ module.exports = { recommended: true, url: docsUrl('jsx-key') }, + + messages: { + missingIterKey: 'Missing "key" prop for element in iterator', + missingIterKeyUsePrag: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead', + 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`' + }, + schema: [{ type: 'object', properties: { @@ -54,12 +63,16 @@ module.exports = { if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) { context.report({ node, - message: 'Missing "key" prop for element in iterator' + messageId: 'missingIterKey' }); } else if (checkFragmentShorthand && node.type === 'JSXFragment') { context.report({ node, - message: `Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use ${reactPragma}.${fragmentPragma} instead` + messageId: 'missingIterKeyUsePrag', + data: { + reactPrag: reactPragma, + fragPrag: fragmentPragma + } }); } } @@ -88,7 +101,7 @@ module.exports = { if (checkKeyMustBeforeSpread && isKeyAfterSpread(node.openingElement.attributes)) { context.report({ node, - message: '`key` prop must 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`' + messageId: 'keyBeforeSpread' }); } return; @@ -97,7 +110,7 @@ module.exports = { if (node.parent.type === 'ArrayExpression') { context.report({ node, - message: 'Missing "key" prop for element in array' + messageId: 'missingArrayKey' }); } }, @@ -110,7 +123,11 @@ module.exports = { if (node.parent.type === 'ArrayExpression') { context.report({ node, - message: `Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use ${reactPragma}.${fragmentPragma} instead` + messageId: 'missingArrayKeyUsePrag', + data: { + reactPrag: reactPragma, + fragPrag: fragmentPragma + } }); } }, diff --git a/lib/rules/jsx-max-depth.js b/lib/rules/jsx-max-depth.js index 980efc57b1..87aeccf97c 100644 --- a/lib/rules/jsx-max-depth.js +++ b/lib/rules/jsx-max-depth.js @@ -21,6 +21,11 @@ module.exports = { recommended: false, url: docsUrl('jsx-max-depth') }, + + messages: { + wrongDepth: 'Expected the depth of nested jsx elements to be <= {{needed}}, but found {{found}}.' + }, + schema: [ { type: 'object', @@ -35,7 +40,6 @@ module.exports = { ] }, create(context) { - const MESSAGE = 'Expected the depth of nested jsx elements to be <= {{needed}}, but found {{found}}.'; const DEFAULT_DEPTH = 2; const option = context.options[0] || {}; @@ -71,7 +75,7 @@ module.exports = { function report(node, depth) { context.report({ node, - message: MESSAGE, + messageId: 'wrongDepth', data: { found: depth, needed: maxDepth diff --git a/lib/rules/jsx-max-props-per-line.js b/lib/rules/jsx-max-props-per-line.js index 387a9ff0c8..208891e070 100644 --- a/lib/rules/jsx-max-props-per-line.js +++ b/lib/rules/jsx-max-props-per-line.js @@ -20,6 +20,11 @@ module.exports = { url: docsUrl('jsx-max-props-per-line') }, fixable: 'code', + + messages: { + newLine: 'Prop `{{prop}}` must be placed on a new line' + }, + schema: [{ type: 'object', properties: { @@ -94,7 +99,10 @@ module.exports = { const name = getPropName(propsInLine[maximum]); context.report({ node: propsInLine[maximum], - message: `Prop \`${name}\` must be placed on a new line`, + messageId: 'newLine', + data: { + prop: name + }, fix: generateFixFunction(propsInLine, maximum) }); } diff --git a/lib/rules/jsx-newline.js b/lib/rules/jsx-newline.js index f0a1f1cca5..9b92beb1e0 100644 --- a/lib/rules/jsx-newline.js +++ b/lib/rules/jsx-newline.js @@ -1,6 +1,7 @@ /** - * @fileoverview Enforce a new line after jsx elements and expressions. + * @fileoverview Require or prevent a new line after jsx elements and expressions. * @author Johnny Zabala + * @author Joseph Stiles */ 'use strict'; @@ -14,12 +15,29 @@ const docsUrl = require('../util/docsUrl'); module.exports = { meta: { docs: { - description: 'Enforce a new line after jsx elements and expressions', + description: 'Require or prevent a new line after jsx elements and expressions.', category: 'Stylistic Issues', recommended: false, url: docsUrl('jsx-newline') }, - fixable: 'code' + fixable: 'code', + + messages: { + require: 'JSX element should start in a new line', + prevent: 'JSX element should not start in a new line' + }, + schema: [ + { + type: 'object', + properties: { + prevent: { + default: false, + type: 'boolean' + } + }, + additionalProperties: false + } + ] }, create(context) { const jsxElementParents = new Set(); @@ -31,26 +49,44 @@ module.exports = { if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') { const firstAdjacentSibling = elements[index + 1]; const secondAdjacentSibling = elements[index + 2]; - if ( - firstAdjacentSibling - && secondAdjacentSibling - && (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText') - // Check adjacent sibling has the proper amount of newlines - && !/\n\s*\n/.test(firstAdjacentSibling.value) - ) { - context.report({ - node: secondAdjacentSibling, - message: 'JSX element should start in a new line', - fix(fixer) { - return fixer.replaceText( - firstAdjacentSibling, - // double the last newline. - sourceCode.getText(firstAdjacentSibling) - .replace(/(\n)(?!.*\1)/g, '\n\n') - ); - } - }); - } + + const hasSibling = firstAdjacentSibling + && secondAdjacentSibling + && (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText'); + + if (!hasSibling) return; + + // Check adjacent sibling has the proper amount of newlines + const isWithoutNewLine = !/\n\s*\n/.test(firstAdjacentSibling.value); + + const prevent = !!(context.options[0] || {}).prevent; + + if (isWithoutNewLine === prevent) return; + + const messageId = prevent + ? 'prevent' + : 'require'; + + const regex = prevent + ? /(\n\n)(?!.*\1)/g + : /(\n)(?!.*\1)/g; + + const replacement = prevent + ? '\n' + : '\n\n'; + + context.report({ + node: secondAdjacentSibling, + messageId, + fix(fixer) { + return fixer.replaceText( + firstAdjacentSibling, + // double or remove the last newline + sourceCode.getText(firstAdjacentSibling) + .replace(regex, replacement) + ); + } + }); } }); }); diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js index 0ddee24996..1b14050326 100644 --- a/lib/rules/jsx-no-bind.js +++ b/lib/rules/jsx-no-bind.js @@ -16,13 +16,6 @@ const jsxUtil = require('../util/jsx'); // Rule Definition // ----------------------------------------------------------------------------- -const violationMessageStore = { - bindCall: 'JSX props should not use .bind()', - arrowFunc: 'JSX props should not use arrow functions', - bindExpression: 'JSX props should not use ::', - func: 'JSX props should not use functions' -}; - module.exports = { meta: { docs: { @@ -32,6 +25,13 @@ module.exports = { url: docsUrl('jsx-no-bind') }, + messages: { + bindCall: 'JSX props should not use .bind()', + arrowFunc: 'JSX props should not use arrow functions', + bindExpression: 'JSX props should not use ::', + func: 'JSX props should not use functions' + }, + schema: [{ type: 'object', properties: { @@ -122,7 +122,10 @@ module.exports = { return violationTypes.find((type) => { if (blockSets[type].has(name)) { - context.report({node, message: violationMessageStore[type]}); + context.report({ + node, + messageId: type + }); return true; } @@ -176,7 +179,8 @@ module.exports = { findVariableViolation(node, valueNode.name); } else if (nodeViolationType) { context.report({ - node, message: violationMessageStore[nodeViolationType] + node, + messageId: nodeViolationType }); } } diff --git a/lib/rules/jsx-no-comment-textnodes.js b/lib/rules/jsx-no-comment-textnodes.js index 0d7fba9f17..566f3f1509 100644 --- a/lib/rules/jsx-no-comment-textnodes.js +++ b/lib/rules/jsx-no-comment-textnodes.js @@ -23,7 +23,7 @@ function checkText(node, context) { ) { context.report({ node, - message: 'Comments inside children section of tag should be placed inside braces' + messageId: 'putCommentInBraces' }); } } @@ -38,6 +38,10 @@ module.exports = { url: docsUrl('jsx-no-comment-textnodes') }, + messages: { + putCommentInBraces: 'Comments inside children section of tag should be placed inside braces' + }, + schema: [{ type: 'object', properties: {}, diff --git a/lib/rules/jsx-no-constructed-context-values.js b/lib/rules/jsx-no-constructed-context-values.js index 95c370d9b1..9c6186cf7d 100644 --- a/lib/rules/jsx-no-constructed-context-values.js +++ b/lib/rules/jsx-no-constructed-context-values.js @@ -98,7 +98,7 @@ function isConstruction(node, callScope) { case 'JSXElement': return {type: 'JSX element', node}; case 'AssignmentExpression': { - const construct = isConstruction(node.right); + const construct = isConstruction(node.right, callScope); if (construct != null) { return { type: 'assignment expression', @@ -110,7 +110,7 @@ function isConstruction(node, callScope) { } case 'TypeCastExpression': case 'TSAsExpression': - return isConstruction(node.expression); + return isConstruction(node.expression, callScope); default: return null; } @@ -167,6 +167,10 @@ module.exports = { } const valueNode = jsxValueAttribute.value; + if (!valueNode) { + // attribute is a boolean shorthand + return; + } if (valueNode.type !== 'JSXExpressionContainer') { // value could be a literal return; diff --git a/lib/rules/jsx-no-duplicate-props.js b/lib/rules/jsx-no-duplicate-props.js index 7d9e00689e..bf76ca78a7 100644 --- a/lib/rules/jsx-no-duplicate-props.js +++ b/lib/rules/jsx-no-duplicate-props.js @@ -21,6 +21,10 @@ module.exports = { url: docsUrl('jsx-no-duplicate-props') }, + messages: { + noDuplicateProps: 'No duplicate props allowed' + }, + schema: [{ type: 'object', properties: { @@ -58,7 +62,7 @@ module.exports = { if (has(props, name)) { context.report({ node: decl, - message: 'No duplicate props allowed' + messageId: 'noDuplicateProps' }); } else { props[name] = 1; diff --git a/lib/rules/jsx-no-literals.js b/lib/rules/jsx-no-literals.js index 21ec54def4..c4c10f16ed 100644 --- a/lib/rules/jsx-no-literals.js +++ b/lib/rules/jsx-no-literals.js @@ -25,6 +25,13 @@ module.exports = { url: docsUrl('jsx-no-literals') }, + messages: { + invalidPropValue: 'Invalid prop value: "{{text}}"', + noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"', + noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"', + literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"' + }, + schema: [{ type: 'object', properties: { @@ -59,22 +66,25 @@ module.exports = { const config = Object.assign({}, defaults, context.options[0] || {}); config.allowedStrings = new Set(config.allowedStrings.map(trimIfString)); - function defaultMessage() { + function defaultMessageId() { if (config.noAttributeStrings) { - return 'Strings not allowed in attributes'; + return 'noStringsInAttributes'; } if (config.noStrings) { - return 'Strings not allowed in JSX files'; + return 'noStringsInJSX'; } - return 'Missing JSX expression container around literal string'; + return 'literalNotInJSXExpression'; } - function reportLiteralNode(node, customMessage) { - const errorMessage = customMessage || defaultMessage(); + function reportLiteralNode(node, messageId) { + messageId = messageId || defaultMessageId(); context.report({ node, - message: `${errorMessage}: “${context.getSourceCode().getText(node).trim()}”` + messageId, + data: { + text: context.getSourceCode().getText(node).trim() + } }); } @@ -149,8 +159,8 @@ module.exports = { const isNodeValueString = node && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string' && !config.allowedStrings.has(node.value.value); if (config.noStrings && !config.ignoreProps && isNodeValueString) { - const customMessage = 'Invalid prop value'; - reportLiteralNode(node, customMessage); + const messageId = 'invalidPropValue'; + reportLiteralNode(node, messageId); } }, diff --git a/lib/rules/jsx-no-script-url.js b/lib/rules/jsx-no-script-url.js index 97c0cc53ce..d46bcab088 100644 --- a/lib/rules/jsx-no-script-url.js +++ b/lib/rules/jsx-no-script-url.js @@ -50,6 +50,12 @@ module.exports = { recommended: false, url: docsUrl('jsx-no-script-url') }, + + messages: { + noScriptURL: 'A future version of React will block javascript: URLs as a security precaution. ' + + 'Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.' + }, + schema: [{ type: 'array', uniqueItems: true, @@ -81,8 +87,7 @@ module.exports = { if (shouldVerifyElement(parent, config) && shouldVerifyProp(node, config) && hasJavaScriptProtocol(node)) { context.report({ node, - message: 'A future version of React will block javascript: URLs as a security precaution. ' - + 'Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.' + messageId: 'noScriptURL' }); } } diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js index 656cd3009f..6ce338bff2 100644 --- a/lib/rules/jsx-no-target-blank.js +++ b/lib/rules/jsx-no-target-blank.js @@ -12,22 +12,28 @@ const linkComponentsUtil = require('../util/linkComponents'); // Rule Definition // ------------------------------------------------------------------------------ -function lastIndexMatching(arr, condition) { - return arr.map(condition).lastIndexOf(true); +function findLastIndex(arr, condition) { + for (let i = arr.length - 1; i >= 0; i -= 1) { + if (condition(arr[i])) { + return i; + } + } + + return -1; } function attributeValuePossiblyBlank(attribute) { - if (!attribute.value) { + if (!attribute || !attribute.value) { return false; } const value = attribute.value; - if (value.type === 'Literal' && typeof value.value === 'string' && value.value.toLowerCase() === '_blank') { - return true; + if (value.type === 'Literal') { + return typeof value.value === 'string' && value.value.toLowerCase() === '_blank'; } if (value.type === 'JSXExpressionContainer') { const expr = value.expression; - if (expr.type === 'Literal' && typeof expr.value === 'string' && expr.value.toLowerCase() === '_blank') { - return true; + if (expr.type === 'Literal') { + return typeof expr.value === 'string' && expr.value.toLowerCase() === '_blank'; } if (expr.type === 'ConditionalExpression') { if (expr.alternate.type === 'Literal' && expr.alternate.value && expr.alternate.value.toLowerCase() === '_blank') { @@ -41,21 +47,15 @@ function attributeValuePossiblyBlank(attribute) { return false; } -function hasTargetBlank(node, warnOnSpreadAttributes, spreadAttributeIndex) { - const targetIndex = lastIndexMatching(node.attributes, (attr) => attr.name && attr.name.name === 'target'); - const foundTargetBlank = targetIndex !== -1 && attributeValuePossiblyBlank(node.attributes[targetIndex]); - return foundTargetBlank || (warnOnSpreadAttributes && targetIndex < spreadAttributeIndex); -} - function hasExternalLink(node, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex) { - const linkIndex = lastIndexMatching(node.attributes, (attr) => attr.name && attr.name.name === linkAttribute); + const linkIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === linkAttribute); const foundExternalLink = linkIndex !== -1 && ((attr) => attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))( node.attributes[linkIndex]); return foundExternalLink || (warnOnSpreadAttributes && linkIndex < spreadAttributeIndex); } function hasDynamicLink(node, linkAttribute) { - const dynamicLinkIndex = lastIndexMatching(node.attributes, (attr) => attr.name + const dynamicLinkIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === linkAttribute && attr.value && attr.value.type === 'JSXExpressionContainer'); @@ -64,35 +64,52 @@ function hasDynamicLink(node, linkAttribute) { } } -function hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex) { - const relIndex = lastIndexMatching(node.attributes, (attr) => (attr.type === 'JSXAttribute' && attr.name.name === 'rel')); +function getStringFromValue(value) { + if (value) { + if (value.type === 'Literal') { + return value.value; + } + if (value.type === 'JSXExpressionContainer') { + if (value.expression.type === 'TemplateLiteral') { + return value.expression.quasis[0].value.cooked; + } + return value.expression && value.expression.value; + } + } + return null; +} +function hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex) { + const relIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXAttribute' && attr.name.name === 'rel')); if (relIndex === -1 || (warnOnSpreadAttributes && relIndex < spreadAttributeIndex)) { return false; } const relAttribute = node.attributes[relIndex]; - const value = relAttribute.value - && (( - relAttribute.value.type === 'Literal' - && relAttribute.value.value - ) || ( - relAttribute.value.type === 'JSXExpressionContainer' - && relAttribute.value.expression - && relAttribute.value.expression.value - )); + const value = getStringFromValue(relAttribute.value); const tags = value && typeof value === 'string' && value.toLowerCase().split(' '); - return tags && (allowReferrer ? tags.indexOf('noopener') >= 0 : tags.indexOf('noreferrer') >= 0); + const noreferrer = tags && tags.indexOf('noreferrer') >= 0; + if (noreferrer) { + return true; + } + return allowReferrer && tags && tags.indexOf('noopener') >= 0; } module.exports = { meta: { + fixable: 'code', docs: { description: 'Forbid `target="_blank"` attribute without `rel="noreferrer"`', category: 'Best Practices', recommended: true, url: docsUrl('jsx-no-target-blank') }, + + messages: { + noTargetBlank: 'Using target="_blank" without rel="noreferrer" ' + + 'is a security risk: see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener' + }, + schema: [{ type: 'object', properties: { @@ -123,9 +140,17 @@ module.exports = { return; } - const spreadAttributeIndex = lastIndexMatching(node.attributes, (attr) => (attr.type === 'JSXSpreadAttribute')); - if (!hasTargetBlank(node, warnOnSpreadAttributes, spreadAttributeIndex)) { - return; + const targetIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === 'target'); + const spreadAttributeIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXSpreadAttribute')); + + if (!attributeValuePossiblyBlank(node.attributes[targetIndex])) { + const hasSpread = spreadAttributeIndex >= 0; + + if (warnOnSpreadAttributes && hasSpread) { + // continue to check below + } else if ((hasSpread && targetIndex < spreadAttributeIndex) || !hasSpread) { + return; + } } const linkAttribute = components.get(node.name.name); @@ -134,8 +159,48 @@ module.exports = { if (hasDangerousLink && !hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex)) { context.report({ node, - message: 'Using target="_blank" without rel="noreferrer" ' - + 'is a security risk: see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener' + messageId: 'noTargetBlank', + fix(fixer) { + // eslint 5 uses `node.attributes`; eslint 6+ uses `node.parent.attributes` + const nodeWithAttrs = node.parent.attributes ? node.parent : node; + // eslint 5 does not provide a `name` property on JSXSpreadElements + const relAttribute = nodeWithAttrs.attributes.find((attr) => attr.name && attr.name.name === 'rel'); + + if (targetIndex < spreadAttributeIndex || (spreadAttributeIndex >= 0 && !relAttribute)) { + return null; + } + + if (!relAttribute) { + return fixer.insertTextAfter(nodeWithAttrs.attributes.slice(-1)[0], ' rel="noreferrer"'); + } + + if (!relAttribute.value) { + return fixer.insertTextAfter(relAttribute, '="noreferrer"'); + } + + if (relAttribute.value.type === 'Literal') { + const parts = relAttribute.value.value + .split('noreferrer') + .filter(Boolean); + return fixer.replaceText(relAttribute.value, `"${parts.concat('noreferrer').join(' ')}"`); + } + + if (relAttribute.value.type === 'JSXExpressionContainer') { + if (relAttribute.value.expression.type === 'Literal') { + if (typeof relAttribute.value.expression.value === 'string') { + const parts = relAttribute.value.expression.value + .split('noreferrer') + .filter(Boolean); + return fixer.replaceText(relAttribute.value.expression, `"${parts.concat('noreferrer').join(' ')}"`); + } + + // for undefined, boolean, number, symbol, bigint, and null + return fixer.replaceText(relAttribute.value, '"noreferrer"'); + } + } + + return null; + } }); } } diff --git a/lib/rules/jsx-no-undef.js b/lib/rules/jsx-no-undef.js index f266d5de64..f4e94c4059 100644 --- a/lib/rules/jsx-no-undef.js +++ b/lib/rules/jsx-no-undef.js @@ -20,6 +20,11 @@ module.exports = { recommended: true, url: docsUrl('jsx-no-undef') }, + + messages: { + undefined: '\'{{identifier}}\' is not defined.' + }, + schema: [{ type: 'object', properties: { @@ -74,7 +79,10 @@ module.exports = { context.report({ node, - message: `'${node.name}' is not defined.` + messageId: 'undefined', + data: { + identifier: node.name + } }); } diff --git a/lib/rules/jsx-one-expression-per-line.js b/lib/rules/jsx-one-expression-per-line.js index 13774cafa1..e236208472 100644 --- a/lib/rules/jsx-one-expression-per-line.js +++ b/lib/rules/jsx-one-expression-per-line.js @@ -25,6 +25,11 @@ module.exports = { url: docsUrl('jsx-one-expression-per-line') }, fixable: 'whitespace', + + messages: { + moveToNewLine: '`{{descriptor}}` must be placed on a new line' + }, + schema: [ { type: 'object', @@ -207,7 +212,10 @@ module.exports = { context.report({ node: nodeToReport, - message: `\`${descriptor}\` must be placed on a new line`, + messageId: 'moveToNewLine', + data: { + descriptor + }, fix(fixer) { return fixer.replaceText(nodeToReport, replaceText); } diff --git a/lib/rules/jsx-pascal-case.js b/lib/rules/jsx-pascal-case.js index d2ee51d4ea..696525dfda 100644 --- a/lib/rules/jsx-pascal-case.js +++ b/lib/rules/jsx-pascal-case.js @@ -6,6 +6,7 @@ 'use strict'; const elementType = require('jsx-ast-utils/elementType'); +const minimatch = require('minimatch'); const docsUrl = require('../util/docsUrl'); const jsxUtil = require('../util/jsx'); @@ -59,6 +60,12 @@ function testAllCaps(name) { return true; } +function ignoreCheck(ignore, name) { + return ignore.some( + (entry) => name === entry || minimatch(name, entry, {noglobstar: true}) + ); +} + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -72,14 +79,29 @@ module.exports = { url: docsUrl('jsx-pascal-case') }, + messages: { + usePascalCase: 'Imported JSX component {{name}} must be in PascalCase', + usePascalOrSnakeCase: 'Imported JSX component {{name}} must be in PascalCase or SCREAMING_SNAKE_CASE' + }, + schema: [{ type: 'object', properties: { allowAllCaps: { type: 'boolean' }, + allowNamespace: { + type: 'boolean' + }, ignore: { - type: 'array' + items: [ + { + type: 'string' + } + ], + minItems: 0, + type: 'array', + uniqueItems: true } }, additionalProperties: false @@ -89,6 +111,7 @@ module.exports = { create(context) { const configuration = context.options[0] || {}; const allowAllCaps = configuration.allowAllCaps || false; + const allowNamespace = configuration.allowNamespace || false; const ignore = configuration.ignore || []; return { @@ -96,30 +119,35 @@ module.exports = { const isCompatTag = jsxUtil.isDOMComponent(node); if (isCompatTag) return undefined; - let name = elementType(node); + const name = elementType(node); + let checkNames = [name]; + let index = 0; - // Get JSXIdentifier if the type is JSXNamespacedName or JSXMemberExpression if (name.lastIndexOf(':') > -1) { - name = name.substring(name.lastIndexOf(':') + 1); + checkNames = name.split(':'); } else if (name.lastIndexOf('.') > -1) { - name = name.substring(name.lastIndexOf('.') + 1); + checkNames = name.split('.'); } - if (name.length === 1) return undefined; - - const isPascalCase = testPascalCase(name); - const isAllowedAllCaps = allowAllCaps && testAllCaps(name); - const isIgnored = ignore.indexOf(name) !== -1; - - if (!isPascalCase && !isAllowedAllCaps && !isIgnored) { - let message = `Imported JSX component ${name} must be in PascalCase`; - - if (allowAllCaps) { - message += ' or SCREAMING_SNAKE_CASE'; + do { + const splitName = checkNames[index]; + if (splitName.length === 1) return undefined; + const isPascalCase = testPascalCase(splitName); + const isAllowedAllCaps = allowAllCaps && testAllCaps(splitName); + const isIgnored = ignoreCheck(ignore, splitName); + + if (!isPascalCase && !isAllowedAllCaps && !isIgnored) { + context.report({ + node, + messageId: allowAllCaps ? 'usePascalOrSnakeCase' : 'usePascalCase', + data: { + name: splitName + } + }); + break; } - - context.report({node, message}); - } + index++; + } while (index < checkNames.length && !allowNamespace); } }; } diff --git a/lib/rules/jsx-props-no-multi-spaces.js b/lib/rules/jsx-props-no-multi-spaces.js index 6f5911bd7a..2a6d1f6357 100644 --- a/lib/rules/jsx-props-no-multi-spaces.js +++ b/lib/rules/jsx-props-no-multi-spaces.js @@ -20,6 +20,12 @@ module.exports = { url: docsUrl('jsx-props-no-multi-spaces') }, fixable: 'code', + + messages: { + noLineGap: 'Expected no line gap between “{{prop1}}” and “{{prop2}}”', + onlyOneSpace: 'Expected only one space between “{{prop1}}” and “{{prop2}}”' + }, + schema: [] }, @@ -59,7 +65,11 @@ module.exports = { if (hasEmptyLines(prev, node)) { context.report({ node, - message: `Expected no line gap between “${getPropName(prev)}” and “${getPropName(node)}”` + messageId: 'noLineGap', + data: { + prop1: getPropName(prev), + prop2: getPropName(node) + } }); } @@ -72,7 +82,11 @@ module.exports = { if (between !== ' ') { context.report({ node, - message: `Expected only one space between "${getPropName(prev)}" and "${getPropName(node)}"`, + messageId: 'onlyOneSpace', + data: { + prop1: getPropName(prev), + prop2: getPropName(node) + }, fix(fixer) { return fixer.replaceTextRange([prev.range[1], node.range[0]], ' '); } diff --git a/lib/rules/jsx-props-no-spreading.js b/lib/rules/jsx-props-no-spreading.js index 3d54a8e665..2f3c424a75 100644 --- a/lib/rules/jsx-props-no-spreading.js +++ b/lib/rules/jsx-props-no-spreading.js @@ -31,6 +31,11 @@ module.exports = { recommended: false, url: docsUrl('jsx-props-no-spreading') }, + + messages: { + noSpreading: 'Prop spreading is forbidden' + }, + schema: [{ allOf: [{ type: 'object', @@ -119,7 +124,7 @@ module.exports = { } context.report({ node, - message: 'Prop spreading is forbidden' + messageId: 'noSpreading' }); } }; diff --git a/lib/rules/jsx-sort-default-props.js b/lib/rules/jsx-sort-default-props.js index d45ca94e1f..1258273bc2 100644 --- a/lib/rules/jsx-sort-default-props.js +++ b/lib/rules/jsx-sort-default-props.js @@ -22,9 +22,12 @@ module.exports = { recommended: false, url: docsUrl('jsx-sort-default-props') }, - // fixable: 'code', + messages: { + propsNotSorted: 'Default prop types declarations should be sorted alphabetically' + }, + schema: [{ type: 'object', properties: { @@ -120,7 +123,7 @@ module.exports = { if (currentPropName < prevPropName) { context.report({ node: curr, - message: 'Default prop types declarations should be sorted alphabetically' + messageId: 'propsNotSorted' // fix }); diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js index 5eb9206b97..5e0402d1ce 100644 --- a/lib/rules/jsx-sort-props.js +++ b/lib/rules/jsx-sort-props.js @@ -185,7 +185,7 @@ function validateReservedFirstConfig(context, reservedFirst) { return function report(decl) { context.report({ node: decl, - message: 'A customized reserved first list must not be empty' + messageId: 'listIsEmpty' }); }; } @@ -193,10 +193,9 @@ function validateReservedFirstConfig(context, reservedFirst) { return function report(decl) { context.report({ node: decl, - message: 'A customized reserved first list must only contain a subset of React reserved props.' - + ' Remove: {{ nonReservedWords }}', + messageId: 'noUnreservedProps', data: { - nonReservedWords: nonReservedWords.toString() + unreservedWords: nonReservedWords.toString() } }); }; @@ -214,6 +213,17 @@ module.exports = { url: docsUrl('jsx-sort-props') }, fixable: 'code', + + 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', + listReservedPropsFirst: 'Reserved props must be listed before all other props', + 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', + sortPropsByAlpha: 'Props should be sorted alphabetically' + }, + schema: [{ type: 'object', properties: { @@ -295,7 +305,7 @@ module.exports = { if (!previousIsReserved && currentIsReserved) { context.report({ node: decl.name, - message: 'Reserved props must be listed before all other props', + messageId: 'listReservedPropsFirst', fix: generateFixerFunction(node, context, reservedList) }); return memo; @@ -311,7 +321,7 @@ module.exports = { // Encountered a non-callback prop after a callback prop context.report({ node: memo.name, - message: 'Callbacks must be listed after all other props', + messageId: 'listCallbacksLast', fix: generateFixerFunction(node, context, reservedList) }); return memo; @@ -325,7 +335,7 @@ module.exports = { if (!currentValue && previousValue) { context.report({ node: memo.name, - message: 'Shorthand props must be listed before all other props', + messageId: 'listShorthandFirst', fix: generateFixerFunction(node, context, reservedList) }); return memo; @@ -339,7 +349,7 @@ module.exports = { if (currentValue && !previousValue) { context.report({ node: memo.name, - message: 'Shorthand props must be listed after all other props', + messageId: 'listShorthandLast', fix: generateFixerFunction(node, context, reservedList) }); return memo; @@ -356,7 +366,7 @@ module.exports = { ) { context.report({ node: decl.name, - message: 'Props should be sorted alphabetically', + messageId: 'sortPropsByAlpha', fix: generateFixerFunction(node, context, reservedList) }); return memo; diff --git a/lib/rules/jsx-space-before-closing.js b/lib/rules/jsx-space-before-closing.js index feeae4506d..96ac126939 100644 --- a/lib/rules/jsx-space-before-closing.js +++ b/lib/rules/jsx-space-before-closing.js @@ -27,6 +27,11 @@ module.exports = { }, fixable: 'code', + messages: { + noSpaceBeforeClose: 'A space is forbidden before closing bracket', + needSpaceBeforeClose: 'A space is required before closing bracket' + }, + schema: [{ enum: ['always', 'never'] }] @@ -35,9 +40,6 @@ module.exports = { create(context) { const configuration = context.options[0] || 'always'; - const NEVER_MESSAGE = 'A space is forbidden before closing bracket'; - const ALWAYS_MESSAGE = 'A space is required before closing bracket'; - // -------------------------------------------------------------------------- // Public // -------------------------------------------------------------------------- @@ -60,7 +62,7 @@ module.exports = { if (configuration === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { context.report({ loc: closingSlash.loc.start, - message: ALWAYS_MESSAGE, + messageId: 'needSpaceBeforeClose', fix(fixer) { return fixer.insertTextBefore(closingSlash, ' '); } @@ -68,7 +70,7 @@ module.exports = { } else if (configuration === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { context.report({ loc: closingSlash.loc.start, - message: NEVER_MESSAGE, + messageId: 'noSpaceBeforeClose', fix(fixer) { const previousToken = sourceCode.getTokenBefore(closingSlash); return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]); diff --git a/lib/rules/jsx-tag-spacing.js b/lib/rules/jsx-tag-spacing.js index 9568672ac6..8355c2e12f 100644 --- a/lib/rules/jsx-tag-spacing.js +++ b/lib/rules/jsx-tag-spacing.js @@ -15,11 +15,6 @@ const docsUrl = require('../util/docsUrl'); function validateClosingSlash(context, node, option) { const sourceCode = context.getSourceCode(); - const SELF_CLOSING_NEVER_MESSAGE = 'Whitespace is forbidden between `/` and `>`; write `/>`'; - const SELF_CLOSING_ALWAYS_MESSAGE = 'Whitespace is required between `/` and `>`; write `/ >`'; - const NEVER_MESSAGE = 'Whitespace is forbidden between `<` and `/`; write ``; write `/>`', + selfCloseSlashNeedSpace: 'Whitespace is required between `/` and `>`; write `/ >`', + closeSlashNoSpace: 'Whitespace is forbidden between `<` and `/`; write ` fixer.replaceText(node, `(${sourceCode.getText(node)})`)); + report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${sourceCode.getText(node)})`)); } if (option === 'parens-new-line' && isMultilines(node)) { @@ -162,20 +164,20 @@ module.exports = { // Strip newline after operator if parens newline is specified report( node, - MISSING_PARENS, + 'missingParens', (fixer) => fixer.replaceTextRange( [tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]], `${trimTokenBeforeNewline(node, tokenBefore)}(\n${start.column > 0 ? ' '.repeat(start.column) : ''}${sourceCode.getText(node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})` ) ); } else { - report(node, MISSING_PARENS, (fixer) => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`)); + report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`)); } } else { const needsOpening = needsOpeningNewLine(node); const needsClosing = needsClosingNewLine(node); if (needsOpening || needsClosing) { - report(node, PARENS_NEW_LINES, (fixer) => { + report(node, 'parensOnNewLines', (fixer) => { const text = sourceCode.getText(node); let fixed = text; if (needsOpening) { diff --git a/lib/rules/no-access-state-in-setstate.js b/lib/rules/no-access-state-in-setstate.js index 53aeab11fe..a0a910842d 100644 --- a/lib/rules/no-access-state-in-setstate.js +++ b/lib/rules/no-access-state-in-setstate.js @@ -19,6 +19,10 @@ module.exports = { category: 'Possible Errors', recommended: false, url: docsUrl('no-access-state-in-setstate') + }, + + messages: { + useCallback: 'Use callback in setState when referencing the previous state.' } }, @@ -82,7 +86,7 @@ module.exports = { if (method.methodName === methodName) { context.report({ node: method.node, - message: 'Use callback in setState when referencing the previous state.' + messageId: 'useCallback' }); } }); @@ -105,7 +109,7 @@ module.exports = { if (isFirstArgumentInSetStateCall(current, node)) { context.report({ node, - message: 'Use callback in setState when referencing the previous state.' + messageId: 'useCallback' }); break; } @@ -157,7 +161,7 @@ module.exports = { .forEach((v) => { context.report({ node: v.node, - message: 'Use callback in setState when referencing the previous state.' + messageId: 'useCallback' }); }); } diff --git a/lib/rules/no-adjacent-inline-elements.js b/lib/rules/no-adjacent-inline-elements.js index 76a65845b8..9e02929bba 100644 --- a/lib/rules/no-adjacent-inline-elements.js +++ b/lib/rules/no-adjacent-inline-elements.js @@ -66,14 +66,11 @@ function isInline(node) { return false; } -const ERROR = 'Child elements which render as inline HTML elements should be separated by a space or wrapped in block level elements.'; - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { - ERROR, meta: { docs: { description: 'Prevent adjacent inline elements not separated by whitespace.', @@ -81,7 +78,11 @@ module.exports = { recommended: false, url: docsUrl('no-adjacent-inline-elements') }, - schema: [] + schema: [], + + messages: { + inlineElement: 'Child elements which render as inline HTML elements should be separated by a space or wrapped in block level elements.' + } }, create(context) { function validate(node, children) { @@ -95,7 +96,7 @@ module.exports = { if (previousIsInline && currentIsInline) { context.report({ node, - message: ERROR + messageId: 'inlineElement' }); return; } diff --git a/lib/rules/no-array-index-key.js b/lib/rules/no-array-index-key.js index ceb2a1676a..6814548d7c 100644 --- a/lib/rules/no-array-index-key.js +++ b/lib/rules/no-array-index-key.js @@ -23,6 +23,10 @@ module.exports = { url: docsUrl('no-array-index-key') }, + messages: { + noArrayIndex: 'Do not use Array index in keys' + }, + schema: [] }, @@ -42,7 +46,6 @@ module.exports = { reduceRight: 2, some: 1 }; - const ERROR_MESSAGE = 'Do not use Array index in keys'; function isArrayIndex(node) { return node.type === 'Identifier' @@ -77,7 +80,7 @@ module.exports = { function getMapIndexParamName(node) { const callee = node.callee; - if (callee.type !== 'MemberExpression') { + if (callee.type !== 'MemberExpression' && callee.type !== 'OptionalMemberExpression') { return null; } if (callee.property.type !== 'Identifier') { @@ -129,7 +132,7 @@ module.exports = { // key={bar} context.report({ node, - message: ERROR_MESSAGE + messageId: 'noArrayIndex' }); return; } @@ -137,7 +140,7 @@ module.exports = { if (node.type === 'TemplateLiteral') { // key={`foo-${bar}`} node.expressions.filter(isArrayIndex).forEach(() => { - context.report({node, message: ERROR_MESSAGE}); + context.report({node, messageId: 'noArrayIndex'}); }); return; @@ -148,16 +151,25 @@ module.exports = { const identifiers = getIdentifiersFromBinaryExpression(node); identifiers.filter(isArrayIndex).forEach(() => { - context.report({node, message: ERROR_MESSAGE}); + context.report({node, messageId: 'noArrayIndex'}); }); } } + function popIndex(node) { + const mapIndexParamName = getMapIndexParamName(node); + if (!mapIndexParamName) { + return; + } + + indexParamNames.pop(); + } + return { - CallExpression(node) { + 'CallExpression, OptionalCallExpression'(node) { if ( node.callee - && node.callee.type === 'MemberExpression' + && (node.callee.type === 'MemberExpression' || node.callee.type === 'OptionalMemberExpression') && ['createElement', 'cloneElement'].indexOf(node.callee.property.name) !== -1 && node.arguments.length > 1 ) { @@ -213,14 +225,8 @@ module.exports = { checkPropValue(value.expression); }, - 'CallExpression:exit'(node) { - const mapIndexParamName = getMapIndexParamName(node); - if (!mapIndexParamName) { - return; - } - - indexParamNames.pop(); - } + 'CallExpression:exit': popIndex, + 'OptionalCallExpression:exit': popIndex }; } }; diff --git a/lib/rules/no-children-prop.js b/lib/rules/no-children-prop.js index 7bc650cffd..1b5171ab19 100644 --- a/lib/rules/no-children-prop.js +++ b/lib/rules/no-children-prop.js @@ -37,6 +37,12 @@ module.exports = { recommended: true, url: docsUrl('no-children-prop') }, + + messages: { + nestChildren: 'Do not pass children as props. Instead, nest children between the opening and closing tags.', + passChildrenAsArgs: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.' + }, + schema: [] }, create(context) { @@ -48,7 +54,7 @@ module.exports = { context.report({ node, - message: 'Do not pass children as props. Instead, nest children between the opening and closing tags.' + messageId: 'nestChildren' }); }, CallExpression(node) { @@ -62,7 +68,7 @@ module.exports = { if (childrenProp) { context.report({ node, - message: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.' + messageId: 'passChildrenAsArgs' }); } } diff --git a/lib/rules/no-danger-with-children.js b/lib/rules/no-danger-with-children.js index a58dc7d365..fedfafd42c 100644 --- a/lib/rules/no-danger-with-children.js +++ b/lib/rules/no-danger-with-children.js @@ -20,6 +20,11 @@ module.exports = { recommended: true, url: docsUrl('no-danger-with-children') }, + + messages: { + dangerWithChildren: 'Only set one of `children` or `props.dangerouslySetInnerHTML`' + }, + schema: [] // no options }, create(context) { @@ -104,7 +109,7 @@ module.exports = { ) { context.report({ node, - message: 'Only set one of `children` or `props.dangerouslySetInnerHTML`' + messageId: 'dangerWithChildren' }); } }, @@ -139,7 +144,7 @@ module.exports = { if (dangerously && hasChildren) { context.report({ node, - message: 'Only set one of `children` or `props.dangerouslySetInnerHTML`' + messageId: 'dangerWithChildren' }); } } diff --git a/lib/rules/no-danger.js b/lib/rules/no-danger.js index 9322c090fe..da3ab9526b 100644 --- a/lib/rules/no-danger.js +++ b/lib/rules/no-danger.js @@ -12,8 +12,6 @@ const jsxUtil = require('../util/jsx'); // Constants // ------------------------------------------------------------------------------ -const DANGEROUS_MESSAGE = 'Dangerous property \'{{name}}\' found'; - const DANGEROUS_PROPERTY_NAMES = [ 'dangerouslySetInnerHTML' ]; @@ -48,6 +46,11 @@ module.exports = { recommended: false, url: docsUrl('no-danger') }, + + messages: { + dangerousProp: 'Dangerous property \'{{name}}\' found' + }, + schema: [] }, @@ -58,7 +61,7 @@ module.exports = { if (jsxUtil.isDOMComponent(node.parent) && isDangerous(node.name.name)) { context.report({ node, - message: DANGEROUS_MESSAGE, + messageId: 'dangerousProp', data: { name: node.name.name } diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js index a630c4d12f..6d2c9dd138 100644 --- a/lib/rules/no-deprecated.js +++ b/lib/rules/no-deprecated.js @@ -24,8 +24,6 @@ const MODULES = { 'react-addons-perf': ['ReactPerf', 'Perf'] }; -const DEPRECATED_MESSAGE = '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}{{refs}}'; - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -38,6 +36,11 @@ module.exports = { recommended: true, url: docsUrl('no-deprecated') }, + + messages: { + deprecated: '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}{{refs}}' + }, + schema: [] }, @@ -122,7 +125,7 @@ module.exports = { const refs = deprecated[methodName][2]; context.report({ node: methodNode || node, - message: DEPRECATED_MESSAGE, + messageId: 'deprecated', data: { oldMethod: methodName, version, diff --git a/lib/rules/no-direct-mutation-state.js b/lib/rules/no-direct-mutation-state.js index 816fbeb3d2..5a7449da9b 100644 --- a/lib/rules/no-direct-mutation-state.js +++ b/lib/rules/no-direct-mutation-state.js @@ -20,6 +20,10 @@ module.exports = { category: 'Possible Errors', recommended: true, url: docsUrl('no-direct-mutation-state') + }, + + messages: { + noDirectMutation: 'Do not mutate state directly. Use setState().' } }, @@ -43,7 +47,7 @@ module.exports = { mutation = component.mutations[i]; context.report({ node: mutation, - message: 'Do not mutate state directly. Use setState().' + messageId: 'noDirectMutation' }); } } diff --git a/lib/rules/no-find-dom-node.js b/lib/rules/no-find-dom-node.js index eb2664fc0a..543bc76b17 100644 --- a/lib/rules/no-find-dom-node.js +++ b/lib/rules/no-find-dom-node.js @@ -19,6 +19,11 @@ module.exports = { recommended: true, url: docsUrl('no-find-dom-node') }, + + messages: { + noFindDOMNode: 'Do not use findDOMNode. It doesn’t work with function components and is deprecated in StrictMode. See https://reactjs.org/docs/react-dom.html#finddomnode' + }, + schema: [] }, @@ -40,7 +45,7 @@ module.exports = { context.report({ node: callee, - message: 'Do not use findDOMNode. It doesn’t work with function components and is deprecated in StrictMode. See https://reactjs.org/docs/react-dom.html#finddomnode' + messageId: 'noFindDOMNode' }); } }; diff --git a/lib/rules/no-is-mounted.js b/lib/rules/no-is-mounted.js index 3132079831..9fd54fa9e6 100644 --- a/lib/rules/no-is-mounted.js +++ b/lib/rules/no-is-mounted.js @@ -19,6 +19,11 @@ module.exports = { recommended: true, url: docsUrl('no-is-mounted') }, + + messages: { + noIsMounted: 'Do not use isMounted' + }, + schema: [] }, @@ -42,7 +47,7 @@ module.exports = { if (ancestors[i].type === 'Property' || ancestors[i].type === 'MethodDefinition') { context.report({ node: callee, - message: 'Do not use isMounted' + messageId: 'noIsMounted' }); break; } diff --git a/lib/rules/no-multi-comp.js b/lib/rules/no-multi-comp.js index 304314bb3c..f60f2dde6d 100644 --- a/lib/rules/no-multi-comp.js +++ b/lib/rules/no-multi-comp.js @@ -21,6 +21,10 @@ module.exports = { url: docsUrl('no-multi-comp') }, + messages: { + onlyOneComponent: 'Declare only one React component per file' + }, + schema: [{ type: 'object', properties: { @@ -37,8 +41,6 @@ module.exports = { const configuration = context.options[0] || {}; const ignoreStateless = configuration.ignoreStateless || false; - const MULTI_COMP_MESSAGE = 'Declare only one React component per file'; - /** * Checks if the component is ignored * @param {Object} component The component being checked. @@ -69,7 +71,7 @@ module.exports = { if (i >= 1) { context.report({ node: list[component].node, - message: MULTI_COMP_MESSAGE + messageId: 'onlyOneComponent' }); } }); diff --git a/lib/rules/no-redundant-should-component-update.js b/lib/rules/no-redundant-should-component-update.js index 3321dffe6f..a0ec96f12b 100644 --- a/lib/rules/no-redundant-should-component-update.js +++ b/lib/rules/no-redundant-should-component-update.js @@ -8,10 +8,6 @@ const Components = require('../util/Components'); const astUtil = require('../util/ast'); const docsUrl = require('../util/docsUrl'); -function errorMessage(node) { - return `${node} does not need shouldComponentUpdate when extending React.PureComponent.`; -} - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -24,6 +20,11 @@ module.exports = { recommended: false, url: docsUrl('no-redundant-should-component-update') }, + + messages: { + noShouldCompUpdate: '{{component}} does not need shouldComponentUpdate when extending React.PureComponent.' + }, + schema: [] }, @@ -67,7 +68,10 @@ module.exports = { const className = getNodeName(node); context.report({ node, - message: errorMessage(className) + messageId: 'noShouldCompUpdate', + data: { + component: className + } }); } } diff --git a/lib/rules/no-render-return-value.js b/lib/rules/no-render-return-value.js index b525590f10..c355286ad7 100644 --- a/lib/rules/no-render-return-value.js +++ b/lib/rules/no-render-return-value.js @@ -20,6 +20,11 @@ module.exports = { recommended: true, url: docsUrl('no-render-return-value') }, + + messages: { + noReturnValue: 'Do not depend on the return value from {{node}}.render' + }, + schema: [] }, @@ -63,7 +68,10 @@ module.exports = { ) { context.report({ node: callee, - message: `Do not depend on the return value from ${callee.object.name}.render` + messageId: 'noReturnValue', + data: { + node: callee.object.name + } }); } } diff --git a/lib/rules/no-set-state.js b/lib/rules/no-set-state.js index 2b5cafcbbe..1fd262ab73 100644 --- a/lib/rules/no-set-state.js +++ b/lib/rules/no-set-state.js @@ -20,6 +20,11 @@ module.exports = { recommended: false, url: docsUrl('no-set-state') }, + + messages: { + noSetState: 'Do not use setState' + }, + schema: [] }, @@ -43,7 +48,7 @@ module.exports = { setStateUsage = component.setStateUsages[i]; context.report({ node: setStateUsage, - message: 'Do not use setState' + messageId: 'noSetState' }); } } diff --git a/lib/rules/no-string-refs.js b/lib/rules/no-string-refs.js index b9ef0d9df3..920a8a5cf7 100644 --- a/lib/rules/no-string-refs.js +++ b/lib/rules/no-string-refs.js @@ -20,6 +20,12 @@ module.exports = { recommended: true, url: docsUrl('no-string-refs') }, + + messages: { + thisRefsDeprecated: 'Using this.refs is deprecated.', + stringInRefDeprecated: 'Using string literals in ref attributes is deprecated.' + }, + schema: [{ type: 'object', properties: { @@ -95,7 +101,7 @@ module.exports = { if (isRefsUsage(node)) { context.report({ node, - message: 'Using this.refs is deprecated.' + messageId: 'thisRefsDeprecated' }); } }, @@ -106,7 +112,7 @@ module.exports = { ) { context.report({ node, - message: 'Using string literals in ref attributes is deprecated.' + messageId: 'stringInRefDeprecated' }); } } diff --git a/lib/rules/no-this-in-sfc.js b/lib/rules/no-this-in-sfc.js index 32589639f9..da5d843690 100644 --- a/lib/rules/no-this-in-sfc.js +++ b/lib/rules/no-this-in-sfc.js @@ -7,12 +7,6 @@ const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); -// ------------------------------------------------------------------------------ -// Constants -// ------------------------------------------------------------------------------ - -const ERROR_MESSAGE = 'Stateless functional components should not use `this`'; - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -25,6 +19,11 @@ module.exports = { recommended: false, url: docsUrl('no-this-in-sfc') }, + + messages: { + noThisInSFC: 'Stateless functional components should not use `this`' + }, + schema: [] }, @@ -37,7 +36,7 @@ module.exports = { } context.report({ node, - message: ERROR_MESSAGE + messageId: 'noThisInSFC' }); } } diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js index f9ce71329b..4352a51d94 100644 --- a/lib/rules/no-typos.js +++ b/lib/rules/no-typos.js @@ -39,6 +39,18 @@ module.exports = { recommended: false, url: docsUrl('no-typos') }, + + messages: { + typoPropTypeChain: 'Typo in prop type chain qualifier: {{name}}', + typoPropType: 'Typo in declared prop type: {{name}}', + typoStaticClassProp: 'Typo in static class property declaration', + typoPropDeclaration: 'Typo in property declaration', + typoLifecycleMethod: 'Typo in component lifecycle method declaration: {{actual}} should be {{expected}}', + staticLifecycleMethod: 'Lifecycle method should be static: {{method}}', + noPropTypesBinding: '`\'prop-types\'` imported without a local `PropTypes` binding.', + noReactBinding: '`\'react\'` imported without a local `React` binding.' + }, + schema: [] }, @@ -50,7 +62,7 @@ module.exports = { if (node.name !== 'isRequired') { context.report({ node, - message: 'Typo in prop type chain qualifier: {{name}}', + messageId: 'typoPropTypeChain', data: {name: node.name} }); } @@ -60,7 +72,7 @@ module.exports = { if (node.name && !PROP_TYPES.some((propTypeName) => propTypeName === node.name)) { context.report({ node, - message: 'Typo in declared prop type: {{name}}', + messageId: 'typoPropType', data: {name: node.name} }); } @@ -134,12 +146,11 @@ module.exports = { } STATIC_CLASS_PROPERTIES.forEach((CLASS_PROP) => { if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) { - const message = isClassProperty - ? 'Typo in static class property declaration' - : 'Typo in property declaration'; context.report({ node: propertyKey, - message + messageId: isClassProperty + ? 'typoStaticClassProp' + : 'typoPropDeclaration' }); } }); @@ -158,7 +169,10 @@ module.exports = { if (!node.static && nodeKeyName.toLowerCase() === method.toLowerCase()) { context.report({ node, - message: `Lifecycle method should be static: ${nodeKeyName}` + messageId: 'staticLifecycleMethod', + data: { + method: nodeKeyName + } }); } }); @@ -167,7 +181,7 @@ module.exports = { if (method.toLowerCase() === nodeKeyName.toLowerCase() && method !== nodeKeyName) { context.report({ node, - message: 'Typo in component lifecycle method declaration: {{actual}} should be {{expected}}', + messageId: 'typoLifecycleMethod', data: {actual: nodeKeyName, expected: method} }); } @@ -177,14 +191,21 @@ module.exports = { return { ImportDeclaration(node) { if (node.source && node.source.value === 'prop-types') { // import PropType from "prop-types" - propTypesPackageName = node.specifiers[0].local.name; + if (node.specifiers.length > 0) { + propTypesPackageName = node.specifiers[0].local.name; + } else { + context.report({ + node, + messageId: 'noPropTypesBinding' + }); + } } else if (node.source && node.source.value === 'react') { // import { PropTypes } from "react" if (node.specifiers.length > 0) { reactPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "react"` } else { context.report({ node, - message: '`\'react\'` imported without a local `React` binding.' + messageId: 'noReactBinding' }); } if (node.specifiers.length >= 1) { diff --git a/lib/rules/no-unescaped-entities.js b/lib/rules/no-unescaped-entities.js index db3ca2c0e8..37535b468f 100644 --- a/lib/rules/no-unescaped-entities.js +++ b/lib/rules/no-unescaped-entities.js @@ -37,6 +37,12 @@ module.exports = { recommended: true, url: docsUrl('no-unescaped-entities') }, + + messages: { + unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.', + unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.' + }, + schema: [{ type: 'object', properties: { @@ -91,16 +97,23 @@ module.exports = { if (typeof entities[j] === 'string') { if (c === entities[j]) { context.report({ + node, loc: {line: i, column: start + index}, - message: `HTML entity, \`${entities[j]}\` , must be escaped.`, - node + messageId: 'unescapedEntity', + data: { + entity: entities[j] + } }); } } else if (c === entities[j].char) { context.report({ + node, loc: {line: i, column: start + index}, - message: `\`${entities[j].char}\` can be escaped with ${entities[j].alternatives.map((alt) => `\`${alt}\``).join(', ')}.`, - node + messageId: 'unescapedEntityAlts', + data: { + entity: entities[j].char, + alts: entities[j].alternatives.map((alt) => `\`${alt}\``).join(', ') + } }); } } diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js index 5ea118922c..a08db66caf 100644 --- a/lib/rules/no-unknown-property.js +++ b/lib/rules/no-unknown-property.js @@ -17,9 +17,6 @@ const DEFAULTS = { ignore: [] }; -const UNKNOWN_MESSAGE = 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead'; -const WRONG_TAG_MESSAGE = 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}'; - const DOM_ATTRIBUTE_NAMES = { 'accept-charset': 'acceptCharset', class: 'className', @@ -224,6 +221,11 @@ module.exports = { }, fixable: 'code', + messages: { + invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}', + unknownProp: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead' + }, + schema: [{ type: 'object', properties: { @@ -263,7 +265,7 @@ module.exports = { if (tagName && allowedTags && /[^A-Z]/.test(tagName.charAt(0)) && allowedTags.indexOf(tagName) === -1) { context.report({ node, - message: WRONG_TAG_MESSAGE, + messageId: 'invalidPropOnTag', data: { name, tagName, @@ -282,7 +284,7 @@ module.exports = { } context.report({ node, - message: UNKNOWN_MESSAGE, + messageId: 'unknownProp', data: { name, standardName diff --git a/lib/rules/no-unsafe.js b/lib/rules/no-unsafe.js index 81c7eccacc..55018969b9 100644 --- a/lib/rules/no-unsafe.js +++ b/lib/rules/no-unsafe.js @@ -22,6 +22,11 @@ module.exports = { recommended: false, url: docsUrl('no-unsafe') }, + + messages: { + unsafeMethod: '{{method}} is unsafe for use in async rendering. Update the component to use {{newMethod}} instead. {{details}}' + }, + schema: [ { type: 'object', @@ -102,7 +107,12 @@ module.exports = { context.report({ node, - message: `${method} is unsafe for use in async rendering. Update the component to use ${newMethod} instead. ${details}` + messageId: 'unsafeMethod', + data: { + method, + newMethod, + details + } }); } diff --git a/lib/rules/no-unstable-nested-components.js b/lib/rules/no-unstable-nested-components.js new file mode 100644 index 0000000000..56db0e06e4 --- /dev/null +++ b/lib/rules/no-unstable-nested-components.js @@ -0,0 +1,468 @@ +/** + * @fileoverview Prevent creating unstable components inside components + * @author Ari Perkkiö + */ + +'use strict'; + +const Components = require('../util/Components'); +const docsUrl = require('../util/docsUrl'); + +// ------------------------------------------------------------------------------ +// Constants +// ------------------------------------------------------------------------------ + +const ERROR_MESSAGE_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.'; +const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.'; +const HOOK_REGEXP = /^use[A-Z0-9].*$/; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Generate error message with given parent component name + * @param {String} parentName Name of the parent component + * @returns {String} Error message with parent component name + */ +function generateErrorMessageWithParentName(parentName) { + return `Declare this component outside parent component "${parentName}" or memoize it.`; +} + +/** + * Check whether given text starts with `render`. Comparison is case-sensitive. + * @param {String} text Text to validate + * @returns {Boolean} + */ +function startsWithRender(text) { + return (text || '').startsWith('render'); +} + +/** + * Get closest parent matching given matcher + * @param {ASTNode} node The AST node + * @param {Function} matcher Method used to match the parent + * @returns {ASTNode} The matching parent node, if any + */ +function getClosestMatchingParent(node, matcher) { + if (!node || !node.parent || node.parent.type === 'Program') { + return; + } + + if (matcher(node.parent)) { + return node.parent; + } + + return getClosestMatchingParent(node.parent, matcher); +} + +/** + * Matcher used to check whether given node is a `createElement` call + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `createElement` call, false if not + */ +function isCreateElementMatcher(node) { + return ( + node + && node.type === 'CallExpression' + && node.callee + && node.callee.property + && node.callee.property.name === 'createElement' + ); +} + +/** + * Matcher used to check whether given node is a `ObjectExpression` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `ObjectExpression`, false if not + */ +function isObjectExpressionMatcher(node) { + return node && node.type === 'ObjectExpression'; +} + +/** + * Matcher used to check whether given node is a `JSXExpressionContainer` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `JSXExpressionContainer`, false if not + */ +function isJSXExpressionContainerMatcher(node) { + return node && node.type === 'JSXExpressionContainer'; +} + +/** + * Matcher used to check whether given node is a `JSXAttribute` of `JSXExpressionContainer` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `JSXAttribute` of `JSXExpressionContainer`, false if not + */ +function isJSXAttributeOfExpressionContainerMatcher(node) { + return ( + node + && node.type === 'JSXAttribute' + && node.value + && node.value.type === 'JSXExpressionContainer' + ); +} + +/** + * Matcher used to check whether given node is a `CallExpression` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `CallExpression`, false if not + */ +function isCallExpressionMatcher(node) { + return node && node.type === 'CallExpression'; +} + +/** + * Check whether given node or its parent is directly inside `map` call + * ```jsx + * {items.map(item =>
  • )} + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is directly inside `map` call, false if not + */ +function isMapCall(node) { + return ( + node + && node.callee + && node.callee.property + && node.callee.property.name === 'map' + ); +} + +/** + * Check whether given node is `ReturnStatement` of a React hook + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `ReturnStatement` of a React hook, false if not + */ +function isReturnStatementOfHook(node) { + if ( + !node + || !node.parent + || node.parent.type !== 'ReturnStatement' + ) { + return false; + } + + const callExpression = getClosestMatchingParent(node, isCallExpressionMatcher); + return ( + callExpression + && callExpression.callee + && HOOK_REGEXP.test(callExpression.callee.name) + ); +} + +/** + * Check whether given node is declared inside a render prop + * ```jsx + *
    } /> + * {() =>
    } + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if component is declared inside a render prop, false if not + */ +function isComponentInRenderProp(node) { + if ( + node + && node.parent + && node.parent.type === 'Property' + && node.parent.key + && startsWithRender(node.parent.key.name) + ) { + return true; + } + + // Check whether component is a render prop used as direct children, e.g. {() =>
    } + if ( + node + && node.parent + && node.parent.type === 'JSXExpressionContainer' + && node.parent.parent + && node.parent.parent.type === 'JSXElement' + ) { + return true; + } + + const jsxExpressionContainer = getClosestMatchingParent(node, isJSXExpressionContainerMatcher); + + // Check whether prop name indicates accepted patterns + if ( + jsxExpressionContainer + && jsxExpressionContainer.parent + && jsxExpressionContainer.parent.type === 'JSXAttribute' + && jsxExpressionContainer.parent.name + && jsxExpressionContainer.parent.name.type === 'JSXIdentifier' + ) { + const propName = jsxExpressionContainer.parent.name.name; + + // Starts with render, e.g.
    } /> + if (startsWithRender(propName)) { + return true; + } + + // Uses children prop explicitly, e.g.
    } /> + if (propName === 'children') { + return true; + } + } + + return false; +} + +/** + * Check whether given node is declared directly inside a render property + * ```jsx + * const rows = { render: () =>
    } + *
    }] } /> + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if component is declared inside a render property, false if not + */ +function isDirectValueOfRenderProperty(node) { + return ( + node + && node.parent + && node.parent.type === 'Property' + && node.parent.key + && node.parent.key.type === 'Identifier' + && startsWithRender(node.parent.key.name) + ); +} + +/** + * Resolve the component name of given node + * @param {ASTNode} node The AST node of the component + * @returns {String} Name of the component, if any + */ +function resolveComponentName(node) { + const parentName = node.id && node.id.name; + if (parentName) return parentName; + + return ( + node.type === 'ArrowFunctionExpression' + && node.parent + && node.parent.id + && node.parent.id.name + ); +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Prevent creating unstable components inside components', + category: 'Possible Errors', + recommended: false, + url: docsUrl('no-unstable-nested-components') + }, + schema: [{ + type: 'object', + properties: { + customValidators: { + type: 'array', + items: { + type: 'string' + } + }, + allowAsProps: { + type: 'boolean' + } + }, + additionalProperties: false + }] + }, + + create: Components.detect((context, components, utils) => { + const allowAsProps = context.options.some((option) => option && option.allowAsProps); + + /** + * Check whether given node is declared inside class component's render block + * ```jsx + * class Component extends React.Component { + * render() { + * class NestedClassComponent extends React.Component { + * ... + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if node is inside class component's render block, false if not + */ + function isInsideRenderMethod(node) { + const parentComponent = utils.getParentComponent(); + + if (!parentComponent || parentComponent.type !== 'ClassDeclaration') { + return false; + } + + return ( + node + && node.parent + && node.parent.type === 'MethodDefinition' + && node.parent.key + && node.parent.key.name === 'render' + ); + } + + /** + * Check whether given node is a function component declared inside class component. + * Util's component detection fails to detect function components inside class components. + * ```jsx + * class Component extends React.Component { + * render() { + * const NestedComponent = () =>
    ; + * ... + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if given node a function component declared inside class component, false if not + */ + function isFunctionComponentInsideClassComponent(node) { + const parentComponent = utils.getParentComponent(); + const parentStatelessComponent = utils.getParentStatelessComponent(); + + return ( + parentComponent + && parentStatelessComponent + && parentComponent.type === 'ClassDeclaration' + && utils.getStatelessComponent(parentStatelessComponent) + && utils.isReturningJSX(node) + ); + } + + /** + * Check whether given node is declared inside `createElement` call's props + * ```js + * React.createElement(Component, { + * footer: () => React.createElement("div", null) + * }) + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is declare inside `createElement` call's props, false if not + */ + function isComponentInsideCreateElementsProp(node) { + if (!components.get(node)) { + return false; + } + + const createElementParent = getClosestMatchingParent(node, isCreateElementMatcher); + + return ( + createElementParent + && createElementParent.arguments + && createElementParent.arguments[1] === getClosestMatchingParent(node, isObjectExpressionMatcher) + ); + } + + /** + * Check whether given node is declared inside a component prop. + * ```jsx + *
    } /> + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if node is a component declared inside prop, false if not + */ + function isComponentInProp(node) { + const jsxAttribute = getClosestMatchingParent(node, isJSXAttributeOfExpressionContainerMatcher); + + if (!jsxAttribute) { + return isComponentInsideCreateElementsProp(node); + } + + return utils.isReturningJSX(node); + } + + /** + * Check whether given node is a stateless component returning non-JSX + * ```jsx + * {{ a: () => null }} + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if node is a stateless component returning non-JSX, false if not + */ + function isStatelessComponentReturningNull(node) { + const component = utils.getStatelessComponent(node); + + return component && !utils.isReturningJSX(component); + } + + /** + * Check whether given node is a unstable nested component + * @param {ASTNode} node The AST node being checked + */ + function validate(node) { + if (!node || !node.parent) { + return; + } + + const isDeclaredInsideProps = isComponentInProp(node); + + if ( + !components.get(node) + && !isFunctionComponentInsideClassComponent(node) + && !isDeclaredInsideProps) { + return; + } + + if ( + // Support allowAsProps option + (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node))) + + // Prevent reporting components created inside Array.map calls + || isMapCall(node) + || isMapCall(node.parent) + + // Do not mark components declared inside hooks (or falsly '() => null' clean-up methods) + || isReturnStatementOfHook(node) + + // Do not mark objects containing render methods + || isDirectValueOfRenderProperty(node) + + // Prevent reporting nested class components twice + || isInsideRenderMethod(node) + + // Prevent falsely reporting deteceted "components" which do not return JSX + || isStatelessComponentReturningNull(node) + ) { + return; + } + + // Get the closest parent component + const parentComponent = getClosestMatchingParent( + node, + (nodeToMatch) => components.get(nodeToMatch) + ); + + if (parentComponent) { + const parentName = resolveComponentName(parentComponent); + + // Exclude lowercase parents, e.g. function createTestComponent() + // React-dom prevents creating lowercase components + if (parentName && parentName[0] === parentName[0].toLowerCase()) { + return; + } + + let message = parentName + ? generateErrorMessageWithParentName(parentName) + : ERROR_MESSAGE_WITHOUT_NAME; + + // Add information about allowAsProps option when component is declared inside prop + if (isDeclaredInsideProps && !allowAsProps) { + message += COMPONENT_AS_PROPS_INFO; + } + + context.report({node, message}); + } + } + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + FunctionDeclaration(node) { validate(node); }, + ArrowFunctionExpression(node) { validate(node); }, + FunctionExpression(node) { validate(node); }, + ClassDeclaration(node) { validate(node); } + }; + }) +}; diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index 0feafdd82d..0df5d1a851 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -24,6 +24,10 @@ module.exports = { url: docsUrl('no-unused-prop-types') }, + messages: { + unusedPropType: '\'{{name}}\' PropType is defined but prop is never used' + }, + schema: [{ type: 'object', properties: { @@ -44,7 +48,6 @@ module.exports = { create: Components.detect((context, components) => { const defaults = {skipShapeProps: true, customValidators: []}; const configuration = Object.assign({}, defaults, context.options[0] || {}); - const UNUSED_MESSAGE = '\'{{name}}\' PropType is defined but prop is never used'; /** * Checks if the component must be validated @@ -111,7 +114,7 @@ module.exports = { if (prop.node && !isPropUsed(component, prop)) { context.report({ node: prop.node.key || prop.node, - message: UNUSED_MESSAGE, + messageId: 'unusedPropType', data: { name: prop.fullName } diff --git a/lib/rules/no-unused-state.js b/lib/rules/no-unused-state.js index f9c862e7bc..d91a94c9a9 100644 --- a/lib/rules/no-unused-state.js +++ b/lib/rules/no-unused-state.js @@ -80,6 +80,11 @@ module.exports = { recommended: false, url: docsUrl('no-unused-state') }, + + messages: { + unusedStateField: 'Unused state field: \'{{name}}\'' + }, + schema: [] }, @@ -213,7 +218,10 @@ module.exports = { if (!classInfo.usedStateFields.has(name)) { context.report({ node, - message: `Unused state field: '${name}'` + messageId: 'unusedStateField', + data: { + name + } }); } } diff --git a/lib/rules/prefer-es6-class.js b/lib/rules/prefer-es6-class.js index b35a85b969..4ea2df9959 100644 --- a/lib/rules/prefer-es6-class.js +++ b/lib/rules/prefer-es6-class.js @@ -21,6 +21,11 @@ module.exports = { url: docsUrl('prefer-es6-class') }, + messages: { + shouldUseES6Class: 'Component should use es6 class instead of createClass', + shouldUseCreateClass: 'Component should use createClass instead of es6 class' + }, + schema: [{ enum: ['always', 'never'] }] @@ -34,7 +39,7 @@ module.exports = { if (utils.isES5Component(node) && configuration === 'always') { context.report({ node, - message: 'Component should use es6 class instead of createClass' + messageId: 'shouldUseES6Class' }); } }, @@ -42,7 +47,7 @@ module.exports = { if (utils.isES6Component(node) && configuration === 'never') { context.report({ node, - message: 'Component should use createClass instead of es6 class' + messageId: 'shouldUseCreateClass' }); } } diff --git a/lib/rules/prefer-read-only-props.js b/lib/rules/prefer-read-only-props.js index d0187d5e57..028cf304cb 100644 --- a/lib/rules/prefer-read-only-props.js +++ b/lib/rules/prefer-read-only-props.js @@ -29,6 +29,11 @@ module.exports = { url: docsUrl('prefer-read-only-props') }, fixable: 'code', + + messages: { + readOnlyProp: 'Prop \'{{name}}\' should be read-only.' + }, + schema: [] }, @@ -53,9 +58,9 @@ module.exports = { if (!isCovariant(prop.node)) { context.report({ node: prop.node, - message: 'Prop \'{{propName}}\' should be read-only.', + messageId: 'readOnlyProp', data: { - propName + name: propName }, fix: (fixer) => { if (!prop.node.variance) { diff --git a/lib/rules/prefer-stateless-function.js b/lib/rules/prefer-stateless-function.js index 641ffc704b..c36a67ed47 100644 --- a/lib/rules/prefer-stateless-function.js +++ b/lib/rules/prefer-stateless-function.js @@ -24,6 +24,11 @@ module.exports = { recommended: false, url: docsUrl('prefer-stateless-function') }, + + messages: { + componentShouldBePure: 'Component should be written as a pure function' + }, + schema: [{ type: 'object', properties: { @@ -372,7 +377,7 @@ module.exports = { } context.report({ node: list[component].node, - message: 'Component should be written as a pure function' + messageId: 'componentShouldBePure' }); }); } diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index 0166ca9ea2..128edffe49 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -24,6 +24,10 @@ module.exports = { url: docsUrl('prop-types') }, + messages: { + missingPropType: '\'{{name}}\' is missing in props validation' + }, + schema: [{ type: 'object', properties: { @@ -52,8 +56,6 @@ module.exports = { const ignored = configuration.ignore || []; const skipUndeclared = configuration.skipUndeclared || false; - const MISSING_MESSAGE = '\'{{name}}\' is missing in props validation'; - /** * Checks if the prop is ignored * @param {String} name Name of the prop to check. @@ -172,7 +174,7 @@ module.exports = { undeclareds.forEach((propType) => { context.report({ node: propType.node, - message: MISSING_MESSAGE, + messageId: 'missingPropType', data: { name: propType.allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]') } diff --git a/lib/rules/react-in-jsx-scope.js b/lib/rules/react-in-jsx-scope.js index 9df35dab76..6d9ab8c8bd 100644 --- a/lib/rules/react-in-jsx-scope.js +++ b/lib/rules/react-in-jsx-scope.js @@ -21,12 +21,16 @@ module.exports = { recommended: true, url: docsUrl('react-in-jsx-scope') }, + + messages: { + notInScope: '\'{{name}}\' must be in scope when using JSX' + }, + schema: [] }, create(context) { const pragma = pragmaUtil.getFromContext(context); - const NOT_DEFINED_MESSAGE = '\'{{name}}\' must be in scope when using JSX'; function checkIfReactIsInScope(node) { const variables = variableUtil.variablesInScope(context); @@ -35,7 +39,7 @@ module.exports = { } context.report({ node, - message: NOT_DEFINED_MESSAGE, + messageId: 'notInScope', data: { name: pragma } diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index a1213f36e0..b6330b116b 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -21,6 +21,11 @@ module.exports = { url: docsUrl('require-default-props') }, + messages: { + noDefaultWithRequired: 'propType "{{name}}" is required and should not have a defaultProps declaration.', + shouldHaveDefault: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.' + }, + schema: [{ type: 'object', properties: { @@ -59,7 +64,7 @@ module.exports = { if (forbidDefaultForRequired && defaultProps[propName]) { context.report({ node: prop.node, - message: 'propType "{{name}}" is required and should not have a defaultProps declaration.', + messageId: 'noDefaultWithRequired', data: {name: propName} }); } @@ -72,7 +77,7 @@ module.exports = { context.report({ node: prop.node, - message: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.', + messageId: 'shouldHaveDefault', data: {name: propName} }); }); diff --git a/lib/rules/require-optimization.js b/lib/rules/require-optimization.js index 54f633fd0c..d674a14480 100644 --- a/lib/rules/require-optimization.js +++ b/lib/rules/require-optimization.js @@ -17,6 +17,10 @@ module.exports = { url: docsUrl('require-optimization') }, + messages: { + noShouldComponentUpdate: 'Component is not optimized. Please add a shouldComponentUpdate method.' + }, + schema: [{ type: 'object', properties: { @@ -32,7 +36,6 @@ module.exports = { }, create: Components.detect((context, components, utils) => { - const MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.'; const configuration = context.options[0] || {}; const allowDecorators = configuration.allowDecorators || []; @@ -139,10 +142,7 @@ module.exports = { function reportMissingOptimization(component) { context.report({ node: component.node, - message: MISSING_MESSAGE, - data: { - component: component.name - } + messageId: 'noShouldComponentUpdate' }); } diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js index 106ae6b8fb..0cc925c9b5 100644 --- a/lib/rules/require-render-return.js +++ b/lib/rules/require-render-return.js @@ -21,7 +21,12 @@ module.exports = { recommended: true, url: docsUrl('require-render-return') }, - schema: [{}] + + messages: { + noRenderReturn: 'Your render method should have a return statement' + }, + + schema: [] }, create: Components.detect((context, components, utils) => { @@ -84,7 +89,7 @@ module.exports = { } context.report({ node: findRenderMethod(list[component].node), - message: 'Your render method should have a return statement' + messageId: 'noRenderReturn' }); }); } diff --git a/lib/rules/self-closing-comp.js b/lib/rules/self-closing-comp.js index d2220ca522..66ff6ced8c 100644 --- a/lib/rules/self-closing-comp.js +++ b/lib/rules/self-closing-comp.js @@ -24,6 +24,10 @@ module.exports = { }, fixable: 'code', + messages: { + notSelfClosing: 'Empty components are self-closing' + }, + schema: [{ type: 'object', properties: { @@ -84,7 +88,7 @@ module.exports = { } context.report({ node, - message: 'Empty components are self-closing', + messageId: 'notSelfClosing', fix(fixer) { // Represents the last character of the JSXOpeningElement, the '>' character const openingElementEnding = node.range[1] - 1; diff --git a/lib/rules/sort-comp.js b/lib/rules/sort-comp.js index 9d25f08d6b..d26dbb9273 100644 --- a/lib/rules/sort-comp.js +++ b/lib/rules/sort-comp.js @@ -89,6 +89,10 @@ module.exports = { url: docsUrl('sort-comp') }, + messages: { + unsortedProps: '{{propA}} should be placed {{position}} {{propB}}' + }, + schema: [{ type: 'object', properties: { @@ -116,9 +120,6 @@ module.exports = { create: Components.detect((context, components) => { const errors = {}; - - const MISPOSITION_MESSAGE = '{{propA}} should be placed {{position}} {{propB}}'; - const methodsOrder = getMethodsOrder(context.options[0]); // -------------------------------------------------------------------------- @@ -309,7 +310,7 @@ module.exports = { context.report({ node: nodeA, - message: MISPOSITION_MESSAGE, + messageId: 'unsortedProps', data: { propA: getPropertyName(nodeA), propB: getPropertyName(nodeB), diff --git a/lib/rules/sort-prop-types.js b/lib/rules/sort-prop-types.js index 9dbf13ad1a..baef6ae4e0 100644 --- a/lib/rules/sort-prop-types.js +++ b/lib/rules/sort-prop-types.js @@ -22,9 +22,14 @@ module.exports = { recommended: false, url: docsUrl('sort-prop-types') }, - // fixable: 'code', + messages: { + requiredPropsFirst: 'Required prop types must be listed before all other prop types', + callbackPropsLast: 'Callback prop types must be listed after all other prop types', + propsNotSorted: 'Prop types declarations should be sorted alphabetically' + }, + schema: [{ type: 'object', properties: { @@ -136,7 +141,7 @@ module.exports = { // Encountered a non-required prop after a required prop context.report({ node: curr, - message: 'Required prop types must be listed before all other prop types' + messageId: 'requiredPropsFirst' // fix }); return curr; @@ -152,7 +157,7 @@ module.exports = { // Encountered a non-callback prop after a callback prop context.report({ node: prev, - message: 'Callback prop types must be listed after all other prop types' + messageId: 'callbackPropsLast' // fix }); return prev; @@ -162,7 +167,7 @@ module.exports = { if (!noSortAlphabetically && currentPropName < prevPropName) { context.report({ node: curr, - message: 'Prop types declarations should be sorted alphabetically' + messageId: 'propsNotSorted' // fix }); return prev; diff --git a/lib/rules/state-in-constructor.js b/lib/rules/state-in-constructor.js index f8f878051d..d8e8f6c2d5 100644 --- a/lib/rules/state-in-constructor.js +++ b/lib/rules/state-in-constructor.js @@ -20,6 +20,12 @@ module.exports = { recommended: false, url: docsUrl('state-in-constructor') }, + + messages: { + stateInitConstructor: 'State initialization should be in a constructor', + stateInitClassProp: 'State initialization should be in a class property' + }, + schema: [{ enum: ['always', 'never'] }] @@ -37,7 +43,7 @@ module.exports = { ) { context.report({ node, - message: 'State initialization should be in a constructor' + messageId: 'stateInitConstructor' }); } }, @@ -50,7 +56,7 @@ module.exports = { ) { context.report({ node, - message: 'State initialization should be in a class property' + messageId: 'stateInitClassProp' }); } } diff --git a/lib/rules/static-property-placement.js b/lib/rules/static-property-placement.js index 69bc6b0d98..318230d2c6 100644 --- a/lib/rules/static-property-placement.js +++ b/lib/rules/static-property-placement.js @@ -23,9 +23,9 @@ const POSITION_SETTINGS = [STATIC_PUBLIC_FIELD, STATIC_GETTER, PROPERTY_ASSIGNME // Rule messages // ------------------------------------------------------------------------------ const ERROR_MESSAGES = { - [STATIC_PUBLIC_FIELD]: '\'{{name}}\' should be declared as a static class property.', - [STATIC_GETTER]: '\'{{name}}\' should be declared as a static getter class function.', - [PROPERTY_ASSIGNMENT]: '\'{{name}}\' should be declared outside the class body.' + [STATIC_PUBLIC_FIELD]: 'notStaticClassProp', + [STATIC_GETTER]: 'notGetterClassFunc', + [PROPERTY_ASSIGNMENT]: 'declareOutsideClass' }; // ------------------------------------------------------------------------------ @@ -56,6 +56,13 @@ module.exports = { url: docsUrl('static-property-placement') }, fixable: null, // or 'code' or 'whitespace' + + messages: { + notStaticClassProp: '\'{{name}}\' should be declared as a static class property.', + notGetterClassFunc: '\'{{name}}\' should be declared as a static getter class function.', + declareOutsideClass: '\'{{name}}\' should be declared outside the class body.' + }, + schema: [ {enum: POSITION_SETTINGS}, { @@ -121,7 +128,7 @@ module.exports = { // Report the error context.report({ node, - message: ERROR_MESSAGES[config[name]], + messageId: ERROR_MESSAGES[config[name]], data: {name} }); } @@ -131,7 +138,13 @@ module.exports = { // Public // ---------------------------------------------------------------------- return { - ClassProperty: (node) => reportNodeIncorrectlyPositioned(node, STATIC_PUBLIC_FIELD), + ClassProperty: (node) => { + if (!utils.getParentES6Component()) { + return; + } + + reportNodeIncorrectlyPositioned(node, STATIC_PUBLIC_FIELD); + }, MemberExpression: (node) => { // If definition type is undefined then it must not be a defining expression or if the definition is inside a @@ -155,7 +168,7 @@ module.exports = { MethodDefinition: (node) => { // If the function is inside a class and is static getter then check if correctly positioned - if (isContextInClass() && node.static && node.kind === 'get') { + if (utils.getParentES6Component() && node.static && node.kind === 'get') { // Report error if needed reportNodeIncorrectlyPositioned(node, STATIC_GETTER); } diff --git a/lib/rules/style-prop-object.js b/lib/rules/style-prop-object.js index 1330b04272..7053755519 100644 --- a/lib/rules/style-prop-object.js +++ b/lib/rules/style-prop-object.js @@ -20,6 +20,11 @@ module.exports = { recommended: false, url: docsUrl('style-prop-object') }, + + messages: { + stylePropNotObject: 'Style prop value must be an object' + }, + schema: [ { type: 'object', @@ -61,7 +66,7 @@ module.exports = { if (isNonNullaryLiteral(variable.defs[0].node.init)) { context.report({ node, - message: 'Style prop value must be an object' + messageId: 'stylePropNotObject' }); } } @@ -92,7 +97,7 @@ module.exports = { } else if (isNonNullaryLiteral(style.value)) { context.report({ node: style.value, - message: 'Style prop value must be an object' + messageId: 'stylePropNotObject' }); } } @@ -122,7 +127,7 @@ module.exports = { if (node.value.type !== 'JSXExpressionContainer' || isNonNullaryLiteral(node.value.expression)) { context.report({ node, - message: 'Style prop value must be an object' + messageId: 'stylePropNotObject' }); } else if (node.value.expression.type === 'Identifier') { checkIdentifiers(node.value.expression); diff --git a/lib/rules/void-dom-elements-no-children.js b/lib/rules/void-dom-elements-no-children.js index d25cc49dde..634eac517b 100644 --- a/lib/rules/void-dom-elements-no-children.js +++ b/lib/rules/void-dom-elements-no-children.js @@ -40,10 +40,6 @@ function isVoidDOMElement(elementName) { return has(VOID_DOM_ELEMENTS, elementName); } -function errorMessage(elementName) { - return `Void DOM element <${elementName} /> cannot receive children.`; -} - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -56,6 +52,11 @@ module.exports = { recommended: false, url: docsUrl('void-dom-elements-no-children') }, + + messages: { + noChildrenInVoidEl: 'Void DOM element <{{element}} /> cannot receive children.' + }, + schema: [] }, @@ -72,7 +73,10 @@ module.exports = { // e.g.
    Foo
    context.report({ node, - message: errorMessage(elementName) + messageId: 'noChildrenInVoidEl', + data: { + element: elementName + } }); } @@ -90,7 +94,10 @@ module.exports = { // e.g.
    context.report({ node, - message: errorMessage(elementName) + messageId: 'noChildrenInVoidEl', + data: { + element: elementName + } }); } }, @@ -127,7 +134,10 @@ module.exports = { // e.g. React.createElement('br', undefined, 'Foo') context.report({ node, - message: errorMessage(elementName) + messageId: 'noChildrenInVoidEl', + data: { + element: elementName + } }); } @@ -145,7 +155,10 @@ module.exports = { // e.g. React.createElement('br', { children: 'Foo' }) context.report({ node, - message: errorMessage(elementName) + messageId: 'noChildrenInVoidEl', + data: { + element: elementName + } }); } } diff --git a/lib/util/makeNoMethodSetStateRule.js b/lib/util/makeNoMethodSetStateRule.js index 00a1e1ce0f..51b7d92e04 100644 --- a/lib/util/makeNoMethodSetStateRule.js +++ b/lib/util/makeNoMethodSetStateRule.js @@ -34,6 +34,10 @@ function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { url: docsUrl(mapTitle(methodName)) }, + messages: { + noSetState: 'Do not use setState in {{name}}' + }, + schema: [{ enum: ['disallow-in-func'] }] @@ -84,7 +88,10 @@ function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { } context.report({ node: callee, - message: `Do not use setState in ${ancestor.key.name}` + messageId: 'noSetState', + data: { + name: ancestor.key.name + } }); return true; }); diff --git a/lib/util/version.js b/lib/util/version.js index 951837b899..8534f10025 100644 --- a/lib/util/version.js +++ b/lib/util/version.js @@ -5,7 +5,9 @@ 'use strict'; +const fs = require('fs'); const resolve = require('resolve'); +const path = require('path'); const error = require('./error'); let warnedForMissingVersion = false; @@ -20,13 +22,37 @@ function resetDetectedVersion() { cachedDetectedReactVersion = undefined; } -function detectReactVersion() { +function resolveBasedir(context) { + let basedir = process.cwd(); + if (context) { + const filename = context.getFilename(); + const dirname = path.dirname(filename); + try { + if (fs.statSync(filename).isFile()) { + // dirname must be dir here + basedir = dirname; + } + } catch (err) { + // https://github.com/eslint/eslint/issues/11989 + if (err.code === 'ENOTDIR') { + // the error code alreay indicates that dirname is a file + basedir = path.dirname(dirname); + } + } + } + return basedir; +} + +// TODO, semver-major: remove context fallback +function detectReactVersion(context) { if (cachedDetectedReactVersion) { return cachedDetectedReactVersion; } + const basedir = resolveBasedir(context); + try { - const reactPath = resolve.sync('react', {basedir: process.cwd()}); + const reactPath = resolve.sync('react', {basedir}); const react = require(reactPath); // eslint-disable-line global-require, import/no-dynamic-require cachedDetectedReactVersion = react.version; return cachedDetectedReactVersion; @@ -50,7 +76,7 @@ function getReactVersionFromContext(context) { if (context.settings && context.settings.react && context.settings.react.version) { let settingsVersion = context.settings.react.version; if (settingsVersion === 'detect') { - settingsVersion = detectReactVersion(); + settingsVersion = detectReactVersion(context); } if (typeof settingsVersion !== 'string') { error('Warning: React version specified in eslint-plugin-react-settings must be a string; ' @@ -66,9 +92,12 @@ function getReactVersionFromContext(context) { return confVer.split('.').map((part) => Number(part)); } -function detectFlowVersion() { +// TODO, semver-major: remove context fallback +function detectFlowVersion(context) { + const basedir = resolveBasedir(context); + try { - const flowPackageJsonPath = resolve.sync('flow-bin/package.json', {basedir: process.cwd()}); + const flowPackageJsonPath = resolve.sync('flow-bin/package.json', {basedir}); const flowPackageJson = require(flowPackageJsonPath); // eslint-disable-line global-require, import/no-dynamic-require return flowPackageJson.version; } catch (e) { @@ -87,7 +116,7 @@ function getFlowVersionFromContext(context) { if (context.settings.react && context.settings.react.flowVersion) { let flowVersion = context.settings.react.flowVersion; if (flowVersion === 'detect') { - flowVersion = detectFlowVersion(); + flowVersion = detectFlowVersion(context); } if (typeof flowVersion !== 'string') { error('Warning: Flow version specified in eslint-plugin-react-settings must be a string; ' diff --git a/markdown.config.js b/markdown.config.js index ea7a04fa45..9f3690fa1e 100644 --- a/markdown.config.js +++ b/markdown.config.js @@ -4,16 +4,30 @@ const {rules} = require('./index'); -const ruleListItems = Object.keys(rules) +const ruleTableRows = Object.keys(rules) .sort() .map((id) => { const {meta} = rules[id]; const {fixable, docs} = meta; - return `* [react/${id}](docs/rules/${id}.md): ${docs.description}${fixable ? ' (fixable)' : ''}`; + return [ + docs.recommended ? '✔' : '', + fixable ? '🔧' : '', + `[react/${id}](docs/rules/${id}.md)`, + docs.description + ].join(' | '); }); -const BASIC_RULES = () => ruleListItems.filter((rule) => !rule.includes('react/jsx-')).join('\n'); -const JSX_RULES = () => ruleListItems.filter((rule) => rule.includes('react/jsx-')).join('\n'); +const buildRulesTable = (rows) => { + const header = '✔ | 🔧 | Rule | Description'; + const separator = ':---: | :---: | :--- | :---'; + + return [header, separator, ...rows] + .map((row) => `| ${row} |`) + .join('\n'); +}; + +const BASIC_RULES = () => buildRulesTable(ruleTableRows.filter((rule) => !rule.includes('react/jsx-'))); +const JSX_RULES = () => buildRulesTable(ruleTableRows.filter((rule) => rule.includes('react/jsx-'))); module.exports = { transforms: { @@ -21,7 +35,6 @@ module.exports = { JSX_RULES }, callback: () => { - // eslint-disable-next-line no-console console.log('The auto-generating of rules finished!'); } }; diff --git a/package.json b/package.json index 2870cbedb3..835fdcd132 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react", - "version": "7.22.0", + "version": "7.23.1", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", @@ -29,28 +29,29 @@ "homepage": "https://github.com/yannickcr/eslint-plugin-react", "bugs": "https://github.com/yannickcr/eslint-plugin-react/issues", "dependencies": { - "array-includes": "^3.1.1", - "array.prototype.flatmap": "^1.2.3", + "array-includes": "^3.1.3", + "array.prototype.flatmap": "^1.2.4", "doctrine": "^2.1.0", "has": "^1.0.3", "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "object.entries": "^1.1.2", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", + "minimatch": "^3.0.4", + "object.entries": "^1.1.3", + "object.fromentries": "^2.0.4", + "object.values": "^1.1.3", "prop-types": "^15.7.2", - "resolve": "^1.18.1", - "string.prototype.matchall": "^4.0.2" + "resolve": "^2.0.0-next.3", + "string.prototype.matchall": "^4.0.4" }, "devDependencies": { - "@types/eslint": "^7.2.3", - "@types/estree": "0.0.45", - "@types/node": "^14.11.2", + "@types/eslint": "^7.2.7", + "@types/estree": "^0.0.46", + "@types/node": "^14.14.35", "@typescript-eslint/parser": "^2.34.0", - "aud": "^1.1.2", + "aud": "^1.1.4", "babel-eslint": "^8.2.6", "coveralls": "^3.1.0", "eslint": "^3 || ^4 || ^5 || ^6 || ^7", - "eslint-config-airbnb-base": "^14.2.0", + "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-eslint-plugin": "^2.3.0", "eslint-plugin-import": "^2.22.1", "espree": "^3.5.4", @@ -59,7 +60,7 @@ "mocha": "^5.2.0", "semver": "^6.3.0", "sinon": "^7.5.0", - "typescript": "^3.9.7", + "typescript": "^3.9.9", "typescript-eslint-parser": "^20.1.1" }, "peerDependencies": { diff --git a/tests/fixtures/version/detect-version-sibling/node_modules/flow-bin/package.json b/tests/fixtures/version/detect-version-sibling/node_modules/flow-bin/package.json new file mode 100644 index 0000000000..a373038252 --- /dev/null +++ b/tests/fixtures/version/detect-version-sibling/node_modules/flow-bin/package.json @@ -0,0 +1,4 @@ +{ + "name": "flow-bin", + "version": "2.92.0" +} diff --git a/tests/fixtures/version/detect-version-sibling/node_modules/react/index.js b/tests/fixtures/version/detect-version-sibling/node_modules/react/index.js new file mode 100644 index 0000000000..568d6dc9fd --- /dev/null +++ b/tests/fixtures/version/detect-version-sibling/node_modules/react/index.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + version: '2.3.4' +}; diff --git a/tests/fixtures/version/detect-version-sibling/node_modules/react/package.json b/tests/fixtures/version/detect-version-sibling/node_modules/react/package.json new file mode 100644 index 0000000000..54271ba23b --- /dev/null +++ b/tests/fixtures/version/detect-version-sibling/node_modules/react/package.json @@ -0,0 +1,5 @@ +{ + "name": "react", + "version": "2.3.4", + "main": "index.js" +} diff --git a/tests/fixtures/version/detect-version-sibling/test.js b/tests/fixtures/version/detect-version-sibling/test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/version/detect-version/detect-version-child/node_modules/flow-bin/package.json b/tests/fixtures/version/detect-version/detect-version-child/node_modules/flow-bin/package.json new file mode 100644 index 0000000000..07dbe6adbb --- /dev/null +++ b/tests/fixtures/version/detect-version/detect-version-child/node_modules/flow-bin/package.json @@ -0,0 +1,4 @@ +{ + "name": "flow-bin", + "version": "3.92.0" +} diff --git a/tests/fixtures/version/detect-version/detect-version-child/node_modules/react/index.js b/tests/fixtures/version/detect-version/detect-version-child/node_modules/react/index.js new file mode 100644 index 0000000000..ad81c76a9d --- /dev/null +++ b/tests/fixtures/version/detect-version/detect-version-child/node_modules/react/index.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + version: '3.4.5' +}; diff --git a/tests/fixtures/version/detect-version/detect-version-child/node_modules/react/package.json b/tests/fixtures/version/detect-version/detect-version-child/node_modules/react/package.json new file mode 100644 index 0000000000..3b3aeee383 --- /dev/null +++ b/tests/fixtures/version/detect-version/detect-version-child/node_modules/react/package.json @@ -0,0 +1,5 @@ +{ + "name": "react", + "version": "3.4.5", + "main": "index.js" +} diff --git a/tests/fixtures/version/detect-version/detect-version-child/test.js b/tests/fixtures/version/detect-version/detect-version-child/test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/version/detect-version/test.js b/tests/fixtures/version/detect-version/test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/helpers/parsers.js b/tests/helpers/parsers.js index 73b7a2248a..f2f4f83f00 100644 --- a/tests/helpers/parsers.js +++ b/tests/helpers/parsers.js @@ -11,9 +11,18 @@ module.exports = { TYPESCRIPT_ESLINT: path.join(__dirname, NODE_MODULES, 'typescript-eslint-parser'), '@TYPESCRIPT_ESLINT': path.join(__dirname, NODE_MODULES, '@typescript-eslint/parser'), TS: function TS(tests) { + if (!Array.isArray(tests) || arguments.length > 1) { + throw new SyntaxError('parsers.TS() takes a single array argument'); + } if (semver.satisfies(version, '>= 5')) { return tests; } return []; + }, + ES2020: function ES2020(tests) { + if (semver.satisfies(version, '>= 6')) { + return tests; + } + return []; } }; diff --git a/tests/index.js b/tests/index.js index ef18b6d3cc..c04936fa26 100644 --- a/tests/index.js +++ b/tests/index.js @@ -38,16 +38,18 @@ describe('deprecated rules', () => { describe('configurations', () => { it('should export a ‘recommended’ configuration', () => { - assert(plugin.configs.recommended); - Object.keys(plugin.configs.recommended.rules).forEach((configName) => { - assert.equal(configName.indexOf('react/'), 0); - const ruleName = configName.slice('react/'.length); - assert(plugin.rules[ruleName]); + const configName = 'recommended'; + assert(plugin.configs[configName]); + + Object.keys(plugin.configs[configName].rules).forEach((ruleName) => { + assert.ok(ruleName.startsWith('react/')); + const subRuleName = ruleName.slice('react/'.length); + assert(plugin.rules[subRuleName]); }); ruleFiles.forEach((ruleName) => { - const inRecommendedConfig = !!plugin.configs.recommended.rules[`react/${ruleName}`]; - const isRecommended = plugin.rules[ruleName].meta.docs.recommended; + const inRecommendedConfig = !!plugin.configs[configName].rules[`react/${ruleName}`]; + const isRecommended = plugin.rules[ruleName].meta.docs[configName]; if (inRecommendedConfig) { assert(isRecommended, `${ruleName} metadata should mark it as recommended`); } else { @@ -57,17 +59,32 @@ describe('configurations', () => { }); it('should export an ‘all’ configuration', () => { - assert(plugin.configs.all); + const configName = 'all'; + assert(plugin.configs[configName]); - Object.keys(plugin.configs.all.rules).forEach((configName) => { - assert.equal(configName.indexOf('react/'), 0); - assert.equal(plugin.configs.all.rules[configName], 2); + Object.keys(plugin.configs[configName].rules).forEach((ruleName) => { + assert.ok(ruleName.startsWith('react/')); + assert.equal(plugin.configs[configName].rules[ruleName], 2); }); ruleFiles.forEach((ruleName) => { const inDeprecatedRules = Boolean(plugin.deprecatedRules[ruleName]); - const inAllConfig = Boolean(plugin.configs.all.rules[`react/${ruleName}`]); - assert(inDeprecatedRules ^ inAllConfig); // eslint-disable-line no-bitwise + const inConfig = typeof plugin.configs[configName].rules[`react/${ruleName}`] !== 'undefined'; + assert(inDeprecatedRules ^ inConfig); // eslint-disable-line no-bitwise + }); + }); + + it('should export a \'jsx-runtime\' configuration', () => { + const configName = 'jsx-runtime'; + assert(plugin.configs[configName]); + + Object.keys(plugin.configs[configName].rules).forEach((ruleName) => { + assert.ok(ruleName.startsWith('react/')); + assert.equal(plugin.configs[configName].rules[ruleName], 0); + + const inDeprecatedRules = Boolean(plugin.deprecatedRules[ruleName]); + const inConfig = typeof plugin.configs[configName].rules[ruleName] !== 'undefined'; + assert(inDeprecatedRules ^ inConfig); // eslint-disable-line no-bitwise }); }); }); diff --git a/tests/lib/rules/boolean-prop-naming.js b/tests/lib/rules/boolean-prop-naming.js index 001cdf5bbe..fdd3da5abf 100644 --- a/tests/lib/rules/boolean-prop-naming.js +++ b/tests/lib/rules/boolean-prop-naming.js @@ -29,7 +29,7 @@ const parserOptions = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('boolean-prop-naming', rule, { - valid: [{ + valid: [].concat({ // Should support both `is` and `has` prefixes by default code: ` var Hello = createReactClass({ @@ -416,9 +416,21 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^is[A-Z]([A-Za-z0-9]?)+', validateNested: true }] - }], + }, parsers.TS([{ + code: ` + type TestFNType = { + isEnabled: boolean + } + const HelloNew = (props: TestFNType) => { return
    }; + `, + options: [{ + rule: '^is[A-Z]([A-Za-z0-9]?)+' + }], + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [] + }])), - invalid: [{ + invalid: [].concat({ // createReactClass components with PropTypes code: ` var Hello = createReactClass({ @@ -430,7 +442,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^is[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // createReactClass components with React.PropTypes @@ -444,7 +457,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^is[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // React.createClass components with PropTypes @@ -463,7 +477,8 @@ ruleTester.run('boolean-prop-naming', rule, { } }, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // ES6 components as React.Component with boolean PropTypes @@ -477,7 +492,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^is[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // ES6 components as Component with non-boolean PropTypes @@ -491,7 +507,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^is[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // ES6 components as React.Component with non-boolean PropTypes @@ -506,7 +523,8 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // ES6 components as React.Component with non-boolean PropTypes and Object.spread syntax @@ -521,7 +539,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^is[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // ES6 components as React.Component with static class property, non-boolean PropTypes, and Object.spread syntax @@ -537,7 +556,8 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // ES6 components as React.Component with non-boolean PropTypes @@ -552,7 +572,8 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -564,7 +585,8 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -578,7 +600,8 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // ES6 components and Flowtype non-booleans @@ -594,7 +617,8 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -612,9 +636,11 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }, { - message: 'Prop name (somethingElse) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'somethingElse', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -632,9 +658,11 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }, { - message: 'Prop name (somethingElse) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'somethingElse', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -651,7 +679,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (showScore) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'showScore', pattern: '^(is|has)[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -668,7 +697,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (showScore) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'showScore', pattern: '^(is|has)[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -685,7 +715,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (showScore) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'showScore', pattern: '^(is|has)[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -702,7 +733,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (showScore) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'showScore', pattern: '^(is|has)[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -721,7 +753,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (showScore) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'showScore', pattern: '^(is|has)[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -741,7 +774,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (showScore) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'showScore', pattern: '^(is|has)[A-Z]([A-Za-z0-9]?)+'} }] }, { // If a custom message is provided, use it. @@ -785,7 +819,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^is[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // Works when a prop isRequired in ES6 with static properties. @@ -807,7 +842,8 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // Works when a prop isRequired in ES6 without static properties. @@ -828,7 +864,8 @@ ruleTester.run('boolean-prop-naming', rule, { rule: '^is[A-Z]([A-Za-z0-9]?)+' }], errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { // inline Flow type @@ -848,7 +885,8 @@ ruleTester.run('boolean-prop-naming', rule, { }], parser: parsers.BABEL_ESLINT, errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -872,7 +910,8 @@ ruleTester.run('boolean-prop-naming', rule, { validateNested: true }], errors: [{ - message: 'Prop name (failingItIs) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'failingItIs', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -898,7 +937,8 @@ ruleTester.run('boolean-prop-naming', rule, { validateNested: true }], errors: [{ - message: 'Prop name (failingItIs) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'failingItIs', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} }] }, { code: ` @@ -913,7 +953,36 @@ ruleTester.run('boolean-prop-naming', rule, { validateNested: true }], errors: [{ - message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + messageId: 'patternMismatch', + data: {propName: 'something', pattern: '^is[A-Z]([A-Za-z0-9]?)+'} + }] + }, parsers.TS([{ + code: ` + type TestConstType = { + enabled: boolean + } + const HelloNew = (props: TestConstType) => { return
    }; + `, + options: [{ + rule: '^is[A-Z]([A-Za-z0-9]?)+' + }], + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: 'Prop name (enabled) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' + }] + }, { + code: ` + type TestFNType = { + enabled: boolean + } + const HelloNew = (props: TestFNType) => { return
    }; + `, + options: [{ + rule: '^is[A-Z]([A-Za-z0-9]?)+' + }], + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: 'Prop name (enabled) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)' }] - }] + }])) }); diff --git a/tests/lib/rules/button-has-type.js b/tests/lib/rules/button-has-type.js index d6a90c9ba1..1db9075f79 100644 --- a/tests/lib/rules/button-has-type.js +++ b/tests/lib/rules/button-has-type.js @@ -76,157 +76,172 @@ ruleTester.run('button-has-type', rule, { { code: '', options: [{forbid: [{element: 'button'}, {element: 'input'}]}], - errors: [ - {message: '', options: [{forbid: [{element: 'button'}, 'input']}], - errors: [ - {message: '', options: [{forbid: ['input', {element: 'button'}]}], - errors: [ - {message: '
    ', '
    ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'}, - {message: 'Expected indentation of 4 space characters but found 3.'}, - {message: 'Expected indentation of 4 space characters but found 3.'}, - {message: 'Expected indentation of 4 space characters but found 3.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }, { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 3 + } + }, { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 3 + } + }, { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 3 + } + }] }, { code: [ '', @@ -1033,7 +1048,12 @@ const Component = () => ( ' ', '' ].join('\n'), - errors: [{message: 'Expected indentation of 4 space characters but found 2.'}] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 2 + } + }] }, { code: [ '', @@ -1046,7 +1066,12 @@ const Component = () => ( ' <>', '' ].join('\n'), - errors: [{message: 'Expected indentation of 4 space characters but found 2.'}] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 2 + } + }] }, { code: [ '<>', @@ -1059,7 +1084,12 @@ const Component = () => ( ' ', '' ].join('\n'), - errors: [{message: 'Expected indentation of 4 space characters but found 2.'}] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 2 + } + }] }, { code: [ '', @@ -1072,7 +1102,12 @@ const Component = () => ( '' ].join('\n'), options: [2], - errors: [{message: 'Expected indentation of 2 space characters but found 4.'}] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 2, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ '', @@ -1085,7 +1120,12 @@ const Component = () => ( '' ].join('\n'), options: ['tab'], - errors: [{message: 'Expected indentation of 1 tab character but found 0.'}] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 1, type: 'tab', characters: 'character', gotten: 0 + } + }] }, { code: [ 'function App() {', @@ -1102,7 +1142,12 @@ const Component = () => ( '}' ].join('\n'), options: [2], - errors: [{message: 'Expected indentation of 2 space characters but found 9.'}] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 2, type: 'space', characters: 'characters', gotten: 9 + } + }] }, { code: [ 'function App() {', @@ -1119,7 +1164,12 @@ const Component = () => ( '}' ].join('\n'), options: [2], - errors: [{message: 'Expected indentation of 2 space characters but found 4.'}] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 2, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ 'function App() {', @@ -1144,7 +1194,12 @@ const Component = () => ( '}' ].join('\n'), options: [2], - errors: [{message: 'Expected indentation of 4 space characters but found 0.'}] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ '', @@ -1156,9 +1211,12 @@ const Component = () => ( ' {test}', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 3.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 3 + } + }] }, { code: [ '', @@ -1178,9 +1236,12 @@ const Component = () => ( ' ))}', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 12 space characters but found 11.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 12, type: 'space', characters: 'characters', gotten: 11 + } + }] }, { code: [ '', @@ -1193,9 +1254,12 @@ const Component = () => ( '' ].join('\n'), options: ['tab'], - errors: [ - {message: 'Expected indentation of 1 tab character but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 1, type: 'tab', characters: 'character', gotten: 0 + } + }] }, { code: [ '', @@ -1216,9 +1280,12 @@ const Component = () => ( '' ].join('\n'), options: ['tab'], - errors: [ - {message: 'Expected indentation of 3 tab characters but found 2.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 3, type: 'tab', characters: 'characters', gotten: 2 + } + }] }, { code: [ '\n', @@ -1231,9 +1298,12 @@ const Component = () => ( '' ].join('\n'), options: ['tab'], - errors: [ - {message: 'Expected indentation of 1 tab character but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 1, type: 'tab', characters: 'character', gotten: 0 + } + }] }, { code: [ '[', @@ -1248,9 +1318,12 @@ const Component = () => ( ']' ].join('\n'), options: [2], - errors: [ - {message: 'Expected indentation of 2 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 2, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ '[', @@ -1266,9 +1339,12 @@ const Component = () => ( ']' ].join('\n'), options: [2], - errors: [ - {message: 'Expected indentation of 2 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 2, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ '\n', @@ -1281,9 +1357,12 @@ const Component = () => ( '' ].join('\n'), options: ['tab'], - errors: [ - {message: 'Expected indentation of 1 tab character but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 1, type: 'tab', characters: 'character', gotten: 0 + } + }] }, { code: [ '\n', @@ -1296,9 +1375,12 @@ const Component = () => ( '' ].join('\n'), options: [2], - errors: [ - {message: 'Expected indentation of 2 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 2, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ '
    ', @@ -1320,9 +1402,12 @@ const Component = () => ( ' }', '
    ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 12 space characters but found 8.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 12, type: 'space', characters: 'characters', gotten: 8 + } + }] }, { code: [ '
    ', @@ -1344,9 +1429,12 @@ const Component = () => ( ' }', '
    ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 12 space characters but found 8.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 12, type: 'space', characters: 'characters', gotten: 8 + } + }] }, { // Multiline ternary // (colon at the end of the first expression) @@ -1360,9 +1448,12 @@ const Component = () => ( ' :', ' ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ 'foo ?', @@ -1375,9 +1466,12 @@ const Component = () => ( ' :', ' <>' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (colon on its own line) @@ -1393,9 +1487,12 @@ const Component = () => ( ':', ' ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (first expression on test line, colon at the end of the first expression) @@ -1407,9 +1504,12 @@ const Component = () => ( 'foo ? :', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 0 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 0, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ 'foo ?', @@ -1424,9 +1524,12 @@ const Component = () => ( ':', ' <>' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (first expression on test line, colon on its own line) @@ -1440,9 +1543,12 @@ const Component = () => ( ':', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 0 space characters but found 6.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 0, type: 'space', characters: 'characters', gotten: 6 + } + }] }, { // Multiline ternary // (colon at the end of the first expression, parenthesized first expression) @@ -1458,9 +1564,12 @@ const Component = () => ( ') :', ' ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ 'foo ? (', @@ -1475,9 +1584,12 @@ const Component = () => ( ') :', ' <>' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (colon on its own line, parenthesized first expression) @@ -1495,9 +1607,12 @@ const Component = () => ( ':', ' ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (colon at the end of the first expression, parenthesized second expression) @@ -1513,9 +1628,12 @@ const Component = () => ( ' ', ' )' ].join('\n'), - errors: [ - {message: 'Expected indentation of 8 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ 'foo ?', @@ -1530,9 +1648,12 @@ const Component = () => ( ' <>', ' )' ].join('\n'), - errors: [ - {message: 'Expected indentation of 8 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { // Multiline ternary // (colon on its own line, parenthesized second expression) @@ -1550,9 +1671,12 @@ const Component = () => ( ' ', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (colon indented on its own line, parenthesized second expression) @@ -1570,9 +1694,12 @@ const Component = () => ( ' ', ' )' ].join('\n'), - errors: [ - {message: 'Expected indentation of 8 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ 'foo ?', @@ -1589,9 +1716,12 @@ const Component = () => ( ' <>', ' )' ].join('\n'), - errors: [ - {message: 'Expected indentation of 8 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { // Multiline ternary // (colon at the end of the first expression, both expression parenthesized) @@ -1609,10 +1739,18 @@ const Component = () => ( ' ', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'}, - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }, + { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ 'foo ? (', @@ -1629,10 +1767,18 @@ const Component = () => ( ' <>', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'}, - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }, + { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (colon on its own line, both expression parenthesized) @@ -1652,10 +1798,18 @@ const Component = () => ( ' ', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'}, - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }, + { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (colon on its own line, both expression parenthesized) @@ -1677,10 +1831,18 @@ const Component = () => ( ' ', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'}, - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }, + { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ 'foo ? (', @@ -1701,10 +1863,18 @@ const Component = () => ( ' <>', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'}, - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }, + { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (first expression on test line, colon at the end of the first expression, parenthesized second expression) @@ -1718,9 +1888,12 @@ const Component = () => ( ' ', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ 'foo ? : (', @@ -1733,9 +1906,12 @@ const Component = () => ( ' <>', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { // Multiline ternary // (first expression on test line, colon on its own line, parenthesized second expression) @@ -1751,9 +1927,12 @@ const Component = () => ( ' ', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ 'foo ? ', @@ -1768,9 +1947,12 @@ const Component = () => ( ' <>', ')' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ '

    ', @@ -1779,9 +1961,12 @@ const Component = () => ( '

    ', '

    ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 2.'} - ], + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 2 + } + }], output: [ '

    ', '

    ', @@ -1815,9 +2000,12 @@ const Component = () => ( ); `, options: [2, {checkAttributes: true}], - errors: [ - {message: 'Expected indentation of 8 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: ` const Component = () => ( @@ -1844,9 +2032,12 @@ const Component = () => ( ); `, options: ['tab', {checkAttributes: true}], - errors: [ - {message: 'Expected indentation of 2 tab characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 2, type: 'tab', characters: 'characters', gotten: 0 + } + }] }, { code: ` function Foo() { @@ -1871,9 +2062,12 @@ const Component = () => ( } `, options: [2, {indentLogicalExpressions: true}], - errors: [ - {message: 'Expected indentation of 12 space characters but found 10.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 12, type: 'space', characters: 'characters', gotten: 10 + } + }] }, { code: [ '', @@ -1892,9 +2086,12 @@ const Component = () => ( ' }}', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 8 space characters but found 12.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 12 + } + }] }, { code: [ '', @@ -1913,9 +2110,12 @@ const Component = () => ( ' })}', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 8 space characters but found 12.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 12 + } + }] }, { code: [ '', @@ -1932,9 +2132,12 @@ const Component = () => ( ' }}', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 8 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ '', @@ -1951,9 +2154,12 @@ const Component = () => ( ' })}', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 8 space characters but found 4.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 8, type: 'space', characters: 'characters', gotten: 4 + } + }] }, { code: [ '
    ', @@ -1965,9 +2171,12 @@ const Component = () => ( ' text', '
    ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ '
    ', @@ -1981,10 +2190,18 @@ const Component = () => ( ' text', '
    ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 2.'}, - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 2 + } + }, + { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }, { code: [ '
    ', @@ -1998,10 +2215,18 @@ const Component = () => ( ' text', '
    ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'}, - {message: 'Expected indentation of 4 space characters but found 2.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }, + { + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 2 + } + }] }, { code: [ '
    ', @@ -2015,9 +2240,12 @@ const Component = () => ( '\ttext', '
    ' ].join('\n'), - errors: [ - {message: 'Expected indentation of 1 tab character but found 2.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 1, type: 'tab', characters: 'character', gotten: 2 + } + }] }, { code: [ '<>', @@ -2030,8 +2258,11 @@ const Component = () => ( ' aaa', '' ].join('\n'), - errors: [ - {message: 'Expected indentation of 4 space characters but found 0.'} - ] + errors: [{ + messageId: 'wrongIndent', + data: { + needed: 4, type: 'space', characters: 'characters', gotten: 0 + } + }] }] }); diff --git a/tests/lib/rules/jsx-key.js b/tests/lib/rules/jsx-key.js index 1b6728a15c..a2c6fa7e00 100644 --- a/tests/lib/rules/jsx-key.js +++ b/tests/lib/rules/jsx-key.js @@ -57,65 +57,77 @@ ruleTester.run('jsx-key', rule, { ], invalid: [].concat({ code: '[];', - errors: [{message: 'Missing "key" prop for element in array'}] + errors: [{messageId: 'missingArrayKey'}] }, { code: '[];', - errors: [{message: 'Missing "key" prop for element in array'}] + errors: [{messageId: 'missingArrayKey'}] }, { code: '[, ];', - errors: [{message: 'Missing "key" prop for element in array'}] + errors: [{messageId: 'missingArrayKey'}] }, { code: '[1, 2 ,3].map(function(x) { return });', - errors: [{message: 'Missing "key" prop for element in iterator'}] + errors: [{messageId: 'missingIterKey'}] }, { code: '[1, 2 ,3].map(x => );', - errors: [{message: 'Missing "key" prop for element in iterator'}] + errors: [{messageId: 'missingIterKey'}] }, { code: '[1, 2 ,3].map(x => { return });', - errors: [{message: 'Missing "key" prop for element in iterator'}] + errors: [{messageId: 'missingIterKey'}] }, { code: '[1, 2, 3]?.map(x => )', parser: parsers.BABEL_ESLINT, - errors: [{message: 'Missing "key" prop for element in iterator'}] - }, parsers.TS({ + errors: [{messageId: 'missingIterKey'}] + }, parsers.TS([{ code: '[1, 2, 3]?.map(x => )', parser: parsers['@TYPESCRIPT_ESLINT'], - errors: [{message: 'Missing "key" prop for element in iterator'}] - }), { + errors: [{messageId: 'missingIterKey'}] + }]), { code: '[1, 2, 3].map(x => <>{x});', parser: parsers.BABEL_ESLINT, options: [{checkFragmentShorthand: true}], settings, - errors: [{message: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use Act.Frag instead'}] + errors: [{ + messageId: 'missingIterKeyUsePrag', + data: { + reactPrag: 'Act', + fragPrag: 'Frag' + } + }] }, { code: '[<>];', parser: parsers.BABEL_ESLINT, options: [{checkFragmentShorthand: true}], settings, - errors: [{message: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use Act.Frag instead'}] + errors: [{ + messageId: 'missingArrayKeyUsePrag', + data: { + reactPrag: 'Act', + fragPrag: 'Frag' + } + }] }, { code: '[];', parser: parsers.BABEL_ESLINT, options: [{checkKeyMustBeforeSpread: true}], settings, - errors: [{message: '`key` prop must 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`'}] + errors: [{messageId: 'keyBeforeSpread'}] }, { code: '[];', parser: parsers.TYPESCRIPT_ESLINT, options: [{checkKeyMustBeforeSpread: true}], settings, - errors: [{message: '`key` prop must 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`'}] + errors: [{messageId: 'keyBeforeSpread'}] }, { code: '[
    ];', parser: parsers.BABEL_ESLINT, options: [{checkKeyMustBeforeSpread: true}], settings, - errors: [{message: '`key` prop must 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`'}] + errors: [{messageId: 'keyBeforeSpread'}] }, { code: '[
    ];', parser: parsers.TYPESCRIPT_ESLINT, options: [{checkKeyMustBeforeSpread: true}], settings, - errors: [{message: '`key` prop must 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`'}] + errors: [{messageId: 'keyBeforeSpread'}] }) }); diff --git a/tests/lib/rules/jsx-max-depth.js b/tests/lib/rules/jsx-max-depth.js index 86451bb7cb..14bc69b99a 100644 --- a/tests/lib/rules/jsx-max-depth.js +++ b/tests/lib/rules/jsx-max-depth.js @@ -123,7 +123,10 @@ ruleTester.run('jsx-max-depth', rule, { '' ].join('\n'), options: [{max: 0}], - errors: [{message: 'Expected the depth of nested jsx elements to be <= 0, but found 1.'}] + errors: [{ + messageId: 'wrongDepth', + data: {needed: 0, found: 1} + }] }, { code: [ '', @@ -131,7 +134,10 @@ ruleTester.run('jsx-max-depth', rule, { '' ].join('\n'), options: [{max: 0}], - errors: [{message: 'Expected the depth of nested jsx elements to be <= 0, but found 1.'}] + errors: [{ + messageId: 'wrongDepth', + data: {needed: 0, found: 1} + }] }, { code: [ '', @@ -141,14 +147,20 @@ ruleTester.run('jsx-max-depth', rule, { '' ].join('\n'), options: [{max: 1}], - errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}] + errors: [{ + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + }] }, { code: [ 'const x =
    ;', '
    {x}
    ' ].join('\n'), options: [{max: 1}], - errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}] + errors: [{ + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + }] }, { code: [ 'const x =
    ;', @@ -156,7 +168,10 @@ ruleTester.run('jsx-max-depth', rule, { '
    {y}
    ' ].join('\n'), options: [{max: 1}], - errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}] + errors: [{ + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + }] }, { code: [ 'const x =
    ;', @@ -165,8 +180,14 @@ ruleTester.run('jsx-max-depth', rule, { ].join('\n'), options: [{max: 1}], errors: [ - {message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}, - {message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'} + { + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + }, + { + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + } ] }, { code: [ @@ -175,7 +196,10 @@ ruleTester.run('jsx-max-depth', rule, { '
    ' ].join('\n'), parser: parsers.BABEL_ESLINT, - errors: [{message: 'Expected the depth of nested jsx elements to be <= 2, but found 3.'}] + errors: [{ + messageId: 'wrongDepth', + data: {needed: 2, found: 3} + }] }, { code: [ '<>', @@ -184,7 +208,10 @@ ruleTester.run('jsx-max-depth', rule, { ].join('\n'), parser: parsers.BABEL_ESLINT, options: [{max: 0}], - errors: [{message: 'Expected the depth of nested jsx elements to be <= 0, but found 1.'}] + errors: [{ + messageId: 'wrongDepth', + data: {needed: 0, found: 1} + }] }, { code: [ '<>', @@ -195,7 +222,10 @@ ruleTester.run('jsx-max-depth', rule, { ].join('\n'), parser: parsers.BABEL_ESLINT, options: [{max: 1}], - errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}] + errors: [{ + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + }] }, { code: [ 'const x = <>;', @@ -205,8 +235,14 @@ ruleTester.run('jsx-max-depth', rule, { parser: parsers.BABEL_ESLINT, options: [{max: 1}], errors: [ - {message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}, - {message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'} + { + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + }, + { + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + } ] }, { code: ` @@ -222,8 +258,39 @@ ruleTester.run('jsx-max-depth', rule, { `, options: [{max: 1}], errors: [ - {message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}, - {message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'} + { + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + }, + { + messageId: 'wrongDepth', + data: {needed: 1, found: 2} + } + ] + }, { + code: ` +
    + + + + +
    {children}
    +
    + +
    +
    +
    +
    +
    + `, + options: [{max: 4}], + errors: [ + { + messageId: 'wrongDepth', + data: {needed: 4, found: 5} + } ] }] }); diff --git a/tests/lib/rules/jsx-max-props-per-line.js b/tests/lib/rules/jsx-max-props-per-line.js index 9167289f45..86e8d9203c 100644 --- a/tests/lib/rules/jsx-max-props-per-line.js +++ b/tests/lib/rules/jsx-max-props-per-line.js @@ -69,7 +69,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { 'bar', 'baz />;' ].join('\n'), - errors: [{message: 'Prop `bar` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'bar'} + }], parserOptions }, { code: ';', @@ -78,14 +81,20 @@ ruleTester.run('jsx-max-props-per-line', rule, { 'baz />;' ].join('\n'), options: [{maximum: 2}], - errors: [{message: 'Prop `baz` must be placed on a new line'}] + errors: [{ + messageId: 'newLine', + data: {prop: 'baz'} + }] }, { code: ';', output: [ ';' ].join('\n'), - errors: [{message: 'Prop `bar` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'bar'} + }], parserOptions }, { code: ';', @@ -93,7 +102,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { ';' ].join('\n'), - errors: [{message: 'Prop `this.props` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'this.props'} + }], parserOptions }, { code: [ @@ -109,7 +121,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { ' baz', '/>' ].join('\n'), - errors: [{message: 'Prop `bar` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'bar'} + }], parserOptions }, { code: [ @@ -125,7 +140,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { ' baz', '/>' ].join('\n'), - errors: [{message: 'Prop `this.props` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'this.props'} + }], parserOptions }, { code: [ @@ -141,7 +159,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { 'bar', '/>' ].join('\n'), - errors: [{message: 'Prop `bar` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'bar'} + }], parserOptions }, { code: [ @@ -153,7 +174,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { '}}', 'bar />' ].join('\n'), - errors: [{message: 'Prop `bar` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'bar'} + }], parserOptions }, { code: [ @@ -166,7 +190,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { 'baz />' ].join('\n'), options: [{maximum: 2}], - errors: [{message: 'Prop `baz` must be placed on a new line'}] + errors: [{ + messageId: 'newLine', + data: {prop: 'baz'} + }] }, { code: [ '' ].join('\n'), - errors: [{message: 'Prop `rest` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'rest'} + }], parserOptions }, { code: [ @@ -191,7 +221,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { '}', 'bar />' ].join('\n'), - errors: [{message: 'Prop `bar` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'bar'} + }], parserOptions }, { code: [ @@ -209,7 +242,10 @@ ruleTester.run('jsx-max-props-per-line', rule, { ' ...rest', '} />' ].join('\n'), - errors: [{message: 'Prop `rest` must be placed on a new line'}], + errors: [{ + messageId: 'newLine', + data: {prop: 'rest'} + }], parserOptions }, { code: [ @@ -226,6 +262,9 @@ ruleTester.run('jsx-max-props-per-line', rule, { '/>' ].join('\n'), options: [{maximum: 2}], - errors: [{message: 'Prop `baz` must be placed on a new line'}] + errors: [{ + messageId: 'newLine', + data: {prop: 'baz'} + }] }] }); diff --git a/tests/lib/rules/jsx-newline.js b/tests/lib/rules/jsx-newline.js index 687a7c7a06..6b21de278e 100644 --- a/tests/lib/rules/jsx-newline.js +++ b/tests/lib/rules/jsx-newline.js @@ -1,6 +1,7 @@ /** - * @fileoverview Enforce a new line after jsx elements and expressions + * @fileoverview Require or prevent a new line after jsx elements and expressions * @author Johnny Zabala + * @author Joseph Stiles */ 'use strict'; @@ -68,7 +69,7 @@ const tests = {
    `, errors: [{ - message: 'JSX element should start in a new line' + messageId: 'require' }] }, { @@ -86,7 +87,7 @@ const tests = {
    `, errors: [{ - message: 'JSX element should start in a new line' + messageId: 'require' }] }, { @@ -104,7 +105,7 @@ const tests = {
    `, errors: [{ - message: 'JSX element should start in a new line' + messageId: 'require' }] }, { @@ -130,7 +131,7 @@ const tests = {
    `, errors: [{ - message: 'JSX element should start in a new line' + messageId: 'require' }] }, { @@ -162,9 +163,9 @@ const tests = {
    `, errors: [ - {message: 'JSX element should start in a new line'}, - {message: 'JSX element should start in a new line'}, - {message: 'JSX element should start in a new line'} + {messageId: 'require'}, + {messageId: 'require'}, + {messageId: 'require'} ] } ] @@ -201,7 +202,7 @@ const advanceFeatTest = { `, errors: [ - {message: 'JSX element should start in a new line'} + {messageId: 'require'} ] } ] @@ -229,3 +230,231 @@ ruleTester.run('jsx-newline', rule, { valid: parsers.TS(advanceFeatTest.valid), invalid: parsers.TS(advanceFeatTest.invalid) }); + +// ------------------------------------------------------------------------------ +// Tests: { prevent: true } +// --------- --------------------------------------------------------------------- + +const preventionTests = { + valid: [ + { + code: ` +
    + + + + {showSomething === true && } + + {showSomethingElse === true ? ( + + ) : ( + + )} +
    + `, + options: [{ + prevent: true + }] + } + ], + invalid: [ + { + output: ` +
    + + +
    + `, + code: ` +
    + + + +
    + `, + errors: [{ + messageId: 'prevent' + }], + options: [{ + prevent: true + }] + }, + { + output: ` +
    + + {showSomething === true && } +
    + `, + code: ` +
    + + + {showSomething === true && } +
    + `, + errors: [{ + messageId: 'prevent' + }], + options: [{ + prevent: true + }] + }, + { + output: ` +
    + {showSomething === true && } + +
    + `, + code: ` +
    + {showSomething === true && } + + +
    + `, + errors: [{ + messageId: 'prevent' + }], + options: [{ + prevent: true + }] + }, + { + output: ` +
    + {showSomething === true && } + {showSomethingElse === true ? ( + + ) : ( + + )} +
    + `, + code: ` +
    + {showSomething === true && } + + {showSomethingElse === true ? ( + + ) : ( + + )} +
    + `, + errors: [{ + messageId: 'prevent' + }], + options: [{ + prevent: true + }] + }, + { + output: ` +
    +
    + + +
    +
    + + +
    +
    + `, + code: ` +
    +
    + + + +
    + +
    + + + +
    +
    + `, + errors: [ + {messageId: 'prevent'}, + {messageId: 'prevent'}, + {messageId: 'prevent'} + ], + options: [{ + prevent: true + }] + } + ] +}; + +const preventionAdvanceFeatTest = { + valid: [ + { + code: ` + <> + + Test + Should be in new line + + `, + options: [{ + prevent: true + }] + } + ], + invalid: [ + { + output: ` + <> + + Test + Should be in new line + + `, + code: ` + <> + + Test + + Should be in new line + + `, + errors: [ + {messageId: 'prevent'} + ], + options: [{ + prevent: true + }] + } + ] +}; + +// // Run tests with default parser +new RuleTester({parserOptions}).run('jsx-newline', rule, preventionTests); + +// // Run tests with babel parser +ruleTester = new RuleTester({parserOptions, parser: parsers.BABEL_ESLINT}); +ruleTester.run('jsx-newline', rule, preventionTests); +ruleTester.run('jsx-newline', rule, preventionAdvanceFeatTest); + +// // Run tests with typescript parser +ruleTester = new RuleTester({parserOptions, parser: parsers.TYPESCRIPT_ESLINT}); +ruleTester.run('jsx-newline', rule, preventionTests); +ruleTester.run('jsx-newline', rule, preventionAdvanceFeatTest); + +ruleTester = new RuleTester({parserOptions, parser: parsers['@TYPESCRIPT_ESLINT']}); +ruleTester.run('jsx-newline', rule, { + valid: parsers.TS(preventionTests.valid), + invalid: parsers.TS(preventionTests.invalid) +}); +ruleTester.run('jsx-newline', rule, { + valid: parsers.TS(preventionAdvanceFeatTest.valid), + invalid: parsers.TS(preventionAdvanceFeatTest.invalid) +}); diff --git a/tests/lib/rules/jsx-no-bind.js b/tests/lib/rules/jsx-no-bind.js index 8730e362ef..d42fc48892 100644 --- a/tests/lib/rules/jsx-no-bind.js +++ b/tests/lib/rules/jsx-no-bind.js @@ -297,19 +297,19 @@ ruleTester.run('jsx-no-bind', rule, { // .bind() { code: '
    ', - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: '
    ', - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: '
    ', - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: '
    ', - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: ` @@ -320,7 +320,7 @@ ruleTester.run('jsx-no-bind', rule, { } }); `, - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: ` @@ -331,7 +331,7 @@ ruleTester.run('jsx-no-bind', rule, { } }; `, - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: [ @@ -342,7 +342,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: [ @@ -353,7 +353,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use .bind()'}], + errors: [{messageId: 'bindCall'}], parser: parsers.BABEL_ESLINT }, { @@ -365,7 +365,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use .bind()'}], + errors: [{messageId: 'bindCall'}], parser: parsers.BABEL_ESLINT }, { @@ -376,7 +376,7 @@ ruleTester.run('jsx-no-bind', rule, { ) }; `, - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: [ @@ -386,7 +386,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: [ @@ -397,7 +397,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: [ @@ -413,8 +413,8 @@ ruleTester.run('jsx-no-bind', rule, { '};' ].join('\n'), errors: [ - {message: 'JSX props should not use .bind()'}, - {message: 'JSX props should not use arrow functions'} + {messageId: 'bindCall'}, + {messageId: 'arrowFunc'} ], parser: parsers.BABEL_ESLINT }, @@ -426,7 +426,7 @@ ruleTester.run('jsx-no-bind', rule, { ) }; `, - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: ` @@ -436,7 +436,7 @@ ruleTester.run('jsx-no-bind', rule, { ) }; `, - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: ` @@ -446,7 +446,7 @@ ruleTester.run('jsx-no-bind', rule, { ) }; `, - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, { code: ` @@ -456,29 +456,29 @@ ruleTester.run('jsx-no-bind', rule, { ) }; `, - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] }, // Arrow functions { code: '
    alert("1337")}>
    ', - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: '
    alert("1337")}>
    ', - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: '
    42}>
    ', - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: '
    { first(); second(); }}>
    ', - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: '
    this._input = c}>
    ', - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: [ @@ -489,7 +489,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use arrow functions'}], + errors: [{messageId: 'arrowFunc'}], parser: parsers.BABEL_ESLINT }, { @@ -501,7 +501,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use arrow functions'}], + errors: [{messageId: 'arrowFunc'}], parser: parsers.BABEL_ESLINT }, { @@ -513,7 +513,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use arrow functions'}], + errors: [{messageId: 'arrowFunc'}], parser: parsers.BABEL_ESLINT }, { @@ -524,7 +524,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: [ @@ -534,7 +534,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: [ @@ -545,7 +545,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: [ @@ -556,7 +556,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use arrow functions'}] + errors: [{messageId: 'arrowFunc'}] }, { code: [ @@ -572,8 +572,8 @@ ruleTester.run('jsx-no-bind', rule, { '};' ].join('\n'), errors: [ - {message: 'JSX props should not use arrow functions'}, - {message: 'JSX props should not use ::'} + {messageId: 'arrowFunc'}, + {messageId: 'bindExpression'} ], parser: parsers.BABEL_ESLINT }, @@ -581,19 +581,19 @@ ruleTester.run('jsx-no-bind', rule, { // Functions { code: '
    ', - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: '
    ', - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: '
    ', - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: '
    ', - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: [ @@ -604,7 +604,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}], + errors: [{messageId: 'func'}], parser: parsers.BABEL_ESLINT }, { @@ -616,7 +616,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}], + errors: [{messageId: 'func'}], parser: parsers.BABEL_ESLINT }, { @@ -628,7 +628,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}], + errors: [{messageId: 'func'}], parser: parsers.BABEL_ESLINT }, { @@ -640,7 +640,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}], + errors: [{messageId: 'func'}], parser: parsers.BABEL_ESLINT }, { @@ -651,7 +651,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: [ @@ -661,7 +661,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: [ @@ -671,7 +671,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: [ @@ -682,7 +682,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: [ @@ -693,7 +693,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: [ @@ -704,7 +704,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '});' ].join('\n'), - errors: [{message: 'JSX props should not use functions'}] + errors: [{messageId: 'func'}] }, { code: [ @@ -720,8 +720,8 @@ ruleTester.run('jsx-no-bind', rule, { '};' ].join('\n'), errors: [ - {message: 'JSX props should not use functions'}, - {message: 'JSX props should not use ::'} + {messageId: 'func'}, + {messageId: 'bindExpression'} ], parser: parsers.BABEL_ESLINT }, @@ -729,17 +729,17 @@ ruleTester.run('jsx-no-bind', rule, { // Bind expression { code: '
    ', - errors: [{message: 'JSX props should not use ::'}], + errors: [{messageId: 'bindExpression'}], parser: parsers.BABEL_ESLINT }, { code: '
    ', - errors: [{message: 'JSX props should not use ::'}], + errors: [{messageId: 'bindExpression'}], parser: parsers.BABEL_ESLINT }, { code: '
    ', - errors: [{message: 'JSX props should not use ::'}], + errors: [{messageId: 'bindExpression'}], parser: parsers.BABEL_ESLINT }, { @@ -751,7 +751,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use ::'}], + errors: [{messageId: 'bindExpression'}], parser: parsers.BABEL_ESLINT }, { @@ -763,7 +763,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use ::'}], + errors: [{messageId: 'bindExpression'}], parser: parsers.BABEL_ESLINT }, { @@ -775,7 +775,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use ::'}], + errors: [{messageId: 'bindExpression'}], parser: parsers.BABEL_ESLINT }, { @@ -791,7 +791,7 @@ ruleTester.run('jsx-no-bind', rule, { ' }', '};' ].join('\n'), - errors: [{message: 'JSX props should not use ::'}], + errors: [{messageId: 'bindExpression'}], parser: parsers.BABEL_ESLINT }, @@ -799,7 +799,7 @@ ruleTester.run('jsx-no-bind', rule, { { code: '', options: [{ignoreDOMComponents: true}], - errors: [{message: 'JSX props should not use .bind()'}] + errors: [{messageId: 'bindCall'}] } ] }); diff --git a/tests/lib/rules/jsx-no-comment-textnodes.js b/tests/lib/rules/jsx-no-comment-textnodes.js index afb17dfdfd..dd6218a46c 100644 --- a/tests/lib/rules/jsx-no-comment-textnodes.js +++ b/tests/lib/rules/jsx-no-comment-textnodes.js @@ -311,7 +311,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -321,7 +321,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -331,7 +331,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -345,7 +345,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -361,7 +361,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -377,7 +377,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` @@ -385,7 +385,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { return /*; }; `, - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] } ].concat(parsers.TS([ { @@ -397,7 +397,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'], - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -407,7 +407,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'], - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -417,7 +417,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'], - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -431,7 +431,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'], - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -447,7 +447,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'], - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] }, { code: ` class Comp1 extends Component { @@ -463,7 +463,7 @@ ruleTester.run('jsx-no-comment-textnodes', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'], - errors: [{message: 'Comments inside children section of tag should be placed inside braces'}] + errors: [{messageId: 'putCommentInBraces'}] } ])) }); diff --git a/tests/lib/rules/jsx-no-constructed-context-values.js b/tests/lib/rules/jsx-no-constructed-context-values.js index c199ab7bf0..957148b641 100644 --- a/tests/lib/rules/jsx-no-constructed-context-values.js +++ b/tests/lib/rules/jsx-no-constructed-context-values.js @@ -29,7 +29,7 @@ const parserOptions = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('react-no-constructed-context-values', rule, { - valid: [ + valid: [].concat( { code: '' }, @@ -105,8 +105,58 @@ ruleTester.run('react-no-constructed-context-values', rule, { return (); } ` + }, + parsers.TS([ + { + code: ` + import React from 'react'; + import MyContext from './MyContext'; + + const value = ''; + + function ContextProvider(props) { + return ( + + {props.children} + + ) + } + `, + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + import React from 'react'; + import MyContext from './MyContext'; + + const value = ''; + + function ContextProvider(props) { + return ( + + {props.children} + + ) + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + } + ]), + { + code: ` + import React from 'react'; + import BooleanContext from './BooleanContext'; + + function ContextProvider(props) { + return ( + + {props.children} + + ) + } + ` } - ], + ), invalid: [ { // Invalid because object construction creates a new identity diff --git a/tests/lib/rules/jsx-no-duplicate-props.js b/tests/lib/rules/jsx-no-duplicate-props.js index fcfcf9ece8..213efebb0f 100644 --- a/tests/lib/rules/jsx-no-duplicate-props.js +++ b/tests/lib/rules/jsx-no-duplicate-props.js @@ -27,7 +27,7 @@ const parserOptions = { const ruleTester = new RuleTester({parserOptions}); const expectedError = { - message: 'No duplicate props allowed', + messageId: 'noDuplicateProps', type: 'JSXAttribute' }; diff --git a/tests/lib/rules/jsx-no-literals.js b/tests/lib/rules/jsx-no-literals.js index 70ca3c84d6..8aec5c3fef 100644 --- a/tests/lib/rules/jsx-no-literals.js +++ b/tests/lib/rules/jsx-no-literals.js @@ -27,22 +27,6 @@ const parserOptions = { // Tests // ------------------------------------------------------------------------------ -function stringsMessage(str) { - return `Strings not allowed in JSX files: “${str}”`; -} - -function jsxMessage(str) { - return `Missing JSX expression container around literal string: “${str}”`; -} - -function invalidProp(str) { - return `Invalid prop value: “${str}”`; -} - -function attributeMessage(str) { - return `Strings not allowed in attributes: “${str}”`; -} - const ruleTester = new RuleTester({parserOptions}); ruleTester.run('jsx-no-literals', rule, { @@ -353,7 +337,10 @@ ruleTester.run('jsx-no-literals', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: jsxMessage('test')}] + errors: [{ + messageId: 'literalNotInJSXExpression', + data: {text: 'test'} + }] }, { code: ` class Comp1 extends Component { @@ -363,7 +350,10 @@ ruleTester.run('jsx-no-literals', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: jsxMessage('test')}] + errors: [{ + messageId: 'literalNotInJSXExpression', + data: {text: 'test'} + }] }, { code: ` class Comp1 extends Component { @@ -374,7 +364,10 @@ ruleTester.run('jsx-no-literals', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: jsxMessage('test')}] + errors: [{ + messageId: 'literalNotInJSXExpression', + data: {text: 'test'} + }] }, { code: ` class Comp1 extends Component { @@ -385,7 +378,10 @@ ruleTester.run('jsx-no-literals', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: jsxMessage('test')}] + errors: [{ + messageId: 'literalNotInJSXExpression', + data: {text: 'test'} + }] }, { code: ` var Hello = createReactClass({ @@ -396,7 +392,10 @@ ruleTester.run('jsx-no-literals', rule, { }); `, parser: parsers.BABEL_ESLINT, - errors: [{message: jsxMessage('hello')}] + errors: [{ + messageId: 'literalNotInJSXExpression', + data: {text: 'hello'} + }] }, { code: ` class Comp1 extends Component { @@ -410,7 +409,10 @@ ruleTester.run('jsx-no-literals', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: jsxMessage('asdjfl')}] + errors: [{ + messageId: 'literalNotInJSXExpression', + data: {text: 'asdjfl'} + }] }, { code: ` class Comp1 extends Component { @@ -426,7 +428,10 @@ ruleTester.run('jsx-no-literals', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: jsxMessage('asdjfl\n test\n foo')}] + errors: [{ + messageId: 'literalNotInJSXExpression', + data: {text: 'asdjfl\n test\n foo'} + }] }, { code: ` class Comp1 extends Component { @@ -442,7 +447,10 @@ ruleTester.run('jsx-no-literals', rule, { } `, parser: parsers.BABEL_ESLINT, - errors: [{message: jsxMessage('test')}] + errors: [{ + messageId: 'literalNotInJSXExpression', + data: {text: 'test'} + }] }, { code: ` @@ -450,10 +458,14 @@ ruleTester.run('jsx-no-literals', rule, { `, options: [{noStrings: true, ignoreProps: false}], - errors: [ - {message: invalidProp('bar="test"')}, - {message: stringsMessage('\'Test\'')} - ] + errors: [{ + messageId: 'invalidPropValue', + data: {text: 'bar="test"'} + }, + { + messageId: 'noStringsInJSX', + data: {text: '\'Test\''} + }] }, { code: ` @@ -461,10 +473,14 @@ ruleTester.run('jsx-no-literals', rule, { `, options: [{noStrings: true, ignoreProps: false}], - errors: [ - {message: invalidProp('bar="test"')}, - {message: stringsMessage('\'Test\'')} - ] + errors: [{ + messageId: 'invalidPropValue', + data: {text: 'bar="test"'} + }, + { + messageId: 'noStringsInJSX', + data: {text: '\'Test\''} + }] }, { code: ` @@ -472,10 +488,14 @@ ruleTester.run('jsx-no-literals', rule, { `, options: [{noStrings: true, ignoreProps: false}], - errors: [ - {message: invalidProp('bar="test"')}, - {message: stringsMessage('Test')} - ] + errors: [{ + messageId: 'invalidPropValue', + data: {text: 'bar="test"'} + }, + { + messageId: 'noStringsInJSX', + data: {text: 'Test'} + }] }, { code: ` @@ -483,40 +503,64 @@ ruleTester.run('jsx-no-literals', rule, { `, options: [{noStrings: true}], - errors: [{message: stringsMessage('`Test`')}] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '`Test`'} + }] }, { code: '', options: [{noStrings: true, ignoreProps: false}], - errors: [{message: stringsMessage('`Test`')}] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '`Test`'} + }] }, { code: '', options: [{noStrings: true, ignoreProps: false}], - errors: [{message: stringsMessage('`${baz}`')}] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '`${baz}`'} + }] }, { code: '', options: [{noStrings: true, ignoreProps: false}], - errors: [{message: stringsMessage('`Test ${baz}`')}] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '`Test ${baz}`'} + }] }, { code: '', options: [{noStrings: true, ignoreProps: false}], - errors: [ - {message: stringsMessage('`foo`')}, - {message: stringsMessage('\'bar\'')} - ] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '`foo`'} + }, + { + messageId: 'noStringsInJSX', + data: {text: '\'bar\''} + }] }, { code: '', options: [{noStrings: true, ignoreProps: false}], - errors: [ - {message: stringsMessage('`foo`')}, - {message: stringsMessage('`bar`')} - ] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '`foo`'} + }, + { + messageId: 'noStringsInJSX', + data: {text: '`bar`'} + }] }, { code: '', options: [{noStrings: true, ignoreProps: false}], - errors: [ - {message: stringsMessage('\'foo\'')}, - {message: stringsMessage('`bar`')} - ] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '\'foo\''} + }, + { + messageId: 'noStringsInJSX', + data: {text: '`bar`'} + }] }, { code: ` class Comp1 extends Component { @@ -526,24 +570,31 @@ ruleTester.run('jsx-no-literals', rule, { } `, options: [{noStrings: true, allowedStrings: ['asd'], ignoreProps: false}], - errors: [ - {message: stringsMessage('\'foo\'')}, - {message: stringsMessage('asdf')} - ] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '\'foo\''} + }, + { + messageId: 'noStringsInJSX', + data: {text: 'asdf'} + }] }, { code: '', options: [{noStrings: true, ignoreProps: false}], - errors: [ - {message: stringsMessage('\'bar\'')} - ] + errors: [{ + messageId: 'noStringsInJSX', + data: {text: '\'bar\''} + }] }, { code: ` blank image `, options: [{noAttributeStrings: true}], - errors: [ - {message: attributeMessage('\'blank image\'')}] + errors: [{ + messageId: 'noStringsInAttributes', + data: {text: '\'blank image\''} + }] } ] }); diff --git a/tests/lib/rules/jsx-no-script-url.js b/tests/lib/rules/jsx-no-script-url.js index 471f19d39f..d38b8db805 100644 --- a/tests/lib/rules/jsx-no-script-url.js +++ b/tests/lib/rules/jsx-no-script-url.js @@ -25,9 +25,6 @@ const parserOptions = { // ------------------------------------------------------------------------------ const ruleTester = new RuleTester({parserOptions}); -const message = 'A future version of React will block javascript: URLs as a security precaution. ' - + 'Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.'; -const defaultErrors = [{message}]; ruleTester.run('jsx-no-script-url', rule, { valid: [ @@ -42,20 +39,20 @@ ruleTester.run('jsx-no-script-url', rule, { ], invalid: [{ code: '', - errors: defaultErrors + errors: [{messageId: 'noScriptURL'}] }, { code: '', - errors: defaultErrors + errors: [{messageId: 'noScriptURL'}] }, { code: '', - errors: defaultErrors + errors: [{messageId: 'noScriptURL'}] }, { code: '', - errors: defaultErrors, + errors: [{messageId: 'noScriptURL'}], options: [[{name: 'Foo', props: ['to', 'href']}]] }, { code: '', - errors: defaultErrors, + errors: [{messageId: 'noScriptURL'}], options: [[{name: 'Foo', props: ['to', 'href']}]] }, { code: ` @@ -64,7 +61,7 @@ ruleTester.run('jsx-no-script-url', rule, {
    `, - errors: [{message}, {message}], + errors: [{messageId: 'noScriptURL'}, {messageId: 'noScriptURL'}], options: [[{name: 'Foo', props: ['to', 'href']}, {name: 'Bar', props: ['link']}]] }] }); diff --git a/tests/lib/rules/jsx-no-target-blank.js b/tests/lib/rules/jsx-no-target-blank.js index dbe9a0e528..703168aef1 100644 --- a/tests/lib/rules/jsx-no-target-blank.js +++ b/tests/lib/rules/jsx-no-target-blank.js @@ -25,10 +25,7 @@ const parserOptions = { // ------------------------------------------------------------------------------ const ruleTester = new RuleTester({parserOptions}); -const defaultErrors = [{ - message: 'Using target="_blank" without rel="noreferrer" is a security risk:' - + ' see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener' -}]; +const defaultErrors = [{messageId: 'noTargetBlank'}]; ruleTester.run('jsx-no-target-blank', rule, { valid: [ @@ -106,88 +103,169 @@ ruleTester.run('jsx-no-target-blank', rule, { code: '', options: [{allowReferrer: true}] }, + { + code: '', + options: [{allowReferrer: true}] + }, { code: '' } ], - invalid: [{ - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - errors: defaultErrors - }, { - code: '', - options: [{enforceDynamicLinks: 'always'}], - errors: defaultErrors - }, { - code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], - errors: defaultErrors - }, { - code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], - errors: defaultErrors - }, { - code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], - errors: defaultErrors - }, { - code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], - errors: defaultErrors - }, { - code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], - errors: defaultErrors - }, { - code: '', - options: [{enforceDynamicLinks: 'always'}], - settings: {linkComponents: ['Link']}, - errors: defaultErrors - }, { - code: '', - options: [{enforceDynamicLinks: 'always'}], - settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}}, - errors: defaultErrors - }] + invalid: [ + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + errors: defaultErrors + }, + { + code: '', + output: '', + options: [{allowReferrer: true}], + errors: defaultErrors + }, + { + code: '', + output: '', + options: [{enforceDynamicLinks: 'always'}], + errors: defaultErrors + }, + { + code: '', + options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + errors: defaultErrors + }, + { + code: '', + options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + errors: defaultErrors + }, + { + code: '', + options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + errors: defaultErrors + }, + { + code: '', + options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + errors: defaultErrors + }, + { + code: '', + options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + errors: defaultErrors + }, + { + code: '', + output: '', + options: [{enforceDynamicLinks: 'always'}], + settings: {linkComponents: ['Link']}, + errors: defaultErrors + }, + { + code: '', + output: '', + options: [{enforceDynamicLinks: 'always'}], + settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}}, + errors: defaultErrors + } + ] }); diff --git a/tests/lib/rules/jsx-no-undef.js b/tests/lib/rules/jsx-no-undef.js index 717622ed05..e2bcb30abb 100644 --- a/tests/lib/rules/jsx-no-undef.js +++ b/tests/lib/rules/jsx-no-undef.js @@ -78,27 +78,32 @@ ruleTester.run('jsx-no-undef', rule, { invalid: [{ code: '/*eslint no-undef:1*/ var React; React.render();', errors: [{ - message: '\'App\' is not defined.' + messageId: 'undefined', + data: {identifier: 'App'} }] }, { code: '/*eslint no-undef:1*/ var React; React.render();', errors: [{ - message: '\'Appp\' is not defined.' + messageId: 'undefined', + data: {identifier: 'Appp'} }] }, { code: '/*eslint no-undef:1*/ var React; React.render();', errors: [{ - message: '\'Apppp\' is not defined.' + messageId: 'undefined', + data: {identifier: 'Apppp'} }] }, { code: '/*eslint no-undef:1*/ var React; React.render();', errors: [{ - message: '\'appp\' is not defined.' + messageId: 'undefined', + data: {identifier: 'appp'} }] }, { code: '/*eslint no-undef:1*/ var React; React.render();', errors: [{ - message: '\'appp\' is not defined.' + messageId: 'undefined', + data: {identifier: 'appp'} }] }, { code: ` @@ -111,7 +116,8 @@ ruleTester.run('jsx-no-undef', rule, { `, parserOptions: Object.assign({sourceType: 'module'}, parserOptions), errors: [{ - message: '\'Text\' is not defined.' + messageId: 'undefined', + data: {identifier: 'Text'} }], options: [{ allowGlobals: false @@ -123,7 +129,8 @@ ruleTester.run('jsx-no-undef', rule, { }, { code: '/*eslint no-undef:1*/ var React; React.render();', errors: [{ - message: '\'Foo\' is not defined.' + messageId: 'undefined', + data: {identifier: 'Foo'} }] }] }); diff --git a/tests/lib/rules/jsx-no-useless-fragment.js b/tests/lib/rules/jsx-no-useless-fragment.js index ca44b4f30a..68554a3a0e 100644 --- a/tests/lib/rules/jsx-no-useless-fragment.js +++ b/tests/lib/rules/jsx-no-useless-fragment.js @@ -223,7 +223,7 @@ ruleTester.run('jsx-no-useless-fragment', rule, { output: ` const Comp = () => ( - ${/* the trailing whitespace here is intentional */ ''} + ${/* eslint-disable-line template-curly-spacing *//* the trailing whitespace here is intentional */ ''} ); `, diff --git a/tests/lib/rules/jsx-one-expression-per-line.js b/tests/lib/rules/jsx-one-expression-per-line.js index c74d6f375e..5332f8fa0e 100644 --- a/tests/lib/rules/jsx-one-expression-per-line.js +++ b/tests/lib/rules/jsx-one-expression-per-line.js @@ -167,7 +167,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '{"foo"}', '' ].join('\n'), - errors: [{message: '`{"foo"}` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: '{"foo"}'} + }], parserOptions }, { code: 'foo', @@ -176,7 +179,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'foo', '' ].join('\n'), - errors: [{message: '`foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'foo'} + }], parserOptions }, { code: [ @@ -192,7 +198,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '`{"bar"}` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{"bar"}'} + } ], parserOptions }, { @@ -209,7 +218,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '` bar` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: ' bar'} + } ], parserOptions }, { @@ -224,7 +236,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Bar` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Bar'} + }], parserOptions }, { code: [ @@ -238,7 +253,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'foo', '
    ' ].join('\n'), - errors: [{message: '`foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'foo'} + }], parserOptions }, { code: [ @@ -252,7 +270,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '{"foo"}', '
    ' ].join('\n'), - errors: [{message: '`{"foo"}` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: '{"foo"}'} + }], parserOptions }, { code: [ @@ -268,7 +289,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '`{ I18n.t(\'baz\') }` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{ I18n.t(\'baz\') }'} + } ], parserOptions }, { @@ -285,9 +309,18 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`{ bar }` must be placed on a new line'}, - {message: '`Text` must be placed on a new line'}, - {message: '`{ I18n.t(\'baz\') }` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{ bar }'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: 'Text'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: '{ I18n.t(\'baz\') }'} + } ], parserOptions @@ -304,8 +337,14 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`Bar` must be placed on a new line'}, - {message: '`Baz` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'Bar'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: 'Baz'} + } ], parserOptions }, { @@ -326,10 +365,22 @@ ruleTester.run('jsx-one-expression-per-line', rule, { ' ' ].join('\n'), errors: [ - {message: '`Bar` must be placed on a new line'}, - {message: '`Baz` must be placed on a new line'}, - {message: '`Bunk` must be placed on a new line'}, - {message: '`Bruno` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'Bar'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: 'Baz'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: 'Bunk'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: 'Bruno'} + } ], parserOptions }, { @@ -343,7 +394,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`Bar` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'Bar'} + } ], parserOptions }, { @@ -358,7 +412,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`Bar` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'Bar'} + } ], parserOptions }, { @@ -375,7 +432,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`Baz` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'Baz'} + } ], parserOptions }, { @@ -392,7 +452,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`{ I18n.t(\'baz\') }` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{ I18n.t(\'baz\') }'} + } ], parserOptions }, { @@ -408,7 +471,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '`input` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'input'} + } ], parserOptions }, { @@ -423,7 +489,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '
    ' ].join('\n'), - errors: [{message: '`span` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'span'} + }], parserOptions }, { code: [ @@ -438,7 +507,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '
    ' ].join('\n'), - errors: [{message: '`input` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'input'} + }], parserOptions }, { code: [ @@ -453,7 +525,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'foo', '
  • ' ].join('\n'), - errors: [{message: '` foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: ' foo'} + }], parserOptions }, { code: [ @@ -469,7 +544,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '`input` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'input'} + } ], parserOptions }, { @@ -487,7 +565,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '`input` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'input'} + } ], parserOptions }, { @@ -504,7 +585,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '`input` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'input'} + } ], parserOptions }, { @@ -520,7 +604,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'bar', '
    ' ].join('\n'), - errors: [{message: '` bar` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: ' bar'} + }], parserOptions }, { code: [ @@ -536,7 +623,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '`{"foo"}` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{"foo"}'} + } ], parserOptions }, { @@ -551,7 +641,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Bar` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Bar'} + }], parserOptions }, { code: [ @@ -563,7 +656,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -575,7 +671,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -587,7 +686,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -601,7 +703,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '/>', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -615,7 +720,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -631,7 +739,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '/>', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -645,7 +756,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -659,7 +773,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '>', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -673,7 +790,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'Foo>', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions }, { code: [ @@ -687,7 +807,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'Foo>', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parserOptions, parser: parsers.BABEL_ESLINT }, { @@ -704,7 +827,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Bar` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Bar'} + }], parserOptions }, { code: [ @@ -720,7 +846,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Bar` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Bar'} + }], parserOptions }, { code: [ @@ -742,7 +871,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '` baz ` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: ' baz '} + } ], parserOptions }, { @@ -760,8 +892,14 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`{"bar"}` must be placed on a new line'}, - {message: '` baz` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{"bar"}'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: ' baz'} + } ], parserOptions }, { @@ -779,7 +917,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`{"bar"}` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{"bar"}'} + } ], parserOptions }, { @@ -801,7 +942,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '` baz` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: ' baz'} + } ], parserOptions }, { @@ -823,8 +967,14 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`{"bar"}` must be placed on a new line'}, - {message: '` baz` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{"bar"}'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: ' baz'} + } ], parserOptions }, { @@ -850,7 +1000,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '` baz` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: ' baz'} + } ], parserOptions }, { @@ -867,7 +1020,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '' ].join('\n'), errors: [ - {message: '`{ foo}` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{ foo}'} + } ], parserOptions }, { @@ -886,7 +1042,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { ' ' ].join('\n'), errors: [ - {message: '`{ foo}` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{ foo}'} + } ], parserOptions }, { @@ -907,7 +1066,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { ' ' ].join('\n'), errors: [ - {message: '`{ foo}` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: '{ foo}'} + } ], parserOptions }, { @@ -918,7 +1080,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}] + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }] }, { code: 'foo', options: [{allow: 'none'}], @@ -927,7 +1092,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'foo', '' ].join('\n'), - errors: [{message: '`foo` must be placed on a new line'}] + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'foo'} + }] }, { code: '{"foo"}', options: [{allow: 'none'}], @@ -936,7 +1104,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '{"foo"}', '' ].join('\n'), - errors: [{message: '`{"foo"}` must be placed on a new line'}] + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: '{"foo"}'} + }] }, { code: [ 'foo', @@ -948,7 +1119,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'foo', '' ].join('\n'), - errors: [{message: '`foo` must be placed on a new line'}] + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'foo'} + }] }, { code: '', options: [{allow: 'literal'}], @@ -957,7 +1131,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}] + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }] }, { code: [ '' ].join('\n'), - errors: [{message: '`baz` must be placed on a new line'}] + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'baz'} + }] }, { code: [ 'foo', @@ -988,7 +1168,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { 'bar', '' ].join('\n'), - errors: [{message: '`foobar` must be placed on a new line'}] + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'foobar'} + }] }, { code: '<>{"foo"}', output: [ @@ -996,7 +1179,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '{"foo"}', '' ].join('\n'), - errors: [{message: '`{"foo"}` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: '{"foo"}'} + }], parser: parsers.BABEL_ESLINT, parserOptions }, { @@ -1011,7 +1197,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '<>', '' ].join('\n'), - errors: [{message: '`<>` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: '<>'} + }], parser: parsers.BABEL_ESLINT, parserOptions }, { @@ -1026,7 +1215,10 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '', '' ].join('\n'), - errors: [{message: '`Foo` must be placed on a new line'}], + errors: [{ + messageId: 'moveToNewLine', + data: {descriptor: 'Foo'} + }], parser: parsers.BABEL_ESLINT, parserOptions }, { @@ -1047,8 +1239,14 @@ ruleTester.run('jsx-one-expression-per-line', rule, { '
    ' ].join('\n'), errors: [ - {message: '`a` must be placed on a new line'}, - {message: '`{a}` must be placed on a new line'} + { + messageId: 'moveToNewLine', + data: {descriptor: 'a'} + }, + { + messageId: 'moveToNewLine', + data: {descriptor: '{a}'} + } ], parser: parsers.BABEL_ESLINT, parserOptions diff --git a/tests/lib/rules/jsx-pascal-case.js b/tests/lib/rules/jsx-pascal-case.js index e7bdc5af75..8e6d5c2e4e 100644 --- a/tests/lib/rules/jsx-pascal-case.js +++ b/tests/lib/rules/jsx-pascal-case.js @@ -76,6 +76,12 @@ ruleTester.run('jsx-pascal-case', rule, { }, { code: '', options: [{ignore: ['IGNORED']}] + }, { + code: '', + options: [{ignore: ['*_D*D']}] + }, { + code: '', + options: [{ignore: ['*_+(DEPRECATED|IGNORED)']}] }, { code: '<$ />' }, { @@ -84,31 +90,81 @@ ruleTester.run('jsx-pascal-case', rule, { code: '

    Hello!

    ' }, { code: '' + }, { + code: '', + options: [{allowNamespace: true}] }], invalid: [{ code: '', - errors: [{message: 'Imported JSX component Test_component must be in PascalCase'}] + errors: [{ + messageId: 'usePascalCase', + data: {name: 'Test_component'} + }] }, { code: '', - errors: [{message: 'Imported JSX component TEST_COMPONENT must be in PascalCase'}] + errors: [{ + messageId: 'usePascalCase', + data: {name: 'TEST_COMPONENT'} + }] }, { code: '', - errors: [{message: 'Imported JSX component YMCA must be in PascalCase'}] + errors: [{ + messageId: 'usePascalCase', + data: {name: 'YMCA'} + }] }, { code: '<_TEST_COMPONENT />', options: [{allowAllCaps: true}], - errors: [{message: 'Imported JSX component _TEST_COMPONENT must be in PascalCase or SCREAMING_SNAKE_CASE'}] + errors: [{ + messageId: 'usePascalOrSnakeCase', + data: {name: '_TEST_COMPONENT'} + }] }, { code: '', options: [{allowAllCaps: true}], - errors: [{message: 'Imported JSX component TEST_COMPONENT_ must be in PascalCase or SCREAMING_SNAKE_CASE'}] + errors: [{ + messageId: 'usePascalOrSnakeCase', + data: {name: 'TEST_COMPONENT_'} + }] }, { code: '<__ />', options: [{allowAllCaps: true}], - errors: [{message: 'Imported JSX component __ must be in PascalCase or SCREAMING_SNAKE_CASE'}] + errors: [{ + messageId: 'usePascalOrSnakeCase', + data: {name: '__'} + }] }, { code: '<$a />', - errors: [{message: 'Imported JSX component $a must be in PascalCase'}] + errors: [{ + messageId: 'usePascalCase', + data: {name: '$a'} + }] + }, { + code: '', + options: [{ignore: ['*_FOO']}], + errors: [{ + messageId: 'usePascalCase', + data: {name: 'Foo_DEPRECATED'} + }] + }, { + code: '', + errors: [{ + messageId: 'usePascalCase', + data: {name: 'h1'} + }] + }, { + code: '<$Typography.P />', + errors: [{ + messageId: 'usePascalCase', + data: {name: '$Typography'} + }] + }, { + code: '', + options: [{allowNamespace: true}], + errors: [{ + messageId: 'usePascalCase', + data: {name: 'STYLED'} + }] }] }); diff --git a/tests/lib/rules/jsx-props-no-multi-spaces.js b/tests/lib/rules/jsx-props-no-multi-spaces.js index d64e8f55f1..90635f729d 100644 --- a/tests/lib/rules/jsx-props-no-multi-spaces.js +++ b/tests/lib/rules/jsx-props-no-multi-spaces.js @@ -138,7 +138,10 @@ ruleTester.run('jsx-props-no-multi-spaces', rule, { output: [ '' ].join('\n'), - errors: [{message: 'Expected only one space between "App" and "foo"'}] + errors: [{ + messageId: 'onlyOneSpace', + data: {prop1: 'App', prop2: 'foo'} + }] }, { code: [ '' @@ -146,7 +149,10 @@ ruleTester.run('jsx-props-no-multi-spaces', rule, { output: [ '' ].join('\n'), - errors: [{message: 'Expected only one space between "foo" and "bar"'}] + errors: [{ + messageId: 'onlyOneSpace', + data: {prop1: 'foo', prop2: 'bar'} + }] }, { code: [ '' @@ -154,7 +160,10 @@ ruleTester.run('jsx-props-no-multi-spaces', rule, { output: [ '' ].join('\n'), - errors: [{message: 'Expected only one space between "foo" and "bar"'}] + errors: [{ + messageId: 'onlyOneSpace', + data: {prop1: 'foo', prop2: 'bar'} + }] }, { code: [ '' @@ -162,10 +171,14 @@ ruleTester.run('jsx-props-no-multi-spaces', rule, { output: [ '' ].join('\n'), - errors: [ - {message: 'Expected only one space between "App" and "foo"'}, - {message: 'Expected only one space between "foo" and "bar"'} - ] + errors: [{ + messageId: 'onlyOneSpace', + data: {prop1: 'App', prop2: 'foo'} + }, + { + messageId: 'onlyOneSpace', + data: {prop1: 'foo', prop2: 'bar'} + }] }, { code: [ '' @@ -173,22 +186,28 @@ ruleTester.run('jsx-props-no-multi-spaces', rule, { output: [ '' ].join('\n'), - errors: [ - {message: 'Expected only one space between "foo" and "test"'}, - {message: 'Expected only one space between "test" and "bar"'} - ] + errors: [{ + messageId: 'onlyOneSpace', + data: {prop1: 'foo', prop2: 'test'} + }, + { + messageId: 'onlyOneSpace', + data: {prop1: 'test', prop2: 'bar'} + }] }, { code: '', output: '', - errors: [ - {message: 'Expected only one space between "Foo.Bar" and "baz"'} - ] + errors: [{ + messageId: 'onlyOneSpace', + data: {prop1: 'Foo.Bar', prop2: 'baz'} + }] }, { code: '', output: '', - errors: [ - {message: 'Expected only one space between "Foobar.Foo.Bar.Baz.Qux.Quux.Quuz.Corge.Grault.Garply.Waldo.Fred.Plugh" and "xyzzy"'} - ] + errors: [{ + messageId: 'onlyOneSpace', + data: {prop1: 'Foobar.Foo.Bar.Baz.Qux.Quux.Quuz.Corge.Grault.Garply.Waldo.Fred.Plugh', prop2: 'xyzzy'} + }] }, { code: `