diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index db30b32abc919..134610583b627 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_type;
pub mod require_property;
pub mod require_property_description;
pub mod require_property_name;
@@ -759,6 +760,7 @@ oxc_macros::declare_all_lint_rules! {
jsdoc::implements_on_classes,
jsdoc::no_defaults,
jsdoc::require_param,
+ jsdoc::require_param_type,
jsdoc::require_property,
jsdoc::require_property_type,
jsdoc::require_property_name,
diff --git a/crates/oxc_linter/src/rules/jsdoc/require_param.rs b/crates/oxc_linter/src/rules/jsdoc/require_param.rs
index 9dd8f40397c5a..e786060b11efd 100644
--- a/crates/oxc_linter/src/rules/jsdoc/require_param.rs
+++ b/crates/oxc_linter/src/rules/jsdoc/require_param.rs
@@ -1,13 +1,9 @@
use lazy_static::lazy_static;
-use oxc_ast::{
- ast::{BindingPattern, BindingPatternKind, Expression, FormalParameters, MethodDefinitionKind},
- AstKind,
-};
+use oxc_ast::{ast::MethodDefinitionKind, AstKind};
use oxc_diagnostics::LabeledSpan;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_semantic::{AstNode, JSDoc};
-use oxc_span::Span;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::Deserialize;
@@ -17,8 +13,8 @@ use crate::{
context::LintContext,
rule::Rule,
utils::{
- get_function_nearest_jsdoc_node, should_ignore_as_avoid, should_ignore_as_internal,
- should_ignore_as_private,
+ collect_params, get_function_nearest_jsdoc_node, should_ignore_as_avoid,
+ should_ignore_as_internal, should_ignore_as_private, ParamKind,
},
};
@@ -258,118 +254,6 @@ impl Rule for RequireParam {
}
}
-#[derive(Debug, Clone)]
-struct Param {
- span: Span,
- name: String,
- is_rest: bool,
-}
-
-#[derive(Debug, Clone)]
-enum ParamKind {
- Single(Param),
- Nested(Vec),
-}
-
-fn collect_params(params: &FormalParameters) -> Vec {
- // NOTE: Property level `is_rest` is implemented.
- // - fn(a, { b1, ...b2 })
- // ^^^^^
- // But Object|Array level `is_rest` is not implemented
- // - fn(a, ...{ b })
- // ^^^^ ^
- // Tests are not covering these cases...
- fn get_param_name(pattern: &BindingPattern, is_rest: bool) -> ParamKind {
- match &pattern.kind {
- BindingPatternKind::BindingIdentifier(ident) => {
- ParamKind::Single(Param { span: ident.span, name: ident.name.to_string(), is_rest })
- }
- BindingPatternKind::ObjectPattern(obj_pat) => {
- let mut collected = vec![];
-
- for prop in &obj_pat.properties {
- let Some(name) = prop.key.name() else { continue };
-
- match get_param_name(&prop.value, false) {
- ParamKind::Single(param) => {
- collected.push(Param { name: format!("{name}"), ..param });
- }
- ParamKind::Nested(params) => {
- collected.push(Param {
- span: prop.span,
- name: format!("{name}"),
- is_rest: false,
- });
-
- for param in params {
- collected.push(Param {
- name: format!("{name}.{}", param.name),
- ..param
- });
- }
- }
- }
- }
-
- if let Some(rest) = &obj_pat.rest {
- match get_param_name(&rest.argument, true) {
- ParamKind::Single(param) => collected.push(param),
- ParamKind::Nested(params) => collected.extend(params),
- }
- }
-
- ParamKind::Nested(collected)
- }
- BindingPatternKind::ArrayPattern(arr_pat) => {
- let mut collected = vec![];
-
- for (idx, elm) in arr_pat.elements.iter().enumerate() {
- let name = format!("\"{idx}\"");
-
- if let Some(pat) = elm {
- match get_param_name(pat, false) {
- ParamKind::Single(param) => collected.push(Param { name, ..param }),
- ParamKind::Nested(params) => collected.extend(params),
- }
- }
- }
-
- if let Some(rest) = &arr_pat.rest {
- match get_param_name(&rest.argument, true) {
- ParamKind::Single(param) => collected.push(param),
- ParamKind::Nested(params) => collected.extend(params),
- }
- }
-
- ParamKind::Nested(collected)
- }
- BindingPatternKind::AssignmentPattern(assign_pat) => match &assign_pat.right {
- Expression::Identifier(_) => get_param_name(&assign_pat.left, false),
- _ => {
- // TODO: If `config.useDefaultObjectProperties` = true,
- // collect default parameters from `assign_pat.right` like:
- // { prop = { a: 1, b: 2 }} => [prop, prop.a, prop.b]
- // get_param_name(&assign_pat.left, false)
- // }
- get_param_name(&assign_pat.left, false)
- }
- },
- }
- }
-
- let mut collected =
- params.items.iter().map(|param| get_param_name(¶m.pattern, false)).collect::>();
-
- if let Some(rest) = ¶ms.rest {
- match get_param_name(&rest.argument, true) {
- ParamKind::Single(param) => collected.push(ParamKind::Single(param)),
- ParamKind::Nested(params) => collected.push(ParamKind::Nested(params)),
- }
- }
-
- collected
-}
-
fn collect_tags<'a>(
jsdocs: &[JSDoc<'a>],
resolved_param_tag_name: &str,
diff --git a/crates/oxc_linter/src/rules/jsdoc/require_param_type.rs b/crates/oxc_linter/src/rules/jsdoc/require_param_type.rs
new file mode 100644
index 0000000000000..576bccce7d37a
--- /dev/null
+++ b/crates/oxc_linter/src/rules/jsdoc/require_param_type.rs
@@ -0,0 +1,255 @@
+use crate::{
+ context::LintContext,
+ rule::Rule,
+ utils::{
+ collect_params, get_function_nearest_jsdoc_node, should_ignore_as_internal,
+ should_ignore_as_private, ParamKind,
+ },
+ AstNode,
+};
+use oxc_ast::AstKind;
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn missing_type_diagnostic(span0: Span) -> OxcDiagnostic {
+ OxcDiagnostic::warn("eslint-plugin-jsdoc(require-param-type): Missing JSDoc `@param` type.")
+ .with_help("Add {type} to `@param` tag.")
+ .with_labels([span0.into()])
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct RequireParamType;
+
+declare_oxc_lint!(
+ /// ### What it does
+ /// Requires that each `@param` tag has a type value (within curly brackets).
+ ///
+ /// ### Why is this bad?
+ /// The type of a parameter should be documented.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// // Passing
+ /// /** @param {SomeType} foo */
+ /// function quux (foo) {}
+ ///
+ /// // Failing
+ /// /** @param foo */
+ /// function quux (foo) {}
+ /// ```
+ RequireParamType,
+ pedantic,
+);
+
+impl Rule for RequireParamType {
+ 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 (type_part, name_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 type exists, skip
+ if type_part.is_some() {
+ 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 {number} foo
+ */
+ function quux (foo) {
+
+ }
+ ",
+ None,
+ None,
+ ),
+ (
+ "
+ /**
+ * @function
+ * @param foo
+ */
+ ",
+ None,
+ None,
+ ),
+ (
+ "
+ /**
+ * @callback
+ * @param foo
+ */
+ ",
+ None,
+ None,
+ ),
+ (
+ "
+ /**
+ * @param {number} foo
+ * @param root
+ * @param {boolean} baz
+ */
+ function quux (foo, {bar}, baz) {
+
+ }
+ ",
+ None,
+ Some(
+ serde_json::json!({ "settings": { "jsdoc": { "exemptDestructuredRootsFromChecks": true, }, } }),
+ ),
+ ),
+ (
+ "
+ /**
+ * @param {number} foo
+ * @param root
+ * @param 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,
+ ),
+ (
+ "
+ /**
+ * @param {a xxx
+ */
+ function quux () {
+ }
+ ",
+ None,
+ None,
+ ),
+ (
+ "
+ /**
+ * @arg foo
+ */
+ function quux (foo) {
+
+ }
+ ",
+ None,
+ Some(
+ serde_json::json!({ "settings": { "jsdoc": { "tagNamePreference": { "param": "arg", }, }, } }),
+ ),
+ ),
+ (
+ "
+ /**
+ * @param {number} foo
+ * @param root
+ * @param {boolean} baz
+ */
+ function quux (foo, {bar}, baz) {
+
+ }
+ ",
+ Some(
+ serde_json::json!([ { "setDefaultDestructuredRootType": true, }, ]),
+ ),
+ None,
+ ),
+ (
+ "
+ /**
+ * @param {number} foo
+ * @param root
+ * @param {boolean} baz
+ */
+ function quux (foo, {bar}, baz) {
+
+ }
+ ",
+ Some(
+ serde_json::json!([ { "setDefaultDestructuredRootType": false, }, ]),
+ ),
+ None,
+ ),
+ ];
+
+ Tester::new(RequireParamType::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/require_param_type.snap b/crates/oxc_linter/src/snapshots/require_param_type.snap
new file mode 100644
index 0000000000000..98e7f68e6beab
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/require_param_type.snap
@@ -0,0 +1,48 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: require_param_type
+---
+ ⚠ eslint-plugin-jsdoc(require-param-type): Missing JSDoc `@param` type.
+ ╭─[require_param_type.tsx:3:17]
+ 2 │ /**
+ 3 │ * @param foo
+ · ──────
+ 4 │ */
+ ╰────
+ help: Add {type} to `@param` tag.
+
+ ⚠ eslint-plugin-jsdoc(require-param-type): Missing JSDoc `@param` type.
+ ╭─[require_param_type.tsx:3:13]
+ 2 │ /**
+ 3 │ * @param {a xxx
+ · ──────
+ 4 │ */
+ ╰────
+ help: Add {type} to `@param` tag.
+
+ ⚠ eslint-plugin-jsdoc(require-param-type): Missing JSDoc `@param` type.
+ ╭─[require_param_type.tsx:3:17]
+ 2 │ /**
+ 3 │ * @arg foo
+ · ────
+ 4 │ */
+ ╰────
+ help: Add {type} to `@param` tag.
+
+ ⚠ eslint-plugin-jsdoc(require-param-type): Missing JSDoc `@param` type.
+ ╭─[require_param_type.tsx:4:17]
+ 3 │ * @param {number} foo
+ 4 │ * @param root
+ · ──────
+ 5 │ * @param {boolean} baz
+ ╰────
+ help: Add {type} to `@param` tag.
+
+ ⚠ eslint-plugin-jsdoc(require-param-type): Missing JSDoc `@param` type.
+ ╭─[require_param_type.tsx:4:17]
+ 3 │ * @param {number} foo
+ 4 │ * @param root
+ · ──────
+ 5 │ * @param {boolean} baz
+ ╰────
+ help: Add {type} to `@param` tag.
diff --git a/crates/oxc_linter/src/utils/jsdoc.rs b/crates/oxc_linter/src/utils/jsdoc.rs
index 3d54d3877903b..3c3a6a0401c35 100644
--- a/crates/oxc_linter/src/utils/jsdoc.rs
+++ b/crates/oxc_linter/src/utils/jsdoc.rs
@@ -1,6 +1,10 @@
use crate::{config::JSDocPluginSettings, context::LintContext, AstNode};
-use oxc_ast::AstKind;
+use oxc_ast::{
+ ast::{BindingPattern, BindingPatternKind, Expression, FormalParameters},
+ AstKind,
+};
use oxc_semantic::JSDoc;
+use oxc_span::Span;
use rustc_hash::FxHashSet;
/// JSDoc is often attached on the parent node of a function.
@@ -113,3 +117,115 @@ pub fn should_ignore_as_avoid(
jsdoc.tags().iter().any(|tag| ignore_tag_names.contains(tag.kind.parsed()))
}
+
+#[derive(Debug, Clone)]
+pub struct Param {
+ pub span: Span,
+ pub name: String,
+ pub is_rest: bool,
+}
+
+#[derive(Debug, Clone)]
+pub enum ParamKind {
+ Single(Param),
+ Nested(Vec),
+}
+
+pub fn collect_params(params: &FormalParameters) -> Vec {
+ // NOTE: Property level `is_rest` is implemented.
+ // - fn(a, { b1, ...b2 })
+ // ^^^^^
+ // But Object|Array level `is_rest` is not implemented
+ // - fn(a, ...{ b })
+ // ^^^^ ^
+ // Tests are not covering these cases...
+ fn get_param_name(pattern: &BindingPattern, is_rest: bool) -> ParamKind {
+ match &pattern.kind {
+ BindingPatternKind::BindingIdentifier(ident) => {
+ ParamKind::Single(Param { span: ident.span, name: ident.name.to_string(), is_rest })
+ }
+ BindingPatternKind::ObjectPattern(obj_pat) => {
+ let mut collected = vec![];
+
+ for prop in &obj_pat.properties {
+ let Some(name) = prop.key.name() else { continue };
+
+ match get_param_name(&prop.value, false) {
+ ParamKind::Single(param) => {
+ collected.push(Param { name: format!("{name}"), ..param });
+ }
+ ParamKind::Nested(params) => {
+ collected.push(Param {
+ span: prop.span,
+ name: format!("{name}"),
+ is_rest: false,
+ });
+
+ for param in params {
+ collected.push(Param {
+ name: format!("{name}.{}", param.name),
+ ..param
+ });
+ }
+ }
+ }
+ }
+
+ if let Some(rest) = &obj_pat.rest {
+ match get_param_name(&rest.argument, true) {
+ ParamKind::Single(param) => collected.push(param),
+ ParamKind::Nested(params) => collected.extend(params),
+ }
+ }
+
+ ParamKind::Nested(collected)
+ }
+ BindingPatternKind::ArrayPattern(arr_pat) => {
+ let mut collected = vec![];
+
+ for (idx, elm) in arr_pat.elements.iter().enumerate() {
+ let name = format!("\"{idx}\"");
+
+ if let Some(pat) = elm {
+ match get_param_name(pat, false) {
+ ParamKind::Single(param) => collected.push(Param { name, ..param }),
+ ParamKind::Nested(params) => collected.extend(params),
+ }
+ }
+ }
+
+ if let Some(rest) = &arr_pat.rest {
+ match get_param_name(&rest.argument, true) {
+ ParamKind::Single(param) => collected.push(param),
+ ParamKind::Nested(params) => collected.extend(params),
+ }
+ }
+
+ ParamKind::Nested(collected)
+ }
+ BindingPatternKind::AssignmentPattern(assign_pat) => match &assign_pat.right {
+ Expression::Identifier(_) => get_param_name(&assign_pat.left, false),
+ _ => {
+ // TODO: If `config.useDefaultObjectProperties` = true,
+ // collect default parameters from `assign_pat.right` like:
+ // { prop = { a: 1, b: 2 }} => [prop, prop.a, prop.b]
+ // get_param_name(&assign_pat.left, false)
+ // }
+ get_param_name(&assign_pat.left, false)
+ }
+ },
+ }
+ }
+
+ let mut collected =
+ params.items.iter().map(|param| get_param_name(¶m.pattern, false)).collect::>();
+
+ if let Some(rest) = ¶ms.rest {
+ match get_param_name(&rest.argument, true) {
+ ParamKind::Single(param) => collected.push(ParamKind::Single(param)),
+ ParamKind::Nested(params) => collected.push(ParamKind::Nested(params)),
+ }
+ }
+
+ collected
+}