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

Commit

Permalink
feat(rome_js_analyzer): rule useValidLang
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Dec 12, 2022
1 parent 4418666 commit 780b72c
Show file tree
Hide file tree
Showing 18 changed files with 515 additions and 12 deletions.
27 changes: 27 additions & 0 deletions crates/rome_aria/src/iso.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 2 additions & 0 deletions crates/rome_aria/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
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 @@ -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",
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 ,] } }
122 changes: 122 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,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
/// <html lang="lorem" />
/// ```
///
/// ```jsx,expect_diagnostic
/// <html lang="en-babab" />
/// ```
///
/// ### Valid
///
/// ```jsx
/// <Html lang="en-babab" />
/// ```
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<AnyJsxElement>;
type State = UseValidLangState;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> 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<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let iso = ctx.iso();
let mut diagnostic = RuleDiagnostic::new(
rule_category!(),
state.attribute_range,
markup! {
"Provide a valid value for the "<Emphasis>"lang"</Emphasis>" 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)
}
}
11 changes: 10 additions & 1 deletion crates/rome_js_analyze/src/aria_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,6 +11,7 @@ use std::sync::Arc;
pub(crate) struct AriaServices {
pub(crate) roles: Arc<AriaRoles>,
pub(crate) properties: Arc<AriaProperties>,
pub(crate) iso: Arc<AriaIso>,
}

impl AriaServices {
Expand All @@ -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 {
Expand All @@ -34,9 +39,13 @@ impl FromServices for AriaServices {
let properties: &Arc<AriaProperties> = services.get_service().ok_or_else(|| {
MissingServicesDiagnostic::new(rule_key.rule_name(), &["AriaProperties"])
})?;
let iso: &Arc<AriaIso> = services
.get_service()
.ok_or_else(|| MissingServicesDiagnostic::new(rule_key.rule_name(), &["AriaIso"]))?;
Ok(Self {
roles: roles.clone(),
properties: properties.clone(),
iso: iso.clone(),
})
}
}
Expand Down
5 changes: 3 additions & 2 deletions crates/rome_js_analyze/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -226,7 +227,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
9 changes: 9 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,9 @@
// invalid
let a = <html lang="lorem" />;
let a = <html lang="en-babab" />;

// 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

0 comments on commit 780b72c

Please sign in to comment.