Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

feat(rome_js_analyzer): rule useValidLang #4045

Merged
merged 3 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/rome_aria/src/iso.rs
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions crates/rome_aria/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::str::FromStr;

pub mod iso;
mod macros;
pub mod properties;
pub mod roles;
Expand Down
39 changes: 38 additions & 1 deletion crates/rome_aria_metadata/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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
}
}

Expand Down
33 changes: 32 additions & 1 deletion crates/rome_aria_metadata/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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",
];
1 change: 1 addition & 0 deletions crates/rome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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",
Expand Down
3 changes: 2 additions & 1 deletion crates/rome_js_analyze/src/aria_analyzers/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ,] } }
131 changes: 131 additions & 0 deletions crates/rome_js_analyze/src/aria_analyzers/nursery/use_valid_lang.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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
/// <html lang="lorem" />
/// ```
///
/// ```jsx,expect_diagnostic
/// <html lang="en-babab" />
/// ```
///
/// ```jsx,expect_diagnostic
/// <html lang="en-GB-typo" />
/// ```
///
/// ### Valid
///
/// ```jsx
/// <Html lang="en-babab" />
/// ```
pub(crate) UseValidLang {
version: "12.0.0",
name: "useValidLang",
recommended: true,
}
}

enum InvalidKind {
Language,
Country,
Value,
}

pub(crate) struct UseValidLangState {
invalid_kind: InvalidKind,
attribute_range: TextRange,
}

impl Rule for UseValidLang {
type Query = Aria<AnyJsxElement>;
type State = UseValidLangState;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> 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_iso_language(language) {
return Some(UseValidLangState {
attribute_range: attribute_value.range(),
invalid_kind: InvalidKind::Language,
});
} else if !ctx.is_valid_iso_country(country) {
return Some(UseValidLangState {
attribute_range: attribute_value.range(),
invalid_kind: InvalidKind::Country,
});
} else if split_value.next().is_some() {
return Some(UseValidLangState {
attribute_range: attribute_value.range(),
invalid_kind: InvalidKind::Value,
});
}
}

(Some(language), None) => {
if !ctx.is_valid_iso_language(language) {
return Some(UseValidLangState {
attribute_range: attribute_value.range(),
invalid_kind: InvalidKind::Language,
});
}
}
_ => {}
}
}
ematipico marked this conversation as resolved.
Show resolved Hide resolved

None
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let mut diagnostic = RuleDiagnostic::new(
rule_category!(),
state.attribute_range,
markup! {
"Provide a valid value for the "<Emphasis>"lang"</Emphasis>" attribute."
},
);
diagnostic = match state.invalid_kind {
InvalidKind::Language => {
let languages = ctx.iso_language_list();
let languages = if languages.len() > 15 {
&languages[..15]
} else {
languages
};

diagnostic.footer_list("Some of valid languages:", languages)
}
InvalidKind::Country => {
let countries = ctx.iso_country_list();
let countries = if countries.len() > 15 {
&countries[..15]
} else {
countries
};

diagnostic.footer_list("Some of valid countries:", countries)
}
InvalidKind::Value => diagnostic,
};
Some(diagnostic)
}
}
17 changes: 17 additions & 0 deletions crates/rome_js_analyze/src/aria_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use rome_analyze::{
FromServices, MissingServicesDiagnostic, Phase, Phases, QueryKey, QueryMatch, Queryable,
RuleKey, ServiceBag,
};
use rome_aria::iso::{countries, is_valid_country, is_valid_language, languages};
use rome_aria::{AriaProperties, AriaRoles};
use rome_js_syntax::JsLanguage;
use rome_rowan::AstNode;
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/rome_js_analyze/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ mod tests {
String::from_utf8(buffer).unwrap()
}

const SOURCE: &str = r#"<span aria-labelledby={``} ></span>"#;
const SOURCE: &str = r#" <span aria-labelledby={``}></span>;"#;

let parsed = parse(SOURCE, FileId::zero(), SourceType::jsx());

Expand Down
10 changes: 10 additions & 0 deletions crates/rome_js_analyze/tests/specs/nursery/useValidLang.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// invalid
let a = <html lang="lorem" />;
let a = <html lang="en-babab" />;
let a = <html lang="en-GB-something" />;

// valid
let a = <Html lang="en-babab" />;
let a = <html lang="en-US"></html>;
let a = <html lang="en"></html>;
let a = <html lang={lang}></html>;
Loading