mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-20 04:29:47 +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 }")
|
@app.get("/things/{ thing_id : str }")
|
||||||
async def read_thing(query: str):
|
async def read_thing(query: str):
|
||||||
return {"query": query}
|
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::ops::Range;
|
||||||
use std::str::CharIndices;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use regex::{CaptureMatches, Regex};
|
||||||
|
|
||||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprSubscript, Parameter, ParameterWithDefault};
|
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprSubscript, Parameter, ParameterWithDefault};
|
||||||
use ruff_python_semantic::{BindingKind, Modules, ScopeKind, SemanticModel};
|
use ruff_python_semantic::{BindingKind, Modules, ScopeKind, SemanticModel};
|
||||||
use ruff_python_stdlib::identifiers::is_identifier;
|
|
||||||
use ruff_text_size::{Ranged, TextSize};
|
use ruff_text_size::{Ranged, TextSize};
|
||||||
|
|
||||||
use crate::Fix;
|
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.
|
// Check if any of the path parameters are not in the function signature.
|
||||||
for (path_param, range) in path_params {
|
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,
|
// If the path parameter is already in the function or the dependency signature,
|
||||||
// we don't need to do anything.
|
// we don't need to do anything.
|
||||||
if named_args.contains(&path_param) {
|
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.
|
/// the parameter name. For example, `/{x}` is a valid parameter, but `/{ x }` is treated literally.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct PathParamIterator<'a> {
|
struct PathParamIterator<'a> {
|
||||||
input: &'a str,
|
inner: CaptureMatches<'a, 'a>,
|
||||||
chars: Peekable<CharIndices<'a>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PathParamIterator<'a> {
|
impl<'a> PathParamIterator<'a> {
|
||||||
fn new(input: &'a str) -> Self {
|
fn new(input: &'a str) -> Self {
|
||||||
PathParamIterator {
|
/// Matches the Starlette pattern for path parameters with optional converters from
|
||||||
input,
|
/// <https://github.com/Kludex/starlette/blob/e18637c68e36d112b1983bc0c8b663681e6a4c50/starlette/routing.py#L121>
|
||||||
chars: input.char_indices().peekable(),
|
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>);
|
type Item = (&'a str, Range<usize>);
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
while let Some((start, c)) = self.chars.next() {
|
self.inner
|
||||||
if c == '{' {
|
.next()
|
||||||
if let Some((end, _)) = self.chars.by_ref().find(|&(_, ch)| ch == '}') {
|
// Extract the first capture group (the path parameter), but return the range of the
|
||||||
let param_content = &self.input[start + 1..end];
|
// whole match (everything in braces and including the braces themselves).
|
||||||
// We ignore text after a colon, since those are path converters
|
.and_then(|capture| Some((capture.get(1)?.as_str(), capture.get(0)?.range())))
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue