diff --git a/.eslintrc b/.eslintrc index b77f8a94e7..fd3ae2193c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,7 @@ "ignorePatterns": [ "coverage/", ".nyc_output/", + "test-published-types/", "tests/fixtures/flat-config/" ], "rules": { diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a06e1a06d..3a957f007c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,12 @@ jobs: - uses: actions/checkout@v4 + - uses: ljharb/actions/node/install@main + with: + node-version: node + skip-install: true + skip-ls-check: true + - uses: mindsers/changelog-reader-action@v2 id: changelog_reader with: @@ -47,13 +53,20 @@ jobs: echo "$_links" >> "${GITHUB_OUTPUT}" echo "${DELIMITER}" >> "${GITHUB_OUTPUT}" + - name: 'concat data > tmp.md' + run: | + cat << 'EOF' > tmp.md + ${{ steps.changelog_reader.outputs.changes }} + ${{ steps.changelog.outputs.links }} + EOF + + - run: cat tmp.md + - id: prune-footnotes run: | - tmp.md < echo "${{ steps.changelog_reader.outputs.changes }}" - tmp.md << echo "${{ steps.changelog.outputs.links }}" DELIMITER=$(uuidgen) echo "body<<${DELIMITER}" >> "${GITHUB_OUTPUT}" - npx gfm-footnotes -i tmp.md > "${GITHUB_OUTPUT}" + npx gfm-footnotes -i tmp.md >> "${GITHUB_OUTPUT}" echo "${DELIMITER}" >> "${GITHUB_OUTPUT}" - uses: softprops/action-gh-release@v2 diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml new file mode 100644 index 0000000000..c0e6b4d033 --- /dev/null +++ b/.github/workflows/type-check.yml @@ -0,0 +1,61 @@ +name: "Types: check published types" + +on: [pull_request, push] + +permissions: + contents: read + +jobs: + test: + name: TS ${{ matrix.ts_version }}, "${{ matrix.ts_lib }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ts_version: + # The official ESLint types are not compatible with TS 3.9 + # - 3.9 + - '4.0' + - 4.1 + - 4.2 + - 4.3 + - 4.4 + - 4.5 + - '5.0' + - 5.5 + - 5.6 + ts_lib: + - es2015 + - es2015,dom + - es2020 + - esnext + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + show-progress: false + + - uses: ljharb/actions/node/install@main + name: 'nvm install lts/* && npm install' + with: + node-version: 'lts/*' + skip-ls-check: true + + - name: build types + run: npm run build-types + + - name: npm install working directory + run: npm install + working-directory: test-published-types + + - name: install typescript version ${{ matrix.ts_version }} + run: npm install --no-save typescript@${{ matrix.ts_version }} + working-directory: test-published-types + + - name: show installed typescript version + run: npm list typescript --depth=0 + working-directory: test-published-types + + - name: check types with lib "${{ matrix.ts_lib }}" + run: npx tsc --lib ${{ matrix.ts_lib }} + working-directory: test-published-types diff --git a/.gitignore b/.gitignore index 453b7fd5f6..43605edffc 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,9 @@ package-lock.json yarn.lock .npmignore + +/lib/**/*.d.ts +/lib/**/*.d.ts.map +!/lib/types.d.ts +/index.d.ts +/index.d.ts.map diff --git a/CHANGELOG.md b/CHANGELOG.md index edfba39195..2714de0fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +## [7.37.0] - 2024.09.26 + +### Added +* add type generation ([#3830][] @voxpelli) +* [`no-unescaped-entities`]: add suggestions ([#3831][] @StyleShit) +* [`forbid-component-props`]: add `allowedForPatterns`/`disallowedForPatterns` options ([#3805][] @Efimenko) +* [`no-unstable-nested-components`]: add `propNamePattern` to support custom render prop naming conventions ([#3826][] @danreeves) + +### Changed +* [readme] flat config example for react 17+ ([#3824][] @GabenGar) + +[7.36.2]: https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.36.1...v7.36.2 +[#3831]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3831 +[#3830]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3830 +[#3826]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3826 +[#3824]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3824 +[#3805]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3805 + ## [7.36.1] - 2024.09.12 ### Fixed diff --git a/README.md b/README.md index 3df80eb2da..26cc5b0a75 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ const reactPlugin = require('eslint-plugin-react'); module.exports = [ … reactPlugin.configs.flat.recommended, // This is not a plugin object, but a shareable config object + reactPlugin.configs.flat['jsx-runtime'], // Add this if you are using React 17+ … ]; ``` @@ -368,7 +369,7 @@ module.exports = [ | [no-string-refs](docs/rules/no-string-refs.md) | Disallow using string references | ☑️ | | | | | | [no-this-in-sfc](docs/rules/no-this-in-sfc.md) | Disallow `this` from being used in stateless functional components | | | | | | | [no-typos](docs/rules/no-typos.md) | Disallow common typos | | | | | | -| [no-unescaped-entities](docs/rules/no-unescaped-entities.md) | Disallow unescaped HTML entities from appearing in markup | ☑️ | | | | | +| [no-unescaped-entities](docs/rules/no-unescaped-entities.md) | Disallow unescaped HTML entities from appearing in markup | ☑️ | | | 💡 | | | [no-unknown-property](docs/rules/no-unknown-property.md) | Disallow usage of unknown DOM property | ☑️ | | 🔧 | | | | [no-unsafe](docs/rules/no-unsafe.md) | Disallow usage of unsafe lifecycle methods | | ☑️ | | | | | [no-unstable-nested-components](docs/rules/no-unstable-nested-components.md) | Disallow creating unstable components inside components | | | | | | diff --git a/build.tsconfig.json b/build.tsconfig.json new file mode 100644 index 0000000000..df874658e8 --- /dev/null +++ b/build.tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig", + "files": [ + "index.js" + ], + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "noEmit": false, + "emitDeclarationOnly": true + } +} diff --git a/docs/rules/forbid-component-props.md b/docs/rules/forbid-component-props.md index 3d796c648d..209b6abba7 100644 --- a/docs/rules/forbid-component-props.md +++ b/docs/rules/forbid-component-props.md @@ -55,7 +55,17 @@ custom message, and a component allowlist: } ``` -For glob string patterns: +Use `disallowedFor` as an exclusion list to warn on props for specific components. `disallowedFor` must have at least one item. + +```js +{ + "propName": "someProp", + "disallowedFor": ["SomeComponent", "AnotherComponent"], + "message": "Avoid using someProp for SomeComponent and AnotherComponent" +} +``` + +For `propNamePattern` glob string patterns: ```js { @@ -65,23 +75,42 @@ For glob string patterns: } ``` -Use `disallowedFor` as an exclusion list to warn on props for specific components. `disallowedFor` must have at least one item. +```js +{ + "propNamePattern": '**-**', + "allowedForPatterns": ["*Component"], + "message": "Avoid using kebab-case except components that match the `*Component` pattern" +} +``` + +Use `allowedForPatterns` for glob string patterns: ```js { "propName": "someProp", - "disallowedFor": ["SomeComponent", "AnotherComponent"], - "message": "Avoid using someProp for SomeComponent and AnotherComponent" + "allowedForPatterns": ["*Component"], + "message": "Avoid using `someProp` except components that match the `*Component` pattern" +} +``` + +Use `disallowedForPatterns` for glob string patterns: + +```js +{ + "propName": "someProp", + "disallowedForPatterns": ["*Component"], + "message": "Avoid using `someProp` for components that match the `*Component` pattern" } ``` -For glob string patterns: +Combine several properties to cover more cases: ```js { - "propNamePattern": "**-**", - "disallowedFor": ["MyComponent"], - "message": "Avoid using kebab-case for MyComponent" + "propName": "someProp", + "allowedFor": ['div'], + "allowedForPatterns": ["*Component"], + "message": "Avoid using `someProp` except `div` and components that match the `*Component` pattern" } ``` diff --git a/docs/rules/no-unescaped-entities.md b/docs/rules/no-unescaped-entities.md index 2f6f8906b6..7146bc484c 100644 --- a/docs/rules/no-unescaped-entities.md +++ b/docs/rules/no-unescaped-entities.md @@ -2,6 +2,8 @@ 💼 This rule is enabled in the ☑️ `recommended` [config](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs). +💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). + This rule prevents characters that you may have meant as JSX escape characters diff --git a/docs/rules/no-unstable-nested-components.md b/docs/rules/no-unstable-nested-components.md index 8117636805..45fa214ef4 100644 --- a/docs/rules/no-unstable-nested-components.md +++ b/docs/rules/no-unstable-nested-components.md @@ -125,6 +125,7 @@ function Component() { { "allowAsProps": true | false, "customValidators": [] /* optional array of validators used for propTypes validation */ + "propNamePattern": string } ] ... @@ -148,6 +149,16 @@ function Component() { } ``` +You can allow other render prop naming conventions by setting the `propNamePattern` option. By default this option is `"render*"`. + +For example, if `propNamePattern` is set to `"*Renderer"` the following pattern is **not** considered warnings: + +```jsx +} +/> +``` + ## 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/lib/rules/forbid-component-props.js b/lib/rules/forbid-component-props.js index 20b11d9218..2dd4412b87 100644 --- a/lib/rules/forbid-component-props.js +++ b/lib/rules/forbid-component-props.js @@ -52,6 +52,11 @@ module.exports = { uniqueItems: true, items: { type: 'string' }, }, + allowedForPatterns: { + type: 'array', + uniqueItems: true, + items: { type: 'string' }, + }, message: { type: 'string' }, }, additionalProperties: false, @@ -66,12 +71,20 @@ module.exports = { minItems: 1, items: { type: 'string' }, }, + disallowedForPatterns: { + type: 'array', + uniqueItems: true, + minItems: 1, + items: { type: 'string' }, + }, message: { type: 'string' }, }, - required: ['disallowedFor'], + anyOf: [ + { required: ['disallowedFor'] }, + { required: ['disallowedForPatterns'] }, + ], additionalProperties: false, }, - { type: 'object', properties: { @@ -81,6 +94,11 @@ module.exports = { uniqueItems: true, items: { type: 'string' }, }, + allowedForPatterns: { + type: 'array', + uniqueItems: true, + items: { type: 'string' }, + }, message: { type: 'string' }, }, additionalProperties: false, @@ -95,9 +113,18 @@ module.exports = { minItems: 1, items: { type: 'string' }, }, + disallowedForPatterns: { + type: 'array', + uniqueItems: true, + minItems: 1, + items: { type: 'string' }, + }, message: { type: 'string' }, }, - required: ['disallowedFor'], + anyOf: [ + { required: ['disallowedFor'] }, + { required: ['disallowedForPatterns'] }, + ], additionalProperties: false, }, ], @@ -114,8 +141,10 @@ module.exports = { const propPattern = value.propNamePattern; const prop = propName || propPattern; const options = { - allowList: typeof value === 'string' ? [] : (value.allowedFor || []), - disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []), + allowList: [].concat(value.allowedFor || []), + allowPatternList: [].concat(value.allowedForPatterns || []), + disallowList: [].concat(value.disallowedFor || []), + disallowPatternList: [].concat(value.disallowedForPatterns || []), message: typeof value === 'string' ? null : value.message, isPattern: !!value.propNamePattern, }; @@ -140,10 +169,40 @@ module.exports = { return false; } + function checkIsTagForbiddenByAllowOptions() { + if (options.allowList.indexOf(tagName) !== -1) { + return false; + } + + if (options.allowPatternList.length === 0) { + return true; + } + + return options.allowPatternList.every( + (pattern) => !minimatch(tagName, pattern) + ); + } + + function checkIsTagForbiddenByDisallowOptions() { + if (options.disallowList.indexOf(tagName) !== -1) { + return true; + } + + if (options.disallowPatternList.length === 0) { + return false; + } + + return options.disallowPatternList.some( + (pattern) => minimatch(tagName, pattern) + ); + } + + const hasDisallowOptions = options.disallowList.length > 0 || options.disallowPatternList.length > 0; + // disallowList should have a least one item (schema configuration) - const isTagForbidden = options.disallowList.length > 0 - ? options.disallowList.indexOf(tagName) !== -1 - : options.allowList.indexOf(tagName) === -1; + const isTagForbidden = hasDisallowOptions + ? checkIsTagForbiddenByDisallowOptions() + : checkIsTagForbiddenByAllowOptions(); // if the tagName is undefined (``), we assume it's a forbidden element return typeof tagName === 'undefined' || isTagForbidden; diff --git a/lib/rules/jsx-no-literals.js b/lib/rules/jsx-no-literals.js index 1d4a30835f..230d33a18d 100644 --- a/lib/rules/jsx-no-literals.js +++ b/lib/rules/jsx-no-literals.js @@ -182,9 +182,8 @@ const elementOverrides = { }, }; -/** @type {import('eslint').Rule.RuleModule} */ module.exports = { - meta: { + meta: /** @type {import('eslint').Rule.RuleModule["meta"]} */ ({ docs: { description: 'Disallow usage of string literals in JSX', category: 'Stylistic Issues', @@ -202,7 +201,7 @@ module.exports = { ), additionalProperties: false, }], - }, + }), create(context) { /** @type {RawConfig} */ diff --git a/lib/rules/no-unescaped-entities.js b/lib/rules/no-unescaped-entities.js index ecf1abc5e1..3ec2cb23b6 100644 --- a/lib/rules/no-unescaped-entities.js +++ b/lib/rules/no-unescaped-entities.js @@ -9,6 +9,7 @@ const docsUrl = require('../util/docsUrl'); const getSourceCode = require('../util/eslint').getSourceCode; const jsxUtil = require('../util/jsx'); const report = require('../util/report'); +const getMessageData = require('../util/message'); // ------------------------------------------------------------------------------ // Rule Definition @@ -34,11 +35,13 @@ const DEFAULTS = [{ const messages = { unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.', unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.', + replaceWithAlt: 'Replace with `{{alt}}`.', }; /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { + hasSuggestions: true, docs: { description: 'Disallow unescaped HTML entities from appearing in markup', category: 'Possible Errors', @@ -117,6 +120,25 @@ module.exports = { entity: entities[j].char, alts: entities[j].alternatives.map((alt) => `\`${alt}\``).join(', '), }, + suggest: entities[j].alternatives.map((alt) => Object.assign( + getMessageData('replaceWithAlt', messages.replaceWithAlt), + { + data: { alt }, + fix(fixer) { + const lineToChange = i - node.loc.start.line; + + const newText = node.raw.split('\n').map((line, idx) => { + if (idx === lineToChange) { + return line.slice(0, index) + alt + line.slice(index + 1); + } + + return line; + }).join('\n'); + + return fixer.replaceText(node, newText); + }, + } + )), }); } } diff --git a/lib/rules/no-unstable-nested-components.js b/lib/rules/no-unstable-nested-components.js index e17f65e487..c330795ff0 100644 --- a/lib/rules/no-unstable-nested-components.js +++ b/lib/rules/no-unstable-nested-components.js @@ -5,6 +5,7 @@ 'use strict'; +const minimatch = require('minimatch'); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); const astUtil = require('../util/ast'); @@ -32,12 +33,13 @@ function generateErrorMessageWithParentName(parentName) { } /** - * Check whether given text starts with `render`. Comparison is case-sensitive. + * Check whether given text matches the pattern passed in. * @param {string} text Text to validate + * @param {string} pattern Pattern to match against * @returns {boolean} */ -function startsWithRender(text) { - return typeof text === 'string' && text.startsWith('render'); +function propMatchesRenderPropPattern(text, pattern) { + return typeof text === 'string' && minimatch(text, pattern); } /** @@ -165,15 +167,16 @@ function isReturnStatementOfHook(node, context) { * ``` * @param {ASTNode} node The AST node * @param {Context} context eslint context + * @param {string} propNamePattern a pattern to match render props against * @returns {boolean} True if component is declared inside a render prop, false if not */ -function isComponentInRenderProp(node, context) { +function isComponentInRenderProp(node, context, propNamePattern) { if ( node && node.parent && node.parent.type === 'Property' && node.parent.key - && startsWithRender(node.parent.key.name) + && propMatchesRenderPropPattern(node.parent.key.name, propNamePattern) ) { return true; } @@ -202,7 +205,7 @@ function isComponentInRenderProp(node, context) { const propName = jsxExpressionContainer.parent.name.name; // Starts with render, e.g.
} /> - if (startsWithRender(propName)) { + if (propMatchesRenderPropPattern(propName, propNamePattern)) { return true; } @@ -222,16 +225,17 @@ function isComponentInRenderProp(node, context) { *
}] } /> * ``` * @param {ASTNode} node The AST node + * @param {string} propNamePattern The pattern to match render props against * @returns {boolean} True if component is declared inside a render property, false if not */ -function isDirectValueOfRenderProperty(node) { +function isDirectValueOfRenderProperty(node, propNamePattern) { return ( node && node.parent && node.parent.type === 'Property' && node.parent.key && node.parent.key.type === 'Identifier' - && startsWithRender(node.parent.key.name) + && propMatchesRenderPropPattern(node.parent.key.name, propNamePattern) ); } @@ -277,6 +281,9 @@ module.exports = { allowAsProps: { type: 'boolean', }, + propNamePattern: { + type: 'string', + }, }, additionalProperties: false, }], @@ -284,6 +291,7 @@ module.exports = { create: Components.detect((context, components, utils) => { const allowAsProps = context.options.some((option) => option && option.allowAsProps); + const propNamePattern = (context.options[0] || {}).propNamePattern || 'render*'; /** * Check whether given node is declared inside class component's render block @@ -418,7 +426,7 @@ module.exports = { if ( // Support allowAsProps option - (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context))) + (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context, propNamePattern))) // Prevent reporting components created inside Array.map calls || isMapCall(node) @@ -428,7 +436,7 @@ module.exports = { || isReturnStatementOfHook(node, context) // Do not mark objects containing render methods - || isDirectValueOfRenderProperty(node) + || isDirectValueOfRenderProperty(node, propNamePattern) // Prevent reporting nested class components twice || isInsideRenderMethod(node) diff --git a/lib/util/makeNoMethodSetStateRule.js b/lib/util/makeNoMethodSetStateRule.js index faee6de163..b6c55d5cf6 100644 --- a/lib/util/makeNoMethodSetStateRule.js +++ b/lib/util/makeNoMethodSetStateRule.js @@ -44,6 +44,12 @@ function shouldBeNoop(context, methodName) { && !testReactVersion(context, '999.999.999'); // for when the version is not specified } +// eslint-disable-next-line valid-jsdoc +/** + * @param {string} methodName + * @param {(context: import('eslint').Rule.RuleContext) => boolean} [shouldCheckUnsafeCb] + * @returns {import('eslint').Rule.RuleModule} + */ module.exports = function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { return { meta: { @@ -90,6 +96,7 @@ module.exports = function makeNoMethodSetStateRule(methodName, shouldCheckUnsafe if ( callee.type !== 'MemberExpression' || callee.object.type !== 'ThisExpression' + || !('name' in callee.property) || callee.property.name !== 'setState' ) { return; diff --git a/package.json b/package.json index cf2a85b153..b9b82dc03a 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "eslint-plugin-react", - "version": "7.36.1", + "version": "7.37.0", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", "scripts": { - "prepack": "npmignore --auto --commentLines=autogenerated", + "clean-built-types": "rm -f $(find . -maxdepth 1 -type f -name '*.d.ts*') $(find lib -type f -name '*.d.ts*' ! -name 'types.d.ts')", + "prebuild-types": "npm run clean-built-types", + "build-types": "tsc -p build.tsconfig.json", + "prepack": "npm run build-types && npmignore --auto --commentLines=autogenerated", "prelint": "npm run lint:docs", "lint:docs": "markdownlint \"**/*.md\"", "postlint:docs": "npm run update:eslint-docs -- --check", @@ -60,12 +63,12 @@ "eslint-config-airbnb-base": "^15.0.0", "eslint-doc-generator": "^1.7.1", "eslint-plugin-eslint-plugin": "^2.3.0 || ^3.5.3 || ^4.0.1 || ^5.0.5", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.30.0", "eslint-remote-tester": "^3.0.1", "eslint-remote-tester-repositories": "^1.0.1", "eslint-scope": "^3.7.3", "espree": "^3.5.4", - "gfm-footnotes": "^1.0.0", + "gfm-footnotes": "^1.0.1", "glob": "=10.3.7", "istanbul": "^0.4.5", "jackspeak": "=2.1.1", @@ -96,6 +99,7 @@ "!lib", "docs/", "test/", + "test-published-types/", "tests/", "*.md", "*.config.js", @@ -103,6 +107,7 @@ ".eslintrc", ".editorconfig", "tsconfig.json", + "build.tsconfig.json", ".markdownlint*", "types" ] diff --git a/test-published-types/.npmrc b/test-published-types/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/test-published-types/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/test-published-types/index.js b/test-published-types/index.js new file mode 100644 index 0000000000..31e6005985 --- /dev/null +++ b/test-published-types/index.js @@ -0,0 +1,12 @@ +'use strict'; + +const react = require('eslint-plugin-react'); + +/** @type {import('eslint').Linter.Config[]} */ +module.exports = [ + { + plugins: { + react, + }, + }, +]; diff --git a/test-published-types/package.json b/test-published-types/package.json new file mode 100644 index 0000000000..ab8a7160c6 --- /dev/null +++ b/test-published-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "eslint-plugin-react-test-published-types", + "private": true, + "version": "0.0.0", + "dependencies": { + "eslint": "^9.11.1", + "eslint-plugin-react": "file:.." + } +} diff --git a/test-published-types/tsconfig.json b/test-published-types/tsconfig.json new file mode 100644 index 0000000000..7fc1500df3 --- /dev/null +++ b/test-published-types/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + + "files": [ + "index.js" + ], + + "compilerOptions": { + "lib": ["esnext"], + "types": ["node"] + } +} diff --git a/tests/lib/rules/forbid-component-props.js b/tests/lib/rules/forbid-component-props.js index d97299e1bd..566860d139 100644 --- a/tests/lib/rules/forbid-component-props.js +++ b/tests/lib/rules/forbid-component-props.js @@ -250,6 +250,78 @@ ruleTester.run('forbid-component-props', rule, { }, ], }, + { + code: ` + const rootElement = ( + + + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + allowedForPatterns: ['*Icon', '*Svg', 'UI*'], + }, + ], + }, + ], + }, + { + code: ` + const rootElement = ( + + + + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + allowedFor: ['ButtonLegacy'], + allowedForPatterns: ['*Icon', '*Svg', 'UI*'], + }, + ], + }, + ], + }, + { + code: ` + const rootElement = ( + + + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + disallowedFor: ['Modal'], + disallowedForPatterns: ['*Legacy', 'Shared*'], + }, + ], + }, + ], + }, ]), invalid: parsers.all([ @@ -679,5 +751,126 @@ ruleTester.run('forbid-component-props', rule, { }, ], }, + { + code: ` + const rootElement = () => ( + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + message: 'className available only for icons', + allowedForPatterns: ['*Icon'], + }, + ], + }, + ], + errors: [ + { + message: 'className available only for icons', + line: 5, + column: 22, + type: 'JSXAttribute', + }, + ], + }, + { + code: ` + const rootElement = () => ( + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + message: 'className available only for icons', + allowedForPatterns: ['*Icon'], + }, + { + propName: 'style', + message: 'style available only for SVGs', + allowedForPatterns: ['*Svg'], + }, + ], + }, + ], + errors: [ + { + message: 'style available only for SVGs', + line: 4, + column: 21, + type: 'JSXAttribute', + }, + { + message: 'className available only for icons', + line: 6, + column: 22, + type: 'JSXAttribute', + }, + ], + }, + { + code: ` + const rootElement = ( + + + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + disallowedFor: ['SomeSvg'], + disallowedForPatterns: ['UI*', '*Icon'], + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + }, + ], + }, + ], + errors: [ + { + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + line: 4, + column: 23, + type: 'JSXAttribute', + }, + { + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + line: 5, + column: 26, + type: 'JSXAttribute', + }, + { + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + line: 6, + column: 22, + type: 'JSXAttribute', + }, + { + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + line: 7, + column: 21, + type: 'JSXAttribute', + }, + ], + }, ]), }); diff --git a/tests/lib/rules/no-unescaped-entities.js b/tests/lib/rules/no-unescaped-entities.js index 227365f5ea..4e62a82eb3 100644 --- a/tests/lib/rules/no-unescaped-entities.js +++ b/tests/lib/rules/no-unescaped-entities.js @@ -135,6 +135,19 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '>', alts: '`>`' }, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: '>' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
> default parser
; + } + }); + `, + }, + ], }, ], }, @@ -152,6 +165,21 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '>', alts: '`>`' }, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: '>' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
first line is ok + so is second + and here are some bad entities: >
+ } + }); + `, + }, + ], }, ], }, @@ -167,14 +195,86 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '\'', alts: '`'`, `‘`, `'`, `’`' }, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: ''' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
Multiple errors: '>> default parser
; + } + }); + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '‘' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
Multiple errors: ‘>> default parser
; + } + }); + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: ''' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
Multiple errors: '>> default parser
; + } + }); + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '’' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
Multiple errors: ’>> default parser
; + } + }); + `, + }, + ], }, { messageId: 'unescapedEntityAlts', data: { entity: '>', alts: '`>`' }, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: '>' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
Multiple errors: '>> default parser
; + } + }); + `, + }, + ], }, { messageId: 'unescapedEntityAlts', data: { entity: '>', alts: '`>`' }, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: '>' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
Multiple errors: '>> default parser
; + } + }); + `, + }, + ], }, ], }, @@ -190,6 +290,19 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '}', alts: '`}`' }, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: '}' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
{"Unbalanced braces - default parser"}}
; + } + }); + `, + }, + ], }, ], }, @@ -207,6 +320,19 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '>', alts: '`>`' }, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: '>' }, + output: ` + var Hello = createReactClass({ + render: function() { + return <>> babel-eslint; + } + }); + `, + }, + ], }, ], }, @@ -225,6 +351,19 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '>', alts: '`>`' }, + suggestions: [{ + messageId: 'replaceWithAlt', + data: { alt: '>' }, + output: ` + var Hello = createReactClass({ + render: function() { + return <>first line is ok + so is second + and here are some bad entities: > + } + }); + `, + }], }, ], }, @@ -240,6 +379,52 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '\'', alts: '`'`, `‘`, `'`, `’`' }, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: ''' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
'
; + } + }); + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '‘' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
; + } + }); + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: ''' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
'
; + } + }); + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '’' }, + output: ` + var Hello = createReactClass({ + render: function() { + return
; + } + }); + `, + }, + ], }, ], }, @@ -256,6 +441,17 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '}', alts: '`}`' }, + suggestions: [{ + messageId: 'replaceWithAlt', + data: { alt: '}' }, + output: ` + var Hello = createReactClass({ + render: function() { + return <>{"Unbalanced braces - babel-eslint"}}; + } + }); + `, + }], }, ], }, @@ -304,6 +500,17 @@ ruleTester.run('no-unescaped-entities', rule, { { messageId: 'unescapedEntityAlts', data: { entity: '&', alts: '`&`' }, + suggestions: [{ + messageId: 'replaceWithAlt', + data: { alt: '&' }, + output: ` + var Hello = createReactClass({ + render: function() { + return foo & bar; + } + }); + `, + }], }, ], options: [ @@ -327,12 +534,72 @@ ruleTester.run('no-unescaped-entities', rule, { data: { entity: '"', alts: '`"`, `“`, `"`, `”`' }, line: 2, column: 30, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: '"' }, + output: ` + + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '“' }, + output: ` + + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '"' }, + output: ` + + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '”' }, + output: ` + + `, + }, + ], }, { messageId: 'unescapedEntityAlts', data: { entity: '"', alts: '`"`, `“`, `"`, `”`' }, line: 2, column: 34, + suggestions: [ + { + messageId: 'replaceWithAlt', + data: { alt: '"' }, + output: ` + + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '“' }, + output: ` + + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '"' }, + output: ` + + `, + }, + { + messageId: 'replaceWithAlt', + data: { alt: '”' }, + output: ` + + `, + }, + ], }, ], } diff --git a/tests/lib/rules/no-unstable-nested-components.js b/tests/lib/rules/no-unstable-nested-components.js index 3e20ad648a..8a9c0aad47 100644 --- a/tests/lib/rules/no-unstable-nested-components.js +++ b/tests/lib/rules/no-unstable-nested-components.js @@ -580,6 +580,18 @@ ruleTester.run('no-unstable-nested-components', rule, { allowAsProps: true, }], }, + { + code: ` + function ParentComponent() { + return
} + /> + } + `, + options: [{ + propNamePattern: '*Renderer', + }], + }, /* TODO These minor cases are currently falsely marked due to component detection { code: `