This commit is contained in:
Jeppe Fihl-Pearson 2025-12-16 16:40:21 -05:00 committed by GitHub
commit 57287a8797
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 287 additions and 0 deletions

View File

@ -1189,6 +1189,16 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Logging, "014") => rules::flake8_logging::rules::ExcInfoOutsideExceptHandler,
(Flake8Logging, "015") => rules::flake8_logging::rules::RootLoggerCall,
// flake8-mock-spec
(Flake8MockSpec, "010") => (RuleGroup::Preview, rules::flake8_mock_spec::rules::Mock),
(Flake8MockSpec, "011") => (RuleGroup::Preview, rules::flake8_mock_spec::rules::MagicMock),
(Flake8MockSpec, "012") => (RuleGroup::Preview, rules::flake8_mock_spec::rules::NonCallableMock),
(Flake8MockSpec, "013") => (RuleGroup::Preview, rules::flake8_mock_spec::rules::AsyncMock),
(Flake8MockSpec, "014") => (RuleGroup::Preview, rules::flake8_mock_spec::rules::ThreadingMock),
(Flake8MockSpec, "020") => (RuleGroup::Preview, rules::flake8_mock_spec::rules::Patch),
(Flake8MockSpec, "021") => (RuleGroup::Preview, rules::flake8_mock_spec::rules::PatchObject),
(Flake8MockSpec, "022") => (RuleGroup::Preview, rules::flake8_mock_spec::rules::PatchMultiple),
_ => return None,
})
}

View File

@ -112,6 +112,9 @@ pub enum Linter {
/// [flake8-logging-format](https://pypi.org/project/flake8-logging-format/)
#[prefix = "G"]
Flake8LoggingFormat,
/// [flake8-mock-spec](https://pypi.org/project/flake8-mock-spec/)
#[prefix = "TMS"]
Flake8MockSpec,
/// [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/)
#[prefix = "INP"]
Flake8NoPep420,

View File

@ -0,0 +1,33 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
#[derive(ViolationMetadata)]
pub(crate) struct AsyncMock;
impl Violation for AsyncMock {
#[derive_message_formats]
fn message(&self) -> String {
"`unittest.mock.AsyncMock` without `spec` or `spec_set` argument".to_string()
}
}
pub(crate) fn mock(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::UNITTEST) {
return;
}
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["unittest", "mock", "AsyncMock"])
})
{
if call.arguments.find_argument("spec", 0).is_none()
&& call.arguments.find_keyword("spec_set").is_none()
{
let mut diagnostic = checker.report_diagnostic(AsyncMock, call.func.range());
}
}
}

View File

@ -0,0 +1,33 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
#[derive(ViolationMetadata)]
pub(crate) struct MagicMock;
impl Violation for MagicMock {
#[derive_message_formats]
fn message(&self) -> String {
"`unittest.mock.MagicMock` without `spec` or `spec_set` argument".to_string()
}
}
pub(crate) fn mock(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::UNITTEST) {
return;
}
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["unittest", "mock", "MagicMock"])
})
{
if call.arguments.find_argument("spec", 0).is_none()
&& call.arguments.find_keyword("spec_set").is_none()
{
let mut diagnostic = checker.report_diagnostic(MagicMock, call.func.range());
}
}
}

View File

@ -0,0 +1,33 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
#[derive(ViolationMetadata)]
pub(crate) struct Mock;
impl Violation for Mock {
#[derive_message_formats]
fn message(&self) -> String {
"`unittest.mock.Mock` without `spec` or `spec_set` argument".to_string()
}
}
pub(crate) fn mock(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::UNITTEST) {
return;
}
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["unittest", "mock", "Mock"])
})
{
if call.arguments.find_argument("spec", 0).is_none()
&& call.arguments.find_keyword("spec_set").is_none()
{
let mut diagnostic = checker.report_diagnostic(Mock, call.func.range());
}
}
}

View File

