mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:15:12 +00:00
[FastAPI
] Avoid introducing invalid syntax in fix for fast-api-non-annotated-dependency
(FAST002
) (#13133)
This commit is contained in:
parent
2e75cfbfe7
commit
df694ca1c1
3 changed files with 189 additions and 59 deletions
|
@ -1,4 +1,4 @@
|
|||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::helpers::map_callable;
|
||||
|
@ -59,14 +59,16 @@ use crate::settings::types::PythonVersion;
|
|||
#[violation]
|
||||
pub struct FastApiNonAnnotatedDependency;
|
||||
|
||||
impl AlwaysFixableViolation for FastApiNonAnnotatedDependency {
|
||||
impl Violation for FastApiNonAnnotatedDependency {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("FastAPI dependency without `Annotated`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Replace with `Annotated`".to_string()
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Annotated`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,64 +77,95 @@ pub(crate) fn fastapi_non_annotated_dependency(
|
|||
checker: &mut Checker,
|
||||
function_def: &ast::StmtFunctionDef,
|
||||
) {
|
||||
if !checker.semantic().seen_module(Modules::FASTAPI) {
|
||||
return;
|
||||
}
|
||||
if !is_fastapi_route(function_def, checker.semantic()) {
|
||||
if !checker.semantic().seen_module(Modules::FASTAPI)
|
||||
|| !is_fastapi_route(function_def, checker.semantic())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut updatable_count = 0;
|
||||
let mut has_non_updatable_default = false;
|
||||
let total_params = function_def.parameters.args.len();
|
||||
|
||||
for parameter in &function_def.parameters.args {
|
||||
if let (Some(annotation), Some(default)) =
|
||||
(¶meter.parameter.annotation, ¶meter.default)
|
||||
{
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(map_callable(default))
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
[
|
||||
"fastapi",
|
||||
"Query"
|
||||
| "Path"
|
||||
| "Body"
|
||||
| "Cookie"
|
||||
| "Header"
|
||||
| "File"
|
||||
| "Form"
|
||||
| "Depends"
|
||||
| "Security"
|
||||
]
|
||||
)
|
||||
})
|
||||
{
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(FastApiNonAnnotatedDependency, parameter.range);
|
||||
let needs_update = matches!(
|
||||
(¶meter.parameter.annotation, ¶meter.default),
|
||||
(Some(_annotation), Some(default)) if is_fastapi_dependency(checker, default)
|
||||
);
|
||||
|
||||
diagnostic.try_set_fix(|| {
|
||||
let module = if checker.settings.target_version >= PythonVersion::Py39 {
|
||||
"typing"
|
||||
} else {
|
||||
"typing_extensions"
|
||||
};
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(module, "Annotated"),
|
||||
function_def.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let content = format!(
|
||||
"{}: {}[{}, {}]",
|
||||
parameter.parameter.name.id,
|
||||
binding,
|
||||
checker.locator().slice(annotation.range()),
|
||||
checker.locator().slice(default.range())
|
||||
);
|
||||
let parameter_edit = Edit::range_replacement(content, parameter.range());
|
||||
Ok(Fix::unsafe_edits(import_edit, [parameter_edit]))
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
if needs_update {
|
||||
updatable_count += 1;
|
||||
// Determine if it's safe to update this parameter:
|
||||
// - if all parameters are updatable its safe.
|
||||
// - if we've encountered a non-updatable parameter with a default value, it's no longer
|
||||
// safe. (https://github.com/astral-sh/ruff/issues/12982)
|
||||
let safe_to_update = updatable_count == total_params || !has_non_updatable_default;
|
||||
create_diagnostic(checker, parameter, safe_to_update);
|
||||
} else if parameter.default.is_some() {
|
||||
has_non_updatable_default = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_fastapi_dependency(checker: &Checker, expr: &ast::Expr) -> bool {
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(map_callable(expr))
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
[
|
||||
"fastapi",
|
||||
"Query"
|
||||
| "Path"
|
||||
| "Body"
|
||||
| "Cookie"
|
||||
| "Header"
|
||||
| "File"
|
||||
| "Form"
|
||||
| "Depends"
|
||||
| "Security"
|
||||
]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn create_diagnostic(
|
||||
checker: &mut Checker,
|
||||
parameter: &ast::ParameterWithDefault,
|
||||
safe_to_update: bool,
|
||||
) {
|
||||
let mut diagnostic = Diagnostic::new(FastApiNonAnnotatedDependency, parameter.range);
|
||||
|
||||
if safe_to_update {
|
||||
if let (Some(annotation), Some(default)) =
|
||||
(¶meter.parameter.annotation, ¶meter.default)
|
||||
{
|
||||
diagnostic.try_set_fix(|| {
|
||||
let module = if checker.settings.target_version >= PythonVersion::Py39 {
|
||||
"typing"
|
||||
} else {
|
||||
"typing_extensions"
|
||||
};
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(module, "Annotated"),
|
||||
parameter.range.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let content = format!(
|
||||
"{}: {}[{}, {}]",
|
||||
parameter.parameter.name.id,
|
||||
binding,
|
||||
checker.locator().slice(annotation.range()),
|
||||
checker.locator().slice(default.range())
|
||||
);
|
||||
let parameter_edit = Edit::range_replacement(content, parameter.range);
|
||||
Ok(Fix::unsafe_edits(import_edit, [parameter_edit]))
|
||||
});
|
||||
}
|
||||
} else {
|
||||
diagnostic.fix = None;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
|
|
@ -261,3 +261,72 @@ FAST002.py:38:5: FAST002 [*] FastAPI dependency without `Annotated`
|
|||
39 40 | ):
|
||||
40 41 | # do stuff
|
||||
41 42 | pass
|
||||
|
||||
FAST002.py:47:5: FAST002 [*] FastAPI dependency without `Annotated`
|
||||
|
|
||||
45 | skip: int,
|
||||
46 | limit: int,
|
||||
47 | current_user: User = Depends(get_current_user),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
|
||||
48 | ):
|
||||
49 | pass
|
||||
|
|
||||
= help: Replace with `Annotated`
|
||||
|
||||
ℹ Unsafe fix
|
||||
12 12 | Security,
|
||||
13 13 | )
|
||||
14 14 | from pydantic import BaseModel
|
||||
15 |+from typing import Annotated
|
||||
15 16 |
|
||||
16 17 | app = FastAPI()
|
||||
17 18 | router = APIRouter()
|
||||
--------------------------------------------------------------------------------
|
||||
44 45 | def get_users(
|
||||
45 46 | skip: int,
|
||||
46 47 | limit: int,
|
||||
47 |- current_user: User = Depends(get_current_user),
|
||||
48 |+ current_user: Annotated[User, Depends(get_current_user)],
|
||||
48 49 | ):
|
||||
49 50 | pass
|
||||
50 51 |
|
||||
|
||||
FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated`
|
||||
|
|
||||
51 | @app.get("/users/")
|
||||
52 | def get_users(
|
||||
53 | current_user: User = Depends(get_current_user),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
|
||||
54 | skip: int = 0,
|
||||
55 | limit: int = 10,
|
||||
|
|
||||
= help: Replace with `Annotated`
|
||||
|
||||
ℹ Unsafe fix
|
||||
12 12 | Security,
|
||||
13 13 | )
|
||||
14 14 | from pydantic import BaseModel
|
||||
15 |+from typing import Annotated
|
||||
15 16 |
|
||||
16 17 | app = FastAPI()
|
||||
17 18 | router = APIRouter()
|
||||
--------------------------------------------------------------------------------
|
||||
50 51 |
|
||||
51 52 | @app.get("/users/")
|
||||
52 53 | def get_users(
|
||||
53 |- current_user: User = Depends(get_current_user),
|
||||
54 |+ current_user: Annotated[User, Depends(get_current_user)],
|
||||
54 55 | skip: int = 0,
|
||||
55 56 | limit: int = 10,
|
||||
56 57 | ):
|
||||
|
||||
FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated`
|
||||
|
|
||||
65 | skip: int = 0,
|
||||
66 | limit: int = 10,
|
||||
67 | current_user: User = Depends(get_current_user),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
|
||||
68 | ):
|
||||
69 | pass
|
||||
|
|
||||
= help: Replace with `Annotated`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue