mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter): add vitest/prefer-lowercase-title rule (#8152)
This pull request implements the [vitest/prefer-lowercase-title](https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-lowercase-title.md) rule. Since there was an existing jest rule with this title, I followed the existing pattern in [no-unused-vars](https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs) to group the jest and vitest rules together in a shared module. I used the existing `jest/prefer-lowercase-title` documentation as a base and modified it where it seemed appropriate. I added a `jest` and `vitest` snapshot suffix for each respective test suite. One item I wasn't 100% about is adding `bench` to the jest test names. Without this change, the vitest test suite fails because of [this check](https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/utils/jest/parse_jest_fn.rs#L108) which validates that we're only parsing valid jest functions from a detected jest file. The unit tests that are sourced from the vitest plugin are all read by the linting host as jest-like files, so adding `bench` as a "valid" jest method allows us to lint a unit test using this keyword. This seemed to me like the least invasive solution to accommodate the new rule without breaking any existing code, but I'm certainly open to alternatives.
This commit is contained in:
parent
1de6f854cb
commit
37909337bd
9 changed files with 405 additions and 254 deletions
318
crates/oxc_linter/src/rules/jest/prefer_lowercase_title/mod.rs
Normal file
318
crates/oxc_linter/src/rules/jest/prefer_lowercase_title/mod.rs
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
use oxc_ast::{ast::Argument, AstKind};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{CompactStr, Span};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::{
|
||||
context::LintContext,
|
||||
rule::Rule,
|
||||
utils::{
|
||||
parse_jest_fn_call, JestFnKind, JestGeneralFnKind, ParsedJestFnCallNew, PossibleJestNode,
|
||||
},
|
||||
};
|
||||
|
||||
fn prefer_lowercase_title_diagnostic(title: &str, span: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn("Enforce lowercase test names")
|
||||
.with_help(format!("`{title:?}`s should begin with lowercase"))
|
||||
.with_label(span)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PreferLowercaseTitleConfig {
|
||||
allowed_prefixes: Vec<CompactStr>,
|
||||
ignore: Vec<CompactStr>,
|
||||
ignore_top_level_describe: bool,
|
||||
lowercase_first_character_only: bool,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for PreferLowercaseTitle {
|
||||
type Target = PreferLowercaseTitleConfig;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PreferLowercaseTitle(Box<PreferLowercaseTitleConfig>);
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// Enforce `it`, `test`, `describe`, and `bench` to have descriptions that begin with a
|
||||
/// lowercase letter. This provides more readable test failures. This rule is not
|
||||
/// enabled by default.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```javascript
|
||||
/// // invalid
|
||||
/// it('Adds 1 + 2 to equal 3', () => {
|
||||
/// expect(sum(1, 2)).toBe(3);
|
||||
/// });
|
||||
///
|
||||
/// // valid
|
||||
/// it('adds 1 + 2 to equal 3', () => {
|
||||
/// expect(sum(1, 2)).toBe(3);
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// ```json
|
||||
/// {
|
||||
/// "jest/prefer-lowercase-title": [
|
||||
/// "error",
|
||||
/// {
|
||||
/// "ignore": ["describe", "test"]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### `ignore`
|
||||
///
|
||||
/// This array option controls which Jest or Vitest functions are checked by this rule. There
|
||||
/// are four possible values:
|
||||
/// - `"describe"`
|
||||
/// - `"test"`
|
||||
/// - `"it"`
|
||||
/// - `"bench"`
|
||||
///
|
||||
/// By default, none of these options are enabled (the equivalent of
|
||||
/// `{ "ignore": [] }`).
|
||||
///
|
||||
/// Example of **correct** code for the `{ "ignore": ["describe"] }` option:
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "ignore": ["describe"] }] */
|
||||
/// describe('Uppercase description');
|
||||
/// ```
|
||||
///
|
||||
/// Example of **correct** code for the `{ "ignore": ["test"] }` option:
|
||||
///
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "ignore": ["test"] }] */
|
||||
/// test('Uppercase description');
|
||||
/// ```
|
||||
///
|
||||
/// Example of **correct** code for the `{ "ignore": ["it"] }` option:
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "ignore": ["it"] }] */
|
||||
/// it('Uppercase description');
|
||||
/// ```
|
||||
///
|
||||
/// ### `allowedPrefixes`
|
||||
/// This array option allows specifying prefixes, which contain capitals that titles
|
||||
/// can start with. This can be useful when writing tests for API endpoints, where
|
||||
/// you'd like to prefix with the HTTP method.
|
||||
/// By default, nothing is allowed (the equivalent of `{ "allowedPrefixes": [] }`).
|
||||
///
|
||||
/// Example of **correct** code for the `{ "allowedPrefixes": ["GET"] }` option:
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "allowedPrefixes": ["GET"] }] */
|
||||
/// describe('GET /live');
|
||||
/// ```
|
||||
///
|
||||
/// ### `ignoreTopLevelDescribe`
|
||||
/// This option can be set to allow only the top-level `describe` blocks to have a
|
||||
/// title starting with an upper-case letter.
|
||||
/// Example of **correct** code for the `{ "ignoreTopLevelDescribe": true }` option:
|
||||
///
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "ignoreTopLevelDescribe": true }] */
|
||||
/// describe('MyClass', () => {
|
||||
/// describe('#myMethod', () => {
|
||||
/// it('does things', () => {
|
||||
/// //
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ### `lowercaseFirstCharacterOnly`
|
||||
/// This option can be set to only validate that the first character of a test name is lowercased.
|
||||
///
|
||||
/// Example of **correct** code for the `{ "lowercaseFirstCharacterOnly": true }` option:
|
||||
///
|
||||
/// ```js
|
||||
/// /* eslint vitest/prefer-lowercase-title: ["error", { "lowercaseFirstCharacterOnly": true }] */
|
||||
/// describe('myClass', () => {
|
||||
/// describe('myMethod', () => {
|
||||
/// it('does things', () => {
|
||||
/// //
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Example of **incorrect** code for the `{ "lowercaseFirstCharacterOnly": true }` option:
|
||||
///
|
||||
/// ```js
|
||||
/// /* eslint vitest/prefer-lowercase-title: ["error", { "lowercaseFirstCharacterOnly": true }] */
|
||||
/// describe('MyClass', () => {
|
||||
/// describe('MyMethod', () => {
|
||||
/// it('does things', () => {
|
||||
/// //
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
PreferLowercaseTitle,
|
||||
jest,
|
||||
style,
|
||||
fix
|
||||
);
|
||||
|
||||
impl Rule for PreferLowercaseTitle {
|
||||
fn from_configuration(value: serde_json::Value) -> Self {
|
||||
let obj = value.get(0);
|
||||
let ignore_top_level_describe = obj
|
||||
.and_then(|config| config.get("ignoreTopLevelDescribe"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let lowercase_first_character_only = obj
|
||||
.and_then(|config| config.get("lowercaseFirstCharacterOnly"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
let ignore = obj
|
||||
.and_then(|config| config.get("ignore"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect())
|
||||
.unwrap_or_default();
|
||||
let allowed_prefixes = obj
|
||||
.and_then(|config| config.get("allowedPrefixes"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
Self(Box::new(PreferLowercaseTitleConfig {
|
||||
allowed_prefixes,
|
||||
ignore,
|
||||
ignore_top_level_describe,
|
||||
lowercase_first_character_only,
|
||||
}))
|
||||
}
|
||||
|
||||
fn run_on_jest_node<'a, 'c>(
|
||||
&self,
|
||||
possible_jest_node: &PossibleJestNode<'a, 'c>,
|
||||
ctx: &'c LintContext<'a>,
|
||||
) {
|
||||
let node = possible_jest_node.node;
|
||||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
|
||||
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let scopes = ctx.scopes();
|
||||
|
||||
let ignores = Self::populate_ignores(&self.ignore);
|
||||
|
||||
if ignores.contains(&jest_fn_call.name.as_ref()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe)) {
|
||||
if self.ignore_top_level_describe && scopes.get_flags(node.scope_id()).is_top() {
|
||||
return;
|
||||
}
|
||||
} else if !matches!(
|
||||
jest_fn_call.kind,
|
||||
JestFnKind::General(JestGeneralFnKind::Test | JestGeneralFnKind::Bench)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(arg) = call_expr.arguments.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Argument::StringLiteral(string_expr) = arg {
|
||||
self.lint_string(ctx, string_expr.value.as_str(), string_expr.span);
|
||||
} else if let Argument::TemplateLiteral(template_expr) = arg {
|
||||
let Some(template_string) = template_expr.quasi() else {
|
||||
return;
|
||||
};
|
||||
self.lint_string(ctx, template_string.as_str(), template_expr.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PreferLowercaseTitle {
|
||||
fn lint_string<'a>(&self, ctx: &LintContext<'a>, literal: &'a str, span: Span) {
|
||||
if literal.is_empty()
|
||||
|| self.allowed_prefixes.iter().any(|name| literal.starts_with(name.as_str()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if self.lowercase_first_character_only {
|
||||
let Some(first_char) = literal.chars().next() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let lower = first_char.to_ascii_lowercase();
|
||||
if first_char == lower {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
for n in 0..literal.chars().count() {
|
||||
let Some(next_char) = literal.chars().nth(n) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let next_lower = next_char.to_ascii_lowercase();
|
||||
|
||||
if next_char != next_lower {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let replacement = if self.lowercase_first_character_only {
|
||||
cow_utils::CowUtils::cow_to_ascii_lowercase(&literal.chars().as_str()[0..1])
|
||||
} else {
|
||||
cow_utils::CowUtils::cow_to_ascii_lowercase(literal)
|
||||
};
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let replacement_len = replacement.len() as u32;
|
||||
|
||||
ctx.diagnostic_with_fix(prefer_lowercase_title_diagnostic(literal, span), |fixer| {
|
||||
fixer.replace(Span::sized(span.start + 1, replacement_len), replacement)
|
||||
});
|
||||
}
|
||||
|
||||
fn populate_ignores(ignore: &[CompactStr]) -> Vec<&str> {
|
||||
let mut ignores: Vec<&str> = vec![];
|
||||
let test_case_name = ["fit", "it", "xit", "test", "xtest"];
|
||||
let describe_alias = ["describe", "fdescribe", "xdescribe"];
|
||||
let test_name = "test";
|
||||
let it_name = "it";
|
||||
let bench_name = "bench";
|
||||
|
||||
if ignore.iter().any(|alias| alias == "describe") {
|
||||
ignores.extend(describe_alias.iter());
|
||||
}
|
||||
|
||||
if ignore.iter().any(|alias| alias == bench_name) {
|
||||
ignores.push(bench_name);
|
||||
}
|
||||
|
||||
if ignore.iter().any(|alias| alias == test_name) {
|
||||
ignores.extend(test_case_name.iter().filter(|alias| alias.ends_with(test_name)));
|
||||
}
|
||||
|
||||
if ignore.iter().any(|alias| alias == it_name) {
|
||||
ignores.extend(test_case_name.iter().filter(|alias| alias.ends_with(it_name)));
|
||||
}
|
||||
|
||||
ignores
|
||||
}
|
||||
}
|
||||
|
|
@ -1,258 +1,7 @@
|
|||
use oxc_ast::{ast::Argument, AstKind};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{CompactStr, Span};
|
||||
|
||||
use crate::{
|
||||
context::LintContext,
|
||||
rule::Rule,
|
||||
utils::{
|
||||
parse_jest_fn_call, JestFnKind, JestGeneralFnKind, ParsedJestFnCallNew, PossibleJestNode,
|
||||
},
|
||||
};
|
||||
|
||||
fn unexpected_lowercase(x0: &str, span1: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn("Enforce lowercase test names")
|
||||
.with_help(format!("`{x0:?}`s should begin with lowercase"))
|
||||
.with_label(span1)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PreferLowercaseTitleConfig {
|
||||
allowed_prefixes: Vec<CompactStr>,
|
||||
ignore: Vec<CompactStr>,
|
||||
ignore_top_level_describe: bool,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for PreferLowercaseTitle {
|
||||
type Target = PreferLowercaseTitleConfig;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PreferLowercaseTitle(Box<PreferLowercaseTitleConfig>);
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// Enforce `it`, `test` and `describe` to have descriptions that begin with a
|
||||
/// lowercase letter. This provides more readable test failures. This rule is not
|
||||
/// enabled by default.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```javascript
|
||||
/// // invalid
|
||||
/// it('Adds 1 + 2 to equal 3', () => {
|
||||
/// expect(sum(1, 2)).toBe(3);
|
||||
/// });
|
||||
///
|
||||
/// // valid
|
||||
/// it('adds 1 + 2 to equal 3', () => {
|
||||
/// expect(sum(1, 2)).toBe(3);
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// ```json
|
||||
/// {
|
||||
/// "jest/prefer-lowercase-title": [
|
||||
/// "error",
|
||||
/// {
|
||||
/// "ignore": ["describe", "test"]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### `ignore`
|
||||
///
|
||||
/// This array option controls which Jest functions are checked by this rule. There
|
||||
/// are three possible values:
|
||||
/// - `"describe"`
|
||||
/// - `"test"`
|
||||
/// - `"it"`
|
||||
///
|
||||
/// By default, none of these options are enabled (the equivalent of
|
||||
/// `{ "ignore": [] }`).
|
||||
///
|
||||
/// Example of **correct** code for the `{ "ignore": ["describe"] }` option:
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "ignore": ["describe"] }] */
|
||||
/// describe('Uppercase description');
|
||||
/// ```
|
||||
///
|
||||
/// Example of **correct** code for the `{ "ignore": ["test"] }` option:
|
||||
///
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "ignore": ["test"] }] */
|
||||
/// test('Uppercase description');
|
||||
/// ```
|
||||
///
|
||||
/// Example of **correct** code for the `{ "ignore": ["it"] }` option:
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "ignore": ["it"] }] */
|
||||
/// it('Uppercase description');
|
||||
/// ```
|
||||
///
|
||||
/// ### `allowedPrefixes`
|
||||
/// This array option allows specifying prefixes, which contain capitals that titles
|
||||
/// can start with. This can be useful when writing tests for API endpoints, where
|
||||
/// you'd like to prefix with the HTTP method.
|
||||
/// By default, nothing is allowed (the equivalent of `{ "allowedPrefixes": [] }`).
|
||||
///
|
||||
/// Example of **correct** code for the `{ "allowedPrefixes": ["GET"] }` option:
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "allowedPrefixes": ["GET"] }] */
|
||||
/// describe('GET /live');
|
||||
/// ```
|
||||
///
|
||||
/// ### `ignoreTopLevelDescribe`
|
||||
/// This option can be set to allow only the top-level `describe` blocks to have a
|
||||
/// title starting with an upper-case letter.
|
||||
/// Example of **correct** code for the `{ "ignoreTopLevelDescribe": true }` option:
|
||||
///
|
||||
/// ```js
|
||||
/// /* eslint jest/prefer-lowercase-title: ["error", { "ignoreTopLevelDescribe": true }] */
|
||||
/// describe('MyClass', () => {
|
||||
/// describe('#myMethod', () => {
|
||||
/// it('does things', () => {
|
||||
/// //
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
PreferLowercaseTitle,
|
||||
jest,
|
||||
style,
|
||||
fix
|
||||
);
|
||||
|
||||
impl Rule for PreferLowercaseTitle {
|
||||
fn from_configuration(value: serde_json::Value) -> Self {
|
||||
let obj = value.get(0);
|
||||
let ignore_top_level_describe = obj
|
||||
.and_then(|config| config.get("ignoreTopLevelDescribe"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let ignore = obj
|
||||
.and_then(|config| config.get("ignore"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect())
|
||||
.unwrap_or_default();
|
||||
let allowed_prefixes = obj
|
||||
.and_then(|config| config.get("allowedPrefixes"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
Self(Box::new(PreferLowercaseTitleConfig {
|
||||
allowed_prefixes,
|
||||
ignore,
|
||||
ignore_top_level_describe,
|
||||
}))
|
||||
}
|
||||
|
||||
fn run_on_jest_node<'a, 'c>(
|
||||
&self,
|
||||
jest_node: &PossibleJestNode<'a, 'c>,
|
||||
ctx: &'c LintContext<'a>,
|
||||
) {
|
||||
self.run(jest_node, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
impl PreferLowercaseTitle {
|
||||
fn run<'a>(&self, possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) {
|
||||
let node = possible_jest_node.node;
|
||||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
|
||||
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let scopes = ctx.scopes();
|
||||
let ignores = Self::populate_ignores(&self.ignore);
|
||||
|
||||
if ignores.contains(&jest_fn_call.name.as_ref()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe)) {
|
||||
if self.ignore_top_level_describe && scopes.get_flags(node.scope_id()).is_top() {
|
||||
return;
|
||||
}
|
||||
} else if !matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Test)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(arg) = call_expr.arguments.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Argument::StringLiteral(string_expr) = arg {
|
||||
self.lint_string(ctx, string_expr.value.as_str(), string_expr.span);
|
||||
} else if let Argument::TemplateLiteral(template_expr) = arg {
|
||||
let Some(template_string) = template_expr.quasi() else {
|
||||
return;
|
||||
};
|
||||
self.lint_string(ctx, template_string.as_str(), template_expr.span);
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_ignores(ignore: &[CompactStr]) -> Vec<&str> {
|
||||
let mut ignores: Vec<&str> = vec![];
|
||||
let test_case_name = ["fit", "it", "xit", "test", "xtest"];
|
||||
let describe_alias = ["describe", "fdescribe", "xdescribe"];
|
||||
let test_name = "test";
|
||||
let it_name = "it";
|
||||
|
||||
if ignore.iter().any(|alias| alias == "describe") {
|
||||
ignores.extend(describe_alias.iter());
|
||||
}
|
||||
|
||||
if ignore.iter().any(|alias| alias == test_name) {
|
||||
ignores.extend(test_case_name.iter().filter(|alias| alias.ends_with(test_name)));
|
||||
}
|
||||
|
||||
if ignore.iter().any(|alias| alias == it_name) {
|
||||
ignores.extend(test_case_name.iter().filter(|alias| alias.ends_with(it_name)));
|
||||
}
|
||||
|
||||
ignores
|
||||
}
|
||||
|
||||
fn lint_string<'a>(&self, ctx: &LintContext<'a>, literal: &'a str, span: Span) {
|
||||
if literal.is_empty()
|
||||
|| self.allowed_prefixes.iter().any(|name| literal.starts_with(name.as_str()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(first_char) = literal.chars().next() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let lower = first_char.to_ascii_lowercase();
|
||||
if first_char == lower {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.diagnostic_with_fix(unexpected_lowercase(literal, span), |fixer| {
|
||||
fixer.replace(Span::sized(span.start + 1, 1), lower.to_string())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use super::PreferLowercaseTitle;
|
||||
use crate::rule::RuleMeta;
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
|
|
@ -630,5 +379,6 @@ fn test() {
|
|||
Tester::new(PreferLowercaseTitle::NAME, PreferLowercaseTitle::PLUGIN, pass, fail)
|
||||
.with_jest_plugin(true)
|
||||
.expect_fix(fix)
|
||||
.with_snapshot_suffix("jest")
|
||||
.test_and_snapshot();
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
mod jest;
|
||||
mod vitest;
|
||||
|
||||
use super::PreferLowercaseTitle;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#[test]
|
||||
fn test() {
|
||||
use super::PreferLowercaseTitle;
|
||||
use crate::rule::RuleMeta;
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass: Vec<(&str, Option<serde_json::Value>)> = vec![
|
||||
("it.each()", None),
|
||||
("it.each()(1)", None),
|
||||
("it.todo();", None),
|
||||
(r#"describe("oo", function () {})"#, None),
|
||||
(r#"test("foo", function () {})"#, None),
|
||||
("test(`123`, function () {})", None),
|
||||
];
|
||||
|
||||
let fail: Vec<(&str, Option<serde_json::Value>)> = vec![
|
||||
(r#"it("Foo MM mm", function () {})"#, None),
|
||||
("test(`Foo MM mm`, function () {})", None),
|
||||
(
|
||||
"test(`SFC Compile`, function () {})",
|
||||
Some(serde_json::json!([{ "lowercaseFirstCharacterOnly": false }])),
|
||||
),
|
||||
("bench(`Foo MM mm`, function () {})", None),
|
||||
];
|
||||
|
||||
let fix: Vec<(&str, &str, Option<serde_json::Value>)> = vec![
|
||||
(r#"it("Foo MM mm", function () {})"#, r#"it("foo MM mm", function () {})"#, None),
|
||||
("test(`Foo MM mm`, function () {})", "test(`foo MM mm`, function () {})", None),
|
||||
(
|
||||
"test(`SFC Compile`, function () {})",
|
||||
"test(`sfc compile`, function () {})",
|
||||
Some(serde_json::json!([{ "lowercaseFirstCharacterOnly": false }])),
|
||||
),
|
||||
("bench(`Foo MM mm`, function () {})", "bench(`foo MM mm`, function () {})", None),
|
||||
];
|
||||
|
||||
Tester::new(PreferLowercaseTitle::NAME, PreferLowercaseTitle::PLUGIN, pass, fail)
|
||||
.expect_fix(fix)
|
||||
.with_jest_plugin(true)
|
||||
.with_vitest_plugin(true)
|
||||
.with_snapshot_suffix("vitest")
|
||||
.test_and_snapshot();
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
⚠ eslint-plugin-jest(prefer-lowercase-title): Enforce lowercase test names
|
||||
╭─[prefer_lowercase_title.tsx:1:4]
|
||||
1 │ it("Foo MM mm", function () {})
|
||||
· ───────────
|
||||
╰────
|
||||
help: `"Foo MM mm"`s should begin with lowercase
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-lowercase-title): Enforce lowercase test names
|
||||
╭─[prefer_lowercase_title.tsx:1:6]
|
||||
1 │ test(`Foo MM mm`, function () {})
|
||||
· ───────────
|
||||
╰────
|
||||
help: `"Foo MM mm"`s should begin with lowercase
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-lowercase-title): Enforce lowercase test names
|
||||
╭─[prefer_lowercase_title.tsx:1:6]
|
||||
1 │ test(`SFC Compile`, function () {})
|
||||
· ─────────────
|
||||
╰────
|
||||
help: `"SFC Compile"`s should begin with lowercase
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-lowercase-title): Enforce lowercase test names
|
||||
╭─[prefer_lowercase_title.tsx:1:7]
|
||||
1 │ bench(`Foo MM mm`, function () {})
|
||||
· ───────────
|
||||
╰────
|
||||
help: `"Foo MM mm"`s should begin with lowercase
|
||||
|
|
@ -24,6 +24,7 @@ pub use crate::utils::jest::parse_jest_fn::{
|
|||
pub const JEST_METHOD_NAMES: phf::Set<&'static str> = phf_set![
|
||||
"afterAll",
|
||||
"afterEach",
|
||||
"bench",
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"describe",
|
||||
|
|
@ -55,6 +56,7 @@ impl JestFnKind {
|
|||
"expect" => Self::Expect,
|
||||
"expectTypeOf" => Self::ExpectTypeOf,
|
||||
"vi" => Self::General(JestGeneralFnKind::Vitest),
|
||||
"bench" => Self::General(JestGeneralFnKind::Bench),
|
||||
"jest" => Self::General(JestGeneralFnKind::Jest),
|
||||
"describe" | "fdescribe" | "xdescribe" => Self::General(JestGeneralFnKind::Describe),
|
||||
"fit" | "it" | "test" | "xit" | "xtest" => Self::General(JestGeneralFnKind::Test),
|
||||
|
|
@ -80,6 +82,7 @@ pub enum JestGeneralFnKind {
|
|||
Test,
|
||||
Jest,
|
||||
Vitest,
|
||||
Bench,
|
||||
}
|
||||
|
||||
/// <https://jestjs.io/docs/configuration#testmatch-arraystring>
|
||||
|
|
|
|||
|
|
@ -567,11 +567,12 @@ fn recurse_extend_node_chain<'a>(
|
|||
}
|
||||
|
||||
// sorted list for binary search.
|
||||
const VALID_JEST_FN_CALL_CHAINS: [[&str; 4]; 51] = [
|
||||
const VALID_JEST_FN_CALL_CHAINS: [[&str; 4]; 52] = [
|
||||
["afterAll", "", "", ""],
|
||||
["afterEach", "", "", ""],
|
||||
["beforeAll", "", "", ""],
|
||||
["beforeEach", "", "", ""],
|
||||
["bench", "", "", ""],
|
||||
["describe", "", "", ""],
|
||||
["describe", "each", "", ""],
|
||||
["describe", "only", "", ""],
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ const VITEST_COMPATIBLE_JEST_RULES: phf::Set<&'static str> = phf::phf_set! {
|
|||
"prefer-expect-resolves",
|
||||
"prefer-hooks-in-order",
|
||||
"prefer-hooks-on-top",
|
||||
"prefer-lowercase-title",
|
||||
"prefer-mock-promise-shorthand",
|
||||
"prefer-strict-equal",
|
||||
"prefer-to-have-length",
|
||||
|
|
|
|||
Loading…
Reference in a new issue