Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flake8-django]: Implement rule DJ012 #3659

Merged
merged 3 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_django/DJ012.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from django.db import models
from django.db.models import Model


class StrBeforeRandomField(models.Model):
"""Model with `__str__` before a random property."""

class Meta:
verbose_name = "test"
verbose_name_plural = "tests"

def __str__(self):
return ""

random_property = "foo"


class StrBeforeFieldModel(models.Model):
"""Model with `__str__` before fields."""

class Meta:
verbose_name = "test"
verbose_name_plural = "tests"

def __str__(self):
return "foobar"

first_name = models.CharField(max_length=32)


class ManagerBeforeField(models.Model):
"""Model with manager before fields."""

objects = "manager"

class Meta:
verbose_name = "test"
verbose_name_plural = "tests"

def __str__(self):
return "foobar"

first_name = models.CharField(max_length=32)


class CustomMethodBeforeStr(models.Model):
"""Model with a custom method before `__str__`."""

class Meta:
verbose_name = "test"
verbose_name_plural = "tests"

def my_method(self):
pass

def __str__(self):
return "foobar"


class GetAbsoluteUrlBeforeSave(Model):
"""Model with `get_absolute_url` method before `save` method.

Subclass this directly using the `Model` class.
"""

def get_absolute_url(self):
pass

def save(self):
pass


class ConstantsAreNotFields(models.Model):
"""Model with an assignment to a constant after `__str__`."""

first_name = models.CharField(max_length=32)

class Meta:
verbose_name = "test"
verbose_name_plural = "tests"

def __str__(self):
pass

MY_CONSTANT = id(1)


class PerfectlyFine(models.Model):
"""Model which has everything in perfect order."""

first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
objects = "manager"

class Meta:
verbose_name = "test"
verbose_name_plural = "tests"

def __str__(self):
return "Perfectly fine!"

def save(self, **kwargs):
super(PerfectlyFine, self).save(**kwargs)

def get_absolute_url(self):
return "http://%s" % self

def my_method(self):
pass

@property
def random_property(self):
return "%s" % self
7 changes: 7 additions & 0 deletions crates/ruff/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,13 @@ where
self.diagnostics.push(diagnostic);
}
}
if self
.settings
.rules
.enabled(Rule::DjangoUnorderedBodyContentInModel)
{
flake8_django::rules::unordered_body_content_in_model(self, bases, body);
}
if self.settings.rules.enabled(Rule::GlobalStatement) {
pylint::rules::global_statement(self, name);
}
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Django, "006") => Rule::DjangoExcludeWithModelForm,
(Flake8Django, "007") => Rule::DjangoAllWithModelForm,
(Flake8Django, "008") => Rule::DjangoModelWithoutDunderStr,
(Flake8Django, "012") => Rule::DjangoUnorderedBodyContentInModel,
(Flake8Django, "013") => Rule::DjangoNonLeadingReceiverDecorator,

_ => return None,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ ruff_macros::register_rules!(
rules::flake8_django::rules::DjangoExcludeWithModelForm,
rules::flake8_django::rules::DjangoAllWithModelForm,
rules::flake8_django::rules::DjangoModelWithoutDunderStr,
rules::flake8_django::rules::DjangoUnorderedBodyContentInModel,
rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator,
);

Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/rules/flake8_django/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod tests {
#[test_case(Rule::DjangoExcludeWithModelForm, Path::new("DJ006.py"); "DJ006")]
#[test_case(Rule::DjangoAllWithModelForm, Path::new("DJ007.py"); "DJ007")]
#[test_case(Rule::DjangoModelWithoutDunderStr, Path::new("DJ008.py"); "DJ008")]
#[test_case(Rule::DjangoUnorderedBodyContentInModel, Path::new("DJ012.py"); "DJ012")]
#[test_case(Rule::DjangoNonLeadingReceiverDecorator, Path::new("DJ013.py"); "DJ013")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
Expand Down
12 changes: 12 additions & 0 deletions crates/ruff/src/rules/flake8_django/rules/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ pub fn is_model_form(checker: &Checker, base: &Expr) -> bool {
})
}

/// Return `true` if the expression is constructor for a Django model field.
pub fn is_model_field(checker: &Checker, expr: &Expr) -> bool {
checker
.ctx
.resolve_call_path(expr)
.map_or(false, |call_path| {
call_path
.as_slice()
.starts_with(&["django", "db", "models"])
})
}

