diff --git a/crates/oxc_linter/src/config/settings/jsdoc.rs b/crates/oxc_linter/src/config/settings/jsdoc.rs index a4682567438e6..b31fe54cd3a4c 100644 --- a/crates/oxc_linter/src/config/settings/jsdoc.rs +++ b/crates/oxc_linter/src/config/settings/jsdoc.rs @@ -71,41 +71,30 @@ pub struct JSDocPluginSettings { } impl JSDocPluginSettings { + /// Only for `check-tag-names` rule + /// Return `Some(reason)` if blocked pub fn check_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()), + 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 { + let reason = |preferred_name: &str| -> String { + format!("Replace tag `@{original_name}` with `@{preferred_name}`.") + }; - pub fn list_preferred_tag_names(&self) -> Vec { - self.tag_name_preference - .iter() - .filter_map(|(_, pref)| match pref { - TagNamePreference::TagNameOnly(replacement) - | TagNamePreference::ObjectWithMessageAndReplacement { replacement, .. } => { - Some(replacement.to_string()) - } - _ => None, - }) - .collect() - } - - /// Resolve original, known tag name to user preferred 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(), + Some(TagNamePreference::TagNameOnly(preferred_name)) => Some(reason(preferred_name)), + Some(TagNamePreference::ObjectWithMessageAndReplacement { message, .. }) => { + Some(message.to_string()) + } _ => { // https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/settings.md#default-preferred-aliases - match original_name { + let aliased_name = match original_name { "virtual" => "abstract", "extends" => "augments", "constructor" => "class", @@ -123,11 +112,42 @@ impl JSDocPluginSettings { "exception" => "throws", "yield" => "yields", _ => original_name, + }; + + if aliased_name != original_name { + return Some(reason(aliased_name)); } - .to_string() + + None } } } + /// 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.as_str()) + } + _ => None, + }) + .collect() + } + + /// Resolve original, known tag name to user preferred 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(), + _ => original_name.to_string(), + } + } } // Deserialize helper types @@ -186,9 +206,6 @@ mod 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": { @@ -208,9 +225,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": { @@ -222,7 +239,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"]); } @@ -242,9 +259,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()) + ); } } diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 6349be573bf62..78b236b777175 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -361,6 +361,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; @@ -693,6 +694,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, diff --git a/crates/oxc_linter/src/rules/jsdoc/check_tag_names.rs b/crates/oxc_linter/src/rules/jsdoc/check_tag_names.rs new file mode 100644 index 0000000000000..a58841a1428ee --- /dev/null +++ b/crates/oxc_linter/src/rules/jsdoc/check_tag_names.rs @@ -0,0 +1,1047 @@ +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use phf::phf_set; +use serde::Deserialize; + +use crate::{context::LintContext, rule::Rule}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-jsdoc(check-tag-names): Invalid tag name found.")] +#[diagnostic(severity(warning), help("{1}"))] +struct CheckTagNamesDiagnostic(#[label] pub Span, String); + +#[derive(Debug, Default, Clone)] +pub struct CheckTagNames(Box); + +declare_oxc_lint!( + /// ### What it does + /// Reports invalid block tag names. + /// Additionally checks for tag names that are redundant when using a type checker such as TypeScript. + /// + /// ### Why is this bad? + /// Using invalid tags can lead to confusion and make the documentation harder to read. + /// + /// ### Example + /// ```javascript + /// // Passing + /// /** @param */ + /// + /// // Failing + /// /** @Param */ + /// /** @foo */ + /// + /// /** + /// * This is redundant when typed. + /// * @type {string} + /// */ + /// ``` + CheckTagNames, + correctness +); + +#[derive(Debug, Default, Clone, Deserialize)] +struct CheckTagnamesConfig { + #[serde(default, rename = "definedTags")] + defined_tags: Vec, + #[serde(default, rename = "jsxTags")] + jsx_tags: bool, + #[serde(default)] + typed: bool, +} + +const VALID_BLOCK_TAGS: phf::Set<&'static str> = phf_set! { + "abstract", + "access", + "alias", + "async", + "augments", + "author", + "borrows", + "callback", + "class", + "classdesc", + "constant", + "constructs", + "copyright", + "default", + "deprecated", + "description", + "enum", + "event", + "example", + "exports", + "external", + "file", + "fires", + "function", + "generator", + "global", + "hideconstructor", + "ignore", + "implements", + "inheritdoc", + "inner", + "instance", + "interface", + "kind", + "lends", + "license", + "listens", + "member", + "memberof", + "memberof!", + "mixes", + "mixin", + // Undocumented, but exists + // https://github.com/jsdoc/jsdoc/blob/a08ac18a11f5b0d93421d1e8ecf632468db2d045/packages/jsdoc-tag/lib/definitions/core.js#L374 + "modifies", + "module", + "name", + "namespace", + "override", + "package", + "param", + "private", + "property", + "protected", + "public", + "readonly", + "requires", + "returns", + "see", + "since", + "static", + "summary", + "this", + "throws", + "todo", + "tutorial", + "type", + "typedef", + "variation", + "version", + "yields", + // JSDoc TS specific + "import", + "internal", + "overload", + "satisfies", + "template", +}; + +const JSX_TAGS: phf::Set<&'static str> = phf_set! { + "jsx", + "jsxFrag", + "jsxImportSource", + "jsxRuntime", +}; + +const ALWAYS_INVALID_TAGS_IF_TYPED: phf::Set<&'static str> = phf_set! { + "augments", + "callback", + "class", + "enum", + "implements", + "private", + "property", + "protected", + "public", + "readonly", + "this", + "type", + "typedef", +}; +const OUTSIDE_AMBIENT_INVALID_TAGS_IF_TYPED: phf::Set<&'static str> = phf_set! { + "abstract", + "access", + "class", + "constant", + "constructs", + // I'm not sure but this seems to be allowed... + // https://github.com/gajus/eslint-plugin-jsdoc/blob/e343ab5b1efaa59b07c600138aee070b4083857e/src/rules/checkTagNames.js#L140 + // "default", + "enum", + "export", + "exports", + "function", + "global", + "inherits", + "instance", + "interface", + "member", + "memberof", + "memberOf", + "method", + "mixes", + "mixin", + "module", + "name", + "namespace", + "override", + "property", + "requires", + "static", + "this", +}; + +impl Rule for CheckTagNames { + fn from_configuration(value: serde_json::Value) -> Self { + value + .as_array() + .and_then(|arr| arr.first()) + .and_then(|value| serde_json::from_value(value.clone()).ok()) + .map_or_else(Self::default, |value| Self(Box::new(value))) + } + + fn run_once(&self, ctx: &LintContext) { + let settings = &ctx.settings().jsdoc; + let config = &self.0; + let user_defined_tags = settings.list_user_defined_tag_names(); + + let is_dts = ctx.file_path().to_str().map_or(false, |p| p.ends_with(".d.ts")); + // NOTE: The original rule seems to check `declare` context by visiting AST nodes. + // https://github.com/gajus/eslint-plugin-jsdoc/blob/e343ab5b1efaa59b07c600138aee070b4083857e/src/rules/checkTagNames.js#L121 + // But... + // - No test case covers this(= only checks inside of `.d.ts`) + // - I've never seen this usage before + // So, I leave this part out for now. + let is_declare = false; + let is_ambient = is_dts || is_declare; + + for jsdoc in ctx.semantic().jsdoc().iter_all() { + for tag in jsdoc.tags() { + let tag_name = tag.kind.parsed(); + + // If user explicitly allowed, skip + if user_defined_tags.contains(&tag_name) + || config.defined_tags.contains(&tag_name.to_string()) + { + continue; + } + + // If user explicitly blocked, report + if let Some(reason) = settings.check_blocked_tag_name(tag_name) { + ctx.diagnostic(CheckTagNamesDiagnostic(tag.kind.span, reason)); + continue; + } + + // If preferred or default aliased, report to use it + if let Some(reason) = settings.check_preferred_tag_name(tag_name) { + ctx.diagnostic(CheckTagNamesDiagnostic(tag.kind.span, reason)); + continue; + } + + // Additional check for `typed` mode + if config.typed { + if ALWAYS_INVALID_TAGS_IF_TYPED.contains(tag_name) { + ctx.diagnostic(CheckTagNamesDiagnostic( + tag.kind.span, + format!("`@{tag_name}` is redundant when using a type system."), + )); + continue; + } + + if tag.kind.parsed() == "template" && tag.comment().parsed().is_empty() { + ctx.diagnostic(CheckTagNamesDiagnostic( + tag.kind.span, + format!("`@{tag_name}` without a name is redundant when using a type system."), + )); + continue; + } + + if !is_ambient && OUTSIDE_AMBIENT_INVALID_TAGS_IF_TYPED.contains(tag_name) { + ctx.diagnostic(CheckTagNamesDiagnostic( + tag.kind.span, + format!("`@{tag_name}` is redundant outside of ambient(`declare` or `.d.ts`) contexts when using a type system."), + )); + continue; + } + } + + // If invalid or unknown, report + let is_valid = (config.jsx_tags && JSX_TAGS.contains(tag_name)) + || VALID_BLOCK_TAGS.contains(tag_name); + if !is_valid { + ctx.diagnostic(CheckTagNamesDiagnostic( + tag.kind.span, + format!("`@{tag_name}` is invalid tag name."), + )); + continue; + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ +(" + /** + * @param foo (pass: valid name) + */ + function quux (foo) { + + } + ", None, None), +(" + /** + * @memberof! foo (pass: valid name) + */ + function quux (foo) { + + } + ", None, None), +(" + /** + * @bar foo (pass: invalid name but defined) + */ + function quux (foo) { + + } + ", Some(serde_json::json!([ + { + "definedTags": [ + "bar", + ], + }, + ])), None), +(" + /** + * @baz @bar foo (pass: invalid names but defined) + */ + function quux (foo) { + + } + ", Some(serde_json::json!([ + { + "definedTags": [ + "baz", "bar", + ], + }, + ])), None), +(" + /** + * @baz @bar foo (pass: invalid names but user preferred) + */ + function quux (foo) { + + } + ", None, Some(serde_json::json!({ + "settings": { "jsdoc": { + "tagNamePreference": { + "param": "baz", + "returns": { + "message": "Prefer `bar`", + "replacement": "bar", + }, + "todo": false, + }, + }}, + }))), +(" + /** + * @arg foo (pass: invalid name but user preferred) + */ + function quux (foo) { + + } + ", None, Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "param": "arg", + }, + }}, + }))), +(" + /** + * @returns (pass: valid name) + */ + function quux (foo) {} + ", None, None), +("", None, None), +(" + /** + * (pass: no tag) + */ + function quux (foo) { + + } + ", None, None), +(" + /** + * @todo (pass: valid name) + */ + function quux () { + + } + ", None, None), +(" + /** + * @extends Foo (pass: invalid name but user preferred) + */ + function quux () { + + } + ", None, Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "augments": { + "message": "@extends is to be used over @augments.", + "replacement": "extends", + }, + }, + }}, + }))), +(" + /** + * (Set tag name preference to itself to get aliases to + * work along with main tag name.) + * @augments Bar + * @extends Foo (pass: invalid name but user preferred) + */ + function quux () { + } + ", None, Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "extends": "extends", + }, + }}, + }))), +(" + /** + * Registers the `target` class as a transient dependency; each time the dependency is resolved a new instance will be created. + * + * @param target - The class / constructor function to register as transient. + * + * @example ```ts + @transient() + class Foo { } + ``` + * @param Time for a new tag (pass: valid names) + */ + export function transient(target?: T): T { + // ... + } + ", None, None), +(" + /** @jsx h */ + /** @jsxFrag Fragment */ + /** @jsxImportSource preact */ + /** @jsxRuntime automatic (pass: valid jsx names)*/ + ", Some(serde_json::json!([ + { + "jsxTags": true, + }, + ])), None), +(" + /** + * @internal (pass: valid name) + */ + ", None, Some(serde_json::json!({ + "settings" : { "jsdoc": { }}, + }))), +(" + /** + * @overload + * @satisfies (pass: valid names) + */ + ", None, Some(serde_json::json!({ + "settings" : { "jsdoc": { }}, + }))), + ( + " + /** + * @module + * A comment related to the module + */ + ", + None, + None, + ), + // Typed + (" + /** @default 0 */ + let a; + ", Some(serde_json::json!([ + { + "typed": true, + }, + ])), None), +(" + /** @template name */ + let a; + ", Some(serde_json::json!([ + { + "typed": true, + }, + ])), None), +(" + /** @param param - takes information */ + function takesOne(param) {} + ", Some(serde_json::json!([ + { + "typed": true, + }, + ])), None), + ]; + + let fail = vec![ + ( + " + /** @typoo {string} (fail: invalid name) */ + let a; + ", + None, + None, + ), + ( + " + /** + * @Param (fail: invalid name) + */ + function quux () { + + } + ", + None, + None, + ), + ( + " + /** + * @foo (fail: invalid name) + */ + function quux () { + + } + ", + None, + None, + ), + ( + " + /** + * @arg foo (fail: invalid name, default aliased) + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + " + /** + * @param foo (fail: valid name but user preferred) + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "param": "arg", + }, + }}, + })), + ), + ( + " + /** + * @constructor foo (fail: invalid name and user preferred) + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "constructor": "cons", + }, + }}, + })), + ), + ( + " + /** + * @arg foo (fail: invalid name and user preferred) + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "arg": "somethingDifferent", + }, + }}, + })), + ), + ( + " + /** + * @param foo (fail: valid name but user preferred) + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "param": "parameter", + }, + }}, + })), + ), + ( + " + /** + * @bar foo (fail: invalid name) + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + " + /** + * @baz @bar foo (fail: invalid name) + */ + function quux (foo) { + + } + ", + Some(serde_json::json!([ + { + "definedTags": [ + "bar", + ], + }, + ])), + None, + ), + ( + " + /** + * @bar + * @baz (fail: invalid name) + */ + function quux (foo) { + + } + ", + Some(serde_json::json!([ + { + "definedTags": [ + "bar", + ], + }, + ])), + None, + ), + ( + " + /** + * @todo (fail: valid name but blocked) + */ + function quux () { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "todo": false, + }, + }}, + })), + ), + ( + " + /** + * @todo (fail: valid name but blocked) + */ + function quux () { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "todo": { + "message": "Please resolve to-dos or add to the tracker", + }, + }, + }}, + })), + ), + ( + " + /** + * @todo (fail: valid name but blocked) + */ + function quux () { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "todo": { + "message": "Please use x-todo instead of todo", + "replacement": "x-todo", + }, + }, + }}, + })), + ), + ( + " + /** + * @property {object} a + * @prop {boolean} b (fail: invalid name, default aliased) + */ + function quux () { + + } + ", + None, + None, + ), + ( + " + /** + * @abc foo (fail: invalid name and user preferred) + * @abcd bar + */ + function quux () { + + } + ", + Some(serde_json::json!([ + { + "definedTags": [ + "abcd", + ], + }, + ])), + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "abc": "abcd", + }, + }}, + })), + ), + ( + " + /** + * @abc (fail: invalid name and user preferred) + * @abcd + */ + function quux () { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "abc": "abcd", + }, + }}, + })), + ), + ( + " + /** @jsx h */ + /** @jsxFrag Fragment */ + /** @jsxImportSource preact */ + /** @jsxRuntime automatic */ + ", + None, + None, + ), + ( + " + /** + * @constructor (fail: invalid name) + */ + function Test() { + this.works = false; + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "returns": "return", + }, + }}, + })), + ), + ( + " + /** + * @todo (fail: valid name but blocked) + */ + function quux () { + + } + ", + None, + Some(serde_json::json!({ + "settings" : { "jsdoc": { + "tagNamePreference": { + "todo": { + "message": "Please don't use todo", + }, + }, + }}, + })), + ), + // Typed + ( + " + /** + * @module + * A comment related to the module + */ + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + "/** @type {string} */let a; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** + * Existing comment. + * @type {string} + */ + let a; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** @typedef {Object} MyObject + * @property {string} id - my id + */ + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** + * @property {string} id - my id + */ + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** @typedef {Object} MyObject */ + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** @typedef {Object} MyObject + */ + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** @abstract */ + let a; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + const a = { + /** @abstract */ + b: true, + }; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** @template */ + let a; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** + * Prior description. + * + * @template + */ + let a; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ]; + + let dts_pass = vec![ + ( + " + /** @default 0 */ + declare let a; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** @abstract */ + let a; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** @abstract */ + declare let a; + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + /** @abstract */ + { declare let a; } + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ( + " + function test() { + /** @abstract */ + declare let a; + } + ", + Some(serde_json::json!([ + { + "typed": true, + }, + ])), + None, + ), + ]; + let dts_fail = vec![( + " + /** @typoo {string} (fail: invalid name) */ + let a; + ", + None, + None, + )]; + + Tester::new(CheckTagNames::NAME, pass, fail).test_and_snapshot(); + // Currently only 1 snapshot can be saved under a rule name + Tester::new(CheckTagNames::NAME, dts_pass, dts_fail).change_rule_path("test.d.ts").test(); +} diff --git a/crates/oxc_linter/src/snapshots/check_tag_names.snap b/crates/oxc_linter/src/snapshots/check_tag_names.snap new file mode 100644 index 0000000000000..05db33a0518f2 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/check_tag_names.snap @@ -0,0 +1,317 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: check_tag_names +--- + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:2:24] + 1 │ + 2 │ /** @typoo {string} (fail: invalid name) */ + · ────── + 3 │ let a; + ╰──── + help: `@typoo` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @Param (fail: invalid name) + · ────── + 4 │ */ + ╰──── + help: `@Param` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @foo (fail: invalid name) + · ──── + 4 │ */ + ╰──── + help: `@foo` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @arg foo (fail: invalid name, default aliased) + · ──── + 4 │ */ + ╰──── + help: Replace tag `@arg` with `@param`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @param foo (fail: valid name but user preferred) + · ────── + 4 │ */ + ╰──── + help: Replace tag `@param` with `@arg`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @constructor foo (fail: invalid name and user preferred) + · ──────────── + 4 │ */ + ╰──── + help: Replace tag `@constructor` with `@cons`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:34] + 2 │ /** + 3 │ * @arg foo (fail: invalid name and user preferred) + · ──── + 4 │ */ + ╰──── + help: Replace tag `@arg` with `@somethingDifferent`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @param foo (fail: valid name but user preferred) + · ────── + 4 │ */ + ╰──── + help: Replace tag `@param` with `@parameter`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @bar foo (fail: invalid name) + · ──── + 4 │ */ + ╰──── + help: `@bar` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @baz @bar foo (fail: invalid name) + · ──── + 4 │ */ + ╰──── + help: `@baz` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:4:27] + 3 │ * @bar + 4 │ * @baz (fail: invalid name) + · ──── + 5 │ */ + ╰──── + help: `@baz` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @todo (fail: valid name but blocked) + · ───── + 4 │ */ + ╰──── + help: Unexpected tag `@todo`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @todo (fail: valid name but blocked) + · ───── + 4 │ */ + ╰──── + help: Please resolve to-dos or add to the tracker + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @todo (fail: valid name but blocked) + · ───── + 4 │ */ + ╰──── + help: Please use x-todo instead of todo + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:4:25] + 3 │ * @property {object} a + 4 │ * @prop {boolean} b (fail: invalid name, default aliased) + · ───── + 5 │ */ + ╰──── + help: Replace tag `@prop` with `@property`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @abc foo (fail: invalid name and user preferred) + · ──── + 4 │ * @abcd bar + ╰──── + help: Replace tag `@abc` with `@abcd`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:34] + 2 │ /** + 3 │ * @abc (fail: invalid name and user preferred) + · ──── + 4 │ * @abcd + ╰──── + help: Replace tag `@abc` with `@abcd`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:2:24] + 1 │ + 2 │ /** @jsx h */ + · ──── + 3 │ /** @jsxFrag Fragment */ + ╰──── + help: `@jsx` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:24] + 2 │ /** @jsx h */ + 3 │ /** @jsxFrag Fragment */ + · ──────── + 4 │ /** @jsxImportSource preact */ + ╰──── + help: `@jsxFrag` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:4:24] + 3 │ /** @jsxFrag Fragment */ + 4 │ /** @jsxImportSource preact */ + · ──────────────── + 5 │ /** @jsxRuntime automatic */ + ╰──── + help: `@jsxImportSource` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:5:24] + 4 │ /** @jsxImportSource preact */ + 5 │ /** @jsxRuntime automatic */ + · ─────────── + 6 │ + ╰──── + help: `@jsxRuntime` is invalid tag name. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:21] + 2 │ /** + 3 │ * @constructor (fail: invalid name) + · ──────────── + 4 │ */ + ╰──── + help: Replace tag `@constructor` with `@class`. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:25] + 2 │ /** + 3 │ * @todo (fail: valid name but blocked) + · ───── + 4 │ */ + ╰──── + help: Please don't use todo + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:15] + 2 │ /** + 3 │ * @module + · ─────── + 4 │ * A comment related to the module + ╰──── + help: `@module` is redundant outside of ambient(`declare` or `.d.ts`) contexts when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:1:5] + 1 │ /** @type {string} */let a; + · ───── + 2 │ + ╰──── + help: `@type` is redundant when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:4:24] + 3 │ * Existing comment. + 4 │ * @type {string} + · ───── + 5 │ */ + ╰──── + help: `@type` is redundant when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:2:22] + 1 │ + 2 │ /** @typedef {Object} MyObject + · ──────── + 3 │ * @property {string} id - my id + ╰──── + help: `@typedef` is redundant when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:21] + 2 │ /** @typedef {Object} MyObject + 3 │ * @property {string} id - my id + · ───────── + 4 │ */ + ╰──── + help: `@property` is redundant when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:21] + 2 │ /** + 3 │ * @property {string} id - my id + · ───────── + 4 │ */ + ╰──── + help: `@property` is redundant when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:2:22] + 1 │ + 2 │ /** @typedef {Object} MyObject */ + · ──────── + 3 │ + ╰──── + help: `@typedef` is redundant when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:2:22] + 1 │ + 2 │ /** @typedef {Object} MyObject + · ──────── + 3 │ */ + ╰──── + help: `@typedef` is redundant when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:2:24] + 1 │ + 2 │ /** @abstract */ + · ───────── + 3 │ let a; + ╰──── + help: `@abstract` is redundant outside of ambient(`declare` or `.d.ts`) contexts when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:3:26] + 2 │ const a = { + 3 │ /** @abstract */ + · ───────── + 4 │ b: true, + ╰──── + help: `@abstract` is redundant outside of ambient(`declare` or `.d.ts`) contexts when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:2:24] + 1 │ + 2 │ /** @template */ + · ───────── + 3 │ let a; + ╰──── + help: `@template` without a name is redundant when using a type system. + + ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. + ╭─[check_tag_names.tsx:5:23] + 4 │ * + 5 │ * @template + · ───────── + 6 │ */ + ╰──── + help: `@template` without a name is redundant when using a type system.