Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use ruff_db::diagnostic::Diagnostic;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::function_type::is_subject_to_liskov_substitution_principle;

use crate::checkers::ast::Checker;
use crate::settings::LinterSettings;
Expand Down Expand Up @@ -191,3 +193,27 @@ pub(super) fn allow_boolean_trap(call: &ast::ExprCall, checker: &Checker) -> boo

false
}

pub(super) fn add_liskov_substitution_principle_help(
diagnostic: &mut Diagnostic,
function_name: &str,
decorator_list: &[ast::Decorator],
checker: &Checker,
) {
let semantic = checker.semantic();
let parent_scope = semantic.current_scope();
let pep8_settings = &checker.settings().pep8_naming;
if is_subject_to_liskov_substitution_principle(
function_name,
decorator_list,
parent_scope,
semantic,
&pep8_settings.classmethod_decorators,
&pep8_settings.staticmethod_decorators,
) {
diagnostic.help(
"Consider adding `@typing.override` if changing the function signature \
would violate the Liskov Substitution Principle",
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use ruff_python_semantic::analyze::visibility;

use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
use crate::rules::flake8_boolean_trap::helpers::{
add_liskov_substitution_principle_help, is_allowed_func_def,
};

/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
Expand Down Expand Up @@ -139,7 +141,9 @@ pub(crate) fn boolean_default_value_positional_argument(
return;
}

checker.report_diagnostic(BooleanDefaultValuePositionalArgument, param.identifier());
let mut diagnostic = checker
.report_diagnostic(BooleanDefaultValuePositionalArgument, param.identifier());
add_liskov_substitution_principle_help(&mut diagnostic, name, decorator_list, checker);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use ruff_python_semantic::analyze::visibility;

use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
use crate::rules::flake8_boolean_trap::helpers::{
add_liskov_substitution_principle_help, is_allowed_func_def,
};

/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
Expand Down Expand Up @@ -149,7 +151,10 @@ pub(crate) fn boolean_type_hint_positional_argument(
return;
}

checker.report_diagnostic(BooleanTypeHintPositionalArgument, parameter.identifier());
let mut diagnostic =
checker.report_diagnostic(BooleanTypeHintPositionalArgument, parameter.identifier());

add_liskov_substitution_principle_help(&mut diagnostic, name, decorator_list, checker);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ FBT001 Boolean-typed positional argument in function definition
| ^^^^^
91 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle

FBT001 Boolean-typed positional argument in function definition
--> FBT.py:100:10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,16 @@ pub(crate) fn invalid_function_name(
return;
}

checker.report_diagnostic(
let mut diagnostic = checker.report_diagnostic(
InvalidFunctionName {
name: name.to_string(),
},
stmt.identifier(),
);
if parent_class.is_some() {
diagnostic.help(
"Consider adding `@typing.override` if this method \
overrides a method from a superclass",
Comment on lines +141 to +142
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of prefer this phrasing over the LSP version, but I don't feel strongly. I'm guessing we can't call the helper function here because the current scope isn't set?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing we can't call the helper function here because the current scope isn't set?

That wasn't really my rationale for doing something slightly different here. The reasons why I did something slightly different here is that this violation is telling you to rename the function rather than change its signature, and renaming the function isn't going to cause a Liskov violation. It's just not necessarily what you want to do if the function is deliberately overriding something from a subclass 😄

);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ N802 Function name `testTest` should be lowercase
| ^^^^^^^^
41 | assert True
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

N802 Function name `bad_Name` should be lowercase
--> N802.py:65:9
Expand All @@ -52,6 +53,7 @@ N802 Function name `bad_Name` should be lowercase
| ^^^^^^^^
66 | pass
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

N802 Function name `dont_GET` should be lowercase
--> N802.py:84:9
Expand All @@ -62,6 +64,7 @@ N802 Function name `dont_GET` should be lowercase
| ^^^^^^^^
85 | pass
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

N802 Function name `dont_OPTIONS` should be lowercase
--> N802.py:95:9
Expand All @@ -72,6 +75,7 @@ N802 Function name `dont_OPTIONS` should be lowercase
| ^^^^^^^^^^^^
96 | pass
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

N802 Function name `dont_OPTIONS` should be lowercase
--> N802.py:106:9
Expand All @@ -82,3 +86,4 @@ N802 Function name `dont_OPTIONS` should be lowercase
| ^^^^^^^^^^^^
107 | pass
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ N802 Function name `stillBad` should be lowercase
| ^^^^^^^^
14 | return super().tearDown()
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
6 changes: 5 additions & 1 deletion crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,15 @@ pub(crate) fn no_self_use(checker: &Checker, scope_id: ScopeId, scope: &Scope) {
.map(|binding_id| semantic.binding(binding_id))
.is_some_and(|binding| binding.kind.is_argument() && binding.is_unused())
{
checker.report_diagnostic(
let mut diagnostic = checker.report_diagnostic(
NoSelfUse {
method_name: name.to_string(),
},
func.identifier(),
);
diagnostic.help(
"Consider adding `@typing.override` if this method overrides \
a method from a superclass",
);
}
}
16 changes: 15 additions & 1 deletion crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::identifier::Identifier;
use ruff_python_semantic::analyze::function_type::is_subject_to_liskov_substitution_principle;
use ruff_python_semantic::analyze::{function_type, visibility};

use crate::Violation;
Expand Down Expand Up @@ -121,11 +122,24 @@ pub(crate) fn too_many_arguments(checker: &Checker, function_def: &ast::StmtFunc
return;
}

checker.report_diagnostic(
let mut diagnostic = checker.report_diagnostic(
TooManyArguments {
c_args: num_arguments,
max_args: checker.settings().pylint.max_args,
},
function_def.identifier(),
);
if is_subject_to_liskov_substitution_principle(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the helper here? Oh, I see it's an FBT helper.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — I could put the helper somewhere more central, but wasn't totally sure where a good place would be :/

&function_def.name,
&function_def.decorator_list,
semantic.current_scope(),
semantic,
&checker.settings().pep8_naming.classmethod_decorators,
&checker.settings().pep8_naming.staticmethod_decorators,
) {
diagnostic.help(
"Consider adding `@typing.override` if changing the function signature \
would violate the Liskov Substitution Principle",
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, identifier::Identifier};
use ruff_python_semantic::analyze::function_type::is_subject_to_liskov_substitution_principle;
use ruff_python_semantic::analyze::{function_type, visibility};

use crate::Violation;
Expand Down Expand Up @@ -125,11 +126,24 @@ pub(crate) fn too_many_positional_arguments(
return;
}

checker.report_diagnostic(
let mut diagnostic = checker.report_diagnostic(
TooManyPositionalArguments {
c_pos: num_positional_args,
max_pos: checker.settings().pylint.max_positional_args,
},
function_def.identifier(),
);
if is_subject_to_liskov_substitution_principle(
&function_def.name,
&function_def.decorator_list,
semantic.current_scope(),
semantic,
&checker.settings().pep8_naming.classmethod_decorators,
&checker.settings().pep8_naming.staticmethod_decorators,
) {
diagnostic.help(
"Consider adding `@typing.override` if changing the function signature \
would violate the Liskov Substitution Principle",
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ PLR0913 Too many arguments in function definition (8 > 5)
| ^
52 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle

PLR0913 Too many arguments in function definition (8 > 5)
--> too_many_arguments.py:58:9
Expand All @@ -58,6 +59,7 @@ PLR0913 Too many arguments in function definition (8 > 5)
| ^
59 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle

PLR0913 Too many arguments in function definition (8 > 5)
--> too_many_arguments.py:66:9
Expand All @@ -67,6 +69,7 @@ PLR0913 Too many arguments in function definition (8 > 5)
| ^
67 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle

PLR0913 Too many arguments in function definition (6 > 5)
--> too_many_arguments.py:70:9
Expand All @@ -76,3 +79,4 @@ PLR0913 Too many arguments in function definition (6 > 5)
| ^
71 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ PLR0917 Too many positional arguments (6/5)
| ^
44 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle

PLR0917 Too many positional arguments (6/5)
--> too_many_positional_arguments.py:47:9
Expand All @@ -43,3 +44,4 @@ PLR0917 Too many positional arguments (6/5)
| ^
48 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ PLR6301 Method `developer_greeting` could be a function, class method, or static
| ^^^^^^^^^^^^^^^^^^
8 | print(f"Greetings {name}!")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `greeting_1` could be a function, class method, or static method
--> no_self_use.py:10:9
Expand All @@ -19,6 +20,7 @@ PLR6301 Method `greeting_1` could be a function, class method, or static method
| ^^^^^^^^^^
11 | print("Hello!")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `greeting_2` could be a function, class method, or static method
--> no_self_use.py:13:9
Expand All @@ -29,6 +31,7 @@ PLR6301 Method `greeting_2` could be a function, class method, or static method
| ^^^^^^^^^^
14 | print("Hi!")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `validate_y` could be a function, class method, or static method
--> no_self_use.py:103:9
Expand All @@ -39,6 +42,7 @@ PLR6301 Method `validate_y` could be a function, class method, or static method
104 | if value <= 0:
105 | raise ValueError("y must be a positive integer")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `non_simple_assignment` could be a function, class method, or static method
--> no_self_use.py:128:9
Expand All @@ -50,6 +54,7 @@ PLR6301 Method `non_simple_assignment` could be a function, class method, or sta
129 | msg = foo = ""
130 | raise NotImplementedError(msg)
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `non_simple_assignment_2` could be a function, class method, or static method
--> no_self_use.py:132:9
Expand All @@ -61,6 +66,7 @@ PLR6301 Method `non_simple_assignment_2` could be a function, class method, or s
133 | msg[0] = ""
134 | raise NotImplementedError(msg)
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `unused_message` could be a function, class method, or static method
--> no_self_use.py:136:9
Expand All @@ -72,6 +78,7 @@ PLR6301 Method `unused_message` could be a function, class method, or static met
137 | msg = ""
138 | raise NotImplementedError("")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `unused_message_2` could be a function, class method, or static method
--> no_self_use.py:140:9
Expand All @@ -83,6 +90,7 @@ PLR6301 Method `unused_message_2` could be a function, class method, or static m
141 | msg = ""
142 | raise NotImplementedError(x)
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `developer_greeting` could be a function, class method, or static method
--> no_self_use.py:145:9
Expand All @@ -92,6 +100,7 @@ PLR6301 Method `developer_greeting` could be a function, class method, or static
| ^^^^^^^^^^^^^^^^^^
146 | print(t"Greetings {name}!")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

PLR6301 Method `tstring` could be a function, class method, or static method
--> no_self_use.py:151:9
Expand All @@ -103,3 +112,4 @@ PLR6301 Method `tstring` could be a function, class method, or static method
152 | msg = t"{x}"
153 | raise NotImplementedError(msg)
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
29 changes: 29 additions & 0 deletions crates/ruff_python_semantic/src/analyze/function_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,35 @@ pub fn classify(
}
}

/// Return `true` if this function is subject to the Liskov Substitution Principle.
///
/// Type checkers will check nearly all methods for compliance with the Liskov Substitution
/// Principle, but some methods are exempt.
pub fn is_subject_to_liskov_substitution_principle(
function_name: &str,
decorator_list: &[Decorator],
parent_scope: &Scope,
semantic: &SemanticModel,
classmethod_decorators: &[String],
staticmethod_decorators: &[String],
) -> bool {
let kind = classify(
function_name,
decorator_list,
parent_scope,
semantic,
classmethod_decorators,
staticmethod_decorators,
);

match (kind, function_name) {
(FunctionType::Function | FunctionType::NewMethod, _) => false,
(FunctionType::Method, "__init__" | "__post_init__" | "__replace__") => false,
(_, "__init_subclass__") => false,
(FunctionType::Method | FunctionType::ClassMethod | FunctionType::StaticMethod, _) => true,
}
}

/// Return `true` if a [`Decorator`] is indicative of a static method.
/// Note: Implicit static methods like `__new__` are not considered.
fn is_static_method(
Expand Down