diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 134610583b627..3e79188cbb119 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -391,6 +391,7 @@ mod jsdoc { pub mod implements_on_classes; pub mod no_defaults; pub mod require_param; + pub mod require_param_description; pub mod require_param_type; pub mod require_property; pub mod require_property_description; @@ -760,6 +761,7 @@ oxc_macros::declare_all_lint_rules! { jsdoc::implements_on_classes, jsdoc::no_defaults, jsdoc::require_param, + jsdoc::require_param_description, jsdoc::require_param_type, jsdoc::require_property, jsdoc::require_property_type, diff --git a/crates/oxc_linter/src/rules/jsdoc/require_param_description.rs b/crates/oxc_linter/src/rules/jsdoc/require_param_description.rs new file mode 100644 index 0000000000000..1c3d386988128 --- /dev/null +++ b/crates/oxc_linter/src/rules/jsdoc/require_param_description.rs @@ -0,0 +1,278 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{ + collect_params, get_function_nearest_jsdoc_node, should_ignore_as_internal, + should_ignore_as_private, ParamKind, + }, + AstNode, +}; + +fn missing_type_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn( + "eslint-plugin-jsdoc(require-param-description): Missing JSDoc `@param` description.", + ) + .with_help("Add description to `@param` tag.") + .with_labels([span0.into()]) +} + +#[derive(Debug, Default, Clone)] +pub struct RequireParamDescription; + +declare_oxc_lint!( + /// ### What it does + /// Requires that each `@param` tag has a description value. + /// + /// ### Why is this bad? + /// The description of a param should be documented. + /// + /// ### Example + /// ```javascript + /// // Passing + /// /** @param foo Foo. */ + /// function quux (foo) {} + /// + /// // Failing + /// /** @param foo */ + /// function quux (foo) {} + /// ``` + RequireParamDescription, + pedantic, +); + +impl Rule for RequireParamDescription { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + // Collected targets from `FormalParameters` + let params_to_check = match node.kind() { + AstKind::Function(func) if !func.is_typescript_syntax() => collect_params(&func.params), + AstKind::ArrowFunctionExpression(arrow_func) => collect_params(&arrow_func.params), + // If not a function, skip + _ => return, + }; + + // If no JSDoc is found, skip + let Some(jsdocs) = get_function_nearest_jsdoc_node(node, ctx) + .and_then(|node| ctx.jsdoc().get_all_by_node(node)) + else { + return; + }; + + let settings = &ctx.settings().jsdoc; + let resolved_param_tag_name = settings.resolve_tag_name("param"); + + let mut root_count = 0; + for jsdoc in jsdocs + .iter() + .filter(|jsdoc| !should_ignore_as_internal(jsdoc, settings)) + .filter(|jsdoc| !should_ignore_as_private(jsdoc, settings)) + { + for tag in jsdoc.tags() { + if tag.kind.parsed() != resolved_param_tag_name { + continue; + } + + let (_, name_part, comment_part) = tag.type_name_comment(); + + if name_part.is_some_and(|name_part| !name_part.parsed().contains('.')) { + root_count += 1; + } + if settings.exempt_destructured_roots_from_checks { + // -1 for count to idx conversion + if let Some(ParamKind::Nested(_)) = params_to_check.get(root_count - 1) { + continue; + } + } + + // If description exists, skip + if !comment_part.parsed().is_empty() { + continue; + }; + + ctx.diagnostic(missing_type_diagnostic(tag.kind.span)); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + " + /** + * + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + " + /** + * @param foo Foo. + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + " + /** + * @function + * @param foo + */ + ", + None, + None, + ), + ( + " + /** + * @callback + * @param foo + */ + ", + None, + None, + ), + ( + " + /** + * Checks if the XML document sort of equals another XML document. + * @param {Object} obj The other object. + * @param {{includeWhiteSpace: (boolean|undefined), + * ignoreElementOrder: (boolean|undefined)}} [options] The options. + * @return {expect.Assertion} The assertion. + */ + expect.Assertion.prototype.xmleql = function (obj, options) { + } + ", + None, + None, + ), + ( + " + /** + * @param {number} foo Foo description + * @param {object} root + * @param {boolean} baz Baz description + */ + function quux (foo, {bar}, baz) { + + } + ", + None, + Some( + serde_json::json!({ "settings": { "jsdoc": { "exemptDestructuredRootsFromChecks": true, }, } }), + ), + ), + ( + " + /** + * @param {number} foo Foo description + * @param {object} root + * @param {object} root.bar + */ + function quux (foo, {bar: {baz}}) { + + } + ", + None, + Some( + serde_json::json!({ "settings": { "jsdoc": { "exemptDestructuredRootsFromChecks": true, }, } }), + ), + ), + ]; + + let fail = vec![ + ( + " + /** + * @param foo + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + " + /** + * @arg foo + */ + function quux (foo) { + + } + ", + None, + Some( + serde_json::json!({ "settings": { "jsdoc": { "tagNamePreference": { "param": "arg", }, }, } }), + ), + ), + ( + " + /** + * @param {number} foo Foo description + * @param {object} root + * @param {boolean} baz Baz description + */ + function quux (foo, {bar}, baz) { + + } + ", + Some( + serde_json::json!([ { "setDefaultDestructuredRootDescription": true, }, ]), + ), + None, + ), + ( + " + /** + * @param {number} foo Foo description + * @param {object} root + * @param {boolean} baz Baz description + */ + function quux (foo, {bar}, baz) { + + } + ", + Some( + serde_json::json!([ { "defaultDestructuredRootDescription": "Root description", "setDefaultDestructuredRootDescription": true, }, ]), + ), + None, + ), + ( + " + /** + * @param {number} foo Foo description + * @param {object} root + * @param {boolean} baz Baz description + */ + function quux (foo, {bar}, baz) { + + } + ", + Some( + serde_json::json!([ { "setDefaultDestructuredRootDescription": false, }, ]), + ), + None, + ), + ]; + + Tester::new(RequireParamDescription::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/require_param_description.snap b/crates/oxc_linter/src/snapshots/require_param_description.snap new file mode 100644 index 0000000000000..01af7b44dc38f --- /dev/null +++ b/crates/oxc_linter/src/snapshots/require_param_description.snap @@ -0,0 +1,48 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: require_param_description +--- + ⚠ eslint-plugin-jsdoc(require-param-description): Missing JSDoc `@param` description. + ╭─[require_param_description.tsx:3:17] + 2 │ /** + 3 │ * @param foo + · ────── + 4 │ */ + ╰──── + help: Add description to `@param` tag. + + ⚠ eslint-plugin-jsdoc(require-param-description): Missing JSDoc `@param` description. + ╭─[require_param_description.tsx:3:17] + 2 │ /** + 3 │ * @arg foo + · ──── + 4 │ */ + ╰──── + help: Add description to `@param` tag. + + ⚠ eslint-plugin-jsdoc(require-param-description): Missing JSDoc `@param` description. + ╭─[require_param_description.tsx:4:17] + 3 │ * @param {number} foo Foo description + 4 │ * @param {object} root + · ────── + 5 │ * @param {boolean} baz Baz description + ╰──── + help: Add description to `@param` tag. + + ⚠ eslint-plugin-jsdoc(require-param-description): Missing JSDoc `@param` description. + ╭─[require_param_description.tsx:4:17] + 3 │ * @param {number} foo Foo description + 4 │ * @param {object} root + · ────── + 5 │ * @param {boolean} baz Baz description + ╰──── + help: Add description to `@param` tag. + + ⚠ eslint-plugin-jsdoc(require-param-description): Missing JSDoc `@param` description. + ╭─[require_param_description.tsx:4:17] + 3 │ * @param {number} foo Foo description + 4 │ * @param {object} root + · ────── + 5 │ * @param {boolean} baz Baz description + ╰──── + help: Add description to `@param` tag.