Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 17 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ mod nextjs {

/// <https://github.com/gajus/eslint-plugin-jsdoc>
mod jsdoc {
pub mod check_access;
pub mod empty_tags;
}

Expand Down Expand Up @@ -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,
}
363 changes: 363 additions & 0 deletions crates/oxc_linter/src/rules/jsdoc/check_access.rs
Original file line number Diff line number Diff line change
@@ -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();
}
Loading
Loading