-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(react): add react/jsx-props-no-spread-multi
rule
#1334
Merged
github-actions
merged 4 commits into
main
from
dependabot/npm_and_yarn/eslint-plugin-react-7.35.0
Aug 1, 2024
Merged
feat(react): add react/jsx-props-no-spread-multi
rule
#1334
github-actions
merged 4 commits into
main
from
dependabot/npm_and_yarn/eslint-plugin-react-7.35.0
Aug 1, 2024
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.34.3 to 7.35.0. - [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases) - [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md) - [Commits](jsx-eslint/eslint-plugin-react@v7.34.3...v7.35.0) --- updated-dependencies: - dependency-name: eslint-plugin-react dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]>
dependabot
bot
added
dependencies
Pull requests that update a dependency file
javascript
Pull requests that update Javascript code
labels
Aug 1, 2024
Diff between eslint-plugin-react 7.34.3 and 7.35.0diff --git a/configs/all.js b/configs/all.js
index v7.34.3..v7.35.0 100644
--- a/configs/all.js
+++ b/configs/all.js
@@ -1,49 +1,13 @@
'use strict';
-const fromEntries = require('object.fromentries');
-const entries = require('object.entries');
+const plugin = require('..');
-const allRules = require('../lib/rules');
+const legacyConfig = plugin.configs.all;
-function filterRules(rules, predicate) {
- return fromEntries(entries(rules).filter((entry) => predicate(entry[1])));
-}
-
-/**
- * @param {object} rules - rules object mapping rule name to rule module
- * @returns {Record<string, 2>}
- */
-function configureAsError(rules) {
- return fromEntries(Object.keys(rules).map((key) => [`react/${key}`, 2]));
-}
-
-const activeRules = filterRules(allRules, (rule) => !rule.meta.deprecated);
-const activeRulesConfig = configureAsError(activeRules);
-
-const deprecatedRules = filterRules(allRules, (rule) => rule.meta.deprecated);
-
module.exports = {
- plugins: {
- /**
- * @type {{
- * deprecatedRules: Record<string, import('eslint').Rule.RuleModule>,
- * rules: Record<string, import('eslint').Rule.RuleModule>,
- * }}
- */
- react: {
- deprecatedRules,
- rules: allRules,
- },
- },
- rules: activeRulesConfig,
- languageOptions: {
- parserOptions: {
- ecmaFeatures: {
- jsx: true,
- },
- },
- },
+ plugins: { react: plugin },
+ rules: legacyConfig.rules,
+ languageOptions: { parserOptions: legacyConfig.parserOptions },
};
-// this is so the `languageOptions` property won't be warned in the new config system
Object.defineProperty(module.exports, 'languageOptions', { enumerable: false });
diff --git a/lib/util/Components.js b/lib/util/Components.js
index v7.34.3..v7.35.0 100644
--- a/lib/util/Components.js
+++ b/lib/util/Components.js
@@ -670,12 +670,5 @@
return null;
}
- let variableInScope;
- const variables = variableUtil.variablesInScope(context, node);
- for (i = 0, j = variables.length; i < j; i++) {
- if (variables[i].name === variableName) {
- variableInScope = variables[i];
- break;
- }
- }
+ const variableInScope = variableUtil.getVariableFromContext(context, node, variableName);
if (!variableInScope) {
return null;
diff --git a/lib/rules/forbid-component-props.js b/lib/rules/forbid-component-props.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/forbid-component-props.js
+++ b/lib/rules/forbid-component-props.js
@@ -6,4 +6,5 @@
'use strict';
+const minimatch = require('minimatch');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
@@ -71,4 +72,33 @@
additionalProperties: false,
},
+
+ {
+ type: 'object',
+ properties: {
+ propNamePattern: { type: 'string' },
+ allowedFor: {
+ type: 'array',
+ uniqueItems: true,
+ items: { type: 'string' },
+ },
+ message: { type: 'string' },
+ },
+ additionalProperties: false,
+ },
+ {
+ type: 'object',
+ properties: {
+ propNamePattern: { type: 'string' },
+ disallowedFor: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 1,
+ items: { type: 'string' },
+ },
+ message: { type: 'string' },
+ },
+ required: ['disallowedFor'],
+ additionalProperties: false,
+ },
],
},
@@ -82,14 +112,29 @@
const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => {
const propName = typeof value === 'string' ? value : value.propName;
+ const propPattern = value.propNamePattern;
+ const prop = propName || propPattern;
const options = {
allowList: typeof value === 'string' ? [] : (value.allowedFor || []),
disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []),
message: typeof value === 'string' ? null : value.message,
+ isPattern: !!value.propNamePattern,
};
- return [propName, options];
+ return [prop, options];
}));
+ function getPropOptions(prop) {
+ // Get config options having pattern
+ const propNamePatternArray = Array.from(forbid.entries()).filter((propEntry) => propEntry[1].isPattern);
+ // Match current prop with pattern options, return if matched
+ const propNamePattern = propNamePatternArray.find((propPatternVal) => minimatch(prop, propPatternVal[0]));
+ // Get options for matched propNamePattern
+ const propNamePatternOptions = propNamePattern && propNamePattern[1];
+
+ const options = forbid.get(prop) || propNamePatternOptions;
+ return options;
+ }
+
function isForbidden(prop, tagName) {
- const options = forbid.get(prop);
+ const options = getPropOptions(prop);
if (!options) {
return false;
@@ -122,5 +167,5 @@
}
- const customMessage = forbid.get(prop).message;
+ const customMessage = getPropOptions(prop).message;
report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
diff --git a/lib/rules/forbid-elements.js b/lib/rules/forbid-elements.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/forbid-elements.js
+++ b/lib/rules/forbid-elements.js
@@ -6,5 +6,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
@@ -61,4 +61,5 @@
const forbidConfiguration = configuration.forbid || [];
+ /** @type {Record<string, { element: string, message?: string }>} */
const indexedForbidConfigs = {};
diff --git a/index.js b/index.js
index v7.34.3..v7.35.0 100644
--- a/index.js
+++ b/index.js
@@ -1,10 +1,26 @@
'use strict';
-const configAll = require('./configs/all');
-const configRecommended = require('./configs/recommended');
-const configRuntime = require('./configs/jsx-runtime');
+const fromEntries = require('object.fromentries');
+const entries = require('object.entries');
const allRules = require('./lib/rules');
+function filterRules(rules, predicate) {
+ return fromEntries(entries(rules).filter((entry) => predicate(entry[1])));
+}
+
+/**
+ * @param {object} rules - rules object mapping rule name to rule module
+ * @returns {Record<string, 2>}
+ */
+function configureAsError(rules) {
+ return fromEntries(Object.keys(rules).map((key) => [`react/${key}`, 2]));
+}
+
+const activeRules = filterRules(allRules, (rule) => !rule.meta.deprecated);
+const activeRulesConfig = configureAsError(activeRules);
+
+const deprecatedRules = filterRules(allRules, (rule) => rule.meta.deprecated);
+
// for legacy config system
const plugins = [
@@ -12,20 +28,82 @@
];
-module.exports = {
- deprecatedRules: configAll.plugins.react.deprecatedRules,
+const plugin = {
+ deprecatedRules,
rules: allRules,
configs: {
- recommended: Object.assign({}, configRecommended, {
- parserOptions: configRecommended.languageOptions.parserOptions,
+ recommended: {
plugins,
- }),
- all: Object.assign({}, configAll, {
- parserOptions: configAll.languageOptions.parserOptions,
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ rules: {
+ 'react/display-name': 2,
+ 'react/jsx-key': 2,
+ 'react/jsx-no-comment-textnodes': 2,
+ 'react/jsx-no-duplicate-props': 2,
+ 'react/jsx-no-target-blank': 2,
+ 'react/jsx-no-undef': 2,
+ 'react/jsx-uses-react': 2,
+ 'react/jsx-uses-vars': 2,
+ 'react/no-children-prop': 2,
+ 'react/no-danger-with-children': 2,
+ 'react/no-deprecated': 2,
+ 'react/no-direct-mutation-state': 2,
+ 'react/no-find-dom-node': 2,
+ 'react/no-is-mounted': 2,
+ 'react/no-render-return-value': 2,
+ 'react/no-string-refs': 2,
+ 'react/no-unescaped-entities': 2,
+ 'react/no-unknown-property': 2,
+ 'react/no-unsafe': 0,
+ 'react/prop-types': 2,
+ 'react/react-in-jsx-scope': 2,
+ 'react/require-render-return': 2,
+ },
+ },
+ all: {
plugins,
- }),
- 'jsx-runtime': Object.assign({}, configRuntime, {
- parserOptions: configRuntime.languageOptions.parserOptions,
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ rules: activeRulesConfig,
+ },
+ 'jsx-runtime': {
plugins,
- }),
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ jsxPragma: null, // for @typescript/eslint-parser
+ },
+ rules: {
+ 'react/react-in-jsx-scope': 0,
+ 'react/jsx-uses-react': 0,
+ },
+ },
},
};
+
+plugin.configs.flat = {
+ recommended: {
+ plugins: { react: plugin },
+ rules: plugin.configs.recommended.rules,
+ languageOptions: { parserOptions: plugin.configs.recommended.parserOptions },
+ },
+ all: {
+ plugins: { react: plugin },
+ rules: plugin.configs.all.rules,
+ languageOptions: { parserOptions: plugin.configs.all.parserOptions },
+ },
+ 'jsx-runtime': {
+ plugins: { react: plugin },
+ rules: plugin.configs['jsx-runtime'].rules,
+ languageOptions: { parserOptions: plugin.configs['jsx-runtime'].parserOptions },
+ },
+};
+
+module.exports = plugin;
diff --git a/lib/rules/index.js b/lib/rules/index.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -3,5 +3,5 @@
/* eslint global-require: 0 */
-/** @type {Record<string, import('eslint').Rule.RuleModule>} */
+/** @satisfies {Record<string, import('eslint').Rule.RuleModule>} */
module.exports = {
'boolean-prop-naming': require('./boolean-prop-naming'),
@@ -51,4 +51,5 @@
'jsx-props-no-multi-spaces': require('./jsx-props-no-multi-spaces'),
'jsx-props-no-spreading': require('./jsx-props-no-spreading'),
+ 'jsx-props-no-spread-multi': require('./jsx-props-no-spread-multi'),
'jsx-sort-default-props': require('./jsx-sort-default-props'),
'jsx-sort-props': require('./jsx-sort-props'),
diff --git a/lib/util/isDestructuredFromPragmaImport.js b/lib/util/isDestructuredFromPragmaImport.js
index v7.34.3..v7.35.0 100644
--- a/lib/util/isDestructuredFromPragmaImport.js
+++ b/lib/util/isDestructuredFromPragmaImport.js
@@ -14,6 +14,5 @@
module.exports = function isDestructuredFromPragmaImport(context, node, variable) {
const pragma = pragmaUtil.getFromContext(context);
- const variables = variableUtil.variablesInScope(context, node);
- const variableInScope = variableUtil.getVariable(variables, variable);
+ const variableInScope = variableUtil.getVariableFromContext(context, node, variable);
if (variableInScope) {
const latestDef = variableUtil.getLatestVariableDefinition(variableInScope);
diff --git a/lib/rules/jsx-closing-bracket-location.js b/lib/rules/jsx-closing-bracket-location.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-closing-bracket-location.js
+++ b/lib/rules/jsx-closing-bracket-location.js
@@ -6,5 +6,7 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
+const repeat = require('string.prototype.repeat');
+
const docsUrl = require('../util/docsUrl');
const getSourceCode = require('../util/eslint').getSourceCode;
@@ -169,5 +171,5 @@
const newColumn = correctColumn || 0;
let indentation;
- let spaces = [];
+ let spaces = '';
switch (expectedLocation) {
case 'props-aligned':
@@ -183,7 +185,7 @@
if (indentation.length + 1 < newColumn) {
// Non-whitespace characters were included in the column offset
- spaces = new Array(+correctColumn + 1 - indentation.length);
+ spaces = repeat(' ', +correctColumn - indentation.length);
}
- return indentation + spaces.join(' ');
+ return indentation + spaces;
}
diff --git a/lib/rules/jsx-closing-tag-location.js b/lib/rules/jsx-closing-tag-location.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-closing-tag-location.js
+++ b/lib/rules/jsx-closing-tag-location.js
@@ -6,6 +6,10 @@
'use strict';
+const repeat = require('string.prototype.repeat');
+const has = require('hasown');
+
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
+const getSourceCode = require('../util/eslint').getSourceCode;
const report = require('../util/report');
@@ -17,6 +21,14 @@
onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.',
matchIndent: 'Expected closing tag to match indentation of opening.',
+ alignWithOpening: 'Expected closing tag to be aligned with the line containing the opening tag',
};
+const defaultOption = 'tag-aligned';
+
+const optionMessageMap = {
+ 'tag-aligned': 'matchIndent',
+ 'line-aligned': 'alignWithOpening',
+};
+
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
@@ -30,29 +42,85 @@
fixable: 'whitespace',
messages,
+ schema: [{
+ anyOf: [
+ {
+ enum: ['tag-aligned', 'line-aligned'],
+ },
+ {
+ type: 'object',
+ properties: {
+ location: {
+ enum: ['tag-aligned', 'line-aligned'],
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ }],
},
create(context) {
+ const config = context.options[0];
+ let option = defaultOption;
+
+ if (typeof config === 'string') {
+ option = config;
+ } else if (typeof config === 'object') {
+ if (has(config, 'location')) {
+ option = config.location;
+ }
+ }
+
+ function getIndentation(openingStartOfLine, opening) {
+ if (option === 'line-aligned') return openingStartOfLine.column;
+ if (option === 'tag-aligned') return opening.loc.start.column;
+ }
+
function handleClosingElement(node) {
if (!node.parent) {
return;
}
+ const sourceCode = getSourceCode(context);
const opening = node.parent.openingElement || node.parent.openingFragment;
+ const openingLoc = sourceCode.getFirstToken(opening).loc.start;
+ const openingLine = sourceCode.lines[openingLoc.line - 1];
+
+ const openingStartOfLine = {
+ column: /^\s*/.exec(openingLine)[0].length,
+ line: openingLoc.line,
+ };
+
if (opening.loc.start.line === node.loc.start.line) {
return;
}
- if (opening.loc.start.column === node.loc.start.column) {
+ if (
+ opening.loc.start.column === node.loc.start.column
+ && option === 'tag-aligned'
+ ) {
return;
}
+ if (
+ openingStartOfLine.column === node.loc.start.column
+ && option === 'line-aligned'
+ ) {
+ return;
+ }
+
const messageId = astUtil.isNodeFirstInLine(context, node)
- ? 'matchIndent'
+ ? optionMessageMap[option]
: 'onOwnLine';
+
report(context, messages[messageId], messageId, {
node,
loc: node.loc,
fix(fixer) {
- const indent = Array(opening.loc.start.column + 1).join(' ');
+ const indent = repeat(
+ ' ',
+ getIndentation(openingStartOfLine, opening)
+ );
+
if (astUtil.isNodeFirstInLine(context, node)) {
return fixer.replaceTextRange(
diff --git a/lib/rules/jsx-curly-spacing.js b/lib/rules/jsx-curly-spacing.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-curly-spacing.js
+++ b/lib/rules/jsx-curly-spacing.js
@@ -12,5 +12,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const getSourceCode = require('../util/eslint').getSourceCode;
diff --git a/lib/rules/jsx-handler-names.js b/lib/rules/jsx-handler-names.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-handler-names.js
+++ b/lib/rules/jsx-handler-names.js
@@ -6,4 +6,5 @@
'use strict';
+const minimatch = require('minimatch');
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
@@ -40,4 +41,9 @@
checkLocalVariables: { type: 'boolean' },
checkInlineFunction: { type: 'boolean' },
+ ignoreComponentNames: {
+ type: 'array',
+ uniqueItems: true,
+ items: { type: 'string' },
+ },
},
additionalProperties: false,
@@ -52,4 +58,9 @@
checkLocalVariables: { type: 'boolean' },
checkInlineFunction: { type: 'boolean' },
+ ignoreComponentNames: {
+ type: 'array',
+ uniqueItems: true,
+ items: { type: 'string' },
+ },
},
additionalProperties: false,
@@ -64,4 +75,9 @@
checkLocalVariables: { type: 'boolean' },
checkInlineFunction: { type: 'boolean' },
+ ignoreComponentNames: {
+ type: 'array',
+ uniqueItems: true,
+ items: { type: 'string' },
+ },
},
additionalProperties: false,
@@ -79,4 +95,14 @@
additionalProperties: false,
},
+ {
+ type: 'object',
+ properties: {
+ ignoreComponentNames: {
+ type: 'array',
+ uniqueItems: true,
+ items: { type: 'string' },
+ },
+ },
+ },
],
}],
@@ -112,6 +138,15 @@
const checkInlineFunction = !!configuration.checkInlineFunction;
+ const ignoreComponentNames = configuration.ignoreComponentNames || [];
+
return {
JSXAttribute(node) {
+ const componentName = node.parent.name.name;
+
+ const isComponentNameIgnored = ignoreComponentNames.some((ignoredComponentNamePattern) => {
+ const isIgnored = minimatch(componentName, ignoredComponentNamePattern);
+ return isIgnored;
+ });
+
if (
!node.value
@@ -125,4 +160,5 @@
)
)
+ || isComponentNameIgnored
) {
return;
diff --git a/lib/rules/jsx-indent-props.js b/lib/rules/jsx-indent-props.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-indent-props.js
+++ b/lib/rules/jsx-indent-props.js
@@ -31,4 +31,6 @@
'use strict';
+const repeat = require('string.prototype.repeat');
+
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
@@ -131,5 +133,6 @@
fix(fixer) {
return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]],
- Array(needed + 1).join(indentType === 'space' ? ' ' : '\t'));
+ repeat(indentType === 'space' ? ' ' : '\t', needed)
+ );
},
});
diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-indent.js
+++ b/lib/rules/jsx-indent.js
@@ -32,4 +32,5 @@
const matchAll = require('string.prototype.matchall');
+const repeat = require('string.prototype.repeat');
const astUtil = require('../util/ast');
@@ -110,5 +111,5 @@
*/
function getFixerFunction(node, needed) {
- const indent = Array(needed + 1).join(indentChar);
+ const indent = repeat(indentChar, needed);
if (node.type === 'JSXText' || node.type === 'Literal') {
diff --git a/lib/rules/jsx-max-depth.js b/lib/rules/jsx-max-depth.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-max-depth.js
+++ b/lib/rules/jsx-max-depth.js
@@ -6,5 +6,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const includes = require('array-includes');
const variableUtil = require('../util/variable');
@@ -89,5 +89,5 @@
}
- function findJSXElementOrFragment(variables, name, previousReferences) {
+ function findJSXElementOrFragment(startNode, name, previousReferences) {
function find(refs, prevRefs) {
for (let i = refs.length - 1; i >= 0; i--) {
@@ -98,5 +98,5 @@
&& writeExpr)
|| ((writeExpr && writeExpr.type === 'Identifier')
- && findJSXElementOrFragment(variables, writeExpr.name, prevRefs));
+ && findJSXElementOrFragment(startNode, writeExpr.name, prevRefs));
}
}
@@ -105,5 +105,5 @@
}
- const variable = variableUtil.getVariable(variables, name);
+ const variable = variableUtil.getVariableFromContext(context, startNode, name);
if (variable && variable.references) {
const containDuplicates = previousReferences.some((ref) => includes(variable.references, ref));
@@ -151,6 +151,5 @@
}
- const variables = variableUtil.variablesInScope(context, node);
- const element = findJSXElementOrFragment(variables, node.expression.name, []);
+ const element = findJSXElementOrFragment(node, node.expression.name, []);
if (element) {
diff --git a/lib/rules/jsx-no-duplicate-props.js b/lib/rules/jsx-no-duplicate-props.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-no-duplicate-props.js
+++ b/lib/rules/jsx-no-duplicate-props.js
@@ -6,5 +6,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
diff --git a/lib/rules/jsx-no-leaked-render.js b/lib/rules/jsx-no-leaked-render.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-no-leaked-render.js
+++ b/lib/rules/jsx-no-leaked-render.js
@@ -162,6 +162,5 @@
return;
}
- const variables = variableUtil.variablesInScope(context, node);
- const leftSideVar = variableUtil.getVariable(variables, leftSide.name);
+ const leftSideVar = variableUtil.getVariableFromContext(context, node, leftSide.name);
if (leftSideVar) {
const leftSideValue = leftSideVar.defs
diff --git a/configs/jsx-runtime.js b/configs/jsx-runtime.js
index v7.34.3..v7.35.0 100644
--- a/configs/jsx-runtime.js
+++ b/configs/jsx-runtime.js
@@ -1,18 +1,13 @@
'use strict';
-const all = require('./all');
+const plugin = require('..');
-module.exports = Object.assign({}, all, {
- languageOptions: Object.assign({}, all.languageOptions, {
- parserOptions: Object.assign({}, all.languageOptions.parserOptions, {
- jsxPragma: null, // for @typescript/eslint-parser
- }),
- }),
- rules: {
- 'react/react-in-jsx-scope': 0,
- 'react/jsx-uses-react': 0,
- },
-});
+const legacyConfig = plugin.configs['jsx-runtime'];
-// this is so the `languageOptions` property won't be warned in the new config system
+module.exports = {
+ plugins: { react: plugin },
+ rules: legacyConfig.rules,
+ languageOptions: { parserOptions: legacyConfig.parserOptions },
+};
+
Object.defineProperty(module.exports, 'languageOptions', { enumerable: false });
diff --git a/lib/rules/jsx-sort-default-props.js b/lib/rules/jsx-sort-default-props.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-sort-default-props.js
+++ b/lib/rules/jsx-sort-default-props.js
@@ -98,6 +98,5 @@
function findVariableByName(node, name) {
const variable = variableUtil
- .variablesInScope(context, node)
- .find((item) => item.name === name);
+ .getVariableFromContext(context, node, name);
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
diff --git a/lib/rules/jsx-wrap-multilines.js b/lib/rules/jsx-wrap-multilines.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/jsx-wrap-multilines.js
+++ b/lib/rules/jsx-wrap-multilines.js
@@ -6,5 +6,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const eslintUtil = require('../util/eslint');
diff --git a/lib/rules/no-array-index-key.js b/lib/rules/no-array-index-key.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/no-array-index-key.js
+++ b/lib/rules/no-array-index-key.js
@@ -6,5 +6,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
@@ -118,4 +118,6 @@
}
+ const name = /** @type {keyof iteratorFunctionsToIndexParamPosition} */ (callee.property.name);
+
const callbackArg = isUsingReactChildren(node)
? node.arguments[1]
@@ -132,5 +134,5 @@
const params = callbackArg.params;
- const indexParamPosition = iteratorFunctionsToIndexParamPosition[callee.property.name];
+ const indexParamPosition = iteratorFunctionsToIndexParamPosition[name];
if (params.length < indexParamPosition + 1) {
return null;
diff --git a/lib/rules/no-danger-with-children.js b/lib/rules/no-danger-with-children.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/no-danger-with-children.js
+++ b/lib/rules/no-danger-with-children.js
@@ -33,6 +33,5 @@
create(context) {
function findSpreadVariable(node, name) {
- return variableUtil.variablesInScope(context, node)
- .find((item) => item.name === name);
+ return variableUtil.getVariableFromContext(context, node, name);
}
/**
@@ -129,6 +128,5 @@
if (props.type === 'Identifier') {
- const variable = variableUtil.variablesInScope(context, node)
- .find((item) => item.name === props.name);
+ const variable = variableUtil.getVariableFromContext(context, node, props.name);
if (variable && variable.defs.length && variable.defs[0].node.init) {
props = variable.defs[0].node.init;
diff --git a/lib/rules/no-danger.js b/lib/rules/no-danger.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/no-danger.js
+++ b/lib/rules/no-danger.js
@@ -6,6 +6,7 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const fromEntries = require('object.fromentries/polyfill')();
+const minimatch = require('minimatch');
const docsUrl = require('../util/docsUrl');
@@ -56,11 +57,30 @@
messages,
- schema: [],
+ schema: [{
+ type: 'object',
+ properties: {
+ customComponentNames: {
+ items: {
+ type: 'string',
+ },
+ minItems: 0,
+ type: 'array',
+ uniqueItems: true,
+ },
+ },
+ }],
},
create(context) {
+ const configuration = context.options[0] || {};
+ const customComponentNames = configuration.customComponentNames || [];
+
return {
JSXAttribute(node) {
- if (jsxUtil.isDOMComponent(node.parent) && isDangerous(node.name.name)) {
+ const functionName = node.parent.name.name;
+
+ const enableCheckingCustomComponent = customComponentNames.some((name) => minimatch(functionName, name));
+
+ if ((enableCheckingCustomComponent || jsxUtil.isDOMComponent(node.parent)) && isDangerous(node.name.name)) {
report(context, messages.dangerousProp, 'dangerousProp', {
node,
diff --git a/lib/rules/no-invalid-html-attribute.js b/lib/rules/no-invalid-html-attribute.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/no-invalid-html-attribute.js
+++ b/lib/rules/no-invalid-html-attribute.js
@@ -9,5 +9,4 @@
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
-const getMessageData = require('../util/message');
// ------------------------------------------------------------------------------
@@ -225,4 +224,5 @@
]);
+/* eslint-disable eslint-plugin/no-unused-message-ids -- false positives, these messageIds are used */
const messages = {
emptyIsMeaningless: 'An empty “{{attributeName}}” attribute is meaningless.',
@@ -265,13 +265,9 @@
node,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveNonString', messages.suggestRemoveNonString),
- {
- data,
- fix(fixer) { return fixer.remove(parentNode); },
- }
- ),
- ],
+ suggest: [{
+ messageId: 'suggestRemoveNonString',
+ data,
+ fix(fixer) { return fixer.remove(parentNode); },
+ }],
});
return;
@@ -284,13 +280,9 @@
node,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveEmpty', messages.suggestRemoveEmpty),
- {
- data,
- fix(fixer) { return fixer.remove(node.parent); },
- }
- ),
- ],
+ suggest: [{
+ messageId: 'suggestRemoveEmpty',
+ data,
+ fix(fixer) { return fixer.remove(node.parent); },
+ }],
});
return;
@@ -308,16 +300,14 @@
};
+ const suggest = [{
+ messageId: 'suggestRemoveInvalid',
+ data,
+ fix(fixer) { return fixer.removeRange(singlePart.range); },
+ }];
+
report(context, messages.neverValid, 'neverValid', {
node,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveInvalid', messages.suggestRemoveInvalid),
- {
- data,
- fix(fixer) { return fixer.removeRange(singlePart.range); },
- }
- ),
- ],
+ suggest,
});
} else if (!allowedTags.has(parentNodeName)) {
@@ -328,16 +318,14 @@
};
+ const suggest = [{
+ messageId: 'suggestRemoveInvalid',
+ data,
+ fix(fixer) { return fixer.removeRange(singlePart.range); },
+ }];
+
report(context, messages.notValidFor, 'notValidFor', {
node,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveInvalid', messages.suggestRemoveInvalid),
- {
- data,
- fix(fixer) { return fixer.removeRange(singlePart.range); },
- }
- ),
- ],
+ suggest,
});
}
@@ -376,31 +364,25 @@
const whitespaceParts = splitIntoRangedParts(node, /(\s+)/g);
for (const whitespacePart of whitespaceParts) {
+ const data = { attributeName };
+
if (whitespacePart.range[0] === (node.range[0] + 1) || whitespacePart.range[1] === (node.range[1] - 1)) {
report(context, messages.spaceDelimited, 'spaceDelimited', {
node,
- data: { attributeName },
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveWhitespaces', messages.suggestRemoveWhitespaces),
- {
- data: { attributeName },
- fix(fixer) { return fixer.removeRange(whitespacePart.range); },
- }
- ),
- ],
+ data,
+ suggest: [{
+ messageId: 'suggestRemoveWhitespaces',
+ data,
+ fix(fixer) { return fixer.removeRange(whitespacePart.range); },
+ }],
});
} else if (whitespacePart.value !== '\u0020') {
report(context, messages.spaceDelimited, 'spaceDelimited', {
node,
- data: { attributeName },
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveWhitespaces', messages.suggestRemoveWhitespaces),
- {
- data: { attributeName },
- fix(fixer) { return fixer.replaceTextRange(whitespacePart.range, '\u0020'); },
- }
- ),
- ],
+ data,
+ suggest: [{
+ messageId: 'suggestRemoveWhitespaces',
+ data,
+ fix(fixer) { return fixer.replaceTextRange(whitespacePart.range, '\u0020'); },
+ }],
});
}
@@ -427,13 +409,9 @@
node: node.name,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveDefault', messages.suggestRemoveDefault),
- {
- data,
- fix(fixer) { return fixer.remove(node); },
- }
- ),
- ],
+ suggest: [{
+ messageId: 'suggestRemoveDefault',
+ data,
+ fix(fixer) { return fixer.remove(node); },
+ }],
});
return;
@@ -448,10 +426,9 @@
node: node.name,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveEmpty', messages.suggestRemoveEmpty),
- { data, fix }
- ),
- ],
+ suggest: [{
+ messageId: 'suggestRemoveEmpty',
+ data,
+ fix,
+ }],
});
return;
@@ -476,10 +453,9 @@
node: node.value,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveDefault', messages.suggestRemoveDefault),
- { data, fix }
- ),
- ],
+ suggest: [{
+ messageId: 'suggestRemoveDefault',
+ data,
+ fix,
+ }],
});
} else if (node.value.expression.type === 'Identifier' && node.value.expression.name === 'undefined') {
@@ -489,10 +465,9 @@
node: node.value,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveDefault', messages.suggestRemoveDefault),
- { data, fix }
- ),
- ],
+ suggest: [{
+ messageId: 'suggestRemoveDefault',
+ data,
+ fix,
+ }],
});
}
@@ -524,13 +499,9 @@
node: value,
data,
- suggest: [
- Object.assign(
- getMessageData('suggestRemoveInvalid', messages.suggestRemoveInvalid),
- {
- data,
- fix(fixer) { return fixer.replaceText(value, value.raw.replace(value.value, '')); },
- }
- ),
- ],
+ suggest: [{
+ messageId: 'suggestRemoveInvalid',
+ data,
+ fix(fixer) { return fixer.replaceText(value, value.raw.replace(value.value, '')); },
+ }],
});
} else if (!validTagSet.has(node.arguments[0].value)) {
diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/no-unknown-property.js
+++ b/lib/rules/no-unknown-property.js
@@ -6,5 +6,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
@@ -492,8 +492,8 @@
function getStandardName(name, context) {
if (has(DOM_ATTRIBUTE_NAMES, name)) {
- return DOM_ATTRIBUTE_NAMES[name];
+ return DOM_ATTRIBUTE_NAMES[/** @type {keyof DOM_ATTRIBUTE_NAMES} */ (name)];
}
if (has(SVGDOM_ATTRIBUTE_NAMES, name)) {
- return SVGDOM_ATTRIBUTE_NAMES[name];
+ return SVGDOM_ATTRIBUTE_NAMES[/** @type {keyof SVGDOM_ATTRIBUTE_NAMES} */ (name)];
}
const names = getDOMPropertyNames(context);
@@ -593,5 +593,5 @@
// Some attributes are allowed on some tags only
- const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[name] : null;
+ const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[/** @type {keyof ATTRIBUTE_TAGS_MAP} */ (name)] : null;
if (tagName && allowedTags) {
// Scenario 1A: Allowed attribute found where not supposed to, report it
diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js
index v7.34.3..v7.35.0 100644
--- a/lib/util/propTypes.js
+++ b/lib/util/propTypes.js
@@ -646,4 +646,15 @@
const rightMostName = getRightMostTypeName(node.typeName);
+ if (
+ leftMostName === 'React'
+ && (
+ rightMostName === 'HTMLAttributes'
+ || rightMostName === 'HTMLElement'
+ || rightMostName === 'HTMLProps'
+ )
+ ) {
+ this.shouldSpecifyClassNameProp = true;
+ }
+
const importedName = localToImportedMap[rightMostName];
const idx = genericTypeParamIndexWherePropsArePresent[
@@ -836,4 +847,12 @@
};
}
+ if (this.shouldSpecifyClassNameProp) {
+ this.declaredPropTypes.className = {
+ fullName: 'className',
+ name: 'className',
+ isRequired: false,
+ };
+ }
+
this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => {
if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) {
@@ -947,6 +966,5 @@
}
case 'Identifier': {
- const firstMatchingVariable = variableUtil.variablesInScope(context, node)
- .find((variableInScope) => variableInScope.name === propTypes.name);
+ const firstMatchingVariable = variableUtil.getVariableFromContext(context, node, propTypes.name);
if (firstMatchingVariable) {
const defInScope = firstMatchingVariable.defs[firstMatchingVariable.defs.length - 1];
diff --git a/lib/util/propTypesSort.js b/lib/util/propTypesSort.js
index v7.34.3..v7.35.0 100644
--- a/lib/util/propTypesSort.js
+++ b/lib/util/propTypesSort.js
@@ -127,4 +127,5 @@
* @param {Boolean=} noSortAlphabetically whether or not to disable alphabetical sorting of the elements.
* @param {Boolean=} sortShapeProp whether or not to sort propTypes defined in PropTypes.shape.
+ * @param {Boolean=} checkTypes whether or not sorting of prop type definitions are checked.
* @returns {Object|*|{range, text}} the sort order of the two elements.
*/
@@ -137,5 +138,6 @@
callbacksLast,
noSortAlphabetically,
- sortShapeProp
+ sortShapeProp,
+ checkTypes
) {
function sortInSource(allNodes, source) {
@@ -184,8 +186,13 @@
const sourceCodeText = getText(context);
+ let separator = '';
source = nodes.reduceRight((acc, attr, index) => {
const sortedAttr = sortedAttributes[index];
const commentNode = commentnodeMap.get(sortedAttr);
let sortedAttrText = sourceCodeText.slice(commentNode.start, commentNode.end);
+ const sortedAttrTextLastChar = sortedAttrText[sortedAttrText.length - 1];
+ if (!separator && [';', ','].some((allowedSep) => sortedAttrTextLastChar === allowedSep)) {
+ separator = sortedAttrTextLastChar;
+ }
if (sortShapeProp && isShapeProp(sortedAttr.value)) {
const shape = getShapeProperties(sortedAttr.value);
@@ -198,5 +205,6 @@
}
}
- return `${acc.slice(0, commentnodeMap.get(attr).start)}${sortedAttrText}${acc.slice(commentnodeMap.get(attr).end)}`;
+ const sortedAttrTextVal = checkTypes && !sortedAttrText.endsWith(separator) ? `${sortedAttrText}${separator}` : sortedAttrText;
+ return `${acc.slice(0, commentnodeMap.get(attr).start)}${sortedAttrTextVal}${acc.slice(commentnodeMap.get(attr).end)}`;
}, source);
});
diff --git a/lib/rules/react-in-jsx-scope.js b/lib/rules/react-in-jsx-scope.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/react-in-jsx-scope.js
+++ b/lib/rules/react-in-jsx-scope.js
@@ -38,6 +38,5 @@
function checkIfReactIsInScope(node) {
- const variables = variableUtil.variablesInScope(context, node);
- if (variableUtil.findVariable(variables, pragma)) {
+ if (variableUtil.getVariableFromContext(context, node, pragma)) {
return;
}
diff --git a/configs/recommended.js b/configs/recommended.js
index v7.34.3..v7.35.0 100644
--- a/configs/recommended.js
+++ b/configs/recommended.js
@@ -1,34 +1,13 @@
'use strict';
-const all = require('./all');
+const plugin = require('..');
-module.exports = Object.assign({}, all, {
- languageOptions: all.languageOptions,
- rules: {
- 'react/display-name': 2,
- 'react/jsx-key': 2,
- 'react/jsx-no-comment-textnodes': 2,
- 'react/jsx-no-duplicate-props': 2,
- 'react/jsx-no-target-blank': 2,
- 'react/jsx-no-undef': 2,
- 'react/jsx-uses-react': 2,
- 'react/jsx-uses-vars': 2,
- 'react/no-children-prop': 2,
- 'react/no-danger-with-children': 2,
- 'react/no-deprecated': 2,
- 'react/no-direct-mutation-state': 2,
- 'react/no-find-dom-node': 2,
- 'react/no-is-mounted': 2,
- 'react/no-render-return-value': 2,
- 'react/no-string-refs': 2,
- 'react/no-unescaped-entities': 2,
- 'react/no-unknown-property': 2,
- 'react/no-unsafe': 0,
- 'react/prop-types': 2,
- 'react/react-in-jsx-scope': 2,
- 'react/require-render-return': 2,
- },
-});
+const legacyConfig = plugin.configs.recommended;
-// this is so the `languageOptions` property won't be warned in the new config system
+module.exports = {
+ plugins: { react: plugin },
+ rules: legacyConfig.rules,
+ languageOptions: { parserOptions: legacyConfig.parserOptions },
+};
+
Object.defineProperty(module.exports, 'languageOptions', { enumerable: false });
diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/require-default-props.js
+++ b/lib/rules/require-default-props.js
@@ -25,4 +25,11 @@
};
+function isPropWithNoDefaulVal(prop) {
+ if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') {
+ return false;
+ }
+ return prop.value.type !== 'AssignmentPattern';
+}
+
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
@@ -135,14 +142,22 @@
}
} else if (props.type === 'ObjectPattern') {
+ // Filter required props with default value and report error
props.properties.filter((prop) => {
- if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') {
- return false;
- }
- const propType = propTypes[prop.key.name];
- if (!propType || propType.isRequired) {
- return false;
- }
- return prop.value.type !== 'AssignmentPattern';
+ const propName = prop && prop.key && prop.key.name;
+ const isPropRequired = propTypes[propName] && propTypes[propName].isRequired;
+ return propTypes[propName] && isPropRequired && !isPropWithNoDefaulVal(prop);
}).forEach((prop) => {
+ report(context, messages.noDefaultWithRequired, 'noDefaultWithRequired', {
+ node: prop,
+ data: { name: prop.key.name },
+ });
+ });
+
+ // Filter non required props with no default value and report error
+ props.properties.filter((prop) => {
+ const propName = prop && prop.key && prop.key.name;
+ const isPropRequired = propTypes[propName] && propTypes[propName].isRequired;
+ return propTypes[propName] && !isPropRequired && isPropWithNoDefaulVal(prop);
+ }).forEach((prop) => {
report(context, messages.shouldAssignObjectDefault, 'shouldAssignObjectDefault', {
node: prop,
diff --git a/lib/rules/sort-comp.js b/lib/rules/sort-comp.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/sort-comp.js
+++ b/lib/rules/sort-comp.js
@@ -6,5 +6,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const entries = require('object.entries');
const values = require('object.values');
@@ -125,4 +125,5 @@
create: Components.detect((context, components) => {
+ /** @satisfies {Record<string, { node: ASTNode, score: number, closest: { distance: number, ref: { node: null | ASTNode, index: number } } }>} */
const errors = {};
const methodsOrder = getMethodsOrder(context.options[0]);
@@ -288,16 +289,17 @@
*/
function dedupeErrors() {
- for (const i in errors) {
- if (has(errors, i)) {
- const index = errors[i].closest.ref.index;
- if (errors[index]) {
- if (errors[i].score > errors[index].score) {
- delete errors[index];
- } else {
- delete errors[i];
- }
+ entries(errors).forEach((entry) => {
+ const i = entry[0];
+ const error = entry[1];
+
+ const index = error.closest.ref.index;
+ if (errors[index]) {
+ if (error.score > errors[index].score) {
+ delete errors[index];
+ } else {
+ delete errors[i];
}
}
- }
+ });
}
diff --git a/lib/rules/sort-default-props.js b/lib/rules/sort-default-props.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/sort-default-props.js
+++ b/lib/rules/sort-default-props.js
@@ -92,6 +92,5 @@
*/
function findVariableByName(node, name) {
- const variable = variableUtil.variablesInScope(context, node)
- .find((item) => item.name === name);
+ const variable = variableUtil.getVariableFromContext(context, node, name);
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
diff --git a/lib/rules/sort-prop-types.js b/lib/rules/sort-prop-types.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/sort-prop-types.js
+++ b/lib/rules/sort-prop-types.js
@@ -130,5 +130,6 @@
callbacksLast,
noSortAlphabetically,
- sortShapeProp
+ sortShapeProp,
+ checkTypes
);
}
@@ -231,5 +232,8 @@
function handleFunctionComponent(node) {
- const firstArg = node.params[0].typeAnnotation && node.params[0].typeAnnotation.typeAnnotation;
+ const firstArg = node.params
+ && node.params.length > 0
+ && node.params[0].typeAnnotation
+ && node.params[0].typeAnnotation.typeAnnotation;
if (firstArg && firstArg.type === 'TSTypeReference') {
const propType = typeAnnotations.get(firstArg.typeName.name)
diff --git a/lib/rules/style-prop-object.js b/lib/rules/style-prop-object.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/style-prop-object.js
+++ b/lib/rules/style-prop-object.js
@@ -62,6 +62,5 @@
*/
function checkIdentifiers(node) {
- const variable = variableUtil.variablesInScope(context, node)
- .find((item) => item.name === node.name);
+ const variable = variableUtil.getVariableFromContext(context, node, node.name);
if (!variable || !variable.defs[0] || !variable.defs[0].node.init) {
diff --git a/lib/util/variable.js b/lib/util/variable.js
index v7.34.3..v7.35.0 100644
--- a/lib/util/variable.js
+++ b/lib/util/variable.js
@@ -6,5 +6,4 @@
'use strict';
-const toReversed = require('array.prototype.toreversed');
const getScope = require('./eslint').getScope;
@@ -30,28 +29,31 @@
/**
- * List all variable in a given scope
+ * Searches for a variable in the given scope.
*
- * Contain a patch for babel-eslint to avoid https://github.com/babel/babel-eslint/issues/21
- *
* @param {Object} context The current rule context.
* @param {ASTNode} node The node to start looking from.
- * @returns {Array} The variables list
+ * @param {string} name The name of the variable to search.
+ * @returns {Object | undefined} Variable if the variable was found, undefined if not.
*/
-function variablesInScope(context, node) {
+function getVariableFromContext(context, node, name) {
let scope = getScope(context, node);
- let variables = scope.variables;
- while (scope.type !== 'global') {
+ while (scope) {
+ let variable = getVariable(scope.variables, name);
+
+ if (!variable && scope.childScopes.length) {
+ variable = getVariable(scope.childScopes[0].variables, name);
+
+ if (!variable && scope.childScopes[0].childScopes.length) {
+ variable = getVariable(scope.childScopes[0].childScopes[0].variables, name);
+ }
+ }
+
+ if (variable) {
+ return variable;
+ }
scope = scope.upper;
- variables = scope.variables.concat(variables);
}
- if (scope.childScopes.length) {
- variables = scope.childScopes[0].variables.concat(variables);
- if (scope.childScopes[0].childScopes.length) {
- variables = scope.childScopes[0].childScopes[0].variables.concat(variables);
- }
- }
-
- return toReversed(variables);
+ return undefined;
}
@@ -64,5 +66,5 @@
*/
function findVariableByName(context, node, name) {
- const variable = getVariable(variablesInScope(context, node), name);
+ const variable = getVariableFromContext(context, node, name);
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
@@ -94,5 +96,5 @@
findVariableByName,
getVariable,
- variablesInScope,
+ getVariableFromContext,
getLatestVariableDefinition,
};
diff --git a/lib/util/version.js b/lib/util/version.js
index v7.34.3..v7.35.0 100644
--- a/lib/util/version.js
+++ b/lib/util/version.js
@@ -13,4 +13,6 @@
const error = require('./error');
+const ULTIMATE_LATEST_SEMVER = '999.999.999';
+
let warnedForMissingVersion = false;
@@ -45,4 +47,35 @@
}
+function convertConfVerToSemver(confVer) {
+ const fullSemverString = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer;
+ return semver.coerce(fullSemverString.split('.').map((part) => Number(part)).join('.'));
+}
+
+let defaultVersion = ULTIMATE_LATEST_SEMVER;
+
+function resetDefaultVersion() {
+ defaultVersion = ULTIMATE_LATEST_SEMVER;
+}
+
+function readDefaultReactVersionFromContext(context) {
+ // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings)
+ if (context.settings && context.settings.react && context.settings.react.defaultVersion) {
+ let settingsDefaultVersion = context.settings.react.defaultVersion;
+ if (typeof settingsDefaultVersion !== 'string') {
+ error('Warning: default React version specified in eslint-pluigin-react-settings must be a string; '
+ + `got "${typeof settingsDefaultVersion}"`);
+ }
+ settingsDefaultVersion = String(settingsDefaultVersion);
+ const result = convertConfVerToSemver(settingsDefaultVersion);
+ if (result) {
+ defaultVersion = result.version;
+ } else {
+ error(`Warning: React version specified in eslint-plugin-react-settings must be a valid semver version, or "detect"; got “${settingsDefaultVersion}”. Falling back to latest version as default.`);
+ }
+ } else {
+ defaultVersion = ULTIMATE_LATEST_SEMVER;
+ }
+}
+
// TODO, semver-major: remove context fallback
function detectReactVersion(context) {
@@ -61,9 +94,12 @@
if (e.code === 'MODULE_NOT_FOUND') {
if (!warnedForMissingVersion) {
- error('Warning: React version was set to "detect" in eslint-plugin-react settings, '
- + 'but the "react" package is not installed. Assuming latest React version for linting.');
+ let sentence2 = 'Assuming latest React version for linting.';
+ if (defaultVersion !== ULTIMATE_LATEST_SEMVER) {
+ sentence2 = `Assuming default React version for linting: "${defaultVersion}".`;
+ }
+ error(`Warning: React version was set to "detect" in eslint-plugin-react settings, but the "react" package is not installed. ${sentence2}`);
warnedForMissingVersion = true;
}
- cachedDetectedReactVersion = '999.999.999';
+ cachedDetectedReactVersion = defaultVersion;
return cachedDetectedReactVersion;
}
@@ -72,7 +108,6 @@
}
-const defaultVersion = '999.999.999';
-
function getReactVersionFromContext(context) {
+ readDefaultReactVersionFromContext(context);
let confVer = defaultVersion;
// .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings)
@@ -92,6 +127,6 @@
warnedForMissingVersion = true;
}
- confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer;
- const result = semver.coerce(confVer.split('.').map((part) => Number(part)).join('.'));
+
+ const result = convertConfVerToSemver(confVer);
if (!result) {
error(`Warning: React version specified in eslint-plugin-react-settings must be a valid semver version, or "detect"; got “${confVer}”`);
@@ -112,5 +147,5 @@
error('Warning: Flow version was set to "detect" in eslint-plugin-react settings, '
+ 'but the "flow-bin" package is not installed. Assuming latest Flow version for linting.');
- return '999.999.999';
+ return ULTIMATE_LATEST_SEMVER;
}
throw e;
@@ -134,6 +169,6 @@
throw 'Could not retrieve flowVersion from settings'; // eslint-disable-line no-throw-literal
}
- confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer;
- const result = semver.coerce(confVer.split('.').map((part) => Number(part)).join('.'));
+
+ const result = convertConfVerToSemver(confVer);
if (!result) {
error(`Warning: Flow version specified in eslint-plugin-react-settings must be a valid semver version, or "detect"; got “${confVer}”`);
@@ -159,3 +194,4 @@
resetWarningFlag,
resetDetectedVersion,
+ resetDefaultVersion,
};
diff --git a/lib/rules/void-dom-elements-no-children.js b/lib/rules/void-dom-elements-no-children.js
index v7.34.3..v7.35.0 100644
--- a/lib/rules/void-dom-elements-no-children.js
+++ b/lib/rules/void-dom-elements-no-children.js
@@ -7,5 +7,5 @@
'use strict';
-const has = require('object.hasown/polyfill')();
+const has = require('hasown');
const docsUrl = require('../util/docsUrl');
diff --git a/package.json b/package.json
index v7.34.3..v7.35.0 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
"name": "eslint-plugin-react",
- "version": "7.34.3",
+ "version": "7.35.0",
"author": "Yannick Croissant <[email protected]>",
"description": "React specific linting rules for ESLint",
@@ -16,5 +16,5 @@
"posttest": "aud --production",
"type-check": "tsc",
- "unit-test": "istanbul cover node_modules/mocha/bin/_mocha tests/lib/**/*.js tests/util/**/*.js tests/index.js",
+ "unit-test": "istanbul cover node_modules/mocha/bin/_mocha tests/lib/**/*.js tests/util/**/*.js tests/index.js tests/flat-config.js",
"update:eslint-docs": "eslint-doc-generator"
},
@@ -29,23 +29,23 @@
"array.prototype.findlast": "^1.2.5",
"array.prototype.flatmap": "^1.3.2",
- "array.prototype.toreversed": "^1.1.2",
"array.prototype.tosorted": "^1.1.4",
"doctrine": "^2.1.0",
"es-iterator-helpers": "^1.0.19",
"estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.1.2",
"object.entries": "^1.1.8",
"object.fromentries": "^2.0.8",
- "object.hasown": "^1.1.4",
"object.values": "^1.2.0",
"prop-types": "^15.8.1",
"resolve": "^2.0.0-next.5",
"semver": "^6.3.1",
- "string.prototype.matchall": "^4.0.11"
+ "string.prototype.matchall": "^4.0.11",
+ "string.prototype.repeat": "^1.0.0"
},
"devDependencies": {
- "@babel/core": "^7.24.7",
- "@babel/eslint-parser": "^7.24.7",
+ "@babel/core": "^7.24.9",
+ "@babel/eslint-parser": "^7.24.8",
"@babel/plugin-syntax-decorators": "^7.24.7",
"@babel/plugin-syntax-do-expressions": "^7.24.7",
@@ -58,5 +58,5 @@
"aud": "^2.0.4",
"babel-eslint": "^8 || ^9 || ^10.1.0",
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8",
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-doc-generator": "^1.7.1",
@@ -64,5 +64,5 @@
"eslint-plugin-import": "^2.29.1",
"eslint-remote-tester": "^3.0.1",
- "eslint-remote-tester-repositories": "^2.0.0",
+ "eslint-remote-tester-repositories": "^1.0.1",
"eslint-scope": "^3.7.3",
"espree": "^3.5.4",
@@ -79,5 +79,5 @@
},
"peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
},
"engines": {
@@ -104,5 +104,6 @@
".editorconfig",
"tsconfig.json",
- ".markdownlint*"
+ ".markdownlint*",
+ "types"
]
}
diff --git a/README.md b/README.md
index v7.34.3..v7.35.0 100644
--- a/README.md
+++ b/README.md
@@ -43,5 +43,7 @@
"version": "detect", // React version. "detect" automatically picks the version you have installed.
// You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
- // It will default to "latest" and warn if missing, and to "detect" in the future
+ // Defaults to the "defaultVersion" setting and warns if missing, and to "detect" in the future
+ "defaultVersion": "", // Default React version to use when the version you have installed cannot be detected.
+ // If not provided, defaults to the latest React version.
"flowVersion": "0.53" // Flow version
},
@@ -204,25 +206,20 @@
<!-- markdownlint-disable-next-line no-duplicate-heading -->
-### Shareable configs
+### Flat Configs
-There're also 3 shareable configs.
+This plugin exports 3 flat configs:
-- `eslint-plugin-react/configs/all`
-- `eslint-plugin-react/configs/recommended`
-- `eslint-plugin-react/configs/jsx-runtime`
+- `flat.all`
+- `flat.recommended`
+- `flat['jsx-runtime']`
-If your eslint.config.js is ESM, include the `.js` extension (e.g. `eslint-plugin-react/recommended.js`). Note that the next semver-major will require omitting the extension for these imports.
+The flat configs are available via the root plugin import. They will configure the plugin under the `react/` namespace and enable JSX in [`languageOptions.parserOptions`](https://eslint.org/docs/latest/use/configure/language-options#specifying-parser-options).
-**Note**: These configurations will import `eslint-plugin-react` and enable JSX in [`languageOptions.parserOptions`](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#configuration-objects).
-
-In the new config system, `plugin:` protocol(e.g. `plugin:react/recommended`) is no longer valid.
-As eslint does not automatically import the preset config (shareable config), you explicitly do it by yourself.
-
```js
-const reactRecommended = require('eslint-plugin-react/configs/recommended');
+const reactPlugin = require('eslint-plugin-react');
module.exports = [
…
- reactRecommended, // This is not a plugin object, but a shareable config object
+ reactPlugin.configs.flat.recommended, // This is not a plugin object, but a shareable config object
…
];
@@ -235,5 +232,5 @@
```js
-const reactRecommended = require('eslint-plugin-react/configs/recommended');
+const reactPlugin = require('eslint-plugin-react');
const globals = require('globals');
@@ -242,7 +239,7 @@
{
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
- ...reactRecommended,
+ ...reactPlugin.configs.flat.recommended,
languageOptions: {
- ...reactRecommended.languageOptions,
+ ...reactPlugin.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
@@ -258,5 +255,5 @@
```js
-const reactRecommended = require('eslint-plugin-react/configs/recommended');
+const reactPlugin = require('eslint-plugin-react');
const globals = require('globals');
@@ -265,5 +262,5 @@
{
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
- ...reactRecommended,
+ ...reactPlugin.configs.flat.recommended,
},
{
@@ -339,4 +336,5 @@
| [jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components | | | | | |
| [jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props | | | 🔧 | | |
+| [jsx-props-no-spread-multi](docs/rules/jsx-props-no-spread-multi.md) | Disallow JSX prop spreading the same identifier multiple times | | | | | |
| [jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Disallow JSX prop spreading | | | | | |
| [jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting | | | | | ❌ |
diff --git a/lib/types.d.ts b/lib/types.d.ts
index v7.34.3..v7.35.0 100644
--- a/lib/types.d.ts
+++ b/lib/types.d.ts
@@ -12,7 +12,8 @@
type JSXElement = ASTNode;
type JSXFragment = ASTNode;
+ type JSXOpeningElement = ASTNode;
type JSXSpreadAttribute = ASTNode;
- type Context = eslint.Rule.RuleContext
+ type Context = eslint.Rule.RuleContext;
type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set<typeof annotation>) => object;
diff --git a/lib/rules/jsx-props-no-spread-multi.js b/lib/rules/jsx-props-no-spread-multi.js
new file mode 100644
index v7.34.3..v7.35.0
--- a/lib/rules/jsx-props-no-spread-multi.js
+++ b/lib/rules/jsx-props-no-spread-multi.js
@@ -0,0 +1,53 @@
+/**
+ * @fileoverview Prevent JSX prop spreading the same expression multiple times
+ * @author Simon Schick
+ */
+
+'use strict';
+
+const docsUrl = require('../util/docsUrl');
+const report = require('../util/report');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+const messages = {
+ noMultiSpreading: 'Spreading the same expression multiple times is forbidden',
+};
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Disallow JSX prop spreading the same identifier multiple times',
+ category: 'Best Practices',
+ recommended: false,
+ url: docsUrl('jsx-props-no-spread-multi'),
+ },
+ messages,
+ },
+
+ create(context) {
+ return {
+ JSXOpeningElement(node) {
+ const spreads = node.attributes.filter(
+ (attr) => attr.type === 'JSXSpreadAttribute'
+ && attr.argument.type === 'Identifier'
+ );
+ if (spreads.length < 2) {
+ return;
+ }
+ // We detect duplicate expressions by their identifier
+ const identifierNames = new Set();
+ spreads.forEach((spread) => {
+ if (identifierNames.has(spread.argument.name)) {
+ report(context, messages.noMultiSpreading, 'noMultiSpreading', {
+ node: spread,
+ });
+ }
+ identifierNames.add(spread.argument.name);
+ });
+ },
+ };
+ },
+};
Command detailsnpm diff [email protected] [email protected] --diff-unified=2 See also the Reported by ybiquitous/[email protected] (Node.js 22.5.1 and npm 10.8.2) |
ybiquitous
changed the title
build(deps): bump eslint-plugin-react from 7.34.3 to 7.35.0
feat(deps): bump eslint-plugin-react from 7.34.3 to 7.35.0
Aug 1, 2024
ybiquitous
changed the title
feat(deps): bump eslint-plugin-react from 7.34.3 to 7.35.0
feat(react): add Aug 1, 2024
react/jsx-props-no-spread-multi
rule
github-actions
bot
deleted the
dependabot/npm_and_yarn/eslint-plugin-react-7.35.0
branch
August 1, 2024 15:15
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
dependencies
Pull requests that update a dependency file
javascript
Pull requests that update Javascript code
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Bumps eslint-plugin-react from 7.34.3 to 7.35.0.
Release notes
Sourced from eslint-plugin-react's releases.
... (truncated)
Changelog
Sourced from eslint-plugin-react's changelog.
Commits
c6fdccd
Update CHANGELOG and bump versiona4b0bbc
[Fix]require-default-props
: report when required props have default valuea08cb93
[Fix]sort-prop-types
: single line type ending without semicolon4b3209b
[meta] no point in supporting eslint 9.0 - 9.6 initiallyca8b11e
[Dev Deps] update@babel/core
,@babel/eslint-parser
597553d
[New]no-danger
: addcustomComponentNames
optionc58f04b
[New]jsx-closing-tag-location
: addline-aligned
option00b89fe
[New] version settings: Allow react defaultVersion to be configurable4d2fd86
[Refactor]variableUtil
: Avoid creating a single flat variable scope for ea...6a83d67
[New]jsx-handler-names
: support ignoring component namesDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase
.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebase
will rebase this PR@dependabot recreate
will recreate this PR, overwriting any edits that have been made to it@dependabot merge
will merge this PR after your CI passes on it@dependabot squash and merge
will squash and merge this PR after your CI passes on it@dependabot cancel merge
will cancel a previously requested merge and block automerging@dependabot reopen
will reopen this PR if it is closed@dependabot close
will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually@dependabot show <dependency name> ignore conditions
will show all of the ignore conditions of the specified dependency@dependabot ignore this major version
will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor version
will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependency
will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)