mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter): eslint-plugin-jest/no-untyped-mock-factory (#2807)
Rule Detail: [link](https://github.com/jest-community/eslint-plugin-jest/blob/main/src/rules/no-untyped-mock-factory.ts) --------- Co-authored-by: wenzhe <mysteryven@gmail.com> Co-authored-by: Dunqing <dengqing0821@gmail.com>
This commit is contained in:
parent
df744b205a
commit
f131442bbf
3 changed files with 474 additions and 0 deletions
|
|
@ -164,6 +164,7 @@ mod jest {
|
||||||
pub mod no_standalone_expect;
|
pub mod no_standalone_expect;
|
||||||
pub mod no_test_prefixes;
|
pub mod no_test_prefixes;
|
||||||
pub mod no_test_return_statement;
|
pub mod no_test_return_statement;
|
||||||
|
pub mod no_untyped_mock_factory;
|
||||||
pub mod prefer_called_with;
|
pub mod prefer_called_with;
|
||||||
pub mod prefer_equality_matcher;
|
pub mod prefer_equality_matcher;
|
||||||
pub mod prefer_expect_resolves;
|
pub mod prefer_expect_resolves;
|
||||||
|
|
@ -484,6 +485,7 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
jest::no_standalone_expect,
|
jest::no_standalone_expect,
|
||||||
jest::no_test_prefixes,
|
jest::no_test_prefixes,
|
||||||
jest::no_test_return_statement,
|
jest::no_test_return_statement,
|
||||||
|
jest::no_untyped_mock_factory,
|
||||||
jest::prefer_called_with,
|
jest::prefer_called_with,
|
||||||
jest::prefer_equality_matcher,
|
jest::prefer_equality_matcher,
|
||||||
jest::prefer_expect_resolves,
|
jest::prefer_expect_resolves,
|
||||||
|
|
|
||||||
414
crates/oxc_linter/src/rules/jest/no_untyped_mock_factory.rs
Normal file
414
crates/oxc_linter/src/rules/jest/no_untyped_mock_factory.rs
Normal file
|
|
@ -0,0 +1,414 @@
|
||||||
|
use crate::{
|
||||||
|
context::LintContext,
|
||||||
|
fixer::Fix,
|
||||||
|
rule::Rule,
|
||||||
|
utils::{collect_possible_jest_call_node, PossibleJestNode},
|
||||||
|
};
|
||||||
|
|
||||||
|
use oxc_ast::{
|
||||||
|
ast::{Argument, Expression},
|
||||||
|
AstKind,
|
||||||
|
};
|
||||||
|
use oxc_diagnostics::{
|
||||||
|
miette::{self, Diagnostic},
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
use oxc_macros::declare_oxc_lint;
|
||||||
|
use oxc_span::{CompactStr, Span};
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
#[error("eslint-plugin-jest(no-untyped-mock-factory): Disallow using `jest.mock()` factories without an explicit type parameter.")]
|
||||||
|
#[diagnostic(
|
||||||
|
severity(warning),
|
||||||
|
help("Add a type parameter to the mock factory such as `typeof import({0:?})`")
|
||||||
|
)]
|
||||||
|
struct AddTypeParameterToModuleMockDiagnostic(CompactStr, #[label] pub Span);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct NoUntypedMockFactory;
|
||||||
|
|
||||||
|
declare_oxc_lint!(
|
||||||
|
/// ### What it does
|
||||||
|
///
|
||||||
|
/// This rule triggers a warning if `mock()` or `doMock()` is used without a generic
|
||||||
|
/// type parameter or return type.
|
||||||
|
///
|
||||||
|
/// ### Why is this bad?
|
||||||
|
///
|
||||||
|
/// By default, `jest.mock` and `jest.doMock` allow any type to be returned by a
|
||||||
|
/// mock factory. A generic type parameter can be used to enforce that the factory
|
||||||
|
/// returns an object with the same shape as the original module, or some other
|
||||||
|
/// strict type. Requiring a type makes it easier to use TypeScript to catch changes
|
||||||
|
/// needed in test mocks when the source module changes.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
///
|
||||||
|
/// // invalid
|
||||||
|
/// ```typescript
|
||||||
|
/// jest.mock('../moduleName', () => {
|
||||||
|
/// return jest.fn(() => 42);
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// jest.mock('./module', () => ({
|
||||||
|
/// ...jest.requireActual('./module'),
|
||||||
|
/// foo: jest.fn(),
|
||||||
|
/// }));
|
||||||
|
///
|
||||||
|
/// jest.mock('random-num', () => {
|
||||||
|
/// return jest.fn(() => 42);
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// // valid
|
||||||
|
/// ```typescript
|
||||||
|
///
|
||||||
|
/// // Uses typeof import()
|
||||||
|
/// jest.mock<typeof import('../moduleName')>('../moduleName', () => {
|
||||||
|
/// return jest.fn(() => 42);
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// jest.mock<typeof import('./module')>('./module', () => ({
|
||||||
|
/// ...jest.requireActual('./module'),
|
||||||
|
/// foo: jest.fn(),
|
||||||
|
/// }));
|
||||||
|
///
|
||||||
|
/// // Uses custom type
|
||||||
|
/// jest.mock<() => number>('random-num', () => {
|
||||||
|
/// return jest.fn(() => 42);
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// // No factory
|
||||||
|
/// jest.mock('random-num');
|
||||||
|
///
|
||||||
|
/// // Virtual mock
|
||||||
|
/// jest.mock(
|
||||||
|
/// '../moduleName',
|
||||||
|
/// () => {
|
||||||
|
/// return jest.fn(() => 42);
|
||||||
|
/// },
|
||||||
|
/// { virtual: true },
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
NoUntypedMockFactory,
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Rule for NoUntypedMockFactory {
|
||||||
|
fn run_once(&self, ctx: &LintContext<'_>) {
|
||||||
|
if !ctx.source_type().is_typescript() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for possible_jest_node in &collect_possible_jest_call_node(ctx) {
|
||||||
|
Self::run(possible_jest_node, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoUntypedMockFactory {
|
||||||
|
fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) {
|
||||||
|
let node = possible_jest_node.node;
|
||||||
|
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Expression::MemberExpression(mem_expr) = &call_expr.callee else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some((property_span, property_name)) = mem_expr.static_property_info() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if call_expr.arguments.len() != 2 && (property_name != "mock" || property_name != "doMock")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(factory_node) = call_expr.arguments.get(1) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if call_expr.type_parameters.is_some() || Self::has_return_type(factory_node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(name_node) = call_expr.arguments.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Argument::Expression(expr) = name_node else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Expression::StringLiteral(string_literal) = expr {
|
||||||
|
ctx.diagnostic_with_fix(
|
||||||
|
AddTypeParameterToModuleMockDiagnostic(
|
||||||
|
string_literal.value.to_compact_str(),
|
||||||
|
property_span,
|
||||||
|
),
|
||||||
|
|| {
|
||||||
|
let mut content = ctx.codegen();
|
||||||
|
content.print_str(b"<typeof import('");
|
||||||
|
content.print_str(string_literal.value.as_bytes());
|
||||||
|
content.print_str(b"')>(");
|
||||||
|
|
||||||
|
Fix::new(
|
||||||
|
content.into_source_text(),
|
||||||
|
Span::new(string_literal.span.start - 1, string_literal.span.start),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if let Expression::Identifier(ident) = expr {
|
||||||
|
ctx.diagnostic(AddTypeParameterToModuleMockDiagnostic(
|
||||||
|
ident.name.to_compact_str(),
|
||||||
|
property_span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_return_type(argument: &Argument) -> bool {
|
||||||
|
let Argument::Expression(expr) = argument else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
match expr {
|
||||||
|
Expression::FunctionExpression(func_expr) => func_expr.return_type.is_some(),
|
||||||
|
Expression::ArrowFunctionExpression(arrow_func_expr) => {
|
||||||
|
arrow_func_expr.return_type.is_some()
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
|
||||||
|
let pass = vec![
|
||||||
|
("jest.mock('random-number');", None),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.mock<typeof import('../moduleName')>('../moduleName', () => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.mock<typeof import('./module')>('./module', () => ({
|
||||||
|
...jest.requireActual('./module'),
|
||||||
|
foo: jest.fn()
|
||||||
|
}));
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.mock<typeof import('foo')>('bar', () => ({
|
||||||
|
...jest.requireActual('bar'),
|
||||||
|
foo: jest.fn()
|
||||||
|
}));
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.doMock('./module', (): typeof import('./module') => ({
|
||||||
|
...jest.requireActual('./module'),
|
||||||
|
foo: jest.fn()
|
||||||
|
}));
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.mock('../moduleName', function (): typeof import('../moduleName') {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.mock('../moduleName', function (): (() => number) {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.mock<() => number>('random-num', () => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest['doMock']<() => number>('random-num', () => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.mock<any>('random-num', () => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
jest.mock(
|
||||||
|
'../moduleName',
|
||||||
|
() => {
|
||||||
|
return jest.fn(() => 42)
|
||||||
|
},
|
||||||
|
{virtual: true},
|
||||||
|
);
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
// Should not match
|
||||||
|
(
|
||||||
|
"
|
||||||
|
mockito<() => number>('foo', () => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let fail = vec => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
const moduleToMock = 'random-num';
|
||||||
|
jest.mock(moduleToMock, () => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let fix = vec => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
"
|
||||||
|
jest['mock']<typeof import('random-num')>('random-num', () => {
|
||||||
|
return jest.fn(() => 42);
|
||||||
|
});
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Tester::new(NoUntypedMockFactory::NAME, pass, fail)
|
||||||
|
.with_jest_plugin(true)
|
||||||
|
.expect_fix(fix)
|
||||||
|
.test_and_snapshot();
|
||||||
|
}
|
||||||
58
crates/oxc_linter/src/snapshots/no_untyped_mock_factory.snap
Normal file
58
crates/oxc_linter/src/snapshots/no_untyped_mock_factory.snap
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
source: crates/oxc_linter/src/tester.rs
|
||||||
|
assertion_line: 151
|
||||||
|
expression: no_untyped_mock_factory
|
||||||
|
---
|
||||||
|
⚠ eslint-plugin-jest(no-untyped-mock-factory): Disallow using `jest.mock()` factories without an explicit type parameter.
|
||||||
|
╭─[no_untyped_mock_factory.tsx:2:22]
|
||||||
|
1 │
|
||||||
|
2 │ jest.mock('../moduleName', () => {
|
||||||
|
· ────
|
||||||
|
3 │ return jest.fn(() => 42);
|
||||||
|
╰────
|
||||||
|
help: Add a type parameter to the mock factory such as `typeof import("../moduleName")`
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(no-untyped-mock-factory): Disallow using `jest.mock()` factories without an explicit type parameter.
|
||||||
|
╭─[no_untyped_mock_factory.tsx:2:22]
|
||||||
|
1 │
|
||||||
|
2 │ jest.mock("./module", () => ({
|
||||||
|
· ────
|
||||||
|
3 │ ...jest.requireActual('./module'),
|
||||||
|
╰────
|
||||||
|
help: Add a type parameter to the mock factory such as `typeof import("./module")`
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(no-untyped-mock-factory): Disallow using `jest.mock()` factories without an explicit type parameter.
|
||||||
|
╭─[no_untyped_mock_factory.tsx:2:22]
|
||||||
|
1 │
|
||||||
|
2 │ jest.mock('random-num', () => {
|
||||||
|
· ────
|
||||||
|
3 │ return jest.fn(() => 42);
|
||||||
|
╰────
|
||||||
|
help: Add a type parameter to the mock factory such as `typeof import("random-num")`
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(no-untyped-mock-factory): Disallow using `jest.mock()` factories without an explicit type parameter.
|
||||||
|
╭─[no_untyped_mock_factory.tsx:2:22]
|
||||||
|
1 │
|
||||||
|
2 │ jest.doMock('random-num', () => {
|
||||||
|
· ──────
|
||||||
|
3 │ return jest.fn(() => 42);
|
||||||
|
╰────
|
||||||
|
help: Add a type parameter to the mock factory such as `typeof import("random-num")`
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(no-untyped-mock-factory): Disallow using `jest.mock()` factories without an explicit type parameter.
|
||||||
|
╭─[no_untyped_mock_factory.tsx:2:22]
|
||||||
|
1 │
|
||||||
|
2 │ jest['mock']('random-num', () => {
|
||||||
|
· ──────
|
||||||
|
3 │ return jest.fn(() => 42);
|
||||||
|
╰────
|
||||||
|
help: Add a type parameter to the mock factory such as `typeof import("random-num")`
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(no-untyped-mock-factory): Disallow using `jest.mock()` factories without an explicit type parameter.
|
||||||
|
╭─[no_untyped_mock_factory.tsx:3:22]
|
||||||
|
2 │ const moduleToMock = 'random-num';
|
||||||
|
3 │ jest.mock(moduleToMock, () => {
|
||||||
|
· ────
|
||||||
|
4 │ return jest.fn(() => 42);
|
||||||
|
╰────
|
||||||
|
help: Add a type parameter to the mock factory such as `typeof import("moduleToMock")`
|
||||||
Loading…
Reference in a new issue