[`flake8-django`] Skip duplicate violations in `DJ012` (#5469)

## Summary

This PR reduces the noise from `DJ012` by emitting a single violation
when you have multiple consecutive violations of the same "type".

For example, given:

```py
class MultipleConsecutiveFields(models.Model):
    """Model that contains multiple out-of-order field definitions in a row."""


    class Meta:
        verbose_name = "test"

    first_name = models.CharField(max_length=32)
    last_name = models.CharField(max_length=32)
```

It's convenient to only error on `first_name`, and not `last_name`,
since we're really flagging that the _section_ is out-of-order.

Closes #5465.
This commit is contained in:
Charlie Marsh 2023-07-02 21:09:49 -04:00 committed by GitHub
parent d0b2fffb87
commit 6cc04d64e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 70 additions and 20 deletions

View File

@ -111,3 +111,19 @@ class PerfectlyFine(models.Model):
@property @property
def random_property(self): def random_property(self):
return "%s" % self return "%s" % self
class MultipleConsecutiveFields(models.Model):
"""Model that contains multiple out-of-order field definitions in a row."""
class Meta:
verbose_name = "test"
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
def get_absolute_url(self):
pass
middle_name = models.CharField(max_length=32)

View File

@ -63,20 +63,23 @@ use super::helpers;
/// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style /// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style
#[violation] #[violation]
pub struct DjangoUnorderedBodyContentInModel { pub struct DjangoUnorderedBodyContentInModel {
elem_type: ContentType, element_type: ContentType,
before: ContentType, prev_element_type: ContentType,
} }
impl Violation for DjangoUnorderedBodyContentInModel { impl Violation for DjangoUnorderedBodyContentInModel {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let DjangoUnorderedBodyContentInModel { elem_type, before } = self; let DjangoUnorderedBodyContentInModel {
format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {elem_type} should come before {before}") element_type,
prev_element_type,
} = self;
format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {element_type} should come before {prev_element_type}")
} }
} }
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub(crate) enum ContentType { enum ContentType {
FieldDeclaration, FieldDeclaration,
ManagerDeclaration, ManagerDeclaration,
MetaClass, MetaClass,
@ -149,24 +152,38 @@ pub(crate) fn unordered_body_content_in_model(
{ {
return; return;
} }
let mut elements_type_found = Vec::new();
// Track all the element types we've seen so far.
let mut element_types = Vec::new();
let mut prev_element_type = None;
for element in body.iter() { for element in body.iter() {
let Some(current_element_type) = get_element_type(element, checker.semantic()) else { let Some(element_type) = get_element_type(element, checker.semantic()) else {
continue; continue;
}; };
let Some(&element_type) = elements_type_found
// Skip consecutive elements of the same type. It's less noisy to only report
// violations at type boundaries (e.g., avoid raising a violation for _every_
// field declaration that's out of order).
if prev_element_type == Some(element_type) {
continue;
}
prev_element_type = Some(element_type);
if let Some(&prev_element_type) = element_types
.iter() .iter()
.find(|&&element_type| element_type > current_element_type) else { .find(|&&prev_element_type| prev_element_type > element_type)
elements_type_found.push(current_element_type); {
continue;
};
let diagnostic = Diagnostic::new( let diagnostic = Diagnostic::new(
DjangoUnorderedBodyContentInModel { DjangoUnorderedBodyContentInModel {
elem_type: current_element_type, element_type,
before: element_type, prev_element_type,
}, },
element.range(), element.range(),
); );
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} else {
element_types.push(element_type);
}
} }
} }

View File

@ -37,4 +37,21 @@ DJ012.py:69:5: DJ012 Order of model's inner classes, methods, and fields does no
| |____________^ DJ012 | |____________^ DJ012
| |
DJ012.py:123:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class
|
121 | verbose_name = "test"
122 |
123 | first_name = models.CharField(max_length=32)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
124 | last_name = models.CharField(max_length=32)
|
DJ012.py:129:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class
|
127 | pass
128 |
129 | middle_name = models.CharField(max_length=32)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
|