diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b35fa69c8..7bf86147d 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -353,6 +353,7 @@ mod nextjs { /// mod jsdoc { + pub mod check_access; pub mod empty_tags; } @@ -673,6 +674,7 @@ oxc_macros::declare_all_lint_rules! { nextjs::no_document_import_in_page, nextjs::no_unwanted_polyfillio, nextjs::no_before_interactive_script_outside_document, + jsdoc::check_access, jsdoc::empty_tags, tree_shaking::no_side_effects_in_initialization, } diff --git a/crates/oxc_linter/src/rules/jsdoc/check_access.rs b/crates/oxc_linter/src/rules/jsdoc/check_access.rs new file mode 100644 index 000000000..ade094f0e --- /dev/null +++ b/crates/oxc_linter/src/rules/jsdoc/check_access.rs @@ -0,0 +1,363 @@ +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use phf::phf_set; +use rustc_hash::FxHashSet; + +use crate::{context::LintContext, rule::Rule}; + +#[derive(Debug, Error, Diagnostic)] +enum CheckAccessDiagnostic { + #[error("eslint-plugin-jsdoc(check-access): Invalid access level is specified.")] + #[diagnostic( + severity(warning), + help("Valid access levels are `package`, `private`, `protected`, and `public`.") + )] + InvalidAccessLevel(#[label] Span), + + #[error("eslint-plugin-jsdoc(check-access): Mixing of @access with @public, @private, @protected, or @package on the same doc block.")] + #[diagnostic( + severity(warning), + help("There should be only one instance of access tag in a JSDoc comment.") + )] + RedundantAccessTags(#[label] Span), +} + +#[derive(Debug, Default, Clone)] +pub struct CheckAccess; + +declare_oxc_lint!( + /// ### What it does + /// Checks that `@access` tags use one of the following values: + /// - "package", "private", "protected", "public" + /// + /// Also reports: + /// - Mixing of `@access` with `@public`, `@private`, `@protected`, or `@package` on the same doc block. + /// - Use of multiple instances of `@access` (or the `@public`, etc) on the same doc block. + /// + /// ### Why is this bad? + /// It is important to have a consistent way of specifying access levels. + /// + /// ### Example + /// ```javascript + /// // Passing + /// /** @access private */ + /// + /// /** @private */ + /// + /// // Failing + /// /** @access private @public */ + /// + /// /** @access invalidlevel */ + /// ``` + CheckAccess, + restriction +); + +const ACCESS_LEVELS: phf::Set<&'static str> = phf_set! { + "package", + "private", + "protected", + "public", +}; + +impl Rule for CheckAccess { + fn run_once(&self, ctx: &LintContext) { + let settings = &ctx.settings().jsdoc; + let resolved_access_tag_name = settings.resolve_tag_name("access"); + + let mut access_related_tag_names = FxHashSet::default(); + access_related_tag_names.insert(resolved_access_tag_name.to_string()); + for level in &ACCESS_LEVELS { + access_related_tag_names.insert(settings.resolve_tag_name(level)); + } + + for jsdoc in ctx.semantic().jsdoc().iter_all() { + let mut access_related_tags_count = 0; + for (span, tag) in jsdoc.tags() { + if access_related_tag_names.contains(tag.kind) { + access_related_tags_count += 1; + } + + // Has valid access level? + if tag.kind == resolved_access_tag_name && !ACCESS_LEVELS.contains(&tag.comment()) { + ctx.diagnostic(CheckAccessDiagnostic::InvalidAccessLevel(*span)); + } + + // Has redundant access level? + if 1 < access_related_tags_count { + ctx.diagnostic(CheckAccessDiagnostic::RedundantAccessTags(*span)); + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + r" + /** + * + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + r" + /** + * @access public + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + r" + /** + * @accessLevel package + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "jsdoc": { + "tagNamePreference": { + "access": "accessLevel", + }, + }, + })), + ), + ( + r" + class MyClass { + /** + * @access private + */ + myClassField = 1 + } + ", + None, + None, + ), + ( + r" + /** + * @public + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + r" + /** + * @private + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "jsdoc": { + "ignorePrivate": true, + }, + })), + ), + ( + r" + (function(exports, require, module, __filename, __dirname) { + // Module code actually lives in here + }); + ", + None, + None, + ), + ]; + + let fail = vec![ + ( + r" + /** + * @access foo + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + r" + /** + * @access foo + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "jsdoc": { + "ignorePrivate": true, + }, + })), + ), + ( + r" + /** + * @accessLevel foo + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "jsdoc": { + "tagNamePreference": { + "access": "accessLevel", + }, + }, + })), + ), + ( + r" + /** + * @access + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "jsdoc": { + "tagNamePreference": { + "access": false, + }, + }, + })), + ), + ( + r" + class MyClass { + /** + * @access + */ + myClassField = 1 + } + ", + None, + None, + ), + ( + r" + /** + * @access public + * @public + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + r" + /** + * @access public + * @access private + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + r" + /** + * @access public + * @access private + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "jsdoc": { + "ignorePrivate": true, + }, + })), + ), + ( + r" + /** + * @public + * @private + */ + function quux (foo) { + + } + ", + None, + None, + ), + ( + r" + /** + * @public + * @private + */ + function quux (foo) { + + } + ", + None, + Some(serde_json::json!({ + "jsdoc": { + "ignorePrivate": true, + }, + })), + ), + ( + r" + /** + * @public + * @public + */ + function quux (foo) { + + } + ", + None, + None, + ), + ]; + + Tester::new(CheckAccess::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/check_access.snap b/crates/oxc_linter/src/snapshots/check_access.snap new file mode 100644 index 000000000..dcb89a397 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/check_access.snap @@ -0,0 +1,102 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: check_access +--- + ⚠ eslint-plugin-jsdoc(check-access): Invalid access level is specified. + ╭─[check_access.tsx:3:17] + 2 │ /** + 3 │ * @access foo + · ─────── + 4 │ */ + ╰──── + help: Valid access levels are `package`, `private`, `protected`, and `public`. + + ⚠ eslint-plugin-jsdoc(check-access): Invalid access level is specified. + ╭─[check_access.tsx:3:17] + 2 │ /** + 3 │ * @access foo + · ─────── + 4 │ */ + ╰──── + help: Valid access levels are `package`, `private`, `protected`, and `public`. + + ⚠ eslint-plugin-jsdoc(check-access): Invalid access level is specified. + ╭─[check_access.tsx:3:25] + 2 │ /** + 3 │ * @accessLevel foo + · ──────────── + 4 │ */ + ╰──── + help: Valid access levels are `package`, `private`, `protected`, and `public`. + + ⚠ eslint-plugin-jsdoc(check-access): Invalid access level is specified. + ╭─[check_access.tsx:3:17] + 2 │ /** + 3 │ * @access + · ─────── + 4 │ */ + ╰──── + help: Valid access levels are `package`, `private`, `protected`, and `public`. + + ⚠ eslint-plugin-jsdoc(check-access): Invalid access level is specified. + ╭─[check_access.tsx:4:15] + 3 │ /** + 4 │ * @access + · ─────── + 5 │ */ + ╰──── + help: Valid access levels are `package`, `private`, `protected`, and `public`. + + ⚠ eslint-plugin-jsdoc(check-access): Mixing of @access with @public, @private, @protected, or @package on the same doc block. + ╭─[check_access.tsx:4:17] + 3 │ * @access public + 4 │ * @public + · ─────── + 5 │ */ + ╰──── + help: There should be only one instance of access tag in a JSDoc comment. + + ⚠ eslint-plugin-jsdoc(check-access): Mixing of @access with @public, @private, @protected, or @package on the same doc block. + ╭─[check_access.tsx:4:17] + 3 │ * @access public + 4 │ * @access private + · ─────── + 5 │ */ + ╰──── + help: There should be only one instance of access tag in a JSDoc comment. + + ⚠ eslint-plugin-jsdoc(check-access): Mixing of @access with @public, @private, @protected, or @package on the same doc block. + ╭─[check_access.tsx:4:17] + 3 │ * @access public + 4 │ * @access private + · ─────── + 5 │ */ + ╰──── + help: There should be only one instance of access tag in a JSDoc comment. + + ⚠ eslint-plugin-jsdoc(check-access): Mixing of @access with @public, @private, @protected, or @package on the same doc block. + ╭─[check_access.tsx:4:17] + 3 │ * @public + 4 │ * @private + · ──────── + 5 │ */ + ╰──── + help: There should be only one instance of access tag in a JSDoc comment. + + ⚠ eslint-plugin-jsdoc(check-access): Mixing of @access with @public, @private, @protected, or @package on the same doc block. + ╭─[check_access.tsx:4:17] + 3 │ * @public + 4 │ * @private + · ──────── + 5 │ */ + ╰──── + help: There should be only one instance of access tag in a JSDoc comment. + + ⚠ eslint-plugin-jsdoc(check-access): Mixing of @access with @public, @private, @protected, or @package on the same doc block. + ╭─[check_access.tsx:4:17] + 3 │ * @public + 4 │ * @public + · ─────── + 5 │ */ + ╰──── + help: There should be only one instance of access tag in a JSDoc comment. diff --git a/crates/oxc_semantic/src/jsdoc/mod.rs b/crates/oxc_semantic/src/jsdoc/mod.rs index 0caa878fb..801a5e87f 100644 --- a/crates/oxc_semantic/src/jsdoc/mod.rs +++ b/crates/oxc_semantic/src/jsdoc/mod.rs @@ -4,3 +4,5 @@ mod parser; pub use builder::JSDocBuilder; pub use finder::JSDocFinder; +pub use parser::JSDoc; +pub use parser::JSDocTag; diff --git a/crates/oxc_semantic/src/jsdoc/parser/mod.rs b/crates/oxc_semantic/src/jsdoc/parser/mod.rs index 56999c1d5..65c45ff0d 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/mod.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/mod.rs @@ -4,3 +4,4 @@ mod parse; mod utils; pub use jsdoc::JSDoc; +pub use jsdoc_tag::JSDocTag; diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 69457251a..d7f3182c9 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -19,7 +19,7 @@ pub use petgraph; pub use builder::{SemanticBuilder, SemanticBuilderReturn}; use class::ClassTable; -pub use jsdoc::JSDocFinder; +pub use jsdoc::{JSDoc, JSDocFinder, JSDocTag}; use oxc_ast::{ast::IdentifierReference, AstKind, Trivias}; use oxc_span::SourceType; pub use oxc_syntax::{