Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Added React 19 `precedence` and `blocking` attributes to known properties with version-gated tag checks, preventing false positives on `<style>`, `<link>`, and `<script>` elements.

## [5.2.3-beta.0] - 2026-04-14

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,18 @@ export const POPOVER_API_PROPS: string[] = [
"onBeforeToggle",
];

export const REACT_19_PROPS: string[] = [
...POPOVER_API_PROPS,
];

/**
Comment on lines +914 to +918
* Tag-specific attributes added in React 19
*/
export const REACT_19_ATTRIBUTE_TAGS_MAP: TagsMap = {
blocking: ["link", "script", "style"],
precedence: ["link", "style"],
};

/**
* Tests React version against a comparator
* @param context ESLint context
Expand Down Expand Up @@ -949,14 +961,26 @@ export function getDOMPropertyNames(context: RuleContext<string, unknown[]>): st

// Popover API props were added in React v19.0.0-rc.0
if (testReactVersion(context, ">=", "19.0.0-rc.0")) {
ALL_DOM_PROPERTY_NAMES.push(...POPOVER_API_PROPS);
ALL_DOM_PROPERTY_NAMES.push(...REACT_19_PROPS);
} else {
ALL_DOM_PROPERTY_NAMES.push(...POPOVER_API_PROPS.map((prop) => prop.toLowerCase()));
ALL_DOM_PROPERTY_NAMES.push(...REACT_19_PROPS.map((prop) => prop.toLowerCase()));
}

return ALL_DOM_PROPERTY_NAMES;
}

/**
* Gets the map of attributes to their allowed tags based on React version
* @param context ESLint rule context
* @returns Map of attributes to allowed tags
*/
export function getAttributeTagsMap(context: RuleContext<string, unknown[]>): TagsMap {
if (testReactVersion(context, ">=", "19.0.0-rc.0")) {
return { ...ATTRIBUTE_TAGS_MAP, ...REACT_19_ATTRIBUTE_TAGS_MAP };
}
return ATTRIBUTE_TAGS_MAP;
}

/**
* Check if a node's parent is a JSX tag that is written with lowercase letters,
* and is not a custom web component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,60 @@ ruleTester.run(RULE_NAME, rule, {
messageId: "invalidPropOnTag",
},
],
}, {
code: '<div precedence="medium" />',
errors: [
{
data: {
name: "precedence",
allowedTags: "link, style",
tagName: "div",
},
messageId: "invalidPropOnTag",
},
],
}, {
code: '<div blocking="render" />',
errors: [
{
data: {
name: "blocking",
allowedTags: "link, script, style",
tagName: "div",
},
messageId: "invalidPropOnTag",
},
],
}, {
code: '<style precedence="default">{`body { color: red; }`}</style>',
settings: {
"react-x": {
version: "18.3.1",
},
},
errors: [
{
data: {
name: "precedence",
},
messageId: "unknownProp",
},
],
}, {
code: '<script blocking="render" />',
settings: {
"react-x": {
version: "18.3.1",
},
},
errors: [
{
data: {
name: "blocking",
},
messageId: "unknownProp",
},
],
}, {
code: tsx`
<div className="App" data-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash-crash:c="customValue">
Expand Down Expand Up @@ -596,6 +650,9 @@ ruleTester.run(RULE_NAME, rule, {
},
{ code: '<line fill="pink" x1="0" y1="80" x2="100" y2="20"></line>' },
{ code: '<link as="audio">Audio content</link>' },
{ code: '<link rel="stylesheet" href="styles.css" precedence="medium" />' },
{ code: '<style href="style.css" precedence="default">{`body { color: red; }`}</style>' },
{ code: '<script async src="script.js" blocking="render" />' },
{
Comment on lines +653 to 656
code:
'<video controlsList="nodownload" controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} onResize={this.onResize}></video>',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { createRule } from "@/utils/create-rule";
import { type RuleContext, type RuleFeature, merge } from "@eslint-react/eslint";
import {
ATTRIBUTE_TAGS_MAP,
getAttributeTagsMap,
getStandardName,
getTagName,
getText,
Expand Down Expand Up @@ -163,8 +163,9 @@ export function create(context: RuleContext<MessageID, Options[]>) {
if (!isValidHTMLTagInJSX(node)) return;

// Check if attribute is allowed only on specific tags
const allowedTags = has(ATTRIBUTE_TAGS_MAP, name)
? ATTRIBUTE_TAGS_MAP[name]
const attributeTagsMap = getAttributeTagsMap(context);
const allowedTags = has(attributeTagsMap, name)
? attributeTagsMap[name]
: null;
Comment on lines 165 to 169

if (tagName != null && allowedTags != null) {
Expand Down
Loading