mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter): eslint-plugin-react: no-unknown-property (#1875)
Based on: - tests: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/tests/lib/rules/no-unknown-property.js - docs: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unknown-property.md - code: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/lib/rules/no-unknown-property.js
This commit is contained in:
parent
f46ed71d8a
commit
c6eb519417
7 changed files with 1057 additions and 2 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
734
crates/oxc_linter/src/rules/react/no_unknown_property.rs
Normal file
734
crates/oxc_linter/src/rules/react/no_unknown_property.rs
Normal 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();
|
||||
}
|
||||
310
crates/oxc_linter/src/snapshots/no_unknown_property.snap
Normal file
310
crates/oxc_linter/src/snapshots/no_unknown_property.snap
Normal 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
|
||||
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue