diff --git a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py index 1d1913407d444..6a3b14838d93e 100644 --- a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py +++ b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py @@ -1,4 +1,6 @@ -from fastapi import FastAPI +from typing import Annotated + +from fastapi import FastAPI, Path app = FastAPI() @@ -82,6 +84,11 @@ async def read_thing( return {"query": query} +@app.get("/books/{name}/{title}") +async def read_thing(*, author: Annotated[str, Path(alias="author_name")], title: str): + return {"author": author, "title": title} + + # OK @app.get("/things/{thing_id}") async def read_thing(thing_id: int, query: str): @@ -118,6 +125,11 @@ async def read_thing(*, author: str, title: str): return {"author": author, "title": title} +@app.get("/books/{name}/{title}") +async def read_thing(*, author: Annotated[str, Path(alias="name")], title: str): + return {"author": author, "title": title} + + # Ignored @app.get("/things/{thing-id}") async def read_thing(query: str): @@ -131,4 +143,4 @@ async def read_thing(query: str): @app.get("/things/{thing_id=}") async def read_thing(query: str): - return {"query": query} \ No newline at end of file + return {"query": query} diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs index 9896e2c4a91d5..edd7c93e0c830 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs @@ -6,7 +6,8 @@ use ruff_diagnostics::Fix; use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; -use ruff_python_semantic::Modules; +use ruff_python_ast::{Expr, Parameter, ParameterWithDefault}; +use ruff_python_semantic::{Modules, SemanticModel}; use ruff_python_stdlib::identifiers::is_identifier; use ruff_text_size::{Ranged, TextSize}; @@ -141,7 +142,10 @@ pub(crate) fn fastapi_unused_path_parameter( .args .iter() .chain(function_def.parameters.kwonlyargs.iter()) - .map(|arg| arg.parameter.name.as_str()) + .map(|ParameterWithDefault { parameter, .. }| { + parameter_alias(parameter, checker.semantic()) + .unwrap_or_else(|| parameter.name.as_str()) + }) .collect(); // Check if any of the path parameters are not in the function signature. @@ -190,6 +194,52 @@ pub(crate) fn fastapi_unused_path_parameter( checker.diagnostics.extend(diagnostics); } +/// Extract the expected in-route name for a given parameter, if it has an alias. +/// For example, given `document_id: Annotated[str, Path(alias="documentId")]`, returns `"documentId"`. +fn parameter_alias<'a>(parameter: &'a Parameter, semantic: &SemanticModel) -> Option<&'a str> { + let Some(annotation) = ¶meter.annotation else { + return None; + }; + + let Expr::Subscript(subscript) = annotation.as_ref() else { + return None; + }; + + let Expr::Tuple(tuple) = subscript.slice.as_ref() else { + return None; + }; + + let Some(Expr::Call(path)) = tuple.elts.get(1) else { + return None; + }; + + // Find the `alias` keyword argument. + let alias = path + .arguments + .find_keyword("alias") + .map(|alias| &alias.value)?; + + // Ensure that it's a literal string. + let Expr::StringLiteral(alias) = alias else { + return None; + }; + + // Verify that the subscript was a `typing.Annotated`. + if !semantic.match_typing_expr(&subscript.value, "Annotated") { + return None; + } + + // Verify that the call was a `fastapi.Path`. + if !semantic + .resolve_qualified_name(&path.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["fastapi", "Path"])) + { + return None; + } + + Some(alias.value.to_str()) +} + /// An iterator to extract parameters from FastAPI route paths. /// /// The iterator yields tuples of the parameter name and the range of the parameter in the input, diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap index 86da96e16dbcf..ca471a1886ca1 100644 --- a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap @@ -1,323 +1,342 @@ --- source: crates/ruff_linter/src/rules/fastapi/mod.rs --- -FAST003.py:7:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature - | -6 | # Errors -7 | @app.get("/things/{thing_id}") - | ^^^^^^^^^^ FAST003 -8 | async def read_thing(query: str): -9 | return {"query": query} - | - = help: Add `thing_id` to function signature +FAST003.py:9:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature + | + 8 | # Errors + 9 | @app.get("/things/{thing_id}") + | ^^^^^^^^^^ FAST003 +10 | async def read_thing(query: str): +11 | return {"query": query} + | + = help: Add `thing_id` to function signature ℹ Unsafe fix -5 5 | -6 6 | # Errors -7 7 | @app.get("/things/{thing_id}") -8 |-async def read_thing(query: str): - 8 |+async def read_thing(query: str, thing_id): -9 9 | return {"query": query} -10 10 | -11 11 | - -FAST003.py:12:23: FAST003 [*] Parameter `isbn` appears in route path, but not in `read_thing` signature - | -12 | @app.get("/books/isbn-{isbn}") +7 7 | +8 8 | # Errors +9 9 | @app.get("/things/{thing_id}") +10 |-async def read_thing(query: str): + 10 |+async def read_thing(query: str, thing_id): +11 11 | return {"query": query} +12 12 | +13 13 | + +FAST003.py:14:23: FAST003 [*] Parameter `isbn` appears in route path, but not in `read_thing` signature + | +14 | @app.get("/books/isbn-{isbn}") | ^^^^^^ FAST003 -13 | async def read_thing(): -14 | ... +15 | async def read_thing(): +16 | ... | = help: Add `isbn` to function signature ℹ Unsafe fix -10 10 | -11 11 | -12 12 | @app.get("/books/isbn-{isbn}") -13 |-async def read_thing(): - 13 |+async def read_thing(isbn): -14 14 | ... -15 15 | -16 16 | - -FAST003.py:17:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature - | -17 | @app.get("/things/{thing_id:path}") +12 12 | +13 13 | +14 14 | @app.get("/books/isbn-{isbn}") +15 |-async def read_thing(): + 15 |+async def read_thing(isbn): +16 16 | ... +17 17 | +18 18 | + +FAST003.py:19:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature + | +19 | @app.get("/things/{thing_id:path}") | ^^^^^^^^^^^^^^^ FAST003 -18 | async def read_thing(query: str): -19 | return {"query": query} +20 | async def read_thing(query: str): +21 | return {"query": query} | = help: Add `thing_id` to function signature ℹ Unsafe fix -15 15 | -16 16 | -17 17 | @app.get("/things/{thing_id:path}") -18 |-async def read_thing(query: str): - 18 |+async def read_thing(query: str, thing_id): -19 19 | return {"query": query} -20 20 | -21 21 | - -FAST003.py:22:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature - | -22 | @app.get("/things/{thing_id : path}") +17 17 | +18 18 | +19 19 | @app.get("/things/{thing_id:path}") +20 |-async def read_thing(query: str): + 20 |+async def read_thing(query: str, thing_id): +21 21 | return {"query": query} +22 22 | +23 23 | + +FAST003.py:24:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature + | +24 | @app.get("/things/{thing_id : path}") | ^^^^^^^^^^^^^^^^^ FAST003 -23 | async def read_thing(query: str): -24 | return {"query": query} +25 | async def read_thing(query: str): +26 | return {"query": query} | = help: Add `thing_id` to function signature ℹ Unsafe fix -20 20 | -21 21 | -22 22 | @app.get("/things/{thing_id : path}") -23 |-async def read_thing(query: str): - 23 |+async def read_thing(query: str, thing_id): -24 24 | return {"query": query} -25 25 | -26 26 | - -FAST003.py:27:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature - | -27 | @app.get("/books/{author}/{title}") +22 22 | +23 23 | +24 24 | @app.get("/things/{thing_id : path}") +25 |-async def read_thing(query: str): + 25 |+async def read_thing(query: str, thing_id): +26 26 | return {"query": query} +27 27 | +28 28 | + +FAST003.py:29:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature + | +29 | @app.get("/books/{author}/{title}") | ^^^^^^^ FAST003 -28 | async def read_thing(author: str): -29 | return {"author": author} +30 | async def read_thing(author: str): +31 | return {"author": author} | = help: Add `title` to function signature ℹ Unsafe fix -25 25 | -26 26 | -27 27 | @app.get("/books/{author}/{title}") -28 |-async def read_thing(author: str): - 28 |+async def read_thing(author: str, title): -29 29 | return {"author": author} -30 30 | -31 31 | - -FAST003.py:32:18: FAST003 [*] Parameter `author_name` appears in route path, but not in `read_thing` signature - | -32 | @app.get("/books/{author_name}/{title}") +27 27 | +28 28 | +29 29 | @app.get("/books/{author}/{title}") +30 |-async def read_thing(author: str): + 30 |+async def read_thing(author: str, title): +31 31 | return {"author": author} +32 32 | +33 33 | + +FAST003.py:34:18: FAST003 [*] Parameter `author_name` appears in route path, but not in `read_thing` signature + | +34 | @app.get("/books/{author_name}/{title}") | ^^^^^^^^^^^^^ FAST003 -33 | async def read_thing(): -34 | ... +35 | async def read_thing(): +36 | ... | = help: Add `author_name` to function signature ℹ Unsafe fix -30 30 | -31 31 | -32 32 | @app.get("/books/{author_name}/{title}") -33 |-async def read_thing(): - 33 |+async def read_thing(author_name): -34 34 | ... -35 35 | -36 36 | - -FAST003.py:32:32: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature - | -32 | @app.get("/books/{author_name}/{title}") +32 32 | +33 33 | +34 34 | @app.get("/books/{author_name}/{title}") +35 |-async def read_thing(): + 35 |+async def read_thing(author_name): +36 36 | ... +37 37 | +38 38 | + +FAST003.py:34:32: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature + | +34 | @app.get("/books/{author_name}/{title}") | ^^^^^^^ FAST003 -33 | async def read_thing(): -34 | ... +35 | async def read_thing(): +36 | ... | = help: Add `title` to function signature ℹ Unsafe fix -30 30 | -31 31 | -32 32 | @app.get("/books/{author_name}/{title}") -33 |-async def read_thing(): - 33 |+async def read_thing(title): -34 34 | ... -35 35 | -36 36 | - -FAST003.py:37:18: FAST003 Parameter `author` appears in route path, but only as a positional-only argument in `read_thing` signature - | -37 | @app.get("/books/{author}/{title}") +32 32 | +33 33 | +34 34 | @app.get("/books/{author_name}/{title}") +35 |-async def read_thing(): + 35 |+async def read_thing(title): +36 36 | ... +37 37 | +38 38 | + +FAST003.py:39:18: FAST003 Parameter `author` appears in route path, but only as a positional-only argument in `read_thing` signature + | +39 | @app.get("/books/{author}/{title}") | ^^^^^^^^ FAST003 -38 | async def read_thing(author: str, title: str, /): -39 | return {"author": author, "title": title} +40 | async def read_thing(author: str, title: str, /): +41 | return {"author": author, "title": title} | -FAST003.py:37:27: FAST003 Parameter `title` appears in route path, but only as a positional-only argument in `read_thing` signature +FAST003.py:39:27: FAST003 Parameter `title` appears in route path, but only as a positional-only argument in `read_thing` signature | -37 | @app.get("/books/{author}/{title}") +39 | @app.get("/books/{author}/{title}") | ^^^^^^^ FAST003 -38 | async def read_thing(author: str, title: str, /): -39 | return {"author": author, "title": title} +40 | async def read_thing(author: str, title: str, /): +41 | return {"author": author, "title": title} | -FAST003.py:42:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature +FAST003.py:44:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature | -42 | @app.get("/books/{author}/{title}/{page}") +44 | @app.get("/books/{author}/{title}/{page}") | ^^^^^^^ FAST003 -43 | async def read_thing( -44 | author: str, +45 | async def read_thing( +46 | author: str, | = help: Add `title` to function signature ℹ Unsafe fix -42 42 | @app.get("/books/{author}/{title}/{page}") -43 43 | async def read_thing( -44 44 | author: str, -45 |- query: str, - 45 |+ query: str, title, -46 46 | ): ... -47 47 | -48 48 | - -FAST003.py:42:35: FAST003 [*] Parameter `page` appears in route path, but not in `read_thing` signature - | -42 | @app.get("/books/{author}/{title}/{page}") +44 44 | @app.get("/books/{author}/{title}/{page}") +45 45 | async def read_thing( +46 46 | author: str, +47 |- query: str, + 47 |+ query: str, title, +48 48 | ): ... +49 49 | +50 50 | + +FAST003.py:44:35: FAST003 [*] Parameter `page` appears in route path, but not in `read_thing` signature + | +44 | @app.get("/books/{author}/{title}/{page}") | ^^^^^^ FAST003 -43 | async def read_thing( -44 | author: str, +45 | async def read_thing( +46 | author: str, | = help: Add `page` to function signature ℹ Unsafe fix -42 42 | @app.get("/books/{author}/{title}/{page}") -43 43 | async def read_thing( -44 44 | author: str, -45 |- query: str, - 45 |+ query: str, page, -46 46 | ): ... -47 47 | -48 48 | - -FAST003.py:49:18: FAST003 [*] Parameter `author` appears in route path, but not in `read_thing` signature - | -49 | @app.get("/books/{author}/{title}") +44 44 | @app.get("/books/{author}/{title}/{page}") +45 45 | async def read_thing( +46 46 | author: str, +47 |- query: str, + 47 |+ query: str, page, +48 48 | ): ... +49 49 | +50 50 | + +FAST003.py:51:18: FAST003 [*] Parameter `author` appears in route path, but not in `read_thing` signature + | +51 | @app.get("/books/{author}/{title}") | ^^^^^^^^ FAST003 -50 | async def read_thing(): -51 | ... +52 | async def read_thing(): +53 | ... | = help: Add `author` to function signature ℹ Unsafe fix -47 47 | -48 48 | -49 49 | @app.get("/books/{author}/{title}") -50 |-async def read_thing(): - 50 |+async def read_thing(author): -51 51 | ... -52 52 | -53 53 | - -FAST003.py:49:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature - | -49 | @app.get("/books/{author}/{title}") +49 49 | +50 50 | +51 51 | @app.get("/books/{author}/{title}") +52 |-async def read_thing(): + 52 |+async def read_thing(author): +53 53 | ... +54 54 | +55 55 | + +FAST003.py:51:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature + | +51 | @app.get("/books/{author}/{title}") | ^^^^^^^ FAST003 -50 | async def read_thing(): -51 | ... +52 | async def read_thing(): +53 | ... | = help: Add `title` to function signature ℹ Unsafe fix -47 47 | -48 48 | -49 49 | @app.get("/books/{author}/{title}") -50 |-async def read_thing(): - 50 |+async def read_thing(title): -51 51 | ... -52 52 | -53 53 | - -FAST003.py:54:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature - | -54 | @app.get("/books/{author}/{title}") +49 49 | +50 50 | +51 51 | @app.get("/books/{author}/{title}") +52 |-async def read_thing(): + 52 |+async def read_thing(title): +53 53 | ... +54 54 | +55 55 | + +FAST003.py:56:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature + | +56 | @app.get("/books/{author}/{title}") | ^^^^^^^ FAST003 -55 | async def read_thing(*, author: str): -56 | ... +57 | async def read_thing(*, author: str): +58 | ... | = help: Add `title` to function signature ℹ Unsafe fix -52 52 | -53 53 | -54 54 | @app.get("/books/{author}/{title}") -55 |-async def read_thing(*, author: str): - 55 |+async def read_thing(title, *, author: str): -56 56 | ... -57 57 | -58 58 | - -FAST003.py:59:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature - | -59 | @app.get("/books/{author}/{title}") +54 54 | +55 55 | +56 56 | @app.get("/books/{author}/{title}") +57 |-async def read_thing(*, author: str): + 57 |+async def read_thing(title, *, author: str): +58 58 | ... +59 59 | +60 60 | + +FAST003.py:61:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature + | +61 | @app.get("/books/{author}/{title}") | ^^^^^^^ FAST003 -60 | async def read_thing(hello, /, *, author: str): -61 | ... +62 | async def read_thing(hello, /, *, author: str): +63 | ... | = help: Add `title` to function signature ℹ Unsafe fix -57 57 | -58 58 | -59 59 | @app.get("/books/{author}/{title}") -60 |-async def read_thing(hello, /, *, author: str): - 60 |+async def read_thing(hello, /, title, *, author: str): -61 61 | ... -62 62 | -63 63 | - -FAST003.py:64:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature - | -64 | @app.get("/things/{thing_id}") +59 59 | +60 60 | +61 61 | @app.get("/books/{author}/{title}") +62 |-async def read_thing(hello, /, *, author: str): + 62 |+async def read_thing(hello, /, title, *, author: str): +63 63 | ... +64 64 | +65 65 | + +FAST003.py:66:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature + | +66 | @app.get("/things/{thing_id}") | ^^^^^^^^^^ FAST003 -65 | async def read_thing( -66 | query: str, +67 | async def read_thing( +68 | query: str, | = help: Add `thing_id` to function signature ℹ Unsafe fix -63 63 | -64 64 | @app.get("/things/{thing_id}") -65 65 | async def read_thing( -66 |- query: str, - 66 |+ query: str, thing_id, -67 67 | ): -68 68 | return {"query": query} -69 69 | - -FAST003.py:71:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature - | -71 | @app.get("/things/{thing_id}") +65 65 | +66 66 | @app.get("/things/{thing_id}") +67 67 | async def read_thing( +68 |- query: str, + 68 |+ query: str, thing_id, +69 69 | ): +70 70 | return {"query": query} +71 71 | + +FAST003.py:73:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature + | +73 | @app.get("/things/{thing_id}") | ^^^^^^^^^^ FAST003 -72 | async def read_thing( -73 | query: str = "default", +74 | async def read_thing( +75 | query: str = "default", | = help: Add `thing_id` to function signature ℹ Unsafe fix -70 70 | -71 71 | @app.get("/things/{thing_id}") -72 72 | async def read_thing( -73 |- query: str = "default", - 73 |+ thing_id, query: str = "default", -74 74 | ): -75 75 | return {"query": query} -76 76 | - -FAST003.py:78:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature - | -78 | @app.get("/things/{thing_id}") +72 72 | +73 73 | @app.get("/things/{thing_id}") +74 74 | async def read_thing( +75 |- query: str = "default", + 75 |+ thing_id, query: str = "default", +76 76 | ): +77 77 | return {"query": query} +78 78 | + +FAST003.py:80:19: FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature + | +80 | @app.get("/things/{thing_id}") | ^^^^^^^^^^ FAST003 -79 | async def read_thing( -80 | *, query: str = "default", +81 | async def read_thing( +82 | *, query: str = "default", | = help: Add `thing_id` to function signature ℹ Unsafe fix -77 77 | -78 78 | @app.get("/things/{thing_id}") -79 79 | async def read_thing( -80 |- *, query: str = "default", - 80 |+ thing_id, *, query: str = "default", -81 81 | ): -82 82 | return {"query": query} -83 83 | +79 79 | +80 80 | @app.get("/things/{thing_id}") +81 81 | async def read_thing( +82 |- *, query: str = "default", + 82 |+ thing_id, *, query: str = "default", +83 83 | ): +84 84 | return {"query": query} +85 85 | + +FAST003.py:87:18: FAST003 [*] Parameter `name` appears in route path, but not in `read_thing` signature + | +87 | @app.get("/books/{name}/{title}") + | ^^^^^^ FAST003 +88 | async def read_thing(*, author: Annotated[str, Path(alias="author_name")], title: str): +89 | return {"author": author, "title": title} + | + = help: Add `name` to function signature + +ℹ Unsafe fix +85 85 | +86 86 | +87 87 | @app.get("/books/{name}/{title}") +88 |-async def read_thing(*, author: Annotated[str, Path(alias="author_name")], title: str): + 88 |+async def read_thing(name, *, author: Annotated[str, Path(alias="author_name")], title: str): +89 89 | return {"author": author, "title": title} +90 90 | +91 91 |