mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter): no-focused-test(eslint-jest-plugin) (#609)
This commit is contained in:
parent
3b9cc474e9
commit
3cf08a256c
7 changed files with 543 additions and 121 deletions
|
|
@ -1,51 +1,63 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use oxc_ast::ast::{CallExpression, Expression, IdentifierReference};
|
||||
use oxc_span::Atom;
|
||||
use oxc_ast::{
|
||||
ast::{CallExpression, Expression, IdentifierName, IdentifierReference, MemberExpression},
|
||||
AstKind,
|
||||
};
|
||||
use oxc_semantic::AstNode;
|
||||
use oxc_span::{Atom, Span};
|
||||
|
||||
use crate::context::LintContext;
|
||||
|
||||
pub enum JestFnKind {
|
||||
Hook,
|
||||
Describe,
|
||||
Test,
|
||||
Expect,
|
||||
Jest,
|
||||
Unknown,
|
||||
}
|
||||
pub fn parse_general_jest_fn_call<'a>(
|
||||
call_expr: &'a CallExpression<'a>,
|
||||
node: &AstNode<'a>,
|
||||
ctx: &LintContext,
|
||||
) -> Option<ParsedGeneralJestFnCall<'a>> {
|
||||
let jest_fn_call = parse_jest_fn_call(call_expr, node, ctx)?;
|
||||
|
||||
impl JestFnKind {
|
||||
pub fn from(name: &str) -> Self {
|
||||
match name {
|
||||
"expect" => Self::Expect,
|
||||
"jest" => Self::Jest,
|
||||
"describe" | "fdescribe" | "xdescribe" => Self::Describe,
|
||||
"fit" | "it" | "test" | "xit" | "xtest" => Self::Test,
|
||||
"beforeAll" | "beforeEach" | "afterAll" | "afterEach" => Self::Hook,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
if let ParsedJestFnCall::GeneralJestFnCall(jest_fn_call) = jest_fn_call {
|
||||
return Some(jest_fn_call);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ParsedJestFnCall<'a> {
|
||||
pub kind: JestFnKind,
|
||||
pub members: Vec<Cow<'a, str>>,
|
||||
pub raw: Cow<'a, str>,
|
||||
None
|
||||
}
|
||||
|
||||
pub fn parse_jest_fn_call<'a>(
|
||||
call_expr: &'a CallExpression,
|
||||
ctx: &'a LintContext,
|
||||
call_expr: &'a CallExpression<'a>,
|
||||
node: &AstNode<'a>,
|
||||
ctx: &LintContext,
|
||||
) -> Option<ParsedJestFnCall<'a>> {
|
||||
let callee = &call_expr.callee;
|
||||
|
||||
// if bailed out, we're not a jest function
|
||||
// If bailed out, we're not jest function
|
||||
let resolved = resolve_to_jest_fn(call_expr, ctx)?;
|
||||
|
||||
let chain = get_node_chain(callee);
|
||||
// only the top level Call expression callee's parent is None, it's not necessary to set it to None, but
|
||||
// I didn't know how to pass Expression to it.
|
||||
let chain = get_node_chain(callee, None);
|
||||
let all_member_expr_except_last = chain
|
||||
.iter()
|
||||
.rev()
|
||||
.skip(1)
|
||||
.all(|member| matches!(member.parent, Some(Expression::MemberExpression(_))));
|
||||
|
||||
// Check every link in the chain except the last is a member expression
|
||||
if !all_member_expr_except_last {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ensure that we're at the "top" of the function call chain otherwise when
|
||||
// parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though
|
||||
// the full chain is not a valid jest function call chain
|
||||
if ctx.nodes().parent_node(node.id()).is_some_and(|parent_node| {
|
||||
matches!(parent_node.kind(), AstKind::CallExpression(_) | AstKind::MemberExpression(_))
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let (Some(first), Some(last)) = (chain.first(), chain.last()) {
|
||||
// if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
|
||||
if last == "each"
|
||||
// If we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
|
||||
if last.is_name_equal("each")
|
||||
&& !matches!(
|
||||
callee,
|
||||
Expression::CallExpression(_) | Expression::TaggedTemplateExpression(_)
|
||||
|
|
@ -54,14 +66,14 @@ pub fn parse_jest_fn_call<'a>(
|
|||
return None;
|
||||
}
|
||||
|
||||
if matches!(callee, Expression::TaggedTemplateExpression(_)) && last != "each" {
|
||||
if matches!(callee, Expression::TaggedTemplateExpression(_)) && last.is_name_unequal("each")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let kind = JestFnKind::from(first);
|
||||
let Some(first_name )= first.name() else { return None };
|
||||
let kind = JestFnKind::from(&first_name);
|
||||
let mut members = Vec::new();
|
||||
let mut iter = chain.into_iter();
|
||||
let first = iter.next().expect("first ident name");
|
||||
let iter = chain.into_iter().skip(1);
|
||||
let rest = iter;
|
||||
|
||||
// every member node must have a member expression as their parent
|
||||
|
|
@ -76,17 +88,19 @@ pub fn parse_jest_fn_call<'a>(
|
|||
} else if members.len() == 1 {
|
||||
VALID_JEST_FN_CALL_CHAINS_2
|
||||
.iter()
|
||||
.any(|chain| chain[0] == name && chain[1] == members[0])
|
||||
.any(|chain| chain[0] == name && members[0].is_name_equal(chain[1]))
|
||||
} else if members.len() == 2 {
|
||||
VALID_JEST_FN_CALL_CHAINS_3
|
||||
.iter()
|
||||
.any(|chain| chain[0] == name && chain[1] == members[0] && chain[2] == members[1])
|
||||
VALID_JEST_FN_CALL_CHAINS_3.iter().any(|chain| {
|
||||
chain[0] == name
|
||||
&& members[0].is_name_equal(chain[1])
|
||||
&& members[1].is_name_equal(chain[2])
|
||||
})
|
||||
} else if members.len() == 3 {
|
||||
VALID_JEST_FN_CALL_CHAINS_4.iter().any(|chain| {
|
||||
chain[0] == name
|
||||
&& chain[1] == members[0]
|
||||
&& chain[2] == members[1]
|
||||
&& chain[3] == members[2]
|
||||
&& members[0].is_name_equal(chain[1])
|
||||
&& members[1].is_name_equal(chain[2])
|
||||
&& members[2].is_name_equal(chain[3])
|
||||
})
|
||||
} else {
|
||||
false
|
||||
|
|
@ -95,22 +109,21 @@ pub fn parse_jest_fn_call<'a>(
|
|||
if !is_valid_jest_call {
|
||||
return None;
|
||||
}
|
||||
return Some(ParsedJestFnCall { kind, members, raw: first });
|
||||
return Some(ParsedJestFnCall::GeneralJestFnCall(ParsedGeneralJestFnCall {
|
||||
kind,
|
||||
members,
|
||||
raw: first_name,
|
||||
}));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
struct ResolvedJestFn<'a> {
|
||||
pub local: &'a Atom,
|
||||
}
|
||||
|
||||
fn resolve_to_jest_fn<'a>(
|
||||
call_expr: &'a CallExpression,
|
||||
ctx: &'a LintContext,
|
||||
) -> Option<ResolvedJestFn<'a>> {
|
||||
let ident = resolve_first_ident(&call_expr.callee)?;
|
||||
|
||||
if ctx.semantic().is_reference_to_global_variable(ident) {
|
||||
return Some(ResolvedJestFn { local: &ident.name });
|
||||
}
|
||||
|
|
@ -128,38 +141,165 @@ fn resolve_first_ident<'a>(expr: &'a Expression) -> Option<&'a IdentifierReferen
|
|||
}
|
||||
}
|
||||
|
||||
/// a.b.c -> ["a", "b"]
|
||||
/// a[`b`] - > ["a", "b"]
|
||||
/// a["b"] - > ["a", "b"]
|
||||
/// a[b] - > ["a", "b"]
|
||||
fn get_node_chain<'a>(expr: &'a Expression) -> Vec<Cow<'a, str>> {
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum JestFnKind {
|
||||
Expect,
|
||||
General(JestGeneralFnKind),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl JestFnKind {
|
||||
pub fn from(name: &str) -> Self {
|
||||
match name {
|
||||
"expect" => Self::Expect,
|
||||
"jest" => Self::General(JestGeneralFnKind::Jest),
|
||||
"describe" | "fdescribe" | "xdescribe" => Self::General(JestGeneralFnKind::Describe),
|
||||
"fit" | "it" | "test" | "xit" | "xtest" => Self::General(JestGeneralFnKind::Test),
|
||||
"beforeAll" | "beforeEach" | "afterAll" | "afterEach" => {
|
||||
Self::General(JestGeneralFnKind::Hook)
|
||||
}
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_general(self) -> Option<JestGeneralFnKind> {
|
||||
match self {
|
||||
Self::General(kind) => Some(kind),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum JestGeneralFnKind {
|
||||
Hook,
|
||||
Describe,
|
||||
Test,
|
||||
Jest,
|
||||
}
|
||||
|
||||
pub enum ParsedJestFnCall<'a> {
|
||||
GeneralJestFnCall(ParsedGeneralJestFnCall<'a>),
|
||||
#[allow(unused)]
|
||||
ExpectFnCall(ParsedExpectFnCall<'a>),
|
||||
}
|
||||
|
||||
pub struct ParsedGeneralJestFnCall<'a> {
|
||||
pub kind: JestFnKind,
|
||||
pub members: Vec<KnownMemberExpressionProperty<'a>>,
|
||||
pub raw: Cow<'a, str>,
|
||||
}
|
||||
|
||||
pub struct ParsedExpectFnCall<'a> {
|
||||
pub kind: JestFnKind,
|
||||
pub members: Vec<KnownMemberExpressionProperty<'a>>,
|
||||
pub raw: Cow<'a, str>,
|
||||
// pub args: Vec<&'a Expression<'a>>
|
||||
// TODO: add `modifiers`, `matcher` for this struct.
|
||||
}
|
||||
|
||||
struct ResolvedJestFn<'a> {
|
||||
pub local: &'a Atom,
|
||||
}
|
||||
|
||||
pub struct KnownMemberExpressionProperty<'a> {
|
||||
pub element: MemberExpressionElement<'a>,
|
||||
pub parent: Option<&'a Expression<'a>>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl<'a> KnownMemberExpressionProperty<'a> {
|
||||
pub fn name(&self) -> Option<Cow<'a, str>> {
|
||||
match &self.element {
|
||||
MemberExpressionElement::Expression(expr) => match expr {
|
||||
Expression::Identifier(ident) => Some(Cow::Borrowed(ident.name.as_str())),
|
||||
Expression::StringLiteral(string_literal) => {
|
||||
Some(Cow::Borrowed(string_literal.value.as_str()))
|
||||
}
|
||||
Expression::TemplateLiteral(template_literal) => Some(Cow::Borrowed(
|
||||
template_literal.quasi().expect("get string content").as_str(),
|
||||
)),
|
||||
_ => None,
|
||||
},
|
||||
MemberExpressionElement::IdentName(ident_name) => {
|
||||
Some(Cow::Borrowed(ident_name.name.as_str()))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn is_name_equal(&self, name: &str) -> bool {
|
||||
self.name().map_or(false, |n| n == name)
|
||||
}
|
||||
pub fn is_name_unequal(&self, name: &str) -> bool {
|
||||
!self.is_name_equal(name)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MemberExpressionElement<'a> {
|
||||
Expression(&'a Expression<'a>),
|
||||
IdentName(&'a IdentifierName),
|
||||
}
|
||||
|
||||
impl<'a> MemberExpressionElement<'a> {
|
||||
pub fn from_member_expr(
|
||||
member_expr: &'a MemberExpression<'a>,
|
||||
) -> Option<(Span, MemberExpressionElement<'a>)> {
|
||||
let Some((span, _)) = member_expr.static_property_info() else { return None };
|
||||
match member_expr {
|
||||
MemberExpression::ComputedMemberExpression(expr) => {
|
||||
Some((span, Self::Expression(&expr.expression)))
|
||||
}
|
||||
MemberExpression::StaticMemberExpression(expr) => {
|
||||
Some((span, Self::IdentName(&expr.property)))
|
||||
}
|
||||
// Jest fn chains don't have private fields, just ignore it.
|
||||
MemberExpression::PrivateFieldExpression(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Port from [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest/blob/a058f22f94774eeea7980ea2d1f24c6808bf3e2c/src/rules/utils/parseJestFnCall.ts#L36-L51)
|
||||
fn get_node_chain<'a>(
|
||||
expr: &'a Expression<'a>,
|
||||
parent: Option<&'a Expression<'a>>,
|
||||
) -> Vec<KnownMemberExpressionProperty<'a>> {
|
||||
let mut chain = Vec::new();
|
||||
|
||||
match expr {
|
||||
Expression::MemberExpression(member_expr) => {
|
||||
chain.extend(get_node_chain(member_expr.object()));
|
||||
if let Some(name) = member_expr.static_property_name() {
|
||||
chain.push(Cow::Borrowed(name));
|
||||
chain.extend(get_node_chain(member_expr.object(), Some(expr)));
|
||||
if let Some((span, element)) = MemberExpressionElement::from_member_expr(member_expr) {
|
||||
chain.push(KnownMemberExpressionProperty { element, parent: Some(expr), span });
|
||||
}
|
||||
}
|
||||
Expression::Identifier(ident) => {
|
||||
chain.push(Cow::Borrowed(ident.name.as_str()));
|
||||
chain.push(KnownMemberExpressionProperty {
|
||||
element: MemberExpressionElement::Expression(expr),
|
||||
parent,
|
||||
span: ident.span,
|
||||
});
|
||||
}
|
||||
Expression::CallExpression(call_expr) => {
|
||||
let sub_chain = get_node_chain(&call_expr.callee);
|
||||
let sub_chain = get_node_chain(&call_expr.callee, Some(expr));
|
||||
chain.extend(sub_chain);
|
||||
}
|
||||
Expression::TaggedTemplateExpression(tagged_expr) => {
|
||||
let sub_chain = get_node_chain(&tagged_expr.tag);
|
||||
let sub_chain = get_node_chain(&tagged_expr.tag, Some(expr));
|
||||
chain.extend(sub_chain);
|
||||
}
|
||||
Expression::StringLiteral(string_literal) => {
|
||||
chain.push(Cow::Borrowed(string_literal.value.as_str()));
|
||||
chain.push(KnownMemberExpressionProperty {
|
||||
element: MemberExpressionElement::Expression(expr),
|
||||
parent,
|
||||
span: string_literal.span,
|
||||
});
|
||||
}
|
||||
Expression::TemplateLiteral(template_literal) => {
|
||||
if template_literal.expressions.is_empty() && template_literal.quasis.len() == 1 {
|
||||
chain.push(Cow::Borrowed(
|
||||
template_literal.quasi().expect("get string content").as_str(),
|
||||
));
|
||||
chain.push(KnownMemberExpressionProperty {
|
||||
element: MemberExpressionElement::Expression(expr),
|
||||
parent,
|
||||
span: template_literal.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
typescript::no_var_requires,
|
||||
jest::no_disabled_tests,
|
||||
jest::no_test_prefixes,
|
||||
jest::no_focused_tests,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ use oxc_span::Span;
|
|||
|
||||
use crate::{
|
||||
context::LintContext,
|
||||
jest_ast_util::{parse_jest_fn_call, JestFnKind, ParsedJestFnCall},
|
||||
jest_ast_util::{
|
||||
parse_general_jest_fn_call, JestFnKind, JestGeneralFnKind, ParsedGeneralJestFnCall,
|
||||
},
|
||||
rule::Rule,
|
||||
AstNode,
|
||||
};
|
||||
|
|
@ -83,12 +85,16 @@ impl Message {
|
|||
impl Rule for NoDisabledTests {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
if let AstKind::CallExpression(call_expr) = node.kind() {
|
||||
if let Some(jest_fn_call) = parse_jest_fn_call(call_expr, ctx) {
|
||||
let ParsedJestFnCall { kind, members, raw } = jest_fn_call;
|
||||
if let Some(jest_fn_call) = parse_general_jest_fn_call(call_expr, node, ctx) {
|
||||
let ParsedGeneralJestFnCall { kind, members, raw } = jest_fn_call;
|
||||
// `test('foo')`
|
||||
if matches!(kind, JestFnKind::Test)
|
||||
let kind = match kind {
|
||||
JestFnKind::Expect | JestFnKind::Unknown => return,
|
||||
JestFnKind::General(kind) => kind,
|
||||
};
|
||||
if matches!(kind, JestGeneralFnKind::Test)
|
||||
&& call_expr.arguments.len() < 2
|
||||
&& members.iter().all(|name| name != "todo")
|
||||
&& members.iter().all(|member| member.is_name_unequal("todo"))
|
||||
{
|
||||
let (error, help) = Message::MissingFunction.details();
|
||||
ctx.diagnostic(NoDisabledTestsDiagnostic(error, help, call_expr.span));
|
||||
|
|
@ -98,7 +104,7 @@ impl Rule for NoDisabledTests {
|
|||
// the only jest functions that are with "x" are "xdescribe", "xtest", and "xit"
|
||||
// `xdescribe('foo', () => {})`
|
||||
if raw.starts_with('x') {
|
||||
let (error, help) = if matches!(kind, JestFnKind::Describe) {
|
||||
let (error, help) = if matches!(kind, JestGeneralFnKind::Describe) {
|
||||
Message::DisabledSuiteWithX.details()
|
||||
} else {
|
||||
Message::DisabledTestWithX.details()
|
||||
|
|
@ -109,8 +115,8 @@ impl Rule for NoDisabledTests {
|
|||
|
||||
// `it.skip('foo', function () {})'`
|
||||
// `describe.skip('foo', function () {})'`
|
||||
if members.iter().any(|name| name == "skip") {
|
||||
let (error, help) = if matches!(kind, JestFnKind::Describe) {
|
||||
if members.iter().any(|member| member.is_name_equal("skip")) {
|
||||
let (error, help) = if matches!(kind, JestGeneralFnKind::Describe) {
|
||||
Message::DisabledSuiteWithSkip.details()
|
||||
} else {
|
||||
Message::DisabledTestWithSkip.details()
|
||||
|
|
@ -178,7 +184,6 @@ fn test() {
|
|||
("describe.skip.each([1, 2, 3])('%s', (a, b) => {});", None),
|
||||
("xdescribe.each([1, 2, 3])('%s', (a, b) => {});", None),
|
||||
("describe[`skip`]('foo', function () {})", None),
|
||||
("describe[`skip`]('foo', function () {})", None),
|
||||
("describe['skip']('foo', function () {})", None),
|
||||
("it.skip('foo', function () {})", None),
|
||||
("it['skip']('foo', function () {})", None),
|
||||
|
|
|
|||
151
crates/oxc_linter/src/rules/jest/no_focused_tests.rs
Normal file
151
crates/oxc_linter/src/rules/jest/no_focused_tests.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
use oxc_ast::AstKind;
|
||||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
thiserror::Error,
|
||||
};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
|
||||
use crate::{
|
||||
context::LintContext,
|
||||
fixer::Fix,
|
||||
jest_ast_util::{
|
||||
parse_general_jest_fn_call, JestFnKind, JestGeneralFnKind, MemberExpressionElement,
|
||||
ParsedGeneralJestFnCall,
|
||||
},
|
||||
rule::Rule,
|
||||
AstNode,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("Unexpected focused test.")]
|
||||
#[diagnostic(severity(warning), help("Remove focus from test."))]
|
||||
struct NoFocusedTestsDiagnostic(#[label] pub Span);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NoFocusedTests;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
/// This rule reminds you to remove `.only` from your tests by raising a warning
|
||||
/// whenever you are using the exclusivity feature.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// Jest has a feature that allows you to focus tests by appending `.only` or
|
||||
/// prepending `f` to a test-suite or a test-case. This feature is really helpful to
|
||||
/// debug a failing test, so you don’t have to execute all of your tests. After you
|
||||
/// have fixed your test and before committing the changes you have to remove
|
||||
/// `.only` to ensure all tests are executed on your build system.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```javascript
|
||||
/// describe.only('foo', () => {});
|
||||
/// it.only('foo', () => {});
|
||||
/// describe['only']('bar', () => {});
|
||||
/// it['only']('bar', () => {});
|
||||
/// test.only('foo', () => {});
|
||||
/// test['only']('bar', () => {});
|
||||
/// fdescribe('foo', () => {});
|
||||
/// fit('foo', () => {});
|
||||
/// fit.each`
|
||||
/// table
|
||||
/// `();
|
||||
/// ```
|
||||
NoFocusedTests,
|
||||
suspicious
|
||||
);
|
||||
|
||||
impl Rule for NoFocusedTests {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
let AstKind::CallExpression(call_expr) = node.kind() else { return };
|
||||
let Some(jest_fn_call) = parse_general_jest_fn_call(call_expr, node, ctx) else { return };
|
||||
let ParsedGeneralJestFnCall { kind, members, raw } = jest_fn_call;
|
||||
if !matches!(
|
||||
kind,
|
||||
JestFnKind::General(JestGeneralFnKind::Describe | JestGeneralFnKind::Test)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if raw.starts_with('f') {
|
||||
ctx.diagnostic_with_fix(NoFocusedTestsDiagnostic(call_expr.span), || {
|
||||
let start = call_expr.span.start;
|
||||
Fix::delete(Span { start, end: start + 1 })
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let only_node = members.iter().find(|member| {
|
||||
member.is_name_equal("only")
|
||||
});
|
||||
if let Some(only_node) = only_node {
|
||||
ctx.diagnostic_with_fix(NoFocusedTestsDiagnostic(call_expr.span), || {
|
||||
let span = only_node.span;
|
||||
let start = span.start - 1;
|
||||
let end = if matches!(only_node.element, MemberExpressionElement::IdentName(_)) {
|
||||
span.end
|
||||
} else {
|
||||
span.end + 1
|
||||
};
|
||||
Fix::delete(Span { start, end })
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
("describe()", None),
|
||||
("it()", None),
|
||||
("describe.skip()", None),
|
||||
("it.skip()", None),
|
||||
("test()", None),
|
||||
("test.skip()", None),
|
||||
("var appliedOnly = describe.only; appliedOnly.apply(describe)", None),
|
||||
("var calledOnly = it.only; calledOnly.call(it)", None),
|
||||
("it.each()()", None),
|
||||
("it.each`table`()", None),
|
||||
("test.each()()", None),
|
||||
("test.each`table`()", None),
|
||||
("test.concurrent()", None),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
("describe.only()", None),
|
||||
// TODO: this need set setting like `settings: { jest: { globalAliases: { describe: ['context'] } } },`
|
||||
// ("context.only()", None),
|
||||
("describe.only.each()()", None),
|
||||
("describe.only.each`table`()", None),
|
||||
("describe[\"only\"]()", None),
|
||||
("it.only()", None),
|
||||
("it.concurrent.only.each``()", None),
|
||||
("it.only.each()()", None),
|
||||
("it.only.each`table`()", None),
|
||||
("it[\"only\"]()", None),
|
||||
("test.only()", None),
|
||||
("test.concurrent.only.each()()", None),
|
||||
("test.only.each()()", None),
|
||||
("test.only.each`table`()", None),
|
||||
("test[\"only\"]()", None),
|
||||
("fdescribe()", None),
|
||||
("fit()", None),
|
||||
("fit.each()()", None),
|
||||
("fit.each`table`()", None),
|
||||
];
|
||||
|
||||
let fix = vec => {})", "describe('foo', () => {})", None),
|
||||
("fdescribe('foo', () => {})", "describe('foo', () => {})", None),
|
||||
];
|
||||
|
||||
let mut tester = Tester::new(NoFocusedTests::NAME, pass, fail);
|
||||
tester.test_and_snapshot();
|
||||
tester.test_fix(fix);
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
use std::borrow::Borrow;
|
||||
|
||||
use oxc_ast::{ast::Expression, AstKind};
|
||||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
|
|
@ -11,7 +9,7 @@ use oxc_span::{Atom, GetSpan, Span};
|
|||
use crate::{
|
||||
context::LintContext,
|
||||
fixer::Fix,
|
||||
jest_ast_util::{parse_jest_fn_call, JestFnKind, ParsedJestFnCall},
|
||||
jest_ast_util::{parse_general_jest_fn_call, JestGeneralFnKind, ParsedGeneralJestFnCall, KnownMemberExpressionProperty},
|
||||
rule::Rule,
|
||||
AstNode,
|
||||
};
|
||||
|
|
@ -50,11 +48,15 @@ declare_oxc_lint!(
|
|||
nursery
|
||||
);
|
||||
|
||||
fn get_preferred_node_names(jest_fn_call: &ParsedJestFnCall) -> Atom {
|
||||
let ParsedJestFnCall { members, raw, .. } = jest_fn_call;
|
||||
fn get_preferred_node_names(jest_fn_call: &ParsedGeneralJestFnCall) -> Atom {
|
||||
let ParsedGeneralJestFnCall { members, raw, .. } = jest_fn_call;
|
||||
|
||||
let preferred_modifier = if raw.starts_with('f') { "only" } else { "skip" };
|
||||
let member_names = members.iter().map(Borrow::borrow).collect::<Vec<&str>>().join(".");
|
||||
let member_names = members
|
||||
.iter()
|
||||
.filter_map(KnownMemberExpressionProperty::name)
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
let name_slice = &raw[1..];
|
||||
|
||||
if member_names.is_empty() {
|
||||
|
|
@ -66,35 +68,33 @@ fn get_preferred_node_names(jest_fn_call: &ParsedJestFnCall) -> Atom {
|
|||
|
||||
impl Rule for NoTestPrefixes {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
if let AstKind::CallExpression(call_expr) = node.kind() {
|
||||
if let Some(jest_fn_call) = parse_jest_fn_call(call_expr, ctx) {
|
||||
let ParsedJestFnCall { kind, raw, .. } = &jest_fn_call;
|
||||
let AstKind::CallExpression(call_expr) = node.kind() else { return };
|
||||
let Some(jest_fn_call) = parse_general_jest_fn_call(call_expr, node, ctx) else { return };
|
||||
let ParsedGeneralJestFnCall { kind, raw, .. } = &jest_fn_call;
|
||||
let Some(kind) = kind.to_general() else {return};
|
||||
|
||||
if !matches!(kind, JestFnKind::Describe | JestFnKind::Test) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !raw.starts_with('f') && !raw.starts_with('x') {
|
||||
return;
|
||||
}
|
||||
|
||||
let span = match &call_expr.callee {
|
||||
Expression::TaggedTemplateExpression(tagged_template_expr) => {
|
||||
tagged_template_expr.tag.span()
|
||||
}
|
||||
Expression::CallExpression(child_call_expr) => child_call_expr.callee.span(),
|
||||
_ => call_expr.callee.span(),
|
||||
};
|
||||
|
||||
let preferred_node_name = get_preferred_node_names(&jest_fn_call);
|
||||
let preferred_node_name_cloned = preferred_node_name.clone();
|
||||
|
||||
ctx.diagnostic_with_fix(
|
||||
NoTestPrefixesDiagnostic(preferred_node_name, span),
|
||||
|| Fix::new(preferred_node_name_cloned.to_string(), span),
|
||||
);
|
||||
}
|
||||
if !matches!(kind, JestGeneralFnKind::Describe | JestGeneralFnKind::Test) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !raw.starts_with('f') && !raw.starts_with('x') {
|
||||
return;
|
||||
}
|
||||
|
||||
let span = match &call_expr.callee {
|
||||
Expression::TaggedTemplateExpression(tagged_template_expr) => {
|
||||
tagged_template_expr.tag.span()
|
||||
}
|
||||
Expression::CallExpression(child_call_expr) => child_call_expr.callee.span(),
|
||||
_ => call_expr.callee.span(),
|
||||
};
|
||||
|
||||
let preferred_node_name = get_preferred_node_names(&jest_fn_call);
|
||||
let preferred_node_name_cloned = preferred_node_name.clone();
|
||||
|
||||
ctx.diagnostic_with_fix(NoTestPrefixesDiagnostic(preferred_node_name, span),
|
||||
|| Fix::new(preferred_node_name_cloned.to_string(), span),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,13 +132,14 @@ fn test() {
|
|||
("xit.each``('foo', function () {})", None),
|
||||
("xtest.each``('foo', function () {})", None),
|
||||
("xit.each([])('foo', function () {})", None),
|
||||
("xtest.each([])('foo', function () {})", None), // TODO: Continue work on it when [#510](https://github.com/Boshen/oxc/issues/510) solved
|
||||
// (r#"import { xit } from '@jest/globals';
|
||||
// xit("foo", function () {})"#, None),
|
||||
// (r#"import { xit as skipThis } from '@jest/globals';
|
||||
// skipThis("foo", function () {})"#, None),
|
||||
// (r#"import { fit as onlyThis } from '@jest/globals';
|
||||
// onlyThis("foo", function () {})"#, None)
|
||||
("xtest.each([])('foo', function () {})", None),
|
||||
// TODO: Continue work on it when [#510](https://github.com/Boshen/oxc/issues/510) solved
|
||||
// (r#"import { xit } from '@jest/globals';
|
||||
// xit("foo", function () {})"#, None),
|
||||
// (r#"import { xit as skipThis } from '@jest/globals';
|
||||
// skipThis("foo", function () {})"#, None),
|
||||
// (r#"import { fit as onlyThis } from '@jest/globals';
|
||||
// onlyThis("foo", function () {})"#, None)
|
||||
];
|
||||
|
||||
Tester::new(NoTestPrefixes::NAME, pass, fail).test_and_snapshot();
|
||||
|
|
|
|||
|
|
@ -30,13 +30,6 @@ expression: no_disabled_tests
|
|||
╰────
|
||||
help: "Remove the appending `.skip`"
|
||||
|
||||
⚠ eslint(jest/no-disabled-tests): "Disabled test suite"
|
||||
╭─[no_disabled_tests.tsx:1:1]
|
||||
1 │ describe[`skip`]('foo', function () {})
|
||||
· ───────────────────────────────────────
|
||||
╰────
|
||||
help: "Remove the appending `.skip`"
|
||||
|
||||
⚠ eslint(jest/no-disabled-tests): "Disabled test suite"
|
||||
╭─[no_disabled_tests.tsx:1:1]
|
||||
1 │ describe['skip']('foo', function () {})
|
||||
|
|
|
|||
131
crates/oxc_linter/src/snapshots/no_focused_tests.snap
Normal file
131
crates/oxc_linter/src/snapshots/no_focused_tests.snap
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
expression: no_focused_tests
|
||||
---
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ describe.only()
|
||||
· ───────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ describe.only.each()()
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ describe.only.each`table`()
|
||||
· ───────────────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ describe["only"]()
|
||||
· ──────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ it.only()
|
||||
· ─────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ it.concurrent.only.each``()
|
||||
· ───────────────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ it.only.each()()
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ it.only.each`table`()
|
||||
· ─────────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ it["only"]()
|
||||
· ────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ test.only()
|
||||
· ───────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ test.concurrent.only.each()()
|
||||
· ─────────────────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ test.only.each()()
|
||||
· ──────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ test.only.each`table`()
|
||||
· ───────────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ test["only"]()
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ fdescribe()
|
||||
· ───────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ fit()
|
||||
· ─────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ fit.each()()
|
||||
· ────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
⚠ Unexpected focused test.
|
||||
╭─[no_focused_tests.tsx:1:1]
|
||||
1 │ fit.each`table`()
|
||||
· ─────────────────
|
||||
╰────
|
||||
help: Remove focus from test.
|
||||
|
||||
|
||||
Loading…
Reference in a new issue