Skip to content

Commit

Permalink
feat(rule): wip solid style prop
Browse files Browse the repository at this point in the history
  • Loading branch information
marvin-j97 committed Apr 14, 2024
1 parent 57eda28 commit e9b06f6
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 13 deletions.
41 changes: 30 additions & 11 deletions crates/biome_configuration/src/linter/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2647,6 +2647,9 @@ pub struct Nursery {
#[doc = "Disallow ternary operators when simpler alternatives exist."]
#[serde(skip_serializing_if = "Option::is_none")]
pub no_useless_ternary: Option<RuleConfiguration<NoUselessTernary>>,
#[doc = "Succinct description of the rule."]
#[serde(skip_serializing_if = "Option::is_none")]
pub style_prop: Option<RuleConfiguration<StyleProp>>,
#[doc = "Disallows package private imports."]
#[serde(skip_serializing_if = "Option::is_none")]
pub use_import_restrictions: Option<RuleConfiguration<UseImportRestrictions>>,
Expand Down Expand Up @@ -2676,7 +2679,7 @@ impl DeserializableValidator for Nursery {
}
impl Nursery {
const GROUP_NAME: &'static str = "nursery";
pub(crate) const GROUP_RULES: [&'static str; 25] = [
pub(crate) const GROUP_RULES: [&'static str; 26] = [
"noBarrelFile",
"noColorInvalidHex",
"noConsole",
Expand All @@ -2698,6 +2701,7 @@ impl Nursery {
"noSuspiciousSemicolonInJsx",
"noUndeclaredDependencies",
"noUselessTernary",
"styleProp",
"useImportRestrictions",
"useJsxKeyInIterable",
"useNodeAssertStrict",
Expand Down Expand Up @@ -2729,7 +2733,7 @@ impl Nursery {
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]),
];
const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 25] = [
const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 26] = [
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]),
Expand All @@ -2755,6 +2759,7 @@ impl Nursery {
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]),
];
#[doc = r" Retrieves the recommended rules"]
pub(crate) fn is_recommended_true(&self) -> bool {
Expand Down Expand Up @@ -2876,26 +2881,31 @@ impl Nursery {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]));
}
}
if let Some(rule) = self.use_import_restrictions.as_ref() {
if let Some(rule) = self.style_prop.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]));
}
}
if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() {
if let Some(rule) = self.use_import_restrictions.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]));
}
}
if let Some(rule) = self.use_node_assert_strict.as_ref() {
if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]));
}
}
if let Some(rule) = self.use_sorted_classes.as_ref() {
if let Some(rule) = self.use_node_assert_strict.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]));
}
}
if let Some(rule) = self.use_sorted_classes.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]));
}
}
index_set
}
pub(crate) fn get_disabled_rules(&self) -> IndexSet<RuleFilter> {
Expand Down Expand Up @@ -3005,26 +3015,31 @@ impl Nursery {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]));
}
}
if let Some(rule) = self.use_import_restrictions.as_ref() {
if let Some(rule) = self.style_prop.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]));
}
}
if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() {
if let Some(rule) = self.use_import_restrictions.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]));
}
}
if let Some(rule) = self.use_node_assert_strict.as_ref() {
if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]));
}
}
if let Some(rule) = self.use_sorted_classes.as_ref() {
if let Some(rule) = self.use_node_assert_strict.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]));
}
}
if let Some(rule) = self.use_sorted_classes.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]));
}
}
index_set
}
#[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"]
Expand All @@ -3038,7 +3053,7 @@ impl Nursery {
pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 11] {
Self::RECOMMENDED_RULES_AS_FILTERS
}
pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 25] {
pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 26] {
Self::ALL_RULES_AS_FILTERS
}
#[doc = r" Select preset rules"]
Expand Down Expand Up @@ -3145,6 +3160,10 @@ impl Nursery {
.no_useless_ternary
.as_ref()
.map(|conf| (conf.level(), conf.get_options())),
"styleProp" => self
.style_prop
.as_ref()
.map(|conf| (conf.level(), conf.get_options())),
"useImportRestrictions" => self
.use_import_restrictions
.as_ref()
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ define_categories! {
"lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console",
"lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback",
"lint/nursery/noDuplicateElseIf": "https://biomejs.dev/linter/rules/no-duplicate-else-if",
"lint/nursery/noDuplicateFontNames": "https://biomejs.dev/linter/rules/no-font-family-duplicate-names",
"lint/nursery/noDuplicateJsonKeys": "https://biomejs.dev/linter/rules/no-duplicate-json-keys",
"lint/nursery/noDuplicateTestHooks": "https://biomejs.dev/linter/rules/no-duplicate-test-hooks",
"lint/nursery/noEvolvingAny": "https://biomejs.dev/linter/rules/no-evolving-any",
"lint/nursery/noExcessiveNestedTestSuites": "https://biomejs.dev/linter/rules/no-excessive-nested-test-suites",
"lint/nursery/noExportsInTest": "https://biomejs.dev/linter/rules/no-exports-in-test",
"lint/nursery/noFocusedTests": "https://biomejs.dev/linter/rules/no-focused-tests",
"lint/nursery/noDuplicateFontNames": "https://biomejs.dev/linter/rules/no-font-family-duplicate-names",
"lint/nursery/noMisplacedAssertion": "https://biomejs.dev/linter/rules/no-misplaced-assertion",
"lint/nursery/noNamespaceImport": "https://biomejs.dev/linter/rules/no-namespace-import",
"lint/nursery/noNodejsModules": "https://biomejs.dev/linter/rules/no-nodejs-modules",
Expand All @@ -130,6 +130,7 @@ define_categories! {
"lint/nursery/noTypeOnlyImportAttributes": "https://biomejs.dev/linter/rules/no-type-only-import-attributes",
"lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies",
"lint/nursery/noUselessTernary": "https://biomejs.dev/linter/rules/no-useless-ternary",
"lint/nursery/styleProp": "https://biomejs.dev/linter/rules/style-prop",
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
"lint/nursery/useJsxKeyInIterable": "https://biomejs.dev/linter/rules/use-jsx-key-in-iterable",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod no_skipped_tests;
pub mod no_suspicious_semicolon_in_jsx;
pub mod no_undeclared_dependencies;
pub mod no_useless_ternary;
pub mod style_prop;
pub mod use_import_restrictions;
pub mod use_jsx_key_in_iterable;
pub mod use_node_assert_strict;
Expand Down Expand Up @@ -47,6 +48,7 @@ declare_group! {
self :: no_suspicious_semicolon_in_jsx :: NoSuspiciousSemicolonInJsx ,
self :: no_undeclared_dependencies :: NoUndeclaredDependencies ,
self :: no_useless_ternary :: NoUselessTernary ,
self :: style_prop :: StyleProp ,
self :: use_import_restrictions :: UseImportRestrictions ,
self :: use_jsx_key_in_iterable :: UseJsxKeyInIterable ,
self :: use_node_assert_strict :: UseNodeAssertStrict ,
Expand Down
176 changes: 176 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/style_prop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
use biome_js_syntax::{jsx_ext::AnyJsxElement, AnyJsxAttribute};
use biome_rowan::TextRange;

declare_rule! {
/// TODO: Succinct description of the rule.
///
/// Require CSS properties in the style prop to be valid and kebab-cased (ex. 'font-size'),
/// not camel-cased (ex. 'fontSize') like in React,
/// and that property values with dimensions are strings,
/// not numbers with implicit 'px' units.
///
/// https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/style-prop.md
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// <div style={{ fontSize: "10px" }}>Hello, world!</div>
/// ```
///
/// ### Valid
///
/// ```js
/// <div style={{ "font-size": "10px" }}>Hello, world!</div>
/// ```
///
pub StyleProp {
version: "next",
name: "styleProp",
// TODO: eslint source solid
recommended: false,
}
}

fn is_kebab_case(input: &str) -> bool {
if input.is_empty() {
return false;
}

for c in input.chars() {
if !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
return false;
}
}

true
}

// TODO: check lengthPercentageRegex = /\b(?:width|height|margin|padding|border-width|font-size)\b/i;

const PROPS_THAT_NEEDS_UNIT: &[&str] = &[
"width",
"height",
"margin",
"padding",
"border-width",
"font-size",
];

fn prop_needs_unit(name: &str) -> bool {
PROPS_THAT_NEEDS_UNIT.contains(&name)
}

pub enum StylePropError {
Kebab(String),
NumberLiteral,
}

impl std::fmt::Display for StylePropError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Kebab(name) => write!(f, "{name} is not kebab-case."),
Self::NumberLiteral => {
write!(f, "This CSS property value should be a string with a unit; Solid does not automatically append a \"px\" unit.")
}
}
}
}

impl Rule for StyleProp {
type Query = Ast<AnyJsxElement>;
type State = Vec<(TextRange, StylePropError)>;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
use biome_js_syntax::AnyJsxAttributeValue::JsxExpressionAttributeValue as AttributeValue;

let node = ctx.query();

let mut invalid_style_attribute = vec![];

for attribute in node.attributes() {
if let AnyJsxAttribute::JsxAttribute(attr) = attribute {
if let Ok(name) = attr.name() {
if name.to_string() == "style" {
if let AttributeValue(maybe_obj) =
attr.initializer().unwrap().value().unwrap()
{
if let Some(obj) =
maybe_obj.expression().unwrap().as_js_object_expression()
{
for member in obj.members().into_iter().flatten() {
if let Some(member) = member.as_js_property_object_member() {
if let Ok(name_token) = member.name() {
let name = name_token.name().unwrap().to_string();

if !is_kebab_case(&name) {
let name_span = name_token
.as_js_literal_member_name()
.unwrap()
.value()
.unwrap()
.text_range();

invalid_style_attribute.push((
name_span,
StylePropError::Kebab(name.clone()),
));
}

if prop_needs_unit(&name) {
if let Ok(value) = member.value() {
// TODO: need to check negative numbers...

if let Some(literal) =
value.as_any_js_literal_expression()
{
if let Some(num) = literal
.as_js_number_literal_expression()
.and_then(|x| x.as_number())
{
if num != 0.0 {
invalid_style_attribute.push((
literal
.value_token()
.unwrap()
.text_range(),
StylePropError::NumberLiteral,
));
}
}
}
}
}
};
}
}
}
};
}
}
}
}

Some(invalid_style_attribute)
}

fn diagnostic(_: &RuleContext<Self>, issues: &Self::State) -> Option<RuleDiagnostic> {
let mut iter = issues.iter();

if let Some((range, err)) = iter.next() {
let mut diagnostic = RuleDiagnostic::new(rule_category!(), range, format!("{err}"));

for (range, err) in iter {
diagnostic = diagnostic.detail(range, format!("{err}"));
}

Some(diagnostic)
} else {
None
}
}
}
1 change: 1 addition & 0 deletions crates/biome_js_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ pub type NoVoidElementsWithChildren = < lint :: correctness :: no_void_elements_
pub type NoVoidTypeReturn =
<lint::correctness::no_void_type_return::NoVoidTypeReturn as biome_analyze::Rule>::Options;
pub type NoWith = <lint::complexity::no_with::NoWith as biome_analyze::Rule>::Options;
pub type StyleProp = <lint::nursery::style_prop::StyleProp as biome_analyze::Rule>::Options;
pub type UseAltText = <lint::a11y::use_alt_text::UseAltText as biome_analyze::Rule>::Options;
pub type UseAnchorContent =
<lint::a11y::use_anchor_content::UseAnchorContent as biome_analyze::Rule>::Options;
Expand Down
27 changes: 27 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/styleProp/invalid.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div style={{ fontSize: "10px" }}>Hello, world!</div>;

<div style={{ backgroundColor: "red" }}>Hello, world!</div>;

<div style={{ "-webkitAlignContent": "center" }}>Hello, world!</div>;

<div style={{ COLOR: "10px" }}>Hello, world!</div>;

<div style={{ unknownStyleProp: "10px" }}>Hello, world!</div>;

<div css={{ fontSize: "10px" }}>Hello, world!</div>;

<div css={{ fontSize: "10px" }}>Hello, world!</div>;

{/* <div style="font-size: 10px;">Hello, world!</div>;
<div style={"font-size: 10px;"}>Hello, world!</div>; */}

{/* <div style="font-size: 10px; missing-value: ;">Hello, world!</div>;
<div style="Super invalid CSS! Not CSS at all!">Hello, world!</div>;
<div style={`font-size: 10px;`}>Hello, world!</div>; */}

<div style={{ "font-size": 10 }}>Hello, world!</div>;

<div style={{ "margin-top": -10 }}>Hello, world!</div>;
Loading

0 comments on commit e9b06f6

Please sign in to comment.