diff --git a/crates/rome_aria/src/iso.rs b/crates/rome_aria/src/iso.rs new file mode 100644 index 00000000000..cb9f352bd44 --- /dev/null +++ b/crates/rome_aria/src/iso.rs @@ -0,0 +1,27 @@ +use rome_aria_metadata::{IsoCountries, IsoLanguages, ISO_COUNTRIES, ISO_LANGUAGES}; +use std::str::FromStr; + +#[derive(Debug, Default)] +pub struct AriaIso; + +impl AriaIso { + /// Returns a list of valid ISO countries + pub fn is_valid_country(&self, country: &str) -> bool { + IsoCountries::from_str(country).is_ok() + } + + /// Returns a list of valid ISO languages + pub fn is_valid_language(&self, language: &str) -> bool { + IsoLanguages::from_str(language).is_ok() + } + + /// An array of all available countries + pub fn countries(&self) -> &'static [&'static str] { + &ISO_COUNTRIES + } + + /// An array of all available languages + pub fn languages(&self) -> &'static [&'static str] { + &ISO_LANGUAGES + } +} diff --git a/crates/rome_aria/src/lib.rs b/crates/rome_aria/src/lib.rs index 3368ec63fc7..71c1db2006a 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 68eed084288..17b4c6ec6d7 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 d0499b9e433..9caffe47bb5 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 58384037098..b01da417c4e 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -68,6 +68,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 2653da16241..20703f3e60a 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 00000000000..bf8443eb8fa --- /dev/null +++ b/crates/rome_js_analyze/src/aria_analyzers/nursery/use_valid_lang.rs @@ -0,0 +1,122 @@ +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, +} + +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 iso = ctx.iso(); + 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 !iso.is_valid_language(language) { + return Some(UseValidLangState { + attribute_range: attribute_value.range(), + error_kind: ErrorKind::InvalidLanguage, + }); + } else if !iso.is_valid_country(country) { + return Some(UseValidLangState { + attribute_range: attribute_value.range(), + error_kind: ErrorKind::InvalidCountry, + }); + } + } + + (Some(language), None) => { + if !iso.is_valid_language(language) { + return Some(UseValidLangState { + attribute_range: attribute_value.range(), + error_kind: ErrorKind::InvalidLanguage, + }); + } + } + _ => return None, + } + } + + None + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + let iso = ctx.iso(); + 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 = iso.languages(); + let languages = if languages.len() > 15 { + &languages[..15] + } else { + languages + }; + + diagnostic.footer_list("Some of valid languages:", languages) + } + ErrorKind::InvalidCountry => { + let countries = iso.countries(); + let countries = if countries.len() > 15 { + &countries[..15] + } else { + countries + }; + + diagnostic.footer_list("Some of valid countries:", countries) + } + }; + Some(diagnostic) + } +} diff --git a/crates/rome_js_analyze/src/aria_services.rs b/crates/rome_js_analyze/src/aria_services.rs index 049144a2b19..3dbd238a8d5 100644 --- a/crates/rome_js_analyze/src/aria_services.rs +++ b/crates/rome_js_analyze/src/aria_services.rs @@ -2,7 +2,7 @@ use rome_analyze::{ FromServices, MissingServicesDiagnostic, Phase, Phases, QueryKey, QueryMatch, Queryable, RuleKey, ServiceBag, }; -use rome_aria::{AriaProperties, AriaRoles}; +use rome_aria::{AriaIso, AriaProperties, AriaRoles}; use rome_js_syntax::JsLanguage; use rome_rowan::AstNode; use std::sync::Arc; @@ -11,6 +11,7 @@ use std::sync::Arc; pub(crate) struct AriaServices { pub(crate) roles: Arc, pub(crate) properties: Arc, + pub(crate) iso: Arc, } impl AriaServices { @@ -21,6 +22,10 @@ impl AriaServices { pub fn aria_properties(&self) -> &AriaProperties { &self.properties } + + pub fn iso(&self) -> &AriaIso { + &self.iso + } } impl FromServices for AriaServices { @@ -34,9 +39,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 5129656a82e..a79b82b2f88 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 00000000000..604f72fad44 --- /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 00000000000..7e3cf1baf73 --- /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 c07d24d36e3..9f468b8e391 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 fb170c5716f..553bf3e938f 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -789,10 +789,12 @@ 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; 33] = [ + pub(crate) const CATEGORY_RULES: [&'static str; 34] = [ "noAccessKey", "noAssignInExpressions", "noBannedTypes", @@ -826,8 +828,9 @@ impl Nursery { "useExponentiationOperator", "useHookAtTopLevel", "useNumericLiterals", + "useValidLang", ]; - const RECOMMENDED_RULES: [&'static str; 24] = [ + const RECOMMENDED_RULES: [&'static str; 25] = [ "noAssignInExpressions", "noBannedTypes", "noConstEnum", @@ -852,8 +855,9 @@ impl Nursery { "useEnumInitializers", "useExhaustiveDependencies", "useNumericLiterals", + "useValidLang", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 24] = [ + 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]), @@ -878,6 +882,7 @@ impl Nursery { RuleFilter::Rule("nursery", Self::CATEGORY_RULES[28]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[29]), 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 get_enabled_rules(&self) -> IndexSet { @@ -904,7 +909,7 @@ impl Nursery { 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>; 24] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 25] { Self::RECOMMENDED_RULES_AS_FILTERS } } diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 58a56acdcee..b6a7cf837cf 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -946,6 +946,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 1ce17ad4530..233c52e137f 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -421,6 +421,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 @@ -716,6 +720,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 58a56acdcee..b6a7cf837cf 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -946,6 +946,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 288c64f41fa..9194d749580 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -674,5 +674,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 00000000000..a119ae9eca2 --- /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 + +``` +