mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
Use TypeChecker for detecting fastapi routes (#15093)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
This commit is contained in:
parent
fd4bea52e5
commit
2fb6b320d8
5 changed files with 88 additions and 11 deletions
|
@ -108,3 +108,18 @@ app = None
|
|||
@app.post("/items/", response_model=Item)
|
||||
async def create_item(item: Item) -> Item:
|
||||
return item
|
||||
|
||||
|
||||
# Routes might be defined inside functions
|
||||
|
||||
|
||||
def setup_app(app_arg: FastAPI, non_app: str) -> None:
|
||||
# Error
|
||||
@app_arg.get("/", response_model=str)
|
||||
async def get_root() -> str:
|
||||
return "Hello World!"
|
||||
|
||||
# Ok
|
||||
@non_app.get("/", response_model=str)
|
||||
async def get_root() -> str:
|
||||
return "Hello World!"
|
||||
|
|
|
@ -80,11 +80,20 @@ async def test() -> str:
|
|||
return ",".join(vals)
|
||||
|
||||
|
||||
# FastApi routes can be async without actually using await
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/count")
|
||||
async def fastapi_route(): # Ok: FastApi routes can be async without actually using await
|
||||
async def fastapi_route():
|
||||
return 1
|
||||
|
||||
|
||||
def setup_app(app_arg: FastAPI, non_app: str) -> None:
|
||||
@app_arg.get("/")
|
||||
async def get_root() -> str:
|
||||
return "Hello World!"
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ mod fastapi_redundant_response_model;
|
|||
mod fastapi_unused_path_parameter;
|
||||
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_semantic::analyze::typing::resolve_assignment;
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
/// Returns `true` if the function is a FastAPI route.
|
||||
|
@ -41,11 +41,11 @@ pub(crate) fn is_fastapi_route_call(call_expr: &ast::ExprCall, semantic: &Semant
|
|||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
resolve_assignment(value, semantic).is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["fastapi", "FastAPI" | "APIRouter"]
|
||||
)
|
||||
})
|
||||
let Some(name) = value.as_name_expr() else {
|
||||
return false;
|
||||
};
|
||||
let Some(binding_id) = semantic.resolve_name(name) else {
|
||||
return false;
|
||||
};
|
||||
typing::is_fastapi_route(semantic.binding(binding_id), semantic)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/fastapi/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
FAST001.py:17:22: FAST001 [*] FastAPI route with redundant `response_model` argument
|
||||
|
|
||||
|
@ -172,4 +171,25 @@ FAST001.py:53:24: FAST001 [*] FastAPI route with redundant `response_model` argu
|
|||
53 |+@router.get("/items/")
|
||||
54 54 | async def create_item(item: Item) -> Item:
|
||||
55 55 | return item
|
||||
56 56 |
|
||||
56 56 |
|
||||
|
||||
FAST001.py:118:23: FAST001 [*] FastAPI route with redundant `response_model` argument
|
||||
|
|
||||
116 | def setup_app(app_arg: FastAPI, non_app: str) -> None:
|
||||
117 | # Error
|
||||
118 | @app_arg.get("/", response_model=str)
|
||||
| ^^^^^^^^^^^^^^^^^^ FAST001
|
||||
119 | async def get_root() -> str:
|
||||
120 | return "Hello World!"
|
||||
|
|
||||
= help: Remove argument
|
||||
|
||||
ℹ Unsafe fix
|
||||
115 115 |
|
||||
116 116 | def setup_app(app_arg: FastAPI, non_app: str) -> None:
|
||||
117 117 | # Error
|
||||
118 |- @app_arg.get("/", response_model=str)
|
||||
118 |+ @app_arg.get("/")
|
||||
119 119 | async def get_root() -> str:
|
||||
120 120 | return "Hello World!"
|
||||
121 121 |
|
||||
|
|
|
@ -786,6 +786,35 @@ impl TypeChecker for PathlibPathChecker {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct FastApiRouteChecker;
|
||||
|
||||
impl FastApiRouteChecker {
|
||||
fn is_fastapi_route_constructor(semantic: &SemanticModel, expr: &Expr) -> bool {
|
||||
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["fastapi", "FastAPI" | "APIRouter"]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeChecker for FastApiRouteChecker {
|
||||
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
||||
Self::is_fastapi_route_constructor(semantic, annotation)
|
||||
}
|
||||
|
||||
fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = initializer else {
|
||||
return false;
|
||||
};
|
||||
|
||||
Self::is_fastapi_route_constructor(semantic, func)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TypeVarLikeChecker;
|
||||
|
||||
impl TypeVarLikeChecker {
|
||||
|
@ -914,6 +943,10 @@ pub fn is_pathlib_path(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|||
check_type::<PathlibPathChecker>(binding, semantic)
|
||||
}
|
||||
|
||||
pub fn is_fastapi_route(binding: &Binding, semantic: &SemanticModel) -> bool {
|
||||
check_type::<FastApiRouteChecker>(binding, semantic)
|
||||
}
|
||||
|
||||
/// Test whether the given binding is for an old-style `TypeVar`, `TypeVarTuple` or a `ParamSpec`.
|
||||
pub fn is_type_var_like(binding: &Binding, semantic: &SemanticModel) -> bool {
|
||||
check_type::<TypeVarLikeChecker>(binding, semantic)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue