From 53a8e7ffbebb19cdd92bd2bcef6c308e0897bf05 Mon Sep 17 00:00:00 2001 From: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com> Date: Thu, 14 Mar 2024 23:34:32 +0900 Subject: [PATCH] feat(linter): Add settings.jsdoc (#2706) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Base work for #2642 🏃🏻 - [x] struct - [x] bool flags - [x] tagNamePreferences --- .../oxc_linter/src/config/settings/jsdoc.rs | 207 ++++++++++++++++++ .../src/config/settings/jsx_a11y.rs | 2 +- crates/oxc_linter/src/config/settings/mod.rs | 14 +- crates/oxc_linter/src/config/settings/next.rs | 4 +- .../oxc_linter/src/config/settings/react.rs | 4 +- 5 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 crates/oxc_linter/src/config/settings/jsdoc.rs diff --git a/crates/oxc_linter/src/config/settings/jsdoc.rs b/crates/oxc_linter/src/config/settings/jsdoc.rs new file mode 100644 index 0000000000000..52ab2bb1df4d4 --- /dev/null +++ b/crates/oxc_linter/src/config/settings/jsdoc.rs @@ -0,0 +1,207 @@ +use rustc_hash::FxHashMap; +use serde::Deserialize; + +/// https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/settings.md +#[derive(Debug, Deserialize, Default)] +pub struct JSDocPluginSettings { + /// For all rules but NOT apply to `check-access` and `empty-tags` rule + #[serde(default, rename = "ignorePrivate")] + pub ignore_private: bool, + /// For all rules but NOT apply to `empty-tags` rule + #[serde(default, rename = "ignoreInternal")] + pub ignore_internal: bool, + + /// Only for `require-(yields|returns|description|example|param|throws)` rule + #[serde(default = "default_true", rename = "ignoreReplacesDocs")] + pub ignore_replaces_docs: bool, + /// Only for `require-(yields|returns|description|example|param|throws)` rule + #[serde(default = "default_true", rename = "overrideReplacesDocs")] + pub override_replaces_docs: bool, + /// Only for `require-(yields|returns|description|example|param|throws)` rule + #[serde(default, rename = "augmentsExtendsReplacesDocs")] + pub arguments_extends_replaces_docs: bool, + /// Only for `require-(yields|returns|description|example|param|throws)` rule + #[serde(default, rename = "implementsReplacesDocs")] + pub implements_replaces_docs: bool, + + /// Only for `require-param-type` and `require-param-description` rule + #[serde(default, rename = "exemptDestructuredRootsFromChecks")] + pub exempt_destructured_roots_from_checks: bool, + + #[serde(default, rename = "tagNamePreference")] + tag_name_preference: FxHashMap, + // + // Not planning to support? + // min_lines: number + // max_lines: number + // mode: string + // + // TODO: Need more investigation to understand these usage... + // + // Only for `check-types` and `no-undefined-types` rule + // preferred_types: Record< + // string, + // false | string | { + // message: string; + // replacement?: false | string; + // skipRootChecking?: boolean; + // } + // > + // + // structured_tags: Record< + // string, + // { + // name?: "text" | "namepath-defining" | "namepath-referencing" | false; + // type?: boolean | string[]; + // required?: ("name" | "type" | "typeOrNameRequired")[]; + // } + // > + // + // I know this but not sure how to implement + // contexts: string[] | { + // disallowName?: string; + // allowName?: string; + // context?: string; + // comment?: string; + // tags?: string[]; + // replacement?: string; + // minimum?: number; + // message?: string; + // forceRequireReturn?: boolean; + // }[] +} + +impl JSDocPluginSettings { + pub fn is_blocked_tag_name(&self, tag_name: &str) -> Option { + 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()), + _ => None, + } + } + + pub fn resolve_tag_name(&self, tag_name: &str) -> String { + match self.tag_name_preference.get(tag_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 tag_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", + _ => tag_name, + } + .to_string() + } + } + } +} + +// Deserialize helper types + +fn default_true() -> bool { + true +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +enum TagNamePreference { + TagNameOnly(String), + ObjectWithMessageAndReplacement { message: String, replacement: String }, + ObjectWithMessage { message: String }, + FalseOnly(bool), // Should care `true`...? +} + +#[cfg(test)] +mod test { + use super::JSDocPluginSettings; + use serde::Deserialize; + + #[test] + fn parse_defaults() { + let settings = JSDocPluginSettings::deserialize(&serde_json::json!({})).unwrap(); + + assert!(!settings.ignore_private); + assert!(!settings.ignore_internal); + assert_eq!(settings.tag_name_preference.len(), 0); + assert!(settings.ignore_replaces_docs); + assert!(settings.override_replaces_docs); + assert!(!settings.arguments_extends_replaces_docs); + assert!(!settings.implements_replaces_docs); + } + + #[test] + fn parse_bools() { + let settings = JSDocPluginSettings::deserialize(&serde_json::json!({ + "ignorePrivate": true, + "ignoreInternal": true, + })) + .unwrap(); + + assert!(settings.ignore_private); + assert!(settings.ignore_internal); + assert_eq!(settings.tag_name_preference.len(), 0); + } + + #[test] + fn resolve_tag_name() { + let settings = JSDocPluginSettings::deserialize(&serde_json::json!({})).unwrap(); + assert_eq!(settings.resolve_tag_name("foo"), "foo".to_string()); + assert_eq!(settings.resolve_tag_name("virtual"), "abstract".to_string()); + assert_eq!(settings.resolve_tag_name("fileoverview"), "file".to_string()); + assert_eq!(settings.resolve_tag_name("overview"), "file".to_string()); + + let settings = JSDocPluginSettings::deserialize(&serde_json::json!({ + "tagNamePreference": { + "foo": "bar", + "virtual": "overridedefault", + "replace": { "message": "noop", "replacement": "noop" }, + "blocked": { "message": "noop" }, + "blocked2": false + } + })) + .unwrap(); + assert_eq!(settings.resolve_tag_name("foo"), "bar".to_string()); + assert_eq!(settings.resolve_tag_name("virtual"), "overridedefault".to_string()); + assert_eq!(settings.resolve_tag_name("replace"), "noop".to_string()); + assert_eq!(settings.resolve_tag_name("blocked"), "blocked".to_string()); + assert_eq!(settings.resolve_tag_name("blocked2"), "blocked2".to_string()); + } + + #[test] + fn is_blocked_tag_name() { + let settings = JSDocPluginSettings::deserialize(&serde_json::json!({})).unwrap(); + assert_eq!(settings.is_blocked_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" } + } + })) + .unwrap(); + assert_eq!(settings.is_blocked_tag_name("foo"), Some("Unexpected tag `@foo`".to_string())); + assert_eq!(settings.is_blocked_tag_name("bar"), Some("do not use bar".to_string())); + assert_eq!(settings.is_blocked_tag_name("baz"), Some("baz is noop now".to_string())); + } +} diff --git a/crates/oxc_linter/src/config/settings/jsx_a11y.rs b/crates/oxc_linter/src/config/settings/jsx_a11y.rs index 0a5b334c76cc5..d1ebf5ceed0f6 100644 --- a/crates/oxc_linter/src/config/settings/jsx_a11y.rs +++ b/crates/oxc_linter/src/config/settings/jsx_a11y.rs @@ -3,7 +3,7 @@ use serde::Deserialize; /// https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#configurations #[derive(Debug, Deserialize, Default)] -pub struct ESLintSettingsJSXA11y { +pub struct JSXA11yPluginSettings { #[serde(rename = "polymorphicPropName")] pub polymorphic_prop_name: Option, #[serde(default)] diff --git a/crates/oxc_linter/src/config/settings/mod.rs b/crates/oxc_linter/src/config/settings/mod.rs index 63324227c1c97..a1c7d410fce5d 100644 --- a/crates/oxc_linter/src/config/settings/mod.rs +++ b/crates/oxc_linter/src/config/settings/mod.rs @@ -1,6 +1,10 @@ -use self::{jsx_a11y::ESLintSettingsJSXA11y, next::ESLintSettingsNext, react::ESLintSettingsReact}; +use self::{ + jsdoc::JSDocPluginSettings, jsx_a11y::JSXA11yPluginSettings, next::NextPluginSettings, + react::ReactPluginSettings, +}; use serde::Deserialize; +mod jsdoc; mod jsx_a11y; mod next; mod react; @@ -15,11 +19,13 @@ mod react; pub struct ESLintSettings { #[serde(default)] #[serde(rename = "jsx-a11y")] - pub jsx_a11y: ESLintSettingsJSXA11y, + pub jsx_a11y: JSXA11yPluginSettings, #[serde(default)] - pub next: ESLintSettingsNext, + pub next: NextPluginSettings, #[serde(default)] - pub react: ESLintSettingsReact, + pub react: ReactPluginSettings, + #[serde(default)] + pub jsdoc: JSDocPluginSettings, } #[cfg(test)] diff --git a/crates/oxc_linter/src/config/settings/next.rs b/crates/oxc_linter/src/config/settings/next.rs index e3b7656d21b4a..f3922c393af5b 100644 --- a/crates/oxc_linter/src/config/settings/next.rs +++ b/crates/oxc_linter/src/config/settings/next.rs @@ -2,13 +2,13 @@ use serde::Deserialize; /// https://nextjs.org/docs/pages/building-your-application/configuring/eslint#eslint-plugin #[derive(Debug, Deserialize, Default)] -pub struct ESLintSettingsNext { +pub struct NextPluginSettings { #[serde(default)] #[serde(rename = "rootDir")] root_dir: OneOrMany, } -impl ESLintSettingsNext { +impl NextPluginSettings { pub fn get_root_dirs(&self) -> Vec { match &self.root_dir { OneOrMany::One(val) => vec![val.clone()], diff --git a/crates/oxc_linter/src/config/settings/react.rs b/crates/oxc_linter/src/config/settings/react.rs index 2c574d80cedda..e7b7bf60a2e0b 100644 --- a/crates/oxc_linter/src/config/settings/react.rs +++ b/crates/oxc_linter/src/config/settings/react.rs @@ -2,7 +2,7 @@ use serde::Deserialize; /// https://github.com/jsx-eslint/eslint-plugin-react#configuration-legacy-eslintrc- #[derive(Debug, Deserialize, Default)] -pub struct ESLintSettingsReact { +pub struct ReactPluginSettings { #[serde(default)] #[serde(rename = "formComponents")] form_components: Vec, @@ -12,7 +12,7 @@ pub struct ESLintSettingsReact { // TODO: More properties should be added } -impl ESLintSettingsReact { +impl ReactPluginSettings { pub fn get_form_component_attrs(&self, name: &str) -> Option> { get_component_attrs_by_name(&self.form_components, name) }