feat(linter/jsdoc): Implement require-param-description (#3621)

Part of #1170

>
https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-param-description.md
This commit is contained in:
Yuji Sugiura 2024-06-11 17:31:53 +09:00 committed by GitHub
parent f6d9ca6e47
commit 110661cc80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 328 additions and 0 deletions

View file

@ -392,6 +392,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;
@ -762,6 +763,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,

View file

@ -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();
}

View file

@ -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.