mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter/jsdoc): Implement no-defaults rule (#3098)
Part of #1170 > https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-defaults.md#repos-sticky-header
This commit is contained in:
parent
0185eb2edc
commit
5866086d17
4 changed files with 506 additions and 2 deletions
|
|
@ -364,6 +364,7 @@ mod jsdoc {
|
||||||
pub mod check_tag_names;
|
pub mod check_tag_names;
|
||||||
pub mod empty_tags;
|
pub mod empty_tags;
|
||||||
pub mod implements_on_classes;
|
pub mod implements_on_classes;
|
||||||
|
pub mod no_defaults;
|
||||||
pub mod require_property;
|
pub mod require_property;
|
||||||
pub mod require_property_description;
|
pub mod require_property_description;
|
||||||
pub mod require_property_name;
|
pub mod require_property_name;
|
||||||
|
|
@ -698,6 +699,7 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
jsdoc::check_tag_names,
|
jsdoc::check_tag_names,
|
||||||
jsdoc::empty_tags,
|
jsdoc::empty_tags,
|
||||||
jsdoc::implements_on_classes,
|
jsdoc::implements_on_classes,
|
||||||
|
jsdoc::no_defaults,
|
||||||
jsdoc::require_property,
|
jsdoc::require_property,
|
||||||
jsdoc::require_property_type,
|
jsdoc::require_property_type,
|
||||||
jsdoc::require_property_name,
|
jsdoc::require_property_name,
|
||||||
|
|
|
||||||
440
crates/oxc_linter/src/rules/jsdoc/no_defaults.rs
Normal file
440
crates/oxc_linter/src/rules/jsdoc/no_defaults.rs
Normal file
|
|
@ -0,0 +1,440 @@
|
||||||
|
use oxc_ast::AstKind;
|
||||||
|
use oxc_diagnostics::{
|
||||||
|
miette::{self, Diagnostic},
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
use oxc_macros::declare_oxc_lint;
|
||||||
|
use oxc_span::Span;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
#[error("eslint-plugin-jsdoc(no-defaults): Defaults are not permitted.")]
|
||||||
|
#[diagnostic(severity(warning), help("{1}"))]
|
||||||
|
struct NoDefaultsDiagnostic(#[label] pub Span, String);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct NoDefaults(Box<NoDefaultsConfig>);
|
||||||
|
|
||||||
|
declare_oxc_lint!(
|
||||||
|
/// ### What it does
|
||||||
|
/// This rule reports defaults being used on the relevant portion of `@param` or `@default`.
|
||||||
|
/// It also optionally reports the presence of the square-bracketed optional arguments at all.
|
||||||
|
///
|
||||||
|
/// ### Why is this bad?
|
||||||
|
/// The rule is intended to prevent the indication of defaults on tags
|
||||||
|
/// where this would be redundant with ES6 default parameters.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```javascript
|
||||||
|
/// // Passing
|
||||||
|
/// /** @param {number} foo */
|
||||||
|
/// function quux (foo) {}
|
||||||
|
/// /** @param foo */
|
||||||
|
/// function quux (foo) {}
|
||||||
|
///
|
||||||
|
/// // Failing
|
||||||
|
/// /** @param {number} [foo="7"] */
|
||||||
|
/// function quux (foo) {}
|
||||||
|
/// ```
|
||||||
|
NoDefaults,
|
||||||
|
correctness
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Deserialize)]
|
||||||
|
struct NoDefaultsConfig {
|
||||||
|
#[serde(default, rename = "noOptionalParamNames")]
|
||||||
|
no_optional_param_names: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the definition root node of a function.
|
||||||
|
/// JSDoc often appears on the parent node of a function.
|
||||||
|
///
|
||||||
|
/// ```js
|
||||||
|
/// /** FunctionDeclaration */
|
||||||
|
/// function foo() {}
|
||||||
|
///
|
||||||
|
/// /** VariableDeclaration > VariableDeclarator > FunctionExpression */
|
||||||
|
/// const bar = function() {}
|
||||||
|
///
|
||||||
|
/// /** VariableDeclaration > VariableDeclarator > ArrowFunctionExpression */
|
||||||
|
/// const baz = () => {}
|
||||||
|
///
|
||||||
|
/// /** MethodDefinition > FunctionExpression */
|
||||||
|
/// class X { quux() {} }
|
||||||
|
///
|
||||||
|
/// /** PropertyDefinition > ArrowFunctionExpression */
|
||||||
|
/// class X { quux = () => {} }
|
||||||
|
/// ```
|
||||||
|
fn get_function_definition_node<'a, 'b>(
|
||||||
|
node: &'b AstNode<'a>,
|
||||||
|
ctx: &'b LintContext<'a>,
|
||||||
|
) -> Option<&'b AstNode<'a>> {
|
||||||
|
match node.kind() {
|
||||||
|
AstKind::Function(f) if f.is_function_declaration() => return Some(node),
|
||||||
|
AstKind::Function(f) if f.is_expression() => {}
|
||||||
|
AstKind::ArrowFunctionExpression(_) => {}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut current_node = node;
|
||||||
|
while let Some(parent_node) = ctx.nodes().parent_node(current_node.id()) {
|
||||||
|
match parent_node.kind() {
|
||||||
|
AstKind::VariableDeclarator(_) | AstKind::ParenthesizedExpression(_) => {
|
||||||
|
current_node = parent_node;
|
||||||
|
}
|
||||||
|
AstKind::MethodDefinition(_)
|
||||||
|
| AstKind::PropertyDefinition(_)
|
||||||
|
| AstKind::VariableDeclaration(_) => return Some(parent_node),
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule for NoDefaults {
|
||||||
|
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<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||||
|
let Some(jsdocs) = get_function_definition_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 config = &self.0;
|
||||||
|
|
||||||
|
for jsdoc in jsdocs {
|
||||||
|
for tag in jsdoc.tags() {
|
||||||
|
let tag_name = tag.kind.parsed();
|
||||||
|
|
||||||
|
if tag_name != resolved_param_tag_name {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (_, Some(name_part), _) = tag.type_name_comment() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !name_part.optional {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match (config.no_optional_param_names, name_part.default) {
|
||||||
|
(true, _) => ctx.diagnostic(NoDefaultsDiagnostic(
|
||||||
|
name_part.span,
|
||||||
|
format!("Optional param names are not permitted on `@{tag_name}` tag."),
|
||||||
|
)),
|
||||||
|
(false, true) => ctx.diagnostic(NoDefaultsDiagnostic(
|
||||||
|
name_part.span,
|
||||||
|
format!("Defaults are not permitted on `@{tag_name}` tag."),
|
||||||
|
)),
|
||||||
|
(false, false) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
|
||||||
|
let pass = vec![
|
||||||
|
(
|
||||||
|
"
|
||||||
|
/**
|
||||||
|
* @param foo
|
||||||
|
*/
|
||||||
|
function quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
/**
|
||||||
|
* @param {number} foo
|
||||||
|
*/
|
||||||
|
function quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
/**
|
||||||
|
* @param {number} foo
|
||||||
|
* @param {number} [bar]
|
||||||
|
*/
|
||||||
|
function quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
// (
|
||||||
|
// "
|
||||||
|
// /**
|
||||||
|
// * @param foo
|
||||||
|
// */
|
||||||
|
// ",
|
||||||
|
// Some(serde_json::json!([
|
||||||
|
// {
|
||||||
|
// "contexts": [
|
||||||
|
// "any",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ])),
|
||||||
|
// None,
|
||||||
|
// ),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {number} foo
|
||||||
|
*/
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
/**
|
||||||
|
* @callback
|
||||||
|
* @param {number} foo
|
||||||
|
*/
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
/**
|
||||||
|
* @param {number} foo
|
||||||
|
*/
|
||||||
|
function quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
",
|
||||||
|
Some(serde_json::json!([
|
||||||
|
{
|
||||||
|
"noOptionalParamNames": true,
|
||||||
|
},
|
||||||
|
])),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
// (
|
||||||
|
// "
|
||||||
|
// /**
|
||||||
|
// * @default
|
||||||
|
// */
|
||||||
|
// const a = {};
|
||||||
|
// ",
|
||||||
|
// Some(serde_json::json!([
|
||||||
|
// {
|
||||||
|
// "contexts": [
|
||||||
|
// "any",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ])),
|
||||||
|
// None,
|
||||||
|
// ),
|
||||||
|
];
|
||||||
|
|
||||||
|
let fail = vec![
|
||||||
|
(
|
||||||
|
r#"
|
||||||
|
/**
|
||||||
|
* @param {number} [foo="7"]
|
||||||
|
*/
|
||||||
|
function quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
/**
|
||||||
|
* @param {number} foo
|
||||||
|
* @param {number} [bar = 1]
|
||||||
|
*/
|
||||||
|
const quux = (foo, bar = 1) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#"
|
||||||
|
class Test {
|
||||||
|
/**
|
||||||
|
* @param {number} [foo="7"]
|
||||||
|
*/
|
||||||
|
quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#"
|
||||||
|
/**
|
||||||
|
* @param {number} [foo="7"]
|
||||||
|
*/
|
||||||
|
function quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
Some(serde_json::json!([
|
||||||
|
{
|
||||||
|
"noOptionalParamNames": true,
|
||||||
|
},
|
||||||
|
])),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
/**
|
||||||
|
* @param {number} [foo]
|
||||||
|
*/
|
||||||
|
function quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
",
|
||||||
|
Some(serde_json::json!([
|
||||||
|
{
|
||||||
|
"noOptionalParamNames": true,
|
||||||
|
},
|
||||||
|
])),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#"
|
||||||
|
/**
|
||||||
|
* @arg {number} [foo="7"]
|
||||||
|
*/
|
||||||
|
function quux (foo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
None,
|
||||||
|
Some(serde_json::json!({ "settings": {
|
||||||
|
"jsdoc": {
|
||||||
|
"tagNamePreference": {
|
||||||
|
"param": "arg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} })),
|
||||||
|
),
|
||||||
|
// (
|
||||||
|
// r#"
|
||||||
|
// /**
|
||||||
|
// * @param {number} [foo="7"]
|
||||||
|
// */
|
||||||
|
// function quux (foo) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// "#,
|
||||||
|
// Some(serde_json::json!([
|
||||||
|
// {
|
||||||
|
// "contexts": [
|
||||||
|
// "any",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ])),
|
||||||
|
// None,
|
||||||
|
// ),
|
||||||
|
// (
|
||||||
|
// r#"
|
||||||
|
// /**
|
||||||
|
// * @function
|
||||||
|
// * @param {number} [foo="7"]
|
||||||
|
// */
|
||||||
|
// "#,
|
||||||
|
// Some(serde_json::json!([
|
||||||
|
// {
|
||||||
|
// "contexts": [
|
||||||
|
// "any",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ])),
|
||||||
|
// None,
|
||||||
|
// ),
|
||||||
|
// (
|
||||||
|
// r#"
|
||||||
|
// /**
|
||||||
|
// * @callback
|
||||||
|
// * @param {number} [foo="7"]
|
||||||
|
// */
|
||||||
|
// "#,
|
||||||
|
// Some(serde_json::json!([
|
||||||
|
// {
|
||||||
|
// "contexts": [
|
||||||
|
// "any",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ])),
|
||||||
|
// None,
|
||||||
|
// ),
|
||||||
|
// (
|
||||||
|
// "
|
||||||
|
// /**
|
||||||
|
// * @default {}
|
||||||
|
// */
|
||||||
|
// const a = {};
|
||||||
|
// ",
|
||||||
|
// Some(serde_json::json!([
|
||||||
|
// {
|
||||||
|
// "contexts": [
|
||||||
|
// "any",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ])),
|
||||||
|
// None,
|
||||||
|
// ),
|
||||||
|
// (
|
||||||
|
// "
|
||||||
|
// /**
|
||||||
|
// * @defaultvalue {}
|
||||||
|
// */
|
||||||
|
// const a = {};
|
||||||
|
// ",
|
||||||
|
// Some(serde_json::json!([
|
||||||
|
// {
|
||||||
|
// "contexts": [
|
||||||
|
// "any",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ])),
|
||||||
|
// Some(serde_json::json!({ "settings": {
|
||||||
|
// "jsdoc": {
|
||||||
|
// "tagNamePreference": {
|
||||||
|
// "default": "defaultvalue",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// } })),
|
||||||
|
// ),
|
||||||
|
];
|
||||||
|
|
||||||
|
Tester::new(NoDefaults::NAME, pass, fail).test_and_snapshot();
|
||||||
|
}
|
||||||
57
crates/oxc_linter/src/snapshots/no_defaults.snap
Normal file
57
crates/oxc_linter/src/snapshots/no_defaults.snap
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
source: crates/oxc_linter/src/tester.rs
|
||||||
|
expression: no_defaults
|
||||||
|
---
|
||||||
|
⚠ eslint-plugin-jsdoc(no-defaults): Defaults are not permitted.
|
||||||
|
╭─[no_defaults.tsx:3:33]
|
||||||
|
2 │ /**
|
||||||
|
3 │ * @param {number} [foo="7"]
|
||||||
|
· ─────────
|
||||||
|
4 │ */
|
||||||
|
╰────
|
||||||
|
help: Defaults are not permitted on `@param` tag.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsdoc(no-defaults): Defaults are not permitted.
|
||||||
|
╭─[no_defaults.tsx:4:33]
|
||||||
|
3 │ * @param {number} foo
|
||||||
|
4 │ * @param {number} [bar = 1]
|
||||||
|
· ─────────
|
||||||
|
5 │ */
|
||||||
|
╰────
|
||||||
|
help: Defaults are not permitted on `@param` tag.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsdoc(no-defaults): Defaults are not permitted.
|
||||||
|
╭─[no_defaults.tsx:4:33]
|
||||||
|
3 │ /**
|
||||||
|
4 │ * @param {number} [foo="7"]
|
||||||
|
· ─────────
|
||||||
|
5 │ */
|
||||||
|
╰────
|
||||||
|
help: Defaults are not permitted on `@param` tag.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsdoc(no-defaults): Defaults are not permitted.
|
||||||
|
╭─[no_defaults.tsx:3:33]
|
||||||
|
2 │ /**
|
||||||
|
3 │ * @param {number} [foo="7"]
|
||||||
|
· ─────────
|
||||||
|
4 │ */
|
||||||
|
╰────
|
||||||
|
help: Optional param names are not permitted on `@param` tag.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsdoc(no-defaults): Defaults are not permitted.
|
||||||
|
╭─[no_defaults.tsx:3:33]
|
||||||
|
2 │ /**
|
||||||
|
3 │ * @param {number} [foo]
|
||||||
|
· ─────
|
||||||
|
4 │ */
|
||||||
|
╰────
|
||||||
|
help: Optional param names are not permitted on `@param` tag.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsdoc(no-defaults): Defaults are not permitted.
|
||||||
|
╭─[no_defaults.tsx:3:31]
|
||||||
|
2 │ /**
|
||||||
|
3 │ * @arg {number} [foo="7"]
|
||||||
|
· ─────────
|
||||||
|
4 │ */
|
||||||
|
╰────
|
||||||
|
help: Defaults are not permitted on `@arg` tag.
|
||||||
|
|
@ -132,18 +132,23 @@ impl<'a> JSDocTagTypePart<'a> {
|
||||||
pub struct JSDocTagTypeNamePart<'a> {
|
pub struct JSDocTagTypeNamePart<'a> {
|
||||||
raw: &'a str,
|
raw: &'a str,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
|
pub optional: bool,
|
||||||
|
pub default: bool,
|
||||||
}
|
}
|
||||||
impl<'a> JSDocTagTypeNamePart<'a> {
|
impl<'a> JSDocTagTypeNamePart<'a> {
|
||||||
pub fn new(part_content: &'a str, span: Span) -> Self {
|
pub fn new(part_content: &'a str, span: Span) -> Self {
|
||||||
debug_assert!(part_content.trim() == part_content);
|
debug_assert!(part_content.trim() == part_content);
|
||||||
|
|
||||||
Self { raw: part_content, span }
|
let optional = part_content.starts_with('[') && part_content.ends_with(']');
|
||||||
|
let default = optional && part_content.contains('=');
|
||||||
|
|
||||||
|
Self { raw: part_content, span, optional, default }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the type name itself.
|
/// Returns the type name itself.
|
||||||
/// `.raw` may be like `[foo = var]`, so extract the name
|
/// `.raw` may be like `[foo = var]`, so extract the name
|
||||||
pub fn parsed(&self) -> &'a str {
|
pub fn parsed(&self) -> &'a str {
|
||||||
if self.raw.starts_with('[') {
|
if self.optional {
|
||||||
let inner = self.raw.trim_start_matches('[').trim_end_matches(']').trim();
|
let inner = self.raw.trim_start_matches('[').trim_end_matches(']').trim();
|
||||||
return inner.split_once('=').map_or(inner, |(v, _)| v.trim());
|
return inner.split_once('=').map_or(inner, |(v, _)| v.trim());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue