diff --git a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py index cd8653026c..8641334f2e 100644 --- a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py +++ b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py @@ -213,3 +213,17 @@ async def get_id_pydantic_full( async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ... @app.get("/{my_id}") async def get_id_init_not_annotated(params = Depends(InitParams)): ... + +@app.get("/things/{ thing_id }") +async def read_thing(query: str): + return {"query": query} + + +@app.get("/things/{ thing_id : path }") +async def read_thing(query: str): + return {"query": query} + + +@app.get("/things/{ thing_id : str }") +async def read_thing(query: str): + 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 e0d215b45c..d4d3674930 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 @@ -457,6 +457,9 @@ fn parameter_alias<'a>(parameter: &'a Parameter, semantic: &SemanticModel) -> Op /// /// The iterator yields tuples of the parameter name and the range of the parameter in the input, /// inclusive of curly braces. +/// +/// FastAPI only recognizes path parameters when there are no leading or trailing spaces around +/// the parameter name. For example, `/{x}` is a valid parameter, but `/{ x }` is treated literally. #[derive(Debug)] struct PathParamIterator<'a> { input: &'a str, @@ -483,7 +486,7 @@ impl<'a> Iterator for PathParamIterator<'a> { // We ignore text after a colon, since those are path converters // See also: https://fastapi.tiangolo.com/tutorial/path-params/?h=path#path-convertor let param_name_end = param_content.find(':').unwrap_or(param_content.len()); - let param_name = ¶m_content[..param_name_end].trim(); + let param_name = ¶m_content[..param_name_end]; #[expect(clippy::range_plus_one)] return Some((param_name, start..end + 1)); 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 62ca78ce45..a3ea2bf7ce 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 @@ -59,25 +59,6 @@ help: Add `thing_id` to function signature 23 | note: This is an unsafe fix and may change runtime behavior -FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature - --> FAST003.py:24:19 - | -24 | @app.get("/things/{thing_id : path}") - | ^^^^^^^^^^^^^^^^^ -25 | async def read_thing(query: str): -26 | return {"query": query} - | -help: Add `thing_id` to function signature -22 | -23 | -24 | @app.get("/things/{thing_id : path}") - - async def read_thing(query: str): -25 + async def read_thing(query: str, thing_id): -26 | return {"query": query} -27 | -28 | -note: This is an unsafe fix and may change runtime behavior - FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature --> FAST003.py:29:27 |