@ -0,0 +1,33 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
#[derive(ViolationMetadata)]
pub(crate) struct NonCallableMock;
impl Violation for NonCallableMock {
#[derive_message_formats]
fn message(&self) -> String {
"`unittest.mock.NonCallableMock` without `spec` or `spec_set` argument".to_string()
}
}
pub(crate) fn mock(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::UNITTEST) {
return;
}
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["unittest", "mock", "NonCallableMock"])
})
{
if call.arguments.find_argument("spec", 0).is_none()
&& call.arguments.find_keyword("spec_set").is_none()
{
let mut diagnostic = checker.report_diagnostic(NonCallableMock, call.func.range());
}
}
}

View File

@ -0,0 +1,36 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
#[derive(ViolationMetadata)]
pub(crate) struct Patch;
impl Violation for Patch {
#[derive_message_formats]
fn message(&self) -> String {
"`unittest.mock.patch` without one any `autospec`, `new`, `new_callable`, `spec` or `spec_set` argument".to_string()
}
}
pub(crate) fn mock(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::UNITTEST) {
return;
}
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["unittest", "mock", "patch"])
})
{
if call.arguments.find_keyword("autospec").is_none()
&& call.arguments.find_argument("new", 1).is_none()
&& call.arguments.find_keyword("new_callable").is_none()
&& call.arguments.find_keyword("spec").is_none()
&& call.arguments.find_keyword("spec_set").is_none()
{
let mut diagnostic = checker.report_diagnostic(Patch, call.func.range());
}
}
}

View File

@ -0,0 +1,35 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
#[derive(ViolationMetadata)]
pub(crate) struct PatchMultiple;
impl Violation for PatchMultiple {
#[derive_message_formats]
fn message(&self) -> String {
"`unittest.mock.patch.multiple` without one any `autospec`, `new`, `new_callable`, `spec` or `spec_set` argument".to_string()
}
}
pub(crate) fn mock(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::UNITTEST) {
return;
}
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["unittest", "mock", "patch", "multiple"])
})
{
if call.arguments.find_keyword("autospec").is_none()
&& call.arguments.find_keyword("new_callable").is_none()
&& call.arguments.find_argument("spec", 1).is_none()
&& call.arguments.find_keyword("spec_set").is_none()
{
let mut diagnostic = checker.report_diagnostic(PatchMultiple, call.func.range());
}
}
}

View File

@ -0,0 +1,36 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
#[derive(ViolationMetadata)]
pub(crate) struct PatchObject;
impl Violation for PatchObject {
#[derive_message_formats]
fn message(&self) -> String {
"`unittest.mock.patch.object` without one any `autospec`, `new`, `new_callable`, `spec` or `spec_set` argument".to_string()
}
}
pub(crate) fn mock(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::UNITTEST) {
return;
}
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["unittest", "mock", "patch", "object"])
})
{
if call.arguments.find_keyword("autospec").is_none()
&& call.arguments.find_argument("new", 2).is_none()
&& call.arguments.find_keyword("new_callable").is_none()
&& call.arguments.find_keyword("spec").is_none()
&& call.arguments.find_keyword("spec_set").is_none()
{
let mut diagnostic = checker.report_diagnostic(PatchObject, call.func.range());
}
}
}

View File

@ -0,0 +1,33 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
#[derive(ViolationMetadata)]
pub(crate) struct ThreadingMock;
impl Violation for ThreadingMock {
#[derive_message_formats]
fn message(&self) -> String {
"`unittest.mock.ThreadingMock` without `spec` or `spec_set` argument".to_string()
}
}
pub(crate) fn mock(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::UNITTEST) {
return;
}
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["unittest", "mock", "ThreadingMock"])
})
{
if call.arguments.find_argument("spec", 0).is_none()
&& call.arguments.find_keyword("spec_set").is_none()
{
let mut diagnostic = checker.report_diagnostic(ThreadingMock, call.func.range());
}
}
}

View File

@ -1486,6 +1486,7 @@ impl<'a> SemanticModel<'a> {
"airflow" => self.seen.insert(Modules::AIRFLOW),
"hashlib" => self.seen.insert(Modules::HASHLIB),
"crypt" => self.seen.insert(Modules::CRYPT),
"unittest" => self.seen.insert(Modules::UNITTEST),
_ => {}
}
}
@ -2254,6 +2255,7 @@ bitflags! {
const AIRFLOW = 1 << 27;
const HASHLIB = 1 << 28;
const CRYPT = 1 << 29;
const UNITTEST = 1 << 30;
}
}