Yuji Sugiura 2024-04-16 14:55:33 +09:00 committed by GitHub
parent 6a53fa367b
commit df2036eea0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 562 additions and 0 deletions

View file

@ -359,6 +359,7 @@ mod nextjs {
/// <https://github.com/gajus/eslint-plugin-jsdoc>
mod jsdoc {
pub mod check_access;
pub mod check_property_names;
pub mod empty_tags;
}
@ -685,6 +686,7 @@ oxc_macros::declare_all_lint_rules! {
nextjs::no_unwanted_polyfillio,
nextjs::no_before_interactive_script_outside_document,
jsdoc::check_access,
jsdoc::check_property_names,
jsdoc::empty_tags,
tree_shaking::no_side_effects_in_initialization,
}

View file

@ -0,0 +1,422 @@
use miette::{miette, LabeledSpan};
use oxc_diagnostics::{
miette::{self, Diagnostic, Severity},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{context::LintContext, rule::Rule};
#[derive(Debug, Error, Diagnostic)]
enum CheckPropertyNamesDiagnostic {
#[error("eslint-plugin-jsdoc(check-property-names): No root defined for @property path.")]
#[diagnostic(
severity(warning),
help("@property path declaration `{1}` appears before any real property.")
)]
NoRoot(#[label] Span, String),
}
#[derive(Debug, Default, Clone)]
pub struct CheckPropertyNames;
declare_oxc_lint!(
/// ### What it does
/// Ensures that property names in JSDoc are not duplicated on the same block and that nested properties have defined roots.
///
/// ### Why is this bad?
/// `@property` tags with the same name can be confusing and may indicate a mistake.
///
/// ### Example
/// ```javascript
/// // Passing
/// /**
/// * @typedef {object} state
/// * @property {number} foo
/// */
/// /**
/// * @typedef {object} state
/// * @property {object} foo
/// * @property {number} foo.bar
/// */
///
/// // Failing
/// /**
/// * @typedef {object} state
/// * @property {number} foo
/// * @property {string} foo
/// */
///
/// /**
/// * @typedef {object} state
/// * @property {number} foo.bar
/// */
/// ```
CheckPropertyNames,
correctness
);
impl Rule for CheckPropertyNames {
fn run_once(&self, ctx: &LintContext) {
let settings = &ctx.settings().jsdoc;
let resolved_property_tag_name = settings.resolve_tag_name("property");
for jsdoc in ctx.semantic().jsdoc().iter_all() {
let mut seen: FxHashMap<&str, FxHashSet<Span>> = FxHashMap::default();
for tag in jsdoc.tags() {
if tag.kind.parsed() != resolved_property_tag_name {
continue;
}
let (_, name_part, _) = tag.type_name_comment();
let Some(name_part) = name_part else {
continue;
};
let type_name = name_part.parsed();
// Check property path has a root
if type_name.contains('.') {
let mut parts = type_name.split('.').collect::<Vec<_>>();
// `foo[].bar` -> `foo[]`
parts.pop();
let parent_name = parts.join(".");
// `foo[]` -> `foo`
let parent_name = parent_name.trim_end_matches("[]");
if !seen.contains_key(&parent_name) {
ctx.diagnostic(CheckPropertyNamesDiagnostic::NoRoot(
name_part.span,
type_name.to_string(),
));
}
}
// Check duplicated(report later)
seen.entry(type_name).or_default().insert(name_part.span);
}
for (type_name, spans) in seen.iter().filter(|(_, spans)| 1 < spans.len()) {
ctx.diagnostic(miette!(
severity = Severity::Warning,
labels = spans
.iter()
.map(|span| LabeledSpan::at(
(span.start as usize)..(span.end as usize),
"Duplicated property".to_string(),
))
.collect::<Vec<_>>(),
help = format!("@property `{type_name}` is duplicated on the same block."),
"eslint-plugin-jsdoc(check-property-names): Duplicate @property found."
));
}
}
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
(
"
/**
*
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property foo
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @prop foo
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property foo
* @property bar
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property foo
* @property foo.foo
* @property bar
*/
",
None,
None,
),
(
"
/**
* Assign the project to a list of employees.
* @typedef {SomeType} SomeTypedef
* @property {object[]} employees - The employees who are responsible for the project.
* @property {string} employees[].name - The name of an employee.
* @property {string} employees[].department - The employee's department.
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property {Error} error Exit code
* @property {number} [code = 1] Exit code
*/
",
None,
None,
),
(
"
/**
* @namespace {SomeType} SomeNamespace
* @property {Error} error Exit code
* @property {number} [code = 1] Exit code
*/
",
None,
None,
),
(
"
/**
* @class
* @property {Error} error Exit code
* @property {number} [code = 1] Exit code
*/
function quux (code = 1) {
this.error = new Error('oops');
this.code = code;
}
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property foo
* @property foo.bar
* @property foo.baz
* @property bar
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property foo
* @property foo.bar
* @property foo.bar.baz
* @property foo.bar.baz.qux
* @property bar
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property {object[]} foo
* @property {object[]} foo[].bar
* @property {number} foo[].bar[].baz
* @property bar
*/
",
None,
None,
),
];
let fail = vec![
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property Foo.Bar
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property foo
* @property Foo.Bar
*/
",
None,
None,
),
(
"
/**
* Assign the project to a list of employees.
* @typedef {SomeType} SomeTypedef
* @property {string} employees[].name - The name of an employee.
* @property {string} employees[].department - The employee's department.
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property foo
* @property foo
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property cfg
* @property cfg.foo
* @property cfg.foo
*/
function quux ({foo, bar}) {
}
",
None,
None,
),
(
"
class Test {
/**
* @typedef {SomeType} SomeTypedef
* @property cfg
* @property cfg.foo
* @property cfg.foo
* @property cfg.foo
*/
quux ({foo, bar}) {
}
}
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property cfg
* @property cfg.foo
* @property [cfg.foo]
* @property baz
*/
function quux ({foo, bar}, baz) {
}
",
None,
None,
),
(
r#"
/**
* @typedef {SomeType} SomeTypedef
* @property cfg
* @property cfg.foo
* @property [cfg.foo="with a default"]
* @property baz
*/
function quux ({foo, bar}, baz) {
}
"#,
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property foo
* @property foo.bar
* @property foo.bar.baz.qux
* @property bar
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @property {object[]} foo
* @property {number} foo[].bar[].baz
* @property bar
*/
",
None,
None,
),
(
"
/**
* @typedef {SomeType} SomeTypedef
* @prop foo
* @prop foo
*/
",
None,
Some(serde_json::json!({
"jsdoc": {
"tagNamePreference": {
"property": "prop",
},
},
})),
),
];
Tester::new(CheckPropertyNames::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,138 @@
---
source: crates/oxc_linter/src/tester.rs
expression: check_property_names
---
⚠ eslint-plugin-jsdoc(check-property-names): No root defined for @property path.
╭─[check_property_names.tsx:4:27]
3 │ * @typedef {SomeType} SomeTypedef
4 │ * @property Foo.Bar
· ───────
5 │ */
╰────
help: @property path declaration `Foo.Bar` appears before any real property.
⚠ eslint-plugin-jsdoc(check-property-names): No root defined for @property path.
╭─[check_property_names.tsx:5:27]
4 │ * @property foo
5 │ * @property Foo.Bar
· ───────
6 │ */
╰────
help: @property path declaration `Foo.Bar` appears before any real property.
⚠ eslint-plugin-jsdoc(check-property-names): No root defined for @property path.
╭─[check_property_names.tsx:5:36]
4 │ * @typedef {SomeType} SomeTypedef
5 │ * @property {string} employees[].name - The name of an employee.
· ────────────────
6 │ * @property {string} employees[].department - The employee's department.
╰────
help: @property path declaration `employees[].name` appears before any real property.
⚠ eslint-plugin-jsdoc(check-property-names): No root defined for @property path.
╭─[check_property_names.tsx:6:36]
5 │ * @property {string} employees[].name - The name of an employee.
6 │ * @property {string} employees[].department - The employee's department.
· ──────────────────────
7 │ */
╰────
help: @property path declaration `employees[].department` appears before any real property.
⚠ eslint-plugin-jsdoc(check-property-names): Duplicate @property found.
╭─[check_property_names.tsx:4:27]
3 │ * @typedef {SomeType} SomeTypedef
4 │ * @property foo
· ─┬─
· ╰── Duplicated property
5 │ * @property foo
· ─┬─
· ╰── Duplicated property
6 │ */
╰────
help: @property `foo` is duplicated on the same block.
⚠ eslint-plugin-jsdoc(check-property-names): Duplicate @property found.
╭─[check_property_names.tsx:5:27]
4 │ * @property cfg
5 │ * @property cfg.foo
· ───┬───
· ╰── Duplicated property
6 │ * @property cfg.foo
· ───┬───
· ╰── Duplicated property
7 │ */
╰────
help: @property `cfg.foo` is duplicated on the same block.
⚠ eslint-plugin-jsdoc(check-property-names): Duplicate @property found.
╭─[check_property_names.tsx:6:27]
5 │ * @property cfg
6 │ * @property cfg.foo
· ───┬───
· ╰── Duplicated property
7 │ * @property cfg.foo
· ───┬───
· ╰── Duplicated property
8 │ * @property cfg.foo
· ───┬───
· ╰── Duplicated property
9 │ */
╰────
help: @property `cfg.foo` is duplicated on the same block.
⚠ eslint-plugin-jsdoc(check-property-names): Duplicate @property found.
╭─[check_property_names.tsx:5:27]
4 │ * @property cfg
5 │ * @property cfg.foo
· ───┬───
· ╰── Duplicated property
6 │ * @property [cfg.foo]
· ────┬────
· ╰── Duplicated property
7 │ * @property baz
╰────
help: @property `cfg.foo` is duplicated on the same block.
⚠ eslint-plugin-jsdoc(check-property-names): Duplicate @property found.
╭─[check_property_names.tsx:5:27]
4 │ * @property cfg
5 │ * @property cfg.foo
· ───┬───
· ╰── Duplicated property
6 │ * @property [cfg.foo="with a default"]
· ─────────────┬────────────
· ╰── Duplicated property
7 │ * @property baz
╰────
help: @property `cfg.foo` is duplicated on the same block.
⚠ eslint-plugin-jsdoc(check-property-names): No root defined for @property path.
╭─[check_property_names.tsx:6:27]
5 │ * @property foo.bar
6 │ * @property foo.bar.baz.qux
· ───────────────
7 │ * @property bar
╰────
help: @property path declaration `foo.bar.baz.qux` appears before any real property.
⚠ eslint-plugin-jsdoc(check-property-names): No root defined for @property path.
╭─[check_property_names.tsx:5:36]
4 │ * @property {object[]} foo
5 │ * @property {number} foo[].bar[].baz
· ───────────────
6 │ * @property bar
╰────
help: @property path declaration `foo[].bar[].baz` appears before any real property.
⚠ eslint-plugin-jsdoc(check-property-names): Duplicate @property found.
╭─[check_property_names.tsx:4:23]
3 │ * @typedef {SomeType} SomeTypedef
4 │ * @prop foo
· ─┬─
· ╰── Duplicated property
5 │ * @prop foo
· ─┬─
· ╰── Duplicated property
6 │ */
╰────
help: @property `foo` is duplicated on the same block.