[`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
def random_property(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
#[violation]
pub struct DjangoUnorderedBodyContentInModel {
elem_type: ContentType,
before: ContentType,
element_type: ContentType,
prev_element_type: ContentType,
}
impl Violation for DjangoUnorderedBodyContentInModel {
#[derive_message_formats]
fn message(&self) -> String {
let DjangoUnorderedBodyContentInModel { elem_type, before } = self;
format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {elem_type} should come before {before}")
let DjangoUnorderedBodyContentInModel {
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)]
pub(crate) enum ContentType {
enum ContentType {
FieldDeclaration,
ManagerDeclaration,
MetaClass,
@ -149,24 +152,38 @@ pub(crate) fn unordered_body_content_in_model(
{
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() {
let Some(current_element_type) = get_element_type(element, checker.semantic()) else {
let Some(element_type) = get_element_type(element, checker.semantic()) else {
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()
.find(|&&element_type| element_type > current_element_type) else {
elements_type_found.push(current_element_type);
continue;
};
let diagnostic = Diagnostic::new(
DjangoUnorderedBodyContentInModel {
elem_type: current_element_type,
before: element_type,
},
element.range(),
);
checker.diagnostics.push(diagnostic);
.find(|&&prev_element_type| prev_element_type > element_type)
{
let diagnostic = Diagnostic::new(
DjangoUnorderedBodyContentInModel {
element_type,
prev_element_type,
},
element.range(),
);
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.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
|