/// Return the name of the field type, if the expression is constructor for a Django model field.
pub fn get_model_field_name<'a>(checker: &'a Checker, expr: &'a Expr) -> Option<&'a str> {
checker.ctx.resolve_call_path(expr).and_then(|call_path| {
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff/src/rules/flake8_django/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ pub use non_leading_receiver_decorator::{
pub use nullable_model_string_field::{
nullable_model_string_field, DjangoNullableModelStringField,
};
pub use unordered_body_content_in_model::{
unordered_body_content_in_model, DjangoUnorderedBodyContentInModel,
};

mod all_with_model_form;
mod exclude_with_model_form;
Expand All @@ -16,3 +19,4 @@ mod locals_in_render_function;
mod model_without_dunder_str;
mod non_leading_receiver_decorator;
mod nullable_model_string_field;
mod unordered_body_content_in_model;
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use std::fmt;

use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind};

use crate::checkers::ast::Checker;

use super::helpers;

/// ## What it does
/// Checks for the order of Model's inner classes, methods, and fields as per
/// the [Django Style Guide].
///
/// ## Why is it bad?
/// The [Django Style Guide] specifies that the order of Model inner classes,
/// attributes and methods should be as follows:
///
/// 1. All database fields
/// 2. Custom manager attributes
/// 3. `class Meta`
/// 4. `def __str__()`
/// 5. `def save()`
/// 6. `def get_absolute_url()`
/// 7. Any custom methods
///
/// ## Examples
/// ```python
/// from django.db import models
///
///
/// class StrBeforeFieldModel(models.Model):
/// class Meta:
/// verbose_name = "test"
/// verbose_name_plural = "tests"
///
/// def __str__(self):
/// return "foobar"
///
/// first_name = models.CharField(max_length=32)
/// last_name = models.CharField(max_length=40)
/// ```
///
/// Use instead:
/// ```python
/// from django.db import models
///
///
/// class StrBeforeFieldModel(models.Model):
/// first_name = models.CharField(max_length=32)
/// last_name = models.CharField(max_length=40)
///
/// class Meta:
/// verbose_name = "test"
/// verbose_name_plural = "tests"
///
/// def __str__(self):
/// return "foobar"
/// ```
///
/// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style
#[violation]
pub struct DjangoUnorderedBodyContentInModel {
pub elem_type: ContentType,
pub before: 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}")
}
}

#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub enum ContentType {
FieldDeclaration,
ManagerDeclaration,
MetaClass,
StrMethod,
SaveMethod,
GetAbsoluteUrlMethod,
CustomMethod,
}

impl fmt::Display for ContentType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContentType::FieldDeclaration => f.write_str("field declaration"),
ContentType::ManagerDeclaration => f.write_str("manager declaration"),
ContentType::MetaClass => f.write_str("`Meta` class"),
ContentType::StrMethod => f.write_str("`__str__` method"),
ContentType::SaveMethod => f.write_str("`save` method"),
ContentType::GetAbsoluteUrlMethod => f.write_str("`get_absolute_url` method"),
ContentType::CustomMethod => f.write_str("custom method"),
}
}
}

fn get_element_type(checker: &Checker, element: &StmtKind) -> Option<ContentType> {
match element {
StmtKind::Assign { targets, value, .. } => {
if let ExprKind::Call { func, .. } = &value.node {
if helpers::is_model_field(checker, func) {
return Some(ContentType::FieldDeclaration);
}
}
let Some(expr) = targets.first() else {
return None;
};
let ExprKind::Name { id, .. } = &expr.node else {
return None;
};
if id == "objects" {
Some(ContentType::ManagerDeclaration)
} else {
None
}
}
StmtKind::ClassDef { name, .. } => {
if name == "Meta" {
Some(ContentType::MetaClass)
} else {
None
}
}
StmtKind::FunctionDef { name, .. } => match name.as_str() {
"__str__" => Some(ContentType::StrMethod),
"save" => Some(ContentType::SaveMethod),
"get_absolute_url" => Some(ContentType::GetAbsoluteUrlMethod),
_ => Some(ContentType::CustomMethod),
},
_ => None,
}
}

/// DJ012
pub fn unordered_body_content_in_model(checker: &mut Checker, bases: &[Expr], body: &[Stmt]) {
if !bases.iter().any(|base| helpers::is_model(checker, base)) {
return;
}
let mut elements_type_found = Vec::new();
for element in body.iter() {
let Some(current_element_type) = get_element_type(checker, &element.node) else {
continue;
};
let Some(&element_type) = elements_type_found
.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,
},
Range::from(element),
);
checker.diagnostics.push(diagnostic);
}
}
Loading