feat(linter): Implement jsdoc/check-access (#2642)

Part of #1170, Finally... 🗻 

Some preparation PRs may be needed in advance.

- [x] settings.jsdoc #2706 
- [x] new struct design #2765
- [x] handle `Span` for diagnostics #2815

Implement plugin itself.

-
https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-access.md
-
https://github.com/gajus/eslint-plugin-jsdoc/blob/main/src/rules/checkAccess.js

I'll send a PR to make this plugin public after confirming that a few
more rules can be implemented without any problems.
This commit is contained in:
Yuji Sugiura 2024-04-04 23:36:39 +09:00 committed by GitHub
parent 6823482ad6
commit aa63b6491f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 471 additions and 1 deletions

View file

@ -353,6 +353,7 @@ mod nextjs {
/// <https://github.com/gajus/eslint-plugin-jsdoc>
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,
}

View file

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

View file

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

View file

@ -4,3 +4,5 @@ mod parser;
pub use builder::JSDocBuilder;
pub use finder::JSDocFinder;
pub use parser::JSDoc;
pub use parser::JSDocTag;

View file

@ -4,3 +4,4 @@ mod parse;
mod utils;
pub use jsdoc::JSDoc;
pub use jsdoc_tag::JSDocTag;

View file

@ -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::{