diff --git a/docs/rules/ban_untagged_deprecation.md b/docs/rules/ban_untagged_deprecation.md new file mode 100644 index 00000000..2ed558b9 --- /dev/null +++ b/docs/rules/ban_untagged_deprecation.md @@ -0,0 +1,20 @@ +`@deprecated` tags must provide additional information, such as the reason for +deprecation or suggested alternatives. + +### Invalid: + +```typescript +/** + * @deprecated + */ +export function oldFunction(): void {} +``` + +### Valid: + +```typescript +/** + * @deprecated since version 2.0 + */ +export function oldFunction(): void {} +``` diff --git a/schemas/rules.v1.json b/schemas/rules.v1.json index 999d048b..66365af7 100644 --- a/schemas/rules.v1.json +++ b/schemas/rules.v1.json @@ -5,6 +5,7 @@ "ban-ts-comment", "ban-types", "ban-unknown-rule-code", + "ban-untagged-deprecation", "ban-untagged-ignore", "ban-untagged-todo", "ban-unused-ignore", diff --git a/src/rules.rs b/src/rules.rs index b02cce1d..0f292623 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -12,6 +12,7 @@ pub mod adjacent_overload_signatures; pub mod ban_ts_comment; pub mod ban_types; pub mod ban_unknown_rule_code; +pub mod ban_untagged_deprecation; pub mod ban_untagged_ignore; pub mod ban_untagged_todo; pub mod ban_unused_ignore; @@ -255,6 +256,7 @@ fn get_all_rules_raw() -> Vec> { Box::new(ban_ts_comment::BanTsComment), Box::new(ban_types::BanTypes), Box::new(ban_unknown_rule_code::BanUnknownRuleCode), + Box::new(ban_untagged_deprecation::BanUntaggedDeprecation), Box::new(ban_untagged_ignore::BanUntaggedIgnore), Box::new(ban_untagged_todo::BanUntaggedTodo), Box::new(ban_unused_ignore::BanUnusedIgnore), diff --git a/src/rules/ban_untagged_deprecation.rs b/src/rules/ban_untagged_deprecation.rs new file mode 100644 index 00000000..aad733d1 --- /dev/null +++ b/src/rules/ban_untagged_deprecation.rs @@ -0,0 +1,132 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use super::{Context, LintRule}; +use crate::Program; +use deno_ast::swc::common::comments::{Comment, CommentKind}; +use deno_ast::{SourceRange, SourceRangedForSpanned}; +use once_cell::sync::Lazy; +use regex::Regex; + +#[derive(Debug)] +pub struct BanUntaggedDeprecation; + +const CODE: &str = "ban-untagged-deprecation"; +const MESSAGE: &str = "The @deprecated tag must include descriptive text"; +const HINT: &str = + "Provide additional context for the @deprecated tag, e.g., '@deprecated since v2.0'"; + +impl LintRule for BanUntaggedDeprecation { + fn code(&self) -> &'static str { + CODE + } + + fn lint_program_with_ast_view( + &self, + context: &mut Context, + _program: Program, + ) { + for comment in context.all_comments() { + extract_violated_deprecation_ranges(comment) + .into_iter() + .for_each(|range| { + context.add_diagnostic_with_hint(range, CODE, MESSAGE, HINT) + }); + } + } + + #[cfg(feature = "docs")] + fn docs(&self) -> &'static str { + include_str!("../../docs/rules/ban_untagged_deprecation.md") + } +} + +/// Returns the ranges of invalid `@deprecated` comments in the given comment. +fn extract_violated_deprecation_ranges(comment: &Comment) -> Vec { + if !is_jsdoc_comment(comment) { + return Vec::new(); + } + + static INVALID_DEPRECATION_REGEX: Lazy = Lazy::new(|| { + Regex::new(r"(?m)^(?:.*\s+|\s*\*\s*)(@deprecated\s*?)$").unwrap() + }); + static BLOCK_COMMENT_OPEN_OFFSET: usize = 2; // Length of the "/*". + + INVALID_DEPRECATION_REGEX + .captures_iter(&comment.text) + .filter_map(|caps| caps.get(1)) + .map(|mat| { + let start = comment.start() + mat.start() + BLOCK_COMMENT_OPEN_OFFSET; + let end = comment.start() + mat.end() + BLOCK_COMMENT_OPEN_OFFSET; + SourceRange::new(start, end) + }) + .collect() +} + +/// Checks if the given comment is a JSDoc-style comment. +fn is_jsdoc_comment(comment: &Comment) -> bool { + comment.kind == CommentKind::Block && comment.text.starts_with('*') +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ban_untagged_deprecation_valid() { + assert_lint_ok! { + BanUntaggedDeprecation, + // @deprecated tag with additional context is valid. + r#"/** @deprecated since v2.0 */"#, + // @deprecated tag in the middle of comments with additional context is valid. + r#"/** + * @param foo - The input value. + * @public @deprecated since v2.0 + * @returns The computed result. + */"#, + // Line comments are not checked. + r#"// @deprecated "#, + // Non-JSDoc block comments are not checked. + r#"/* @deprecated */"#, + // More than two stars before @deprecated are not treated as JSDoc tag. + r#"/***@deprecated + **@deprecated + ***@deprecated +/"#, + // Invalid JSDoc tags are not treated as @deprecated. + r#"/** @deprecatedtmp */"#, + r#"/** tmp@deprecated */"#, + }; + } + + #[test] + fn ban_untagged_deprecation_invalid() { + assert_lint_err! { + BanUntaggedDeprecation, + // @deprecated tag without additional texts is invalid. + r#"/** @deprecated */"#: [{ col: 4, line: 1, message: MESSAGE, hint: HINT }], + r#"/** + *@deprecated + */"#: [{ col: 2, line: 2, message: MESSAGE, hint: HINT }], + r#"/** + * @deprecated + */"#: [{ col: 3, line: 2, message: MESSAGE, hint: HINT }], + r#"/** +@deprecated +*/"#: [{ col: 0, line: 2, message: MESSAGE, hint: HINT }], + r#"/** + @deprecated + */"#: [{ col: 3, line: 2, message: MESSAGE, hint: HINT }], + r#"/** + * This function is @deprecated + */"#: [{ col: 20, line: 2, message: MESSAGE, hint: HINT }], + // Multiple violations in a single JSDoc comment. + r#"/** +* @deprecated +* @deprecated +*/"#: [ + { col: 2, line: 2, message: MESSAGE, hint: HINT }, + { col: 2, line: 3, message: MESSAGE, hint: HINT }, + ], + } + } +} diff --git a/www/static/docs.json b/www/static/docs.json index bcaba751..8f32536d 100644 --- a/www/static/docs.json +++ b/www/static/docs.json @@ -27,6 +27,11 @@ "recommended" ] }, + { + "code": "ban-untagged-deprecation", + "docs": "`@deprecated` tags must provide additional information, such as the reason for\ndeprecation or suggested alternatives.\n\n### Invalid:\n\n```typescript\n/**\n * @deprecated\n */\nexport function oldFunction(): void {}\n```\n\n### Valid:\n\n```typescript\n/**\n * @deprecated since version 2.0\n */\nexport function oldFunction(): void {}\n```\n", + "tags": [] + }, { "code": "ban-untagged-ignore", "docs": "Requires `deno-lint-ignore` to be annotated with one or more rule names.\n\nIgnoring all rules can mask unexpected or future problems. Therefore you need to\nexplicitly specify which rule(s) are to be ignored.\n\n### Invalid:\n\n```typescript\n// deno-lint-ignore\nexport function duplicateArgumentsFn(a, b, a) {}\n```\n\n### Valid:\n\n```typescript\n// deno-lint-ignore no-dupe-args\nexport function duplicateArgumentsFn(a, b, a) {}\n```\n",