mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-20 12:35:40 +00:00
[fastapi] Fix false positives for path parameters that FastAPI doesn't recognize (FAST003) (#20687)
## Summary Fixes #20680 --------- Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
parent
db91ac7dce
commit
537ec5f012
2 changed files with 46 additions and 27 deletions
|
|
@ -227,3 +227,32 @@ async def read_thing(query: str):
|
|||
@app.get("/things/{ thing_id : str }")
|
||||
async def read_thing(query: str):
|
||||
return {"query": query}
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/20680
|
||||
# These should NOT trigger FAST003 because FastAPI doesn't recognize them as path parameters
|
||||
|
||||
# Non-ASCII characters in parameter name
|
||||
@app.get("/f1/{用户身份}")
|
||||
async def f1():
|
||||
return locals()
|
||||
|
||||
# Space in parameter name
|
||||
@app.get("/f2/{x: str}")
|
||||
async def f2():
|
||||
return locals()
|
||||
|
||||
# Non-ASCII converter
|
||||
@app.get("/f3/{complex_number:ℂ}")
|
||||
async def f3():
|
||||
return locals()
|
||||
|
||||
# Mixed non-ASCII characters
|
||||
@app.get("/f4/{用户_id}")
|
||||
async def f4():
|
||||
return locals()
|
||||
|
||||
# Space in parameter name with converter
|
||||
@app.get("/f5/{param: int}")
|
||||
async def f5():
|
||||
return locals()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use std::iter::Peekable;
|
||||
use std::ops::Range;
|
||||
use std::str::CharIndices;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use regex::{CaptureMatches, Regex};
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprSubscript, Parameter, ParameterWithDefault};
|
||||
use ruff_python_semantic::{BindingKind, Modules, ScopeKind, SemanticModel};
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::Fix;
|
||||
|
|
@ -165,11 +165,6 @@ pub(crate) fn fastapi_unused_path_parameter(
|
|||
|
||||
// Check if any of the path parameters are not in the function signature.
|
||||
for (path_param, range) in path_params {
|
||||
// Ignore invalid identifiers (e.g., `user-id`, as opposed to `user_id`)
|
||||
if !is_identifier(path_param) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the path parameter is already in the function or the dependency signature,
|
||||
// we don't need to do anything.
|
||||
if named_args.contains(&path_param) {
|
||||
|
|
@ -461,15 +456,19 @@ fn parameter_alias<'a>(parameter: &'a Parameter, semantic: &SemanticModel) -> Op
|
|||
/// the parameter name. For example, `/{x}` is a valid parameter, but `/{ x }` is treated literally.
|
||||
#[derive(Debug)]
|
||||
struct PathParamIterator<'a> {
|
||||
input: &'a str,
|
||||
chars: Peekable<CharIndices<'a>>,
|
||||
inner: CaptureMatches<'a, 'a>,
|
||||
}
|
||||
|
||||
impl<'a> PathParamIterator<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
PathParamIterator {
|
||||
input,
|
||||
chars: input.char_indices().peekable(),
|
||||
/// Matches the Starlette pattern for path parameters with optional converters from
|
||||
/// <https://github.com/Kludex/starlette/blob/e18637c68e36d112b1983bc0c8b663681e6a4c50/starlette/routing.py#L121>
|
||||
static FASTAPI_PATH_PARAM_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::[a-zA-Z_][a-zA-Z0-9_]*)?\}").unwrap()
|
||||
});
|
||||
|
||||
Self {
|
||||
inner: FASTAPI_PATH_PARAM_REGEX.captures_iter(input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -478,19 +477,10 @@ impl<'a> Iterator for PathParamIterator<'a> {
|
|||
type Item = (&'a str, Range<usize>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some((start, c)) = self.chars.next() {
|
||||
if c == '{' {
|
||||
if let Some((end, _)) = self.chars.by_ref().find(|&(_, ch)| ch == '}') {
|
||||
let param_content = &self.input[start + 1..end];
|
||||
// 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];
|
||||
|
||||
return Some((param_name, start..end + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
self.inner
|
||||
.next()
|
||||
// Extract the first capture group (the path parameter), but return the range of the
|
||||
// whole match (everything in braces and including the braces themselves).
|
||||
.and_then(|capture| Some((capture.get(1)?.as_str(), capture.get(0)?.range())))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue