From ef58adf7fde7f640fe5fac3ad8367c654ad38a94 Mon Sep 17 00:00:00 2001 From: ematipico Date: Mon, 12 Dec 2022 15:41:19 +0000 Subject: [PATCH] feat(rome_js_analyzer): rule `useValidLang` --- crates/rome_aria/src/iso.rs | 22 +++ crates/rome_aria/src/lib.rs | 2 + crates/rome_aria_metadata/build.rs | 39 +++++- crates/rome_aria_metadata/src/lib.rs | 33 ++++- .../src/categories.rs | 1 + .../src/aria_analyzers/nursery.rs | 3 +- .../aria_analyzers/nursery/use_valid_lang.rs | 129 ++++++++++++++++++ crates/rome_js_analyze/src/aria_services.rs | 23 +++- crates/rome_js_analyze/src/lib.rs | 5 +- .../tests/specs/nursery/useValidLang.jsx | 9 ++ .../tests/specs/nursery/useValidLang.jsx.snap | 85 ++++++++++++ crates/rome_js_syntax/src/jsx_ext.rs | 61 ++++++++- .../src/configuration/linter/rules.rs | 99 +++++++++----- editors/vscode/configuration_schema.json | 11 ++ npm/backend-jsonrpc/src/workspace.ts | 5 + npm/rome/configuration_schema.json | 11 ++ website/src/pages/lint/rules/index.mdx | 6 + website/src/pages/lint/rules/useValidLang.md | 83 +++++++++++ 18 files changed, 584 insertions(+), 43 deletions(-) create mode 100644 crates/rome_aria/src/iso.rs create mode 100644 crates/rome_js_analyze/src/aria_analyzers/nursery/use_valid_lang.rs create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx.snap create mode 100644 website/src/pages/lint/rules/useValidLang.md diff --git a/crates/rome_aria/src/iso.rs b/crates/rome_aria/src/iso.rs new file mode 100644 index 000000000000..2fe1e9b3e8bc --- /dev/null +++ b/crates/rome_aria/src/iso.rs @@ -0,0 +1,22 @@ +use rome_aria_metadata::{IsoCountries, IsoLanguages, ISO_COUNTRIES, ISO_LANGUAGES}; +use std::str::FromStr; + +/// Returns a list of valid ISO countries +pub fn is_valid_country(country: &str) -> bool { + IsoCountries::from_str(country).is_ok() +} + +/// Returns a list of valid ISO languages +pub fn is_valid_language(language: &str) -> bool { + IsoLanguages::from_str(language).is_ok() +} + +/// An array of all available countries +pub fn countries() -> &'static [&'static str] { + &ISO_COUNTRIES +} + +/// An array of all available languages +pub fn languages() -> &'static [&'static str] { + &ISO_LANGUAGES +} diff --git a/crates/rome_aria/src/lib.rs b/crates/rome_aria/src/lib.rs index 3368ec63fc7d..71c1db2006aa 100644 --- a/crates/rome_aria/src/lib.rs +++ b/crates/rome_aria/src/lib.rs @@ -1,9 +1,11 @@ use std::str::FromStr; +pub mod iso; mod macros; pub mod properties; pub mod roles; +pub use iso::AriaIso; pub use properties::AriaProperties; pub(crate) use roles::AriaRoleDefinition; pub use roles::AriaRoles; diff --git a/crates/rome_aria_metadata/build.rs b/crates/rome_aria_metadata/build.rs index 68eed0842889..17b4c6ec6d7c 100644 --- a/crates/rome_aria_metadata/build.rs +++ b/crates/rome_aria_metadata/build.rs @@ -145,6 +145,37 @@ pub const ARIA_DOCUMENT_STRUCTURE_ROLES: [&str; 25] = [ "toolbar", ]; +const ISO_COUNTRIES: [&str; 233] = [ + "AF", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW", "AU", "AT", "AZ", "BS", + "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BA", "BW", "BR", "IO", "VG", "BN", + "BG", "BF", "MM", "BI", "KH", "CM", "CA", "CV", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", + "KM", "CK", "CR", "HR", "CU", "CY", "CZ", "CD", "DK", "DJ", "DM", "DO", "EC", "EG", "SV", "GQ", + "ER", "EE", "ET", "FK", "FO", "FJ", "FI", "FR", "PF", "GA", "GM", "GE", "DE", "GH", "GI", "GR", + "GL", "GD", "GU", "GT", "GN", "GW", "GY", "HT", "VA", "HN", "HK", "HU", "IS", "IN", "ID", "IR", + "IQ", "IE", "IM", "IL", "IT", "CI", "JM", "JP", "JE", "JO", "KZ", "KE", "KI", "KW", "KG", "LA", + "LV", "LB", "LS", "LR", "LY", "LI", "LT", "LU", "MO", "MK", "MG", "MW", "MY", "MV", "ML", "MT", + "MH", "MR", "MU", "YT", "MX", "FM", "MD", "MC", "MN", "ME", "MS", "MA", "MZ", "NA", "NR", "NP", + "NL", "AN", "NC", "NZ", "NI", "NE", "NG", "NU", "KP", "MP", "NO", "OM", "PK", "PW", "PA", "PG", + "PY", "PE", "PH", "PN", "PL", "PT", "PR", "QA", "CG", "RO", "RU", "RW", "BL", "SH", "KN", "LC", + "MF", "PM", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SK", "SI", "SB", "SO", + "ZA", "KR", "ES", "LK", "SD", "SR", "SJ", "SZ", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", + "TG", "TK", "TO", "TT", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "AE", "GB", "US", "UY", "VI", + "UZ", "VU", "VE", "VN", "WF", "EH", "YE", "ZM", "ZW", +]; + +const ISO_LANGUAGES: [&str; 150] = [ + "ab", "aa", "af", "sq", "am", "ar", "an", "hy", "as", "ay", "az", "ba", "eu", "bn", "dz", "bh", + "bi", "br", "bg", "my", "be", "km", "ca", "zh", "zh-Hans", "zh-Hant", "co", "hr", "cs", "da", + "nl", "en", "eo", "et", "fo", "fa", "fj", "fi", "fr", "fy", "gl", "gd", "gv", "ka", "de", "el", + "kl", "gn", "gu", "ht", "ha", "he", "iw", "hi", "hu", "is", "io", "id", "in", "ia", "ie", "iu", + "ik", "ga", "it", "ja", "jv", "kn", "ks", "kk", "rw", "ky", "rn", "ko", "ku", "lo", "la", "lv", + "li", "ln", "lt", "mk", "mg", "ms", "ml", "mt", "mi", "mr", "mo", "mn", "na", "ne", "no", "oc", + "or", "om", "ps", "pl", "pt", "pa", "qu", "rm", "ro", "ru", "sm", "sg", "sa", "sr", "sh", "st", + "tn", "sn", "ii", "sd", "si", "ss", "sk", "sl", "so", "es", "su", "sw", "sv", "tl", "tg", "ta", + "tt", "te", "th", "bo", "ti", "to", "ts", "tr", "tk", "tw", "ug", "uk", "ur", "uz", "vi", "vo", + "wa", "cy", "wo", "xh", "yi", "ji", "yo", "zu", +]; + fn main() -> io::Result<()> { let aria_properties = generate_properties(); let aria_roles = generate_roles(); @@ -158,7 +189,7 @@ fn main() -> io::Result<()> { let ast = tokens.to_string(); let out_dir = env::var("OUT_DIR").unwrap(); - fs::write(PathBuf::from(out_dir).join("enums.rs"), ast)?; + fs::write(PathBuf::from(out_dir).join("roles_and_properties.rs"), ast)?; Ok(()) } @@ -200,10 +231,16 @@ fn generate_roles() -> TokenStream { "AriaDocumentStructureRolesEnum", ); + let iso_countries = generate_enums(ISO_COUNTRIES.len(), ISO_COUNTRIES.iter(), "IsoCountries"); + + let iso_languages = generate_enums(ISO_LANGUAGES.len(), ISO_LANGUAGES.iter(), "IsoLanguages"); + quote! { #widget_roles #abstract_roles #document_structure_roles + #iso_countries + #iso_languages } } diff --git a/crates/rome_aria_metadata/src/lib.rs b/crates/rome_aria_metadata/src/lib.rs index d0499b9e4337..9caffe47bb5b 100644 --- a/crates/rome_aria_metadata/src/lib.rs +++ b/crates/rome_aria_metadata/src/lib.rs @@ -1 +1,32 @@ -include!(concat!(env!("OUT_DIR"), "/enums.rs")); +include!(concat!(env!("OUT_DIR"), "/roles_and_properties.rs")); + +pub const ISO_COUNTRIES: [&str; 233] = [ + "AF", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW", "AU", "AT", "AZ", "BS", + "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BA", "BW", "BR", "IO", "VG", "BN", + "BG", "BF", "MM", "BI", "KH", "CM", "CA", "CV", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", + "KM", "CK", "CR", "HR", "CU", "CY", "CZ", "CD", "DK", "DJ", "DM", "DO", "EC", "EG", "SV", "GQ", + "ER", "EE", "ET", "FK", "FO", "FJ", "FI", "FR", "PF", "GA", "GM", "GE", "DE", "GH", "GI", "GR", + "GL", "GD", "GU", "GT", "GN", "GW", "GY", "HT", "VA", "HN", "HK", "HU", "IS", "IN", "ID", "IR", + "IQ", "IE", "IM", "IL", "IT", "CI", "JM", "JP", "JE", "JO", "KZ", "KE", "KI", "KW", "KG", "LA", + "LV", "LB", "LS", "LR", "LY", "LI", "LT", "LU", "MO", "MK", "MG", "MW", "MY", "MV", "ML", "MT", + "MH", "MR", "MU", "YT", "MX", "FM", "MD", "MC", "MN", "ME", "MS", "MA", "MZ", "NA", "NR", "NP", + "NL", "AN", "NC", "NZ", "NI", "NE", "NG", "NU", "KP", "MP", "NO", "OM", "PK", "PW", "PA", "PG", + "PY", "PE", "PH", "PN", "PL", "PT", "PR", "QA", "CG", "RO", "RU", "RW", "BL", "SH", "KN", "LC", + "MF", "PM", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SK", "SI", "SB", "SO", + "ZA", "KR", "ES", "LK", "SD", "SR", "SJ", "SZ", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", + "TG", "TK", "TO", "TT", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "AE", "GB", "US", "UY", "VI", + "UZ", "VU", "VE", "VN", "WF", "EH", "YE", "ZM", "ZW", +]; + +pub const ISO_LANGUAGES: [&str; 150] = [ + "ab", "aa", "af", "sq", "am", "ar", "an", "hy", "as", "ay", "az", "ba", "eu", "bn", "dz", "bh", + "bi", "br", "bg", "my", "be", "km", "ca", "zh", "zh-Hans", "zh-Hant", "co", "hr", "cs", "da", + "nl", "en", "eo", "et", "fo", "fa", "fj", "fi", "fr", "fy", "gl", "gd", "gv", "ka", "de", "el", + "kl", "gn", "gu", "ht", "ha", "he", "iw", "hi", "hu", "is", "io", "id", "in", "ia", "ie", "iu", + "ik", "ga", "it", "ja", "jv", "kn", "ks", "kk", "rw", "ky", "rn", "ko", "ku", "lo", "la", "lv", + "li", "ln", "lt", "mk", "mg", "ms", "ml", "mt", "mi", "mr", "mo", "mn", "na", "ne", "no", "oc", + "or", "om", "ps", "pl", "pt", "pa", "qu", "rm", "ro", "ru", "sm", "sg", "sa", "sr", "sh", "st", + "tn", "sn", "ii", "sd", "si", "ss", "sk", "sl", "so", "es", "su", "sw", "sv", "tl", "tg", "ta", + "tt", "te", "th", "bo", "ti", "to", "ts", "tr", "tk", "tw", "ug", "uk", "ur", "uz", "vi", "vo", + "wa", "cy", "wo", "xh", "yi", "ji", "yo", "zu", +]; diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index 8beb8c451f10..3a1170a2ee1f 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -70,6 +70,7 @@ define_dategories! { "lint/nursery/useAriaPropTypes": "https://docs.rome.tools/lint/rules/useAriaPropTypes", "lint/nursery/useCamelCase": "https://docs.rome.tools/lint/rules/useCamelCase", "lint/nursery/useConst":"https://docs.rome.tools/lint/rules/useConst", + "lint/nursery/useValidLang":"https://docs.rome.tools/lint/rules/useValidLang", "lint/nursery/useDefaultParameterLast":"https://docs.rome.tools/lint/rules/useDefaultParameterLast", "lint/nursery/useDefaultSwitchClauseLast":"https://docs.rome.tools/lint/rules/useDefaultSwitchClauseLast", "lint/nursery/useEnumInitializers":"https://docs.rome.tools/lint/rules/useEnumInitializers", diff --git a/crates/rome_js_analyze/src/aria_analyzers/nursery.rs b/crates/rome_js_analyze/src/aria_analyzers/nursery.rs index 2653da162416..20703f3e60ae 100644 --- a/crates/rome_js_analyze/src/aria_analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/aria_analyzers/nursery.rs @@ -3,4 +3,5 @@ use rome_analyze::declare_group; mod use_aria_prop_types; mod use_aria_props_for_role; -declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: use_aria_prop_types :: UseAriaPropTypes , self :: use_aria_props_for_role :: UseAriaPropsForRole ,] } } +mod use_valid_lang; +declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: use_aria_prop_types :: UseAriaPropTypes , self :: use_aria_props_for_role :: UseAriaPropsForRole , self :: use_valid_lang :: UseValidLang ,] } } diff --git a/crates/rome_js_analyze/src/aria_analyzers/nursery/use_valid_lang.rs b/crates/rome_js_analyze/src/aria_analyzers/nursery/use_valid_lang.rs new file mode 100644 index 000000000000..b8f7a6c207a0 --- /dev/null +++ b/crates/rome_js_analyze/src/aria_analyzers/nursery/use_valid_lang.rs @@ -0,0 +1,129 @@ +use crate::aria_services::Aria; +use rome_analyze::context::RuleContext; +use rome_analyze::{declare_rule, Rule, RuleDiagnostic}; +use rome_console::markup; +use rome_js_syntax::jsx_ext::AnyJsxElement; +use rome_rowan::{AstNode, TextRange}; +declare_rule! { + /// Ensure that the attribute passed to the `lang` attribute is a correct ISO language and/or country. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// + /// ``` + /// + /// ```jsx,expect_diagnostic + /// + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// + /// ``` + pub(crate) UseValidLang { + version: "12.0.0", + name: "useValidLang", + recommended: true, + } +} + +enum ErrorKind { + InvalidLanguage, + InvalidCountry, + InvalidValue, +} + +pub(crate) struct UseValidLangState { + error_kind: ErrorKind, + attribute_range: TextRange, +} + +impl Rule for UseValidLang { + type Query = Aria; + type State = UseValidLangState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let element_text = node.name().ok()?.as_jsx_name()?.value_token().ok()?; + if element_text.text_trimmed() == "html" { + let attribute = node.find_attribute_by_name("lang")?; + let attribute_value = attribute.initializer()?.value().ok()?; + let attribute_text = attribute_value.inner_text_value().ok()??; + let mut split_value = attribute_text.text().split('-'); + match (split_value.next(), split_value.next()) { + (Some(language), Some(country)) => { + if !ctx.is_valid_language(language) { + return Some(UseValidLangState { + attribute_range: attribute_value.range(), + error_kind: ErrorKind::InvalidLanguage, + }); + } else if !ctx.is_valid_country(country) { + return Some(UseValidLangState { + attribute_range: attribute_value.range(), + error_kind: ErrorKind::InvalidCountry, + }); + } + } + + (Some(language), None) => { + if !ctx.is_valid_language(language) { + return Some(UseValidLangState { + attribute_range: attribute_value.range(), + error_kind: ErrorKind::InvalidLanguage, + }); + } + } + _ => { + if split_value.next().is_some() { + return Some(UseValidLangState { + attribute_range: attribute_value.range(), + error_kind: ErrorKind::InvalidValue, + }); + } + } + } + } + + None + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + let mut diagnostic = RuleDiagnostic::new( + rule_category!(), + state.attribute_range, + markup! { + "Provide a valid value for the ""lang"" attribute." + }, + ); + diagnostic = match state.error_kind { + ErrorKind::InvalidLanguage => { + let languages = ctx.iso_language_list(); + let languages = if languages.len() > 15 { + &languages[..15] + } else { + languages + }; + + diagnostic.footer_list("Some of valid languages:", languages) + } + ErrorKind::InvalidCountry => { + let countries = ctx.iso_country_list(); + let countries = if countries.len() > 15 { + &countries[..15] + } else { + countries + }; + + diagnostic.footer_list("Some of valid countries:", countries) + } + ErrorKind::InvalidValue => diagnostic, + };y + Some(diagnostic) + } +} diff --git a/crates/rome_js_analyze/src/aria_services.rs b/crates/rome_js_analyze/src/aria_services.rs index 049144a2b19d..2a9f69ad38e3 100644 --- a/crates/rome_js_analyze/src/aria_services.rs +++ b/crates/rome_js_analyze/src/aria_services.rs @@ -2,7 +2,8 @@ use rome_analyze::{ FromServices, MissingServicesDiagnostic, Phase, Phases, QueryKey, QueryMatch, Queryable, RuleKey, ServiceBag, }; -use rome_aria::{AriaProperties, AriaRoles}; +use rome_aria::iso::{countries, is_valid_country, is_valid_language, languages}; +use rome_aria::{AriaIso, AriaProperties, AriaRoles}; use rome_js_syntax::JsLanguage; use rome_rowan::AstNode; use std::sync::Arc; @@ -21,6 +22,22 @@ impl AriaServices { pub fn aria_properties(&self) -> &AriaProperties { &self.properties } + + pub fn is_valid_iso_language(&self, language: &str) -> bool { + is_valid_language(language) + } + + pub fn is_valid_iso_country(&self, country: &str) -> bool { + is_valid_country(country) + } + + pub fn iso_country_list(&self) -> &'static [&'static str] { + countries() + } + + pub fn iso_language_list(&self) -> &'static [&'static str] { + languages() + } } impl FromServices for AriaServices { @@ -34,9 +51,13 @@ impl FromServices for AriaServices { let properties: &Arc = services.get_service().ok_or_else(|| { MissingServicesDiagnostic::new(rule_key.rule_name(), &["AriaProperties"]) })?; + let iso: &Arc = services + .get_service() + .ok_or_else(|| MissingServicesDiagnostic::new(rule_key.rule_name(), &["AriaIso"]))?; Ok(Self { roles: roles.clone(), properties: properties.clone(), + iso: iso.clone(), }) } } diff --git a/crates/rome_js_analyze/src/lib.rs b/crates/rome_js_analyze/src/lib.rs index 5129656a82ed..a79b82b2f88d 100644 --- a/crates/rome_js_analyze/src/lib.rs +++ b/crates/rome_js_analyze/src/lib.rs @@ -9,7 +9,7 @@ use rome_analyze::{ DeserializableRuleOptions, InspectMatcher, LanguageRoot, MatchQueryParams, MetadataRegistry, Phases, RuleAction, RuleRegistry, ServiceBag, SuppressionKind, SyntaxVisitor, }; -use rome_aria::{AriaProperties, AriaRoles}; +use rome_aria::{AriaIso, AriaProperties, AriaRoles}; use rome_diagnostics::{category, Diagnostic, FileId}; use rome_js_syntax::suppression::SuppressionDiagnostic; use rome_js_syntax::{suppression::parse_suppression_comment, JsLanguage}; @@ -174,6 +174,7 @@ where services.insert_service(Arc::new(AriaRoles::default())); services.insert_service(Arc::new(AriaProperties::default())); + services.insert_service(Arc::new(AriaIso::default())); analyzer.run(AnalyzerContext { file_id, root: root.clone(), @@ -226,7 +227,7 @@ mod tests { String::from_utf8(buffer).unwrap() } - const SOURCE: &str = r#""#; + const SOURCE: &str = r#" ;"#; let parsed = parse(SOURCE, FileId::zero(), SourceType::jsx()); diff --git a/crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx b/crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx new file mode 100644 index 000000000000..604f72fad44d --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx @@ -0,0 +1,9 @@ +// invalid +let a = ; +let a = ; + +// valid +let a = ; +let a = ; +let a = ; +let a = ; diff --git a/crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx.snap b/crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx.snap new file mode 100644 index 000000000000..7e3cf1baf731 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx.snap @@ -0,0 +1,85 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: useValidLang.jsx +--- +# Input +```js +// invalid +let a = ; +let a = ; + +// valid +let a = ; +let a = ; +let a = ; +let a = ; + +``` + +# Diagnostics +``` +useValidLang.jsx:2:20 lint/nursery/useValidLang ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the lang attribute. + + 1 │ // invalid + > 2 │ let a = ; + │ ^^^^^^^ + 3 │ let a = ; + 4 │ + + i Some of valid languages: + + - ab + - aa + - af + - sq + - am + - ar + - an + - hy + - as + - ay + - az + - ba + - eu + - bn + - dz + + +``` + +``` +useValidLang.jsx:3:20 lint/nursery/useValidLang ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the lang attribute. + + 1 │ // invalid + 2 │ let a = ; + > 3 │ let a = ; + │ ^^^^^^^^^^ + 4 │ + 5 │ // valid + + i Some of valid countries: + + - AF + - AL + - DZ + - AS + - AD + - AO + - AI + - AQ + - AG + - AR + - AM + - AW + - AU + - AT + - AZ + + +``` + + diff --git a/crates/rome_js_syntax/src/jsx_ext.rs b/crates/rome_js_syntax/src/jsx_ext.rs index c07d24d36e3b..9f468b8e391b 100644 --- a/crates/rome_js_syntax/src/jsx_ext.rs +++ b/crates/rome_js_syntax/src/jsx_ext.rs @@ -1,8 +1,9 @@ use std::collections::HashSet; use crate::{ - AnyJsxAttribute, AnyJsxElementName, JsSyntaxToken, JsxAttribute, JsxAttributeList, JsxName, - JsxOpeningElement, JsxSelfClosingElement, JsxString, TextSize, + AnyJsExpression, AnyJsLiteralExpression, AnyJsxAttribute, AnyJsxAttributeValue, + AnyJsxElementName, JsSyntaxToken, JsxAttribute, JsxAttributeList, JsxName, JsxOpeningElement, + JsxSelfClosingElement, JsxString, TextSize, }; use rome_rowan::{declare_node_union, AstNode, AstNodeList, SyntaxResult, SyntaxTokenText}; @@ -409,3 +410,59 @@ impl JsxAttribute { .unwrap_or(false) } } + +impl AnyJsxAttributeValue { + /// Retrieves the text value of the attribute + /// + /// If the attribute is not a text or a text-like node, [Node] is returned. + /// + /// ## Examples + /// + /// ``` + /// use rome_js_factory::make::{ident, js_string_literal_expression, jsx_attribute, jsx_attribute_initializer_clause, jsx_expression_attribute_value, jsx_name, jsx_string, token}; + /// use rome_js_syntax::{AnyJsExpression, AnyJsLiteralExpression, AnyJsxAttributeName, AnyJsxAttributeValue, T}; + /// let attribute = AnyJsxAttributeValue::JsxString( + /// jsx_string(ident("en")) + /// ); + /// assert_eq!(attribute.inner_text_value().unwrap().unwrap(), "en"); + /// let attribute = AnyJsxAttributeValue::JsxExpressionAttributeValue( + /// jsx_expression_attribute_value( + /// token(T!['{']), + /// AnyJsExpression::AnyJsLiteralExpression( + /// AnyJsLiteralExpression::JsStringLiteralExpression( + /// js_string_literal_expression(ident("en")) + /// ) + /// ), + /// token(T!['}']), + /// ) + /// ); + /// assert_eq!(attribute.inner_text_value().unwrap().unwrap(), "en"); + /// ``` + pub fn inner_text_value(&self) -> SyntaxResult> { + let result = match self { + AnyJsxAttributeValue::JsxString(string) => Some(string.inner_string_text()?), + AnyJsxAttributeValue::JsxExpressionAttributeValue(expression) => { + match expression.expression()? { + AnyJsExpression::JsTemplateExpression(template) => { + template.elements().iter().next().and_then(|chunk| { + Some( + chunk + .as_js_template_chunk_element()? + .template_chunk_token() + .ok()? + .token_text_trimmed(), + ) + }) + } + AnyJsExpression::AnyJsLiteralExpression( + AnyJsLiteralExpression::JsStringLiteralExpression(string), + ) => Some(string.inner_string_text()?), + _ => None, + } + } + _ => return Ok(None), + }; + + Ok(result) + } +} diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index 916e43938b32..6eb3b710466a 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -184,7 +184,9 @@ impl Rules { None } } - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } #[doc = r" It returns a tuple of filters. The first element of the tuple are the enabled rules,"] #[doc = r" while the second element are the disabled rules."] #[doc = r""] @@ -419,7 +421,9 @@ impl A11y { RuleFilter::Rule("a11y", Self::CATEGORY_RULES[7]), RuleFilter::Rule("a11y", Self::CATEGORY_RULES[8]), ]; - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } pub(crate) fn get_enabled_rules(&self) -> IndexSet { IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| { if conf.is_enabled() { @@ -439,7 +443,9 @@ impl A11y { })) } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] - pub(crate) fn has_rule(rule_name: &str) -> bool { Self::CATEGORY_RULES.contains(&rule_name) } + pub(crate) fn has_rule(rule_name: &str) -> bool { + Self::CATEGORY_RULES.contains(&rule_name) + } #[doc = r" Checks if, given a rule name, it is marked as recommended"] pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) @@ -525,7 +531,9 @@ impl Complexity { RuleFilter::Rule("complexity", Self::CATEGORY_RULES[4]), RuleFilter::Rule("complexity", Self::CATEGORY_RULES[5]), ]; - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } pub(crate) fn get_enabled_rules(&self) -> IndexSet { IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| { if conf.is_enabled() { @@ -545,7 +553,9 @@ impl Complexity { })) } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] - pub(crate) fn has_rule(rule_name: &str) -> bool { Self::CATEGORY_RULES.contains(&rule_name) } + pub(crate) fn has_rule(rule_name: &str) -> bool { + Self::CATEGORY_RULES.contains(&rule_name) + } #[doc = r" Checks if, given a rule name, it is marked as recommended"] pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) @@ -654,7 +664,9 @@ impl Correctness { RuleFilter::Rule("correctness", Self::CATEGORY_RULES[9]), RuleFilter::Rule("correctness", Self::CATEGORY_RULES[10]), ]; - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } pub(crate) fn get_enabled_rules(&self) -> IndexSet { IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| { if conf.is_enabled() { @@ -674,7 +686,9 @@ impl Correctness { })) } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] - pub(crate) fn has_rule(rule_name: &str) -> bool { Self::CATEGORY_RULES.contains(&rule_name) } + pub(crate) fn has_rule(rule_name: &str) -> bool { + Self::CATEGORY_RULES.contains(&rule_name) + } #[doc = r" Checks if, given a rule name, it is marked as recommended"] pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) @@ -729,8 +743,6 @@ struct NurserySchema { no_assign_in_expressions: Option, #[doc = "Disallow certain types."] no_banned_types: Option, - #[doc = "Disallow comma operator."] - no_comma_operator: Option, #[doc = "Disallow TypeScript const enum"] no_const_enum: Option, #[doc = "Disallow returning a value from a constructor."] @@ -769,8 +781,6 @@ struct NurserySchema { no_var: Option, #[doc = "Disallow returning a value from a function with the return type 'void'"] no_void_type_return: Option, - #[doc = "Disallow with statements in non-strict contexts."] - no_with: Option, #[doc = "Enforce that ARIA state and property values are valid."] use_aria_prop_types: Option, #[doc = "Enforce that elements with ARIA roles must have all required ARIA attributes for that role."] @@ -793,14 +803,15 @@ struct NurserySchema { use_hook_at_top_level: Option, #[doc = "Disallow parseInt() and Number.parseInt() in favor of binary, octal, and hexadecimal literals"] use_numeric_literals: Option, + #[doc = "Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country."] + use_valid_lang: Option, } impl Nursery { const CATEGORY_NAME: &'static str = "nursery"; - pub(crate) const CATEGORY_RULES: [&'static str; 35] = [ + pub(crate) const CATEGORY_RULES: [&'static str; 34] = [ "noAccessKey", "noAssignInExpressions", "noBannedTypes", - "noCommaOperator", "noConstEnum", "noConstructorReturn", "noDistractingElements", @@ -820,7 +831,6 @@ impl Nursery { "noUselessSwitchCase", "noVar", "noVoidTypeReturn", - "noWith", "useAriaPropTypes", "useAriaPropsForRole", "useCamelCase", @@ -832,11 +842,11 @@ impl Nursery { "useExponentiationOperator", "useHookAtTopLevel", "useNumericLiterals", + "useValidLang", ]; - const RECOMMENDED_RULES: [&'static str; 26] = [ + const RECOMMENDED_RULES: [&'static str; 25] = [ "noAssignInExpressions", "noBannedTypes", - "noCommaOperator", "noConstEnum", "noConstructorReturn", "noDistractingElements", @@ -852,7 +862,6 @@ impl Nursery { "noUselessSwitchCase", "noVar", "noVoidTypeReturn", - "noWith", "useAriaPropsForRole", "useConst", "useDefaultParameterLast", @@ -860,8 +869,9 @@ impl Nursery { "useEnumInitializers", "useExhaustiveDependencies", "useNumericLiterals", + "useValidLang", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 26] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 25] = [ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[1]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[2]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[3]), @@ -872,24 +882,25 @@ impl Nursery { RuleFilter::Rule("nursery", Self::CATEGORY_RULES[8]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[9]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[10]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[11]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[14]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[13]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[16]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[17]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[18]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[19]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[20]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[21]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[22]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[23]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[25]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[26]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[27]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[28]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[29]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[30]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[31]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[34]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[32]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[33]), ]; - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } pub(crate) fn get_enabled_rules(&self) -> IndexSet { IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| { if conf.is_enabled() { @@ -909,12 +920,14 @@ impl Nursery { })) } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] - pub(crate) fn has_rule(rule_name: &str) -> bool { Self::CATEGORY_RULES.contains(&rule_name) } + pub(crate) fn has_rule(rule_name: &str) -> bool { + Self::CATEGORY_RULES.contains(&rule_name) + } #[doc = r" Checks if, given a rule name, it is marked as recommended"] pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 26] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 25] { Self::RECOMMENDED_RULES_AS_FILTERS } } @@ -967,7 +980,9 @@ impl Performance { const RECOMMENDED_RULES: [&'static str; 1] = ["noDelete"]; const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 1] = [RuleFilter::Rule("performance", Self::CATEGORY_RULES[0])]; - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } pub(crate) fn get_enabled_rules(&self) -> IndexSet { IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| { if conf.is_enabled() { @@ -987,7 +1002,9 @@ impl Performance { })) } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] - pub(crate) fn has_rule(rule_name: &str) -> bool { Self::CATEGORY_RULES.contains(&rule_name) } + pub(crate) fn has_rule(rule_name: &str) -> bool { + Self::CATEGORY_RULES.contains(&rule_name) + } #[doc = r" Checks if, given a rule name, it is marked as recommended"] pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) @@ -1055,7 +1072,9 @@ impl Security { RuleFilter::Rule("security", Self::CATEGORY_RULES[0]), RuleFilter::Rule("security", Self::CATEGORY_RULES[1]), ]; - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } pub(crate) fn get_enabled_rules(&self) -> IndexSet { IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| { if conf.is_enabled() { @@ -1075,7 +1094,9 @@ impl Security { })) } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] - pub(crate) fn has_rule(rule_name: &str) -> bool { Self::CATEGORY_RULES.contains(&rule_name) } + pub(crate) fn has_rule(rule_name: &str) -> bool { + Self::CATEGORY_RULES.contains(&rule_name) + } #[doc = r" Checks if, given a rule name, it is marked as recommended"] pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) @@ -1184,7 +1205,9 @@ impl Style { RuleFilter::Rule("style", Self::CATEGORY_RULES[11]), RuleFilter::Rule("style", Self::CATEGORY_RULES[12]), ]; - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } pub(crate) fn get_enabled_rules(&self) -> IndexSet { IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| { if conf.is_enabled() { @@ -1204,7 +1227,9 @@ impl Style { })) } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] - pub(crate) fn has_rule(rule_name: &str) -> bool { Self::CATEGORY_RULES.contains(&rule_name) } + pub(crate) fn has_rule(rule_name: &str) -> bool { + Self::CATEGORY_RULES.contains(&rule_name) + } #[doc = r" Checks if, given a rule name, it is marked as recommended"] pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) @@ -1342,7 +1367,9 @@ impl Suspicious { RuleFilter::Rule("suspicious", Self::CATEGORY_RULES[14]), RuleFilter::Rule("suspicious", Self::CATEGORY_RULES[15]), ]; - pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } + pub(crate) fn is_recommended(&self) -> bool { + !matches!(self.recommended, Some(false)) + } pub(crate) fn get_enabled_rules(&self) -> IndexSet { IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| { if conf.is_enabled() { @@ -1362,7 +1389,9 @@ impl Suspicious { })) } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] - pub(crate) fn has_rule(rule_name: &str) -> bool { Self::CATEGORY_RULES.contains(&rule_name) } + pub(crate) fn has_rule(rule_name: &str) -> bool { + Self::CATEGORY_RULES.contains(&rule_name) + } #[doc = r" Checks if, given a rule name, it is marked as recommended"] pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index ee247f404e2a..d1829fecc598 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -968,6 +968,17 @@ "type": "null" } ] + }, + "useValidLang": { + "description": "Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] } } }, diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index 7375456ebacc..5de9e8ae7540 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -429,6 +429,10 @@ export interface Nursery { * Disallow parseInt() and Number.parseInt() in favor of binary, octal, and hexadecimal literals */ useNumericLiterals?: RuleConfiguration; + /** + * Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country. + */ + useValidLang?: RuleConfiguration; } /** * A list of rules that belong to this group @@ -726,6 +730,7 @@ export type Category = | "lint/nursery/useAriaPropTypes" | "lint/nursery/useCamelCase" | "lint/nursery/useConst" + | "lint/nursery/useValidLang" | "lint/nursery/useDefaultParameterLast" | "lint/nursery/useDefaultSwitchClauseLast" | "lint/nursery/useEnumInitializers" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index ee247f404e2a..d1829fecc598 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -968,6 +968,17 @@ "type": "null" } ] + }, + "useValidLang": { + "description": "Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] } } }, diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index fe63d04c5e6d..f6774bf20b1e 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -686,5 +686,11 @@ component functions. Disallow parseInt() and Number.parseInt() in favor of binary, octal, and hexadecimal literals +
+

+ useValidLang +

+Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country. +
diff --git a/website/src/pages/lint/rules/useValidLang.md b/website/src/pages/lint/rules/useValidLang.md new file mode 100644 index 000000000000..a119ae9eca26 --- /dev/null +++ b/website/src/pages/lint/rules/useValidLang.md @@ -0,0 +1,83 @@ +--- +title: Lint Rule useValidLang +parent: lint/rules/index +--- + +# useValidLang (since v12.0.0) + +Ensure that the attribute passed to the `lang` attribute is a correct ISO language and/or country. + +## Examples + +### Invalid + +```jsx + +``` + +
nursery/useValidLang.js:1:12 lint/nursery/useValidLang ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Provide a valid value for the lang attribute.
+  
+  > 1 │ <html lang="lorem" />
+              ^^^^^^^
+    2 │ 
+  
+   Some of valid languages:
+  
+  - ab
+  - aa
+  - af
+  - sq
+  - am
+  - ar
+  - an
+  - hy
+  - as
+  - ay
+  - az
+  - ba
+  - eu
+  - bn
+  - dz
+  
+
+ +```jsx + +``` + +
nursery/useValidLang.js:1:12 lint/nursery/useValidLang ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Provide a valid value for the lang attribute.
+  
+  > 1 │ <html lang="en-babab" />
+              ^^^^^^^^^^
+    2 │ 
+  
+   Some of valid countries:
+  
+  - AF
+  - AL
+  - DZ
+  - AS
+  - AD
+  - AO
+  - AI
+  - AQ
+  - AG
+  - AR
+  - AM
+  - AW
+  - AU
+  - AT
+  - AZ
+  
+
+ +### Valid + +```jsx + +``` +