Valerii Smirnov 2024-01-07 13:23:32 +01:00 committed by GitHub
parent f46ed71d8a
commit c6eb519417
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1057 additions and 2 deletions

2
Cargo.lock generated
View file

@ -1710,7 +1710,6 @@ dependencies = [
"oxc_parser",
"oxc_semantic",
"oxc_span",
"rayon",
"ropey",
"serde",
"serde_json",
@ -1749,6 +1748,7 @@ dependencies = [
"regex",
"rust-lapper",
"rustc-hash",
"serde",
"serde_json",
]

View file

@ -32,7 +32,6 @@ env_logger = { workspace = true }
futures = { workspace = true }
ignore = { workspace = true, features = ["simd-accel"] }
miette = { workspace = true, features = ["fancy-no-backtrace"] }
rayon = { workspace = true }
ropey = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tower-lsp = { workspace = true, features = ["proposed"] }

View file

@ -34,6 +34,7 @@ oxc_resolver = { version = "1.1.0" }
rayon = { workspace = true }
lazy_static = { workspace = true } # used in oxc_macros
serde_json = { workspace = true }
serde = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
phf = { workspace = true, features = ["macros"] }

View file

@ -152,6 +152,7 @@ mod react {
pub mod no_render_return_value;
pub mod no_string_refs;
pub mod no_unescaped_entities;
pub mod no_unknown_property;
pub mod react_in_jsx_scope;
}
@ -469,6 +470,7 @@ oxc_macros::declare_all_lint_rules! {
react::no_string_refs,
react::no_unescaped_entities,
react::no_is_mounted,
react::no_unknown_property,
import::default,
import::named,
import::no_cycle,

View file

@ -0,0 +1,734 @@
use itertools::Itertools;
use once_cell::sync::Lazy;
use oxc_ast::{
ast::{JSXAttributeItem, JSXAttributeName},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use phf::{phf_map, phf_set, Map, Set};
use regex::Regex;
use serde::Deserialize;
use std::collections::hash_map::HashMap;
use std::collections::hash_set::HashSet;
use crate::{
context::LintContext,
rule::Rule,
utils::{get_element_type, get_prop_name},
AstNode,
};
#[derive(Debug, Error, Diagnostic)]
enum NoUnknownPropertyDiagnostic {
#[error("eslint-plugin-react(no-unknown-property): Invalid property found")]
#[diagnostic(severity(warning), help("Property '{1}' is only allowed on: {2}"))]
InvalidPropOnTag(#[label] Span, String, String),
#[error("eslint-plugin-react(no-unknown-property): React does not recognize data-* props with uppercase characters on a DOM element")]
#[diagnostic(severity(warning), help("Use '{1}' instead"))]
DataLowercaseRequired(#[label] Span, String),
#[error("eslint-plugin-react(no-unknown-property): Unknown property found")]
#[diagnostic(severity(warning), help("Use '{1}' instead"))]
UnknownPropWithStandardName(#[label] Span, String),
#[error("eslint-plugin-react(no-unknown-property): Unknown property found")]
#[diagnostic(severity(warning), help("Remove unknown property"))]
UnknownProp(#[label] Span),
}
#[derive(Debug, Default, Clone)]
pub struct NoUnknownProperty(Box<NoUnknownPropertyConfig>);
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoUnknownPropertyConfig {
#[serde(default)]
ignore: HashSet<String>,
#[serde(default)]
require_data_lowercase: bool,
}
declare_oxc_lint!(
/// ### What it does
/// Disallow usage of unknown DOM property.
///
/// ### Why is this bad?
/// You can use unknown property name that has no effect.
///
/// ### Example
/// ```jsx
/// // Unknown properties
/// const Hello = <div class="hello">Hello World</div>;
/// const Alphabet = <div abc="something">Alphabet</div>;
///
/// // Invalid aria-* attribute
/// const IconButton = <div aria-foo="bar" />;
/// ```
NoUnknownProperty,
correctness
);
const ATTRIBUTE_TAGS_MAP: Map<&'static str, Set<&'static str>> = phf_map! {
"abbr" => phf_set! {"th", "td"},
"charset" => phf_set! {"meta"},
"checked" => phf_set! {"input"},
// image is required for SVG support, all other tags are HTML.
"crossOrigin" => phf_set! {"script", "img", "video", "audio", "link", "image"},
"displaystyle" => phf_set! {"math"},
// https://html.spec.whatwg.org/multipage/links.html#downloading-resources
"download" => phf_set! {"a", "area"},
"fill" => phf_set! {
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
// Fill color
"altGlyph",
"circle",
"ellipse",
"g",
"line",
"marker",
"mask",
"path",
"polygon",
"polyline",
"rect",
"svg",
"symbol",
"text",
"textPath",
"tref",
"tspan",
"use",
// Animation final state
"animate",
"animateColor",
"animateMotion",
"animateTransform",
"set",
},
"focusable" => phf_set! {"svg"},
"imageSizes" => phf_set! {"link"},
"imageSrcSet" => phf_set! {"link"},
"property" => phf_set! {"meta"},
"viewBox" => phf_set! {"marker", "pattern", "svg", "symbol", "view"},
"as" => phf_set! {"link"},
"align" => phf_set! {
"applet", "caption", "col", "colgroup", "hr", "iframe", "img", "table", "tbody", "td",
"tfoot", "th", "thead", "tr",
},
// deprecated, but known
"valign" => phf_set! {"tr", "td", "th", "thead", "tbody", "tfoot", "colgroup", "col"}, // deprecated, but known
"noModule" => phf_set! {"script"},
// Media events allowed only on audio and video tags, see https://github.com/facebook/react/blob/256aefbea1449869620fb26f6ec695536ab453f5/CHANGELOG.md#notable-enhancements
"onAbort" => phf_set! {"audio", "video"},
"onCancel" => phf_set! {"dialog"},
"onCanPlay" => phf_set! {"audio", "video"},
"onCanPlayThrough" => phf_set! {"audio", "video"},
"onClose" => phf_set! {"dialog"},
"onDurationChange" => phf_set! {"audio", "video"},
"onEmptied" => phf_set! {"audio", "video"},
"onEncrypted" => phf_set! {"audio", "video"},
"onEnded" => phf_set! {"audio", "video"},
"onError" => phf_set! {"audio", "video", "img", "link", "source", "script", "picture", "iframe"},
"onLoad" => phf_set! {"script", "img", "link", "picture", "iframe", "object", "source"},
"onLoadedData" => phf_set! {"audio", "video"},
"onLoadedMetadata" => phf_set! {"audio", "video"},
"onLoadStart" => phf_set! {"audio", "video"},
"onPause" => phf_set! {"audio", "video"},
"onPlay" => phf_set! {"audio", "video"},
"onPlaying" => phf_set! {"audio", "video"},
"onProgress" => phf_set! {"audio", "video"},
"onRateChange" => phf_set! {"audio", "video"},
"onResize" => phf_set! {"audio", "video"},
"onSeeked" => phf_set! {"audio", "video"},
"onSeeking" => phf_set! {"audio", "video"},
"onStalled" => phf_set! {"audio", "video"},
"onSuspend" => phf_set! {"audio", "video"},
"onTimeUpdate" => phf_set! {"audio", "video"},
"onVolumeChange" => phf_set! {"audio", "video"},
"onWaiting" => phf_set! {"audio", "video"},
"autoPictureInPicture" => phf_set! {"video"},
"controls" => phf_set! {"audio", "video"},
"controlsList" => phf_set! {"audio", "video"},
"disablePictureInPicture" => phf_set! {"video"},
"disableRemotePlayback" => phf_set! {"audio", "video"},
"loop" => phf_set! {"audio", "video"},
"muted" => phf_set! {"audio", "video"},
"playsInline" => phf_set! {"video"},
"allowFullScreen" => phf_set! {"iframe", "video"},
"webkitAllowFullScreen" => phf_set! {"iframe", "video"},
"mozAllowFullScreen" => phf_set! {"iframe", "video"},
"poster" => phf_set! {"video"},
"preload" => phf_set! {"audio", "video"},
"scrolling" => phf_set! {"iframe"},
"returnValue" => phf_set! {"dialog"},
"webkitDirectory" => phf_set! {"input"},
};
const DOM_PROPERTIES_NAMES: Set<&'static str> = phf_set! {
// Global attributes - can be used on any HTML/DOM element
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
"dir", "draggable", "hidden", "id", "lang", "nonce", "part", "slot", "style", "title", "translate", "inert",
// Element specific attributes
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
// To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
"accept", "action", "allow", "alt", "as", "async", "buffered", "capture", "challenge", "cite", "code", "cols",
"content", "coords", "csp", "data", "decoding", "default", "defer", "disabled", "form",
"headers", "height", "high", "href", "icon", "importance", "integrity", "kind", "label",
"language", "loading", "list", "loop", "low", "manifest", "max", "media", "method", "min", "multiple", "muted",
"name", "open", "optimum", "pattern", "ping", "placeholder", "poster", "preload", "profile",
"rel", "required", "reversed", "role", "rows", "sandbox", "scope", "seamless", "selected", "shape", "size", "sizes",
"span", "src", "start", "step", "summary", "target", "type", "value", "width", "wmode", "wrap",
// SVG attributes
// See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
"accumulate", "additive", "alphabetic", "amplitude", "ascent", "azimuth", "bbox", "begin",
"bias", "by", "clip", "color", "cursor", "cx", "cy", "d", "decelerate", "descent", "direction",
"display", "divisor", "dur", "dx", "dy", "elevation", "end", "exponent", "fill", "filter",
"format", "from", "fr", "fx", "fy", "g1", "g2", "hanging", "hreflang", "ideographic",
"in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kerning", "local", "mask", "mode",
"offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "path",
"points", "r", "radius", "restart", "result", "rotate", "rx", "ry", "scale",
"seed", "slope", "spacing", "speed", "stemh", "stemv", "string", "stroke", "to", "transform",
"u1", "u2", "unicode", "values", "version", "visibility", "widths", "x", "x1", "x2", "xmlns",
"y", "y1", "y2", "z",
// OpenGraph meta tag attributes
"property",
// React specific attributes
"ref", "key", "children",
// Non-standard
"results", "security",
// Video specific
"controls",
// TWO WORD DOM_PROPERTIES_NAMES
// Global attributes - can be used on any HTML/DOM element
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
"accessKey", "autoCapitalize", "autoFocus", "contentEditable", "enterKeyHint", "exportParts",
"inputMode", "itemID", "itemRef", "itemProp", "itemScope", "itemType", "spellCheck", "tabIndex",
// Element specific attributes
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
// To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
"acceptCharset", "autoComplete", "autoPlay", "border", "cellPadding", "cellSpacing", "classID", "codeBase",
"colSpan", "contextMenu", "dateTime", "encType", "formAction", "formEncType", "formMethod", "formNoValidate", "formTarget",
"frameBorder", "hrefLang", "httpEquiv", "imageSizes", "imageSrcSet", "isMap", "keyParams", "keyType", "marginHeight", "marginWidth",
"maxLength", "mediaGroup", "minLength", "noValidate", "onAnimationEnd", "onAnimationIteration", "onAnimationStart",
"onBlur", "onChange", "onClick", "onContextMenu", "onCopy", "onCompositionEnd", "onCompositionStart",
"onCompositionUpdate", "onCut", "onDoubleClick", "onDrag", "onDragEnd", "onDragEnter", "onDragExit", "onDragLeave",
"onError", "onFocus", "onInput", "onKeyDown", "onKeyPress", "onKeyUp", "onLoad", "onWheel", "onDragOver",
"onDragStart", "onDrop", "onMouseDown", "onMouseEnter", "onMouseLeave", "onMouseMove", "onMouseOut", "onMouseOver",
"onMouseUp", "onPaste", "onScroll", "onSelect", "onSubmit", "onToggle", "onTransitionEnd", "radioGroup", "readOnly", "referrerPolicy",
"rowSpan", "srcDoc", "srcLang", "srcSet", "useMap",
// SVG attributes
// See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
"crossOrigin", "accentHeight", "alignmentBaseline", "arabicForm", "attributeName",
"attributeType", "baseFrequency", "baselineShift", "baseProfile", "calcMode", "capHeight",
"clipPathUnits", "clipPath", "clipRule", "colorInterpolation", "colorInterpolationFilters",
"colorProfile", "colorRendering", "contentScriptType", "contentStyleType", "diffuseConstant",
"dominantBaseline", "edgeMode", "enableBackground", "fillOpacity", "fillRule", "filterRes",
"filterUnits", "floodColor", "floodOpacity", "fontFamily", "fontSize", "fontSizeAdjust",
"fontStretch", "fontStyle", "fontVariant", "fontWeight", "glyphName",
"glyphOrientationHorizontal", "glyphOrientationVertical", "glyphRef", "gradientTransform",
"gradientUnits", "horizAdvX", "horizOriginX", "imageRendering", "kernelMatrix",
"kernelUnitLength", "keyPoints", "keySplines", "keyTimes", "lengthAdjust", "letterSpacing",
"lightingColor", "limitingConeAngle", "markerEnd", "markerMid", "markerStart", "markerHeight",
"markerUnits", "markerWidth", "maskContentUnits", "maskUnits", "mathematical", "numOctaves",
"overlinePosition", "overlineThickness", "panose1", "paintOrder", "pathLength",
"patternContentUnits", "patternTransform", "patternUnits", "pointerEvents", "pointsAtX",
"pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits",
"refX", "refY", "rendering-intent", "repeatCount", "repeatDur",
"requiredExtensions", "requiredFeatures", "shapeRendering", "specularConstant",
"specularExponent", "spreadMethod", "startOffset", "stdDeviation", "stitchTiles", "stopColor",
"stopOpacity", "strikethroughPosition", "strikethroughThickness", "strokeDasharray",
"strokeDashoffset", "strokeLinecap", "strokeLinejoin", "strokeMiterlimit", "strokeOpacity",
"strokeWidth", "surfaceScale", "systemLanguage", "tableValues", "targetX", "targetY",
"textAnchor", "textDecoration", "textRendering", "textLength", "transformOrigin",
"underlinePosition", "underlineThickness", "unicodeBidi", "unicodeRange", "unitsPerEm",
"vAlphabetic", "vHanging", "vIdeographic", "vMathematical", "vectorEffect", "vertAdvY",
"vertOriginX", "vertOriginY", "viewBox", "viewTarget", "wordSpacing", "writingMode", "xHeight",
"xChannelSelector", "xlinkActuate", "xlinkArcrole", "xlinkHref", "xlinkRole", "xlinkShow",
"xlinkTitle", "xlinkType", "xmlBase", "xmlLang", "xmlnsXlink", "xmlSpace", "yChannelSelector",
"zoomAndPan",
// Safari/Apple specific, no listing available
"autoCorrect", // https://stackoverflow.com/questions/47985384/html-autocorrect-for-text-input-is-not-working
"autoSave", // https://stackoverflow.com/questions/25456396/what-is-autosave-attribute-supposed-to-do-how-do-i-use-it
// React specific attributes https://reactjs.org/docs/dom-elements.html#differences-in-attributes
"className", "dangerouslySetInnerHTML", "defaultValue", "defaultChecked", "htmlFor",
// Events" capture events
"onBeforeInput",
"onInvalid", "onReset", "onTouchCancel", "onTouchEnd", "onTouchMove", "onTouchStart", "suppressContentEditableWarning", "suppressHydrationWarning",
"onAbort", "onCanPlay", "onCanPlayThrough", "onDurationChange", "onEmptied", "onEncrypted", "onEnded",
"onLoadedData", "onLoadedMetadata", "onLoadStart", "onPause", "onPlay", "onPlaying", "onProgress", "onRateChange", "onResize",
"onSeeked", "onSeeking", "onStalled", "onSuspend", "onTimeUpdate", "onVolumeChange", "onWaiting",
"onCopyCapture", "onCutCapture", "onPasteCapture", "onCompositionEndCapture", "onCompositionStartCapture", "onCompositionUpdateCapture",
"onFocusCapture", "onBlurCapture", "onChangeCapture", "onBeforeInputCapture", "onInputCapture", "onResetCapture", "onSubmitCapture",
"onInvalidCapture", "onLoadCapture", "onErrorCapture", "onKeyDownCapture", "onKeyPressCapture", "onKeyUpCapture",
"onAbortCapture", "onCanPlayCapture", "onCanPlayThroughCapture", "onDurationChangeCapture", "onEmptiedCapture", "onEncryptedCapture",
"onEndedCapture", "onLoadedDataCapture", "onLoadedMetadataCapture", "onLoadStartCapture", "onPauseCapture", "onPlayCapture",
"onPlayingCapture", "onProgressCapture", "onRateChangeCapture", "onSeekedCapture", "onSeekingCapture", "onStalledCapture", "onSuspendCapture",
"onTimeUpdateCapture", "onVolumeChangeCapture", "onWaitingCapture", "onSelectCapture", "onTouchCancelCapture", "onTouchEndCapture",
"onTouchMoveCapture", "onTouchStartCapture", "onScrollCapture", "onWheelCapture", "onAnimationEndCapture",
"onAnimationStartCapture", "onTransitionEndCapture",
"onAuxClick", "onAuxClickCapture", "onClickCapture", "onContextMenuCapture", "onDoubleClickCapture",
"onDragCapture", "onDragEndCapture", "onDragEnterCapture", "onDragExitCapture", "onDragLeaveCapture",
"onDragOverCapture", "onDragStartCapture", "onDropCapture", "onMouseDownCapture",
"onMouseMoveCapture", "onMouseOutCapture", "onMouseOverCapture", "onMouseUpCapture",
// Video specific
"autoPictureInPicture", "controlsList", "disablePictureInPicture", "disableRemotePlayback",
// React on props
"onGotPointerCaptureCapture",
"onLostPointerCapture",
"onLostPointerCaptureCapture",
"onPointerCancel",
"onPointerCancelCapture",
"onPointerDown",
"onPointerDownCapture",
"onPointerEnter",
"onPointerEnterCapture",
"onPointerLeave",
"onPointerLeaveCapture",
"onPointerMove",
"onPointerMoveCapture",
"onPointerOut",
"onPointerOutCapture",
"onPointerOver",
"onPointerOverCapture",
"onPointerUp",
"onPointerUpCapture",
};
const ARIA_PROPERTIES: Set<&'static str> = phf_set! {
// See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
// Global attributes
"aria-atomic", "aria-braillelabel", "aria-brailleroledescription", "aria-busy", "aria-controls", "aria-current",
"aria-describedby", "aria-description", "aria-details",
"aria-disabled", "aria-dropeffect", "aria-errormessage", "aria-flowto", "aria-grabbed", "aria-haspopup",
"aria-hidden", "aria-invalid", "aria-keyshortcuts", "aria-label", "aria-labelledby", "aria-live",
"aria-owns", "aria-relevant", "aria-roledescription",
// Widget attributes
"aria-autocomplete", "aria-checked", "aria-expanded", "aria-level", "aria-modal", "aria-multiline", "aria-multiselectable",
"aria-orientation", "aria-placeholder", "aria-pressed", "aria-readonly", "aria-required", "aria-selected",
"aria-sort", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext",
// Relationship attributes
"aria-activedescendant", "aria-colcount", "aria-colindex", "aria-colindextext", "aria-colspan",
"aria-posinset", "aria-rowcount", "aria-rowindex", "aria-rowindextext", "aria-rowspan", "aria-setsize",
};
const DOM_ATTRIBUTES_TO_CAMEL: Map<&'static str, &'static str> = phf_map! {
"accept-charset" => "acceptCharset",
"class" => "className",
"http-equiv" => "httpEquiv",
"crossorigin" => "crossOrigin",
"for" => "htmlFor",
"nomodule" => "noModule",
// svg
"accent-height" => "accentHeight",
"alignment-baseline" => "alignmentBaseline",
"arabic-form" => "arabicForm",
"baseline-shift" => "baselineShift",
"cap-height" => "capHeight",
"clip-path" => "clipPath",
"clip-rule" => "clipRule",
"color-interpolation" => "colorInterpolation",
"color-interpolation-filters" => "colorInterpolationFilters",
"color-profile" => "colorProfile",
"color-rendering" => "colorRendering",
"dominant-baseline" => "dominantBaseline",
"enable-background" => "enableBackground",
"fill-opacity" => "fillOpacity",
"fill-rule" => "fillRule",
"flood-color" => "floodColor",
"flood-opacity" => "floodOpacity",
"font-family" => "fontFamily",
"font-size" => "fontSize",
"font-size-adjust" => "fontSizeAdjust",
"font-stretch" => "fontStretch",
"font-style" => "fontStyle",
"font-variant" => "fontVariant",
"font-weight" => "fontWeight",
"glyph-name" => "glyphName",
"glyph-orientation-horizontal" => "glyphOrientationHorizontal",
"glyph-orientation-vertical" => "glyphOrientationVertical",
"horiz-adv-x" => "horizAdvX",
"horiz-origin-x" => "horizOriginX",
"image-rendering" => "imageRendering",
"letter-spacing" => "letterSpacing",
"lighting-color" => "lightingColor",
"marker-end" => "markerEnd",
"marker-mid" => "markerMid",
"marker-start" => "markerStart",
"overline-position" => "overlinePosition",
"overline-thickness" => "overlineThickness",
"paint-order" => "paintOrder",
"panose-1" => "panose1",
"pointer-events" => "pointerEvents",
"rendering-intent" => "renderingIntent",
"shape-rendering" => "shapeRendering",
"stop-color" => "stopColor",
"stop-opacity" => "stopOpacity",
"strikethrough-position" => "strikethroughPosition",
"strikethrough-thickness" => "strikethroughThickness",
"stroke-dasharray" => "strokeDasharray",
"stroke-dashoffset" => "strokeDashoffset",
"stroke-linecap" => "strokeLinecap",
"stroke-linejoin" => "strokeLinejoin",
"stroke-miterlimit" => "strokeMiterlimit",
"stroke-opacity" => "strokeOpacity",
"stroke-width" => "strokeWidth",
"text-anchor" => "textAnchor",
"text-decoration" => "textDecoration",
"text-rendering" => "textRendering",
"underline-position" => "underlinePosition",
"underline-thickness" => "underlineThickness",
"unicode-bidi" => "unicodeBidi",
"unicode-range" => "unicodeRange",
"units-per-em" => "unitsPerEm",
"v-alphabetic" => "vAlphabetic",
"v-hanging" => "vHanging",
"v-ideographic" => "vIdeographic",
"v-mathematical" => "vMathematical",
"vector-effect" => "vectorEffect",
"vert-adv-y" => "vertAdvY",
"vert-origin-x" => "vertOriginX",
"vert-origin-y" => "vertOriginY",
"word-spacing" => "wordSpacing",
"writing-mode" => "writingMode",
"x-height" => "xHeight",
"xlink:actuate" => "xlinkActuate",
"xlink:arcrole" => "xlinkArcrole",
"xlink:href" => "xlinkHref",
"xlink:role" => "xlinkRole",
"xlink:show" => "xlinkShow",
"xlink:title" => "xlinkTitle",
"xlink:type" => "xlinkType",
"xml:base" => "xmlBase",
"xml:lang" => "xmlLang",
"xml:space" => "xmlSpace",
};
const DOM_PROPERTIES_IGNORE_CASE: [&str; 5] = [
"charset",
"allowFullScreen",
"webkitAllowFullScreen",
"mozAllowFullScreen",
"webkitDirectory",
];
static DOM_PROPERTIES_LOWER_MAP: Lazy<HashMap<String, &'static str>> = Lazy::new(|| {
DOM_PROPERTIES_NAMES.iter().map(|it| (it.to_lowercase(), *it)).collect::<HashMap<_, _>>()
});
///
/// Checks if an attribute name is a valid `data-*` attribute:
/// if the name starts with "data-" and has alphanumeric words (browsers require lowercase, but React and TS lowercase them),
/// not start with any casing of "xml", and separated by hyphens (-) (which is also called "kebab case" or "dash case"),
/// then the attribute is a valid data attribute.
///
fn is_valid_data_attr(name: &str) -> bool {
static DATA_ATTR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^data(-?[^:]*)$").unwrap());
!name.to_lowercase().starts_with("data-xml") && DATA_ATTR_REGEX.is_match(name)
}
fn normalize_attribute_case(name: &str) -> &str {
DOM_PROPERTIES_IGNORE_CASE
.iter()
.find(|camel_name| camel_name.to_lowercase() == name.to_lowercase())
.unwrap_or(&name)
}
fn has_uppercase(name: &str) -> bool {
name.contains(char::is_uppercase)
}
impl Rule for NoUnknownProperty {
fn from_configuration(value: serde_json::Value) -> Self {
value
.as_array()
.and_then(|arr| arr.first())
.and_then(|value| serde_json::from_value(value.clone()).ok())
.map_or_else(Self::default, |value| Self(Box::new(value)))
}
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
static HTML_TAG_CONVENTION: Lazy<Regex> = Lazy::new(|| Regex::new("^[a-z][^-]*$").unwrap());
let AstKind::JSXOpeningElement(el) = &node.kind() else {
return;
};
let Some(el_type) = get_element_type(ctx, el) else {
return;
};
// fbt/fbs nodes are bonkers, let's not go there
if !el_type.starts_with(char::is_lowercase) || el_type == "fbt" || el_type == "fbs" {
return;
}
let is_valid_html_tag = HTML_TAG_CONVENTION.is_match(el_type.as_str())
&& el.attributes.iter().all(|attr| {
let JSXAttributeItem::Attribute(jsx_attr) = attr else {
return true;
};
let JSXAttributeName::Identifier(ident) = &jsx_attr.name else {
return true;
};
ident.name.as_str() != "is"
});
el.attributes
.iter()
.filter_map(|attr| match &attr {
JSXAttributeItem::Attribute(regular) => Some(&**regular),
JSXAttributeItem::SpreadAttribute(_) => None,
})
.for_each(|attr| {
let span = attr.name.span();
let actual_name = get_prop_name(&attr.name);
if self.0.ignore.contains(&(actual_name)) {
return;
};
if is_valid_data_attr(actual_name.as_str()) {
if self.0.require_data_lowercase && has_uppercase(actual_name.as_str()) {
ctx.diagnostic(NoUnknownPropertyDiagnostic::DataLowercaseRequired(
span,
actual_name.to_lowercase(),
));
}
return;
};
if ARIA_PROPERTIES.contains(actual_name.as_str()) || !is_valid_html_tag {
return;
};
let name = normalize_attribute_case(actual_name.as_str());
if let Some(tags) = ATTRIBUTE_TAGS_MAP.get(name) {
if !tags.contains(el_type.as_str()) {
ctx.diagnostic(NoUnknownPropertyDiagnostic::InvalidPropOnTag(
span,
actual_name.to_string(),
tags.iter().join(", "),
));
}
return;
}
if DOM_PROPERTIES_NAMES.contains(name) {
return;
}
DOM_PROPERTIES_LOWER_MAP
.get(&name.to_lowercase())
.or_else(|| DOM_ATTRIBUTES_TO_CAMEL.get(name))
.map_or_else(
|| {
ctx.diagnostic(NoUnknownPropertyDiagnostic::UnknownProp(span));
},
|prop| {
ctx.diagnostic(
NoUnknownPropertyDiagnostic::UnknownPropWithStandardName(
span,
(*prop).to_string(),
),
);
},
);
});
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
(r#"<App class="bar" />;"#, None),
(r#"<App for="bar" />;"#, None),
(r#"<App someProp="bar" />;"#, None),
(r#"<Foo.bar for="bar" />;"#, None),
(r#"<App accept-charset="bar" />;"#, None),
(r#"<App http-equiv="bar" />;"#, None),
(r#"<App xlink:href="bar" />;"#, None),
(r#"<App clip-path="bar" />;"#, None),
(r#"<div className="bar"></div>;"#, None),
(r"<div onMouseDown={this._onMouseDown}></div>;", None),
(r#"<a href="someLink" download="foo">Read more</a>"#, None),
(r#"<area download="foo" />"#, None),
(r#"<img src="cat_keyboard.jpeg" alt="A cat sleeping on a keyboard" align="top" />"#, None),
(r#"<input type="password" required />"#, None),
(r#"<input ref={this.input} type="radio" />"#, None),
(r#"<input type="file" webkitdirectory="" />"#, None),
(r#"<input type="file" webkitDirectory="" />"#, None),
(r#"<div inert children="anything" />"#, None),
(r#"<iframe scrolling="?" onLoad={a} onError={b} align="top" />"#, None),
(r#"<input key="bar" type="radio" />"#, None),
(r"<button disabled>You cannot click me</button>;", None),
(
r#"<svg key="lock" viewBox="box" fill={10} d="d" stroke={1} strokeWidth={2} strokeLinecap={3} strokeLinejoin={4} transform="something" clipRule="else" x1={5} x2="6" y1="7" y2="8"></svg>"#,
None,
),
(r#"<g fill="\#7B82A0" fillRule="evenodd"></g>"#, None),
(r#"<mask fill="\#7B82A0"></mask>"#, None),
(r#"<symbol fill="\#7B82A0"></symbol>"#, None),
(r#"<meta property="og:type" content="website" />"#, None),
(
r#"<input type="checkbox" checked={checked} disabled={disabled} id={id} onChange={onChange} />"#,
None,
),
(r"<video playsInline />", None),
(r"<img onError={foo} onLoad={bar} />", None),
(r"<picture inert={false} onError={foo} onLoad={bar} />", None),
(r"<iframe onError={foo} onLoad={bar} />", None),
(r"<script onLoad={bar} onError={foo} />", None),
(r"<source onLoad={bar} onError={foo} />", None),
(r"<link onLoad={bar} onError={foo} />", None),
(
r#"<link rel="preload" as="image" href="someHref" imageSrcSet="someImageSrcSet" imageSizes="someImageSizes" />"#,
None,
),
(r"<object onLoad={bar} />", None),
(r"<video allowFullScreen webkitAllowFullScreen mozAllowFullScreen />", None),
(r"<iframe allowFullScreen webkitAllowFullScreen mozAllowFullScreen />", None),
(r#"<table border="1" />"#, None),
(r#"<th abbr="abbr" />"#, None),
(r#"<td abbr="abbr" />"#, None),
(r"<div onPointerDown={this.onDown} onPointerUp={this.onUp} />", None),
(r#"<input type="checkbox" defaultChecked={this.state.checkbox} />"#, None),
(
r"<div onTouchStart={this.startAnimation} onTouchEnd={this.stopAnimation} onTouchCancel={this.cancel} onTouchMove={this.move} onMouseMoveCapture={this.capture} onTouchCancelCapture={this.log} />",
None,
),
(r#"<meta charset="utf-8" />;"#, None),
(r#"<meta charSet="utf-8" />;"#, None),
(r#"<div class="foo" is="my-elem"></div>;"#, None),
(r#"<div {...this.props} class="foo" is="my-elem"></div>;"#, None),
(r#"<atom-panel class="foo"></atom-panel>;"#, None),
(r#"<div data-foo="bar"></div>;"#, None),
(r#"<div data-foo-bar="baz"></div>;"#, None),
(r#"<div data-parent="parent"></div>;"#, None),
(r#"<div data-index-number="1234"></div>;"#, None),
(r#"<div data-e2e-id="5678"></div>;"#, None),
(r#"<div data-testID="bar" data-under_sCoRe="bar" />;"#, None),
(
r#"<div data-testID="bar" data-under_sCoRe="bar" />;"#,
Some(serde_json::json!([{ "requireDataLowercase": false }])),
),
(r#"<div class="bar"></div>;"#, Some(serde_json::json!([{ "ignore": ["class"] }]))),
(r#"<div someProp="bar"></div>;"#, Some(serde_json::json!([{ "ignore": ["someProp"] }]))),
(r"<div css={{flex: 1}}></div>;", Some(serde_json::json!([{ "ignore": ["css"] }]))),
(r#"<button aria-haspopup="true">Click me to open pop up</button>;"#, None),
(r#"<button aria-label="Close" onClick={someThing.close} />;"#, None),
(r"<script crossOrigin noModule />", None),
(r"<audio crossOrigin />", None),
(r"<svg focusable><image crossOrigin /></svg>", None),
(r"<details onToggle={this.onToggle}>Some details</details>", None),
(
r#"<path fill="pink" d="M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z"></path>"#,
None,
),
(r#"<line fill="pink" x1="0" y1="80" x2="100" y2="20"></line>"#, None),
(r#"<link as="audio">Audio content</link>"#, None),
(
r#"<video controlsList="nodownload" controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} onResize={this.onResize}></video>"#,
None,
),
(
r#"<audio controlsList="nodownload" controls={this.controls} crossOrigin="anonymous" disableRemotePlayback loop muted preload="none" src="something" onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onError={this.error} onResize={this.onResize}></audio>"#,
None,
),
(
r#"<marker id={markerId} viewBox="0 0 2 2" refX="1" refY="1" markerWidth="1" markerHeight="1" orient="auto" />"#,
None,
),
(r#"<pattern id="pattern" viewBox="0,0,10,10" width="10%" height="10%" />"#, None),
(r#"<symbol id="myDot" width="10" height="10" viewBox="0 0 2 2" />"#, None),
(r#"<view id="one" viewBox="0 0 100 100" />"#, None),
(r#"<hr align="top" />"#, None),
(r#"<applet align="top" />"#, None),
(r#"<marker fill="\#000" />"#, None),
(
r#"<dialog onClose={handler} open id="dialog" returnValue="something" onCancel={handler2} />"#,
None,
),
(
r#"
<table align="top">
<caption align="top">Table Caption</caption>
<colgroup valign="top" align="top">
<col valign="top" align="top"/>
</colgroup>
<thead valign="top" align="top">
<tr valign="top" align="top">
<th valign="top" align="top">Header</th>
<td valign="top" align="top">Cell</td>
</tr>
</thead>
<tbody valign="top" align="top" />
<tfoot valign="top" align="top" />
</table>
"#,
None,
),
(r#"<fbt desc="foo" doNotExtract />;"#, None),
(r#"<fbs desc="foo" doNotExtract />;"#, None),
(r#"<math displaystyle="true" />;"#, None),
(
r#"
<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="customValue">
Hello, world!
</div>
"#,
None,
),
];
let fail = vec![
(r#"<div allowTransparency="true" />"#, None),
(r#"<div hasOwnProperty="should not be allowed property"></div>;"#, None),
(r#"<div abc="should not be allowed property"></div>;"#, None),
(r#"<div aria-fake="should not be allowed property"></div>;"#, None),
(r#"<div someProp="bar"></div>;"#, None),
(r#"<div class="bar"></div>;"#, None),
(r#"<div for="bar"></div>;"#, None),
(r#"<div accept-charset="bar"></div>;"#, None),
(r#"<div http-equiv="bar"></div>;"#, None),
(r#"<div accesskey="bar"></div>;"#, None),
(r#"<div onclick="bar"></div>;"#, None),
(r#"<div onmousedown="bar"></div>;"#, None),
(r#"<div onMousedown="bar"></div>;"#, None),
(r#"<use xlink:href="bar" />;"#, None),
(r#"<rect clip-path="bar" />;"#, None),
(r"<script crossorigin nomodule />", None),
(r"<div crossorigin />", None),
(r"<div crossOrigin />", None),
(r#"<div as="audio" />"#, None),
(
r"<div onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onResize={this.resize} onError={this.error} />",
None,
),
(r"<div onLoad={this.load} />", None),
(r#"<div fill="pink" />"#, None),
(
r"<div controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} allowFullScreen></div>",
None,
),
(r#"<div download="foo" />"#, None),
(r#"<div imageSrcSet="someImageSrcSet" />"#, None),
(r#"<div imageSizes="someImageSizes" />"#, None),
(r#"<div data-xml-anything="invalid" />"#, None),
(
r#"<div data-testID="bar" data-under_sCoRe="bar" />;"#,
Some(serde_json::json!([{ "requireDataLowercase": true }])),
),
(r#"<div abbr="abbr" />"#, None),
(r#"<div webkitDirectory="" />"#, None),
(r#"<div webkitdirectory="" />"#, None),
(
r#"
<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">
Hello, world!
</div>
"#,
None,
),
];
Tester::new(NoUnknownProperty::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,310 @@
---
source: crates/oxc_linter/src/tester.rs
assertion_line: 144
expression: no_unknown_property
---
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div allowTransparency="true" />
· ─────────────────
╰────
help: Remove unknown property
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div hasOwnProperty="should not be allowed property"></div>;
· ──────────────
╰────
help: Remove unknown property
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div abc="should not be allowed property"></div>;
· ───
╰────
help: Remove unknown property
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div aria-fake="should not be allowed property"></div>;
· ─────────
╰────
help: Remove unknown property
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div someProp="bar"></div>;
· ────────
╰────
help: Remove unknown property
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div class="bar"></div>;
· ─────
╰────
help: Use 'className' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div for="bar"></div>;
· ───
╰────
help: Use 'htmlFor' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div accept-charset="bar"></div>;
· ──────────────
╰────
help: Use 'acceptCharset' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div http-equiv="bar"></div>;
· ──────────
╰────
help: Use 'httpEquiv' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div accesskey="bar"></div>;
· ─────────
╰────
help: Use 'accessKey' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onclick="bar"></div>;
· ───────
╰────
help: Use 'onClick' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onmousedown="bar"></div>;
· ───────────
╰────
help: Use 'onMouseDown' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onMousedown="bar"></div>;
· ───────────
╰────
help: Use 'onMouseDown' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <use xlink:href="bar" />;
· ──────────
╰────
help: Use 'xlinkHref' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <rect clip-path="bar" />;
· ─────────
╰────
help: Use 'clipPath' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <script crossorigin nomodule />
· ───────────
╰────
help: Use 'crossOrigin' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <script crossorigin nomodule />
· ────────
╰────
help: Use 'noModule' instead
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div crossorigin />
· ───────────
╰────
help: Use 'crossOrigin' instead
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div crossOrigin />
· ───────────
╰────
help: Property 'crossOrigin' is only allowed on: script, audio, link, image, video, img
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div as="audio" />
· ──
╰────
help: Property 'as' is only allowed on: link
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onResize={this.resize} onError={this.error} />
· ───────
╰────
help: Property 'onAbort' is only allowed on: video, audio
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onResize={this.resize} onError={this.error} />
· ────────────────
╰────
help: Property 'onDurationChange' is only allowed on: video, audio
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onResize={this.resize} onError={this.error} />
· ─────────
╰────
help: Property 'onEmptied' is only allowed on: video, audio
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onResize={this.resize} onError={this.error} />
· ───────
╰────
help: Property 'onEnded' is only allowed on: video, audio
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onResize={this.resize} onError={this.error} />
· ────────
╰────
help: Property 'onResize' is only allowed on: video, audio
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onResize={this.resize} onError={this.error} />
· ───────
╰────
help: Property 'onError' is only allowed on: source, link, script, audio, video, iframe, img, picture
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div onLoad={this.load} />
· ──────
╰────
help: Property 'onLoad' is only allowed on: source, iframe, picture, script, object, img, link
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div fill="pink" />
· ────
╰────
help: Property 'fill' is only allowed on: rect, ellipse, textPath, animateMotion, tref, animate, marker, svg, animateColor, use, symbol, polygon, text, circle, polyline, path, altGlyph, tspan,
set, g, line, animateTransform, mask
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} allowFullScreen></div>
· ────────
╰────
help: Property 'controls' is only allowed on: video, audio
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} allowFullScreen></div>
· ────
╰────
help: Property 'loop' is only allowed on: video, audio
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} allowFullScreen></div>
· ─────
╰────
help: Property 'muted' is only allowed on: video, audio
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} allowFullScreen></div>
· ───────────
╰────
help: Property 'playsInline' is only allowed on: video
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} allowFullScreen></div>
· ───────────────
╰────
help: Property 'allowFullScreen' is only allowed on: iframe, video
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div download="foo" />
· ────────
╰────
help: Property 'download' is only allowed on: area, a
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div imageSrcSet="someImageSrcSet" />
· ───────────
╰────
help: Property 'imageSrcSet' is only allowed on: link
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div imageSizes="someImageSizes" />
· ──────────
╰────
help: Property 'imageSizes' is only allowed on: link
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div data-xml-anything="invalid" />
· ─────────────────
╰────
help: Remove unknown property
⚠ eslint-plugin-react(no-unknown-property): React does not recognize data-* props with uppercase characters on a DOM element
╭─[no_unknown_property.tsx:1:1]
1 │ <div data-testID="bar" data-under_sCoRe="bar" />;
· ───────────
╰────
help: Use 'data-testid' instead
⚠ eslint-plugin-react(no-unknown-property): React does not recognize data-* props with uppercase characters on a DOM element
╭─[no_unknown_property.tsx:1:1]
1 │ <div data-testID="bar" data-under_sCoRe="bar" />;
· ────────────────
╰────
help: Use 'data-under_score' instead
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div abbr="abbr" />
· ────
╰────
help: Property 'abbr' is only allowed on: td, th
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div webkitDirectory="" />
· ───────────────
╰────
help: Property 'webkitDirectory' is only allowed on: input
⚠ eslint-plugin-react(no-unknown-property): Invalid property found
╭─[no_unknown_property.tsx:1:1]
1 │ <div webkitdirectory="" />
· ───────────────
╰────
help: Property 'webkitdirectory' is only allowed on: input
⚠ eslint-plugin-react(no-unknown-property): Unknown property found
╭─[no_unknown_property.tsx:1:1]
1 │
2 │ <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">
· ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3 │ Hello, world!
╰────
help: Remove unknown property

View file

@ -54,6 +54,15 @@ pub fn get_prop_value<'a, 'b>(item: &'b JSXAttributeItem<'a>) -> Option<&'b JSXA
}
}
pub fn get_prop_name(item: &JSXAttributeName) -> String {
match item {
JSXAttributeName::NamespacedName(name) => {
format!("{}:{}", name.namespace.name.as_str(), name.property.name.as_str())
}
JSXAttributeName::Identifier(ident) => ident.name.to_string(),
}
}
pub fn get_literal_prop_value<'a>(item: &'a JSXAttributeItem<'_>) -> Option<&'a str> {
get_prop_value(item).and_then(|v| {
if let JSXAttributeValue::StringLiteral(s) = v {