Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter/jsdoc): Implement check-tag-names rule #3029

Merged
merged 12 commits into from
Apr 23, 2024
119 changes: 83 additions & 36 deletions crates/oxc_linter/src/config/settings/jsdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,62 +70,86 @@ pub struct JSDocPluginSettings {
// }[]
}

// https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/settings.md#default-preferred-aliases
fn get_default_aliased_tag_name(original_name: &str) -> Option<String> {
let aliased_name = match original_name {
"virtual" => "abstract",
"extends" => "augments",
"constructor" => "class",
"const" => "constant",
"defaultvalue" => "default",
"desc" => "description",
"host" => "external",
"fileoverview" | "overview" => "file",
"emits" => "fires",
"func" | "method" => "function",
"var" => "member",
"arg" | "argument" => "param",
"prop" => "property",
"return" => "returns",
"exception" => "throws",
"yield" => "yields",
_ => original_name,
}
.to_string();

if aliased_name != original_name {
return Some(aliased_name);
}
None
}

impl JSDocPluginSettings {
/// Only for `check-tag-names` rule
/// Return `Some(reason)` if blocked
pub fn check_blocked_tag_name(&self, tag_name: &str) -> Option<String> {
match self.tag_name_preference.get(tag_name) {
Some(TagNamePreference::FalseOnly(_)) => Some(format!("Unexpected tag `@{tag_name}`")),
Some(
TagNamePreference::ObjectWithMessage { message }
| TagNamePreference::ObjectWithMessageAndReplacement { message, .. },
) => Some(message.to_string()),
Some(TagNamePreference::FalseOnly(_)) => Some(format!("Unexpected tag `@{tag_name}`.")),
Some(TagNamePreference::ObjectWithMessage { message }) => Some(message.to_string()),
_ => None,
}
}
/// Only for `check-tag-names` rule
/// Return `Some(reason)` if replacement found or default aliased
pub fn check_preferred_tag_name(&self, original_name: &str) -> Option<String> {
let reason = |preferred_name: &str| -> String {
format!("Replace tag `@{original_name}` with `@{preferred_name}`.")
};

pub fn list_preferred_tag_names(&self) -> Vec<String> {
match self.tag_name_preference.get(original_name) {
Some(TagNamePreference::TagNameOnly(preferred_name)) => Some(reason(preferred_name)),
Some(TagNamePreference::ObjectWithMessageAndReplacement { message, .. }) => {
Some(message.to_string())
}
_ => get_default_aliased_tag_name(original_name)
.filter(|preferred_name| preferred_name != original_name)
.map(|preferred_name| reason(&preferred_name)),
}
}
/// Only for `check-tag-names` rule
/// Return all user replacement tag names
pub fn list_user_defined_tag_names(&self) -> Vec<&str> {
self.tag_name_preference
.iter()
.filter_map(|(_, pref)| match pref {
TagNamePreference::TagNameOnly(replacement)
| TagNamePreference::ObjectWithMessageAndReplacement { replacement, .. } => {
Some(replacement.to_string())
Some(replacement.as_str())
}
_ => None,
})
.collect()
}

/// Resolve original, known tag name to user preferred name
/// Resolve original, known tag name to user preferred, default aliased name
/// If not defined, return original name
pub fn resolve_tag_name(&self, original_name: &str) -> String {
match self.tag_name_preference.get(original_name) {
Some(
TagNamePreference::TagNameOnly(replacement)
| TagNamePreference::ObjectWithMessageAndReplacement { replacement, .. },
) => replacement.to_string(),
_ => {
// https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/settings.md#default-preferred-aliases
match original_name {
"virtual" => "abstract",
"extends" => "augments",
"constructor" => "class",
"const" => "constant",
"defaultvalue" => "default",
"desc" => "description",
"host" => "external",
"fileoverview" | "overview" => "file",
"emits" => "fires",
"func" | "method" => "function",
"var" => "member",
"arg" | "argument" => "param",
"prop" => "property",
"return" => "returns",
"exception" => "throws",
"yield" => "yields",
_ => original_name,
}
.to_string()
}
_ => get_default_aliased_tag_name(original_name).unwrap_or(original_name.to_string()),
}
}
}
Expand Down Expand Up @@ -208,9 +232,9 @@ mod test {
}

#[test]
fn list_preferred_tag_names() {
fn list_user_defined_tag_names() {
let settings = JSDocPluginSettings::deserialize(&serde_json::json!({})).unwrap();
assert_eq!(settings.list_preferred_tag_names().len(), 0);
assert_eq!(settings.list_user_defined_tag_names().len(), 0);

let settings = JSDocPluginSettings::deserialize(&serde_json::json!({
"tagNamePreference": {
Expand All @@ -222,7 +246,7 @@ mod test {
}
}))
.unwrap();
let mut preferred = settings.list_preferred_tag_names();
let mut preferred = settings.list_user_defined_tag_names();
preferred.sort_unstable();
assert_eq!(preferred, vec!["bar", "noop", "overridedefault"]);
}
Expand All @@ -242,9 +266,32 @@ mod test {
.unwrap();
assert_eq!(
settings.check_blocked_tag_name("foo"),
Some("Unexpected tag `@foo`".to_string())
Some("Unexpected tag `@foo`.".to_string())
);
assert_eq!(settings.check_blocked_tag_name("bar"), Some("do not use bar".to_string()));
assert_eq!(settings.check_blocked_tag_name("baz"), Some("baz is noop now".to_string()));
assert_eq!(settings.check_blocked_tag_name("baz"), None);
}

#[test]
fn check_preferred_tag_name() {
let settings = JSDocPluginSettings::deserialize(&serde_json::json!({})).unwrap();
assert_eq!(settings.check_preferred_tag_name("foo"), None);

let settings = JSDocPluginSettings::deserialize(&serde_json::json!({
"tagNamePreference": {
"foo": false,
"bar": { "message": "do not use bar" },
"baz": { "message": "baz is noop now", "replacement": "noop" },
"qux": "quux"
}
}))
.unwrap();
assert_eq!(settings.check_preferred_tag_name("foo"), None,);
assert_eq!(settings.check_preferred_tag_name("bar"), None);
assert_eq!(settings.check_preferred_tag_name("baz"), Some("baz is noop now".to_string()));
assert_eq!(
settings.check_preferred_tag_name("qux"),
Some("Replace tag `@qux` with `@quux`.".to_string())
);
}
}
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ mod nextjs {
mod jsdoc {
pub mod check_access;
pub mod check_property_names;
pub mod check_tag_names;
pub mod empty_tags;
pub mod require_property;
pub mod require_property_description;
Expand Down Expand Up @@ -691,6 +692,7 @@ oxc_macros::declare_all_lint_rules! {
nextjs::no_before_interactive_script_outside_document,
jsdoc::check_access,
jsdoc::check_property_names,
jsdoc::check_tag_names,
jsdoc::empty_tags,
jsdoc::require_property,
jsdoc::require_property_type,
Expand Down
Loading
Loading