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: `