Compare commits
1 Commits
0.6.1
...
split-comp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7294e556 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,29 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.6.1
|
||||
|
||||
This is a hotfix release to address an issue with `ruff-pre-commit`. In v0.6,
|
||||
Ruff changed its behavior to lint and format Jupyter notebooks by default;
|
||||
however, due to an oversight, these files were still excluded by default if
|
||||
Ruff was run via pre-commit, leading to inconsistent behavior.
|
||||
This has [now been fixed](https://github.com/astral-sh/ruff-pre-commit/pull/96).
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`fastapi`\] Implement `fast-api-unused-path-parameter` (`FAST003`) ([#12638](https://github.com/astral-sh/ruff/pull/12638))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pylint`\] Rename `too-many-positional` to `too-many-positional-arguments` (`R0917`) ([#12905](https://github.com/astral-sh/ruff/pull/12905))
|
||||
|
||||
### Server
|
||||
|
||||
- Fix crash when applying "fix-all" code-action to notebook cells ([#12929](https://github.com/astral-sh/ruff/pull/12929))
|
||||
|
||||
### Other changes
|
||||
|
||||
- \[`flake8-naming`\]: Respect import conventions (`N817`) ([#12922](https://github.com/astral-sh/ruff/pull/12922))
|
||||
|
||||
## 0.6.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.6.0) for a migration guide and overview of the changes!
|
||||
|
||||
@@ -361,7 +361,7 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
|
||||
downstream jobs manually if needed.
|
||||
1. Verify the GitHub release:
|
||||
1. The Changelog should match the content of `CHANGELOG.md`
|
||||
1. Append the contributors from the `scripts/release.sh` script
|
||||
1. Append the contributors from the `bump.sh` script
|
||||
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
|
||||
1. One can determine if an update is needed when
|
||||
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2060,7 +2060,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2252,7 +2252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2572,7 +2572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
||||
@@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.6.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.1/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.6.0/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.0/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.1
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -168,24 +168,6 @@ impl ModuleName {
|
||||
};
|
||||
Some(Self(name))
|
||||
}
|
||||
|
||||
/// Extend `self` with the components of `other`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// let mut module_name = ModuleName::new_static("foo").unwrap();
|
||||
/// module_name.extend(&ModuleName::new_static("bar").unwrap());
|
||||
/// assert_eq!(&module_name, "foo.bar");
|
||||
/// module_name.extend(&ModuleName::new_static("baz.eggs.ham").unwrap());
|
||||
/// assert_eq!(&module_name, "foo.bar.baz.eggs.ham");
|
||||
/// ```
|
||||
pub fn extend(&mut self, other: &ModuleName) {
|
||||
self.0.push('.');
|
||||
self.0.push_str(other);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ModuleName {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::iter::FusedIterator;
|
||||
|
||||
pub(crate) use module::Module;
|
||||
pub use resolver::resolve_module;
|
||||
pub(crate) use resolver::{file_to_module, SearchPaths};
|
||||
pub(crate) use resolver::SearchPaths;
|
||||
use ruff_db::system::SystemPath;
|
||||
pub use typeshed::vendored_typeshed_stubs;
|
||||
|
||||
|
||||
@@ -77,9 +77,3 @@ pub enum ModuleKind {
|
||||
/// A python package (`foo/__init__.py` or `foo/__init__.pyi`)
|
||||
Package,
|
||||
}
|
||||
|
||||
impl ModuleKind {
|
||||
pub const fn is_package(self) -> bool {
|
||||
matches!(self, ModuleKind::Package)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,103 +528,6 @@ y = 2
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_parameter_symbols() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
pass
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(&global_table), vec!["f", "str", "int"]);
|
||||
|
||||
let [(function_scope_id, _function_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("Expected a function scope")
|
||||
};
|
||||
|
||||
let function_table = index.symbol_table(function_scope_id);
|
||||
assert_eq!(
|
||||
names(&function_table),
|
||||
vec!["a", "b", "c", "args", "d", "kwargs"],
|
||||
);
|
||||
|
||||
let use_def = index.use_def_map(function_scope_id);
|
||||
for name in ["a", "b", "c", "d"] {
|
||||
let [definition] = use_def.public_definitions(
|
||||
function_table
|
||||
.symbol_id_by_name(name)
|
||||
.expect("symbol exists"),
|
||||
) else {
|
||||
panic!("Expected parameter definition for {name}");
|
||||
};
|
||||
assert!(matches!(
|
||||
definition.node(&db),
|
||||
DefinitionKind::ParameterWithDefault(_)
|
||||
));
|
||||
}
|
||||
for name in ["args", "kwargs"] {
|
||||
let [definition] = use_def.public_definitions(
|
||||
function_table
|
||||
.symbol_id_by_name(name)
|
||||
.expect("symbol exists"),
|
||||
) else {
|
||||
panic!("Expected parameter definition for {name}");
|
||||
};
|
||||
assert!(matches!(definition.node(&db), DefinitionKind::Parameter(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lambda_parameter_symbols() {
|
||||
let TestCase { db, file } = test_case("lambda a, b, c=1, *args, d=2, **kwargs: None");
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert!(names(&global_table).is_empty());
|
||||
|
||||
let [(lambda_scope_id, _lambda_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("Expected a lambda scope")
|
||||
};
|
||||
|
||||
let lambda_table = index.symbol_table(lambda_scope_id);
|
||||
assert_eq!(
|
||||
names(&lambda_table),
|
||||
vec!["a", "b", "c", "args", "d", "kwargs"],
|
||||
);
|
||||
|
||||
let use_def = index.use_def_map(lambda_scope_id);
|
||||
for name in ["a", "b", "c", "d"] {
|
||||
let [definition] = use_def
|
||||
.public_definitions(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
else {
|
||||
panic!("Expected parameter definition for {name}");
|
||||
};
|
||||
assert!(matches!(
|
||||
definition.node(&db),
|
||||
DefinitionKind::ParameterWithDefault(_)
|
||||
));
|
||||
}
|
||||
for name in ["args", "kwargs"] {
|
||||
let [definition] = use_def
|
||||
.public_definitions(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
else {
|
||||
panic!("Expected parameter definition for {name}");
|
||||
};
|
||||
assert!(matches!(definition.node(&db), DefinitionKind::Parameter(_)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test case to validate that the comprehension scope is correctly identified and that the target
|
||||
/// variable is defined only in the comprehension scope and not in the global scope.
|
||||
#[test]
|
||||
|
||||
@@ -368,16 +368,6 @@ where
|
||||
.add_or_update_symbol(function_def.name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
self.add_definition(symbol, function_def);
|
||||
|
||||
// The default value of the parameters needs to be evaluated in the
|
||||
// enclosing scope.
|
||||
for default in function_def
|
||||
.parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.visit_expr(default);
|
||||
}
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
||||
function_def.type_params.as_deref(),
|
||||
@@ -388,16 +378,6 @@ where
|
||||
}
|
||||
|
||||
builder.push_scope(NodeWithScopeRef::Function(function_def));
|
||||
|
||||
// Add symbols and definitions for the parameters to the function scope.
|
||||
for parameter in &*function_def.parameters {
|
||||
let symbol = builder.add_or_update_symbol(
|
||||
parameter.name().id().clone(),
|
||||
SymbolFlags::IS_DEFINED,
|
||||
);
|
||||
builder.add_definition(symbol, parameter);
|
||||
}
|
||||
|
||||
builder.visit_body(&function_def.body);
|
||||
builder.pop_scope()
|
||||
},
|
||||
@@ -594,29 +574,9 @@ where
|
||||
}
|
||||
ast::Expr::Lambda(lambda) => {
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
// The default value of the parameters needs to be evaluated in the
|
||||
// enclosing scope.
|
||||
for default in parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.visit_expr(default);
|
||||
}
|
||||
self.visit_parameters(parameters);
|
||||
}
|
||||
self.push_scope(NodeWithScopeRef::Lambda(lambda));
|
||||
|
||||
// Add symbols and definitions for the parameters to the lambda scope.
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
for parameter in &**parameters {
|
||||
let symbol = self.add_or_update_symbol(
|
||||
parameter.name().id().clone(),
|
||||
SymbolFlags::IS_DEFINED,
|
||||
);
|
||||
self.add_definition(symbol, parameter);
|
||||
}
|
||||
}
|
||||
|
||||
self.visit_expr(lambda.body.as_ref());
|
||||
}
|
||||
ast::Expr::If(ast::ExprIf {
|
||||
@@ -694,14 +654,6 @@ where
|
||||
self.pop_scope();
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_parameters(&mut self, parameters: &'ast ruff_python_ast::Parameters) {
|
||||
// Intentionally avoid walking default expressions, as we handle them in the enclosing
|
||||
// scope.
|
||||
for parameter in parameters.iter().map(ast::AnyParameterRef::as_parameter) {
|
||||
self.visit_parameter(parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
||||
@@ -45,7 +45,6 @@ pub(crate) enum DefinitionNodeRef<'a> {
|
||||
Assignment(AssignmentDefinitionNodeRef<'a>),
|
||||
AnnotatedAssignment(&'a ast::StmtAnnAssign),
|
||||
Comprehension(ComprehensionDefinitionNodeRef<'a>),
|
||||
Parameter(ast::AnyParameterRef<'a>),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> {
|
||||
@@ -96,12 +95,6 @@ impl<'a> From<ComprehensionDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ast::AnyParameterRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node: ast::AnyParameterRef<'a>) -> Self {
|
||||
Self::Parameter(node)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::StmtImportFrom,
|
||||
@@ -157,14 +150,6 @@ impl DefinitionNodeRef<'_> {
|
||||
first,
|
||||
})
|
||||
}
|
||||
DefinitionNodeRef::Parameter(parameter) => match parameter {
|
||||
ast::AnyParameterRef::Variadic(parameter) => {
|
||||
DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
ast::AnyParameterRef::NonVariadic(parameter) => {
|
||||
DefinitionKind::ParameterWithDefault(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +168,6 @@ impl DefinitionNodeRef<'_> {
|
||||
}) => target.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::Comprehension(ComprehensionDefinitionNodeRef { node, first: _ }) => node.into(),
|
||||
Self::Parameter(node) => match node {
|
||||
ast::AnyParameterRef::Variadic(parameter) => parameter.into(),
|
||||
ast::AnyParameterRef::NonVariadic(parameter) => parameter.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,8 +182,6 @@ pub enum DefinitionKind {
|
||||
Assignment(AssignmentDefinitionKind),
|
||||
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
||||
Comprehension(ComprehensionDefinitionKind),
|
||||
Parameter(AstNodeRef<ast::Parameter>),
|
||||
ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -294,15 +273,3 @@ impl From<&ast::Comprehension> for DefinitionNodeKey {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::Parameter> for DefinitionNodeKey {
|
||||
fn from(node: &ast::Parameter) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::ParameterWithDefault> for DefinitionNodeKey {
|
||||
fn from(node: &ast::ParameterWithDefault) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,33 +145,8 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Unbound)
|
||||
}
|
||||
|
||||
pub const fn is_never(&self) -> bool {
|
||||
matches!(self, Type::Never)
|
||||
}
|
||||
|
||||
pub fn may_be_unbound(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::Unbound => true,
|
||||
Type::Union(union) => union.contains(db, Type::Unbound),
|
||||
// Unbound can't appear in an intersection, because an intersection with Unbound
|
||||
// simplifies to just Unbound.
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn replace_unbound_with(&self, db: &'db dyn Db, replacement: Type<'db>) -> Type<'db> {
|
||||
match self {
|
||||
Type::Unbound => replacement,
|
||||
Type::Union(union) => union
|
||||
.elements(db)
|
||||
.into_iter()
|
||||
.fold(UnionBuilder::new(db), |builder, ty| {
|
||||
builder.add(ty.replace_unbound_with(db, replacement))
|
||||
})
|
||||
.build(),
|
||||
ty => *ty,
|
||||
}
|
||||
pub const fn is_unknown(&self) -> bool {
|
||||
matches!(self, Type::Unknown)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@@ -201,7 +201,6 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
self.negative.retain(|elem| !pos.contains(elem));
|
||||
}
|
||||
Type::Never => {}
|
||||
Type::Unbound => {}
|
||||
_ => {
|
||||
if !self.positive.remove(&ty) {
|
||||
self.negative.insert(ty);
|
||||
@@ -215,13 +214,9 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
|
||||
// Never is a subtype of all types
|
||||
if self.positive.contains(&Type::Never) {
|
||||
self.positive.retain(Type::is_never);
|
||||
self.negative.clear();
|
||||
}
|
||||
|
||||
if self.positive.contains(&Type::Unbound) {
|
||||
self.positive.retain(Type::is_unbound);
|
||||
self.positive.clear();
|
||||
self.negative.clear();
|
||||
self.positive.insert(Type::Never);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,26 +426,4 @@ mod tests {
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_unbound() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unbound)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Unbound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_unbound() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Unbound)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
//!
|
||||
//! Inferring types at any of the three region granularities returns a [`TypeInference`], which
|
||||
//! holds types for every [`Definition`] and expression within the inferred region.
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use salsa;
|
||||
use salsa::plumbing::AsId;
|
||||
@@ -33,7 +31,7 @@ use ruff_python_ast::{ExprContext, TypeParams};
|
||||
|
||||
use crate::builtins::builtins_scope;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{file_to_module, resolve_module};
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::ast_ids::{HasScopedAstId, HasScopedUseId, ScopedExpressionId};
|
||||
use crate::semantic_index::definition::{Definition, DefinitionKind, DefinitionNodeKey};
|
||||
use crate::semantic_index::expression::Expression;
|
||||
@@ -309,12 +307,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
definition,
|
||||
);
|
||||
}
|
||||
DefinitionKind::Parameter(parameter) => {
|
||||
self.infer_parameter_definition(parameter, definition);
|
||||
}
|
||||
DefinitionKind::ParameterWithDefault(parameter_with_default) => {
|
||||
self.infer_parameter_with_default_definition(parameter_with_default, definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,13 +421,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.map(|decorator| self.infer_decorator(decorator))
|
||||
.collect();
|
||||
|
||||
for default in parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.infer_expression(default);
|
||||
}
|
||||
|
||||
// If there are type params, parameters and returns are evaluated in that scope.
|
||||
if type_params.is_none() {
|
||||
self.infer_parameters(parameters);
|
||||
@@ -473,12 +458,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let ast::ParameterWithDefault {
|
||||
range: _,
|
||||
parameter,
|
||||
default: _,
|
||||
default,
|
||||
} = parameter_with_default;
|
||||
|
||||
self.infer_optional_expression(parameter.annotation.as_deref());
|
||||
|
||||
self.infer_definition(parameter_with_default);
|
||||
self.infer_parameter(parameter);
|
||||
self.infer_optional_expression(default.as_deref());
|
||||
}
|
||||
|
||||
fn infer_parameter(&mut self, parameter: &ast::Parameter) {
|
||||
@@ -487,29 +470,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
name: _,
|
||||
annotation,
|
||||
} = parameter;
|
||||
|
||||
self.infer_optional_expression(annotation.as_deref());
|
||||
|
||||
self.infer_definition(parameter);
|
||||
}
|
||||
|
||||
fn infer_parameter_with_default_definition(
|
||||
&mut self,
|
||||
_parameter_with_default: &ast::ParameterWithDefault,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// TODO(dhruvmanila): Infer types from annotation or default expression
|
||||
self.types.definitions.insert(definition, Type::Unknown);
|
||||
}
|
||||
|
||||
fn infer_parameter_definition(
|
||||
&mut self,
|
||||
_parameter: &ast::Parameter,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// TODO(dhruvmanila): Annotation expression is resolved at the enclosing scope, infer the
|
||||
// parameter type from there
|
||||
self.types.definitions.insert(definition, Type::Unknown);
|
||||
}
|
||||
|
||||
fn infer_class_definition_statement(&mut self, class: &ast::StmtClassDef) {
|
||||
@@ -824,7 +785,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
asname: _,
|
||||
} = alias;
|
||||
|
||||
let module_ty = self.module_ty_from_name(ModuleName::new(name));
|
||||
let module_ty = self.module_ty_from_name(name);
|
||||
self.types.definitions.insert(definition, module_ty);
|
||||
}
|
||||
|
||||
@@ -862,68 +823,20 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_optional_expression(cause.as_deref());
|
||||
}
|
||||
|
||||
/// Given a `from .foo import bar` relative import, resolve the relative module
|
||||
/// we're importing `bar` from into an absolute [`ModuleName`]
|
||||
/// using the name of the module we're currently analyzing.
|
||||
///
|
||||
/// - `level` is the number of dots at the beginning of the relative module name:
|
||||
/// - `from .foo.bar import baz` => `level == 1`
|
||||
/// - `from ...foo.bar import baz` => `level == 3`
|
||||
/// - `tail` is the relative module name stripped of all leading dots:
|
||||
/// - `from .foo import bar` => `tail == "foo"`
|
||||
/// - `from ..foo.bar import baz` => `tail == "foo.bar"`
|
||||
fn relative_module_name(&self, tail: Option<&str>, level: NonZeroU32) -> Option<ModuleName> {
|
||||
let Some(module) = file_to_module(self.db, self.file) else {
|
||||
tracing::debug!("Failed to resolve file {:?} to a module", self.file);
|
||||
return None;
|
||||
};
|
||||
let mut level = level.get();
|
||||
if module.kind().is_package() {
|
||||
level -= 1;
|
||||
}
|
||||
let mut module_name = module.name().to_owned();
|
||||
for _ in 0..level {
|
||||
module_name = module_name.parent()?;
|
||||
}
|
||||
if let Some(tail) = tail {
|
||||
if let Some(valid_tail) = ModuleName::new(tail) {
|
||||
module_name.extend(&valid_tail);
|
||||
} else {
|
||||
tracing::debug!("Failed to resolve relative import due to invalid syntax");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(module_name)
|
||||
}
|
||||
|
||||
fn infer_import_from_definition(
|
||||
&mut self,
|
||||
import_from: &ast::StmtImportFrom,
|
||||
alias: &ast::Alias,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// TODO:
|
||||
// - Absolute `*` imports (`from collections import *`)
|
||||
// - Relative `*` imports (`from ...foo import *`)
|
||||
// - Submodule imports (`from collections import abc`,
|
||||
// where `abc` is a submodule of the `collections` package)
|
||||
//
|
||||
// For the last item, see the currently skipped tests
|
||||
// `follow_relative_import_bare_to_module()` and
|
||||
// `follow_nonexistent_import_bare_to_module()`.
|
||||
let ast::StmtImportFrom { module, level, .. } = import_from;
|
||||
tracing::trace!("Resolving imported object {alias:?} from statement {import_from:?}");
|
||||
let module_name = if let Some(level) = NonZeroU32::new(*level) {
|
||||
self.relative_module_name(module.as_deref(), level)
|
||||
let ast::StmtImportFrom { module, .. } = import_from;
|
||||
let module_ty = if let Some(module) = module {
|
||||
self.module_ty_from_name(module)
|
||||
} else {
|
||||
let module_name = module
|
||||
.as_ref()
|
||||
.expect("Non-relative import should always have a non-None `module`!");
|
||||
ModuleName::new(module_name)
|
||||
// TODO support relative imports
|
||||
Type::Unknown
|
||||
};
|
||||
|
||||
let module_ty = self.module_ty_from_name(module_name);
|
||||
|
||||
let ast::Alias {
|
||||
range: _,
|
||||
name,
|
||||
@@ -946,10 +859,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn module_ty_from_name(&self, module_name: Option<ModuleName>) -> Type<'db> {
|
||||
module_name
|
||||
.and_then(|module_name| resolve_module(self.db, module_name))
|
||||
.map_or(Type::Unbound, |module| Type::Module(module.file()))
|
||||
fn module_ty_from_name(&self, name: &ast::Identifier) -> Type<'db> {
|
||||
let module = ModuleName::new(&name.id).and_then(|name| resolve_module(self.db, name));
|
||||
module
|
||||
.map(|module| Type::Module(module.file()))
|
||||
.unwrap_or(Type::Unbound)
|
||||
}
|
||||
|
||||
fn infer_decorator(&mut self, decorator: &ast::Decorator) -> Type<'db> {
|
||||
@@ -1363,13 +1277,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} = lambda_expression;
|
||||
|
||||
if let Some(parameters) = parameters {
|
||||
for default in parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.infer_expression(default);
|
||||
}
|
||||
|
||||
self.infer_parameters(parameters);
|
||||
}
|
||||
|
||||
@@ -1447,22 +1354,18 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let symbol = symbols.symbol_by_name(id).unwrap();
|
||||
if !symbol.is_defined() || !self.scope.is_function_like(self.db) {
|
||||
// implicit global
|
||||
let unbound_ty = if file_scope_id == FileScopeId::global() {
|
||||
let mut unbound_ty = if file_scope_id == FileScopeId::global() {
|
||||
Type::Unbound
|
||||
} else {
|
||||
global_symbol_ty_by_name(self.db, self.file, id)
|
||||
};
|
||||
// fallback to builtins
|
||||
if unbound_ty.may_be_unbound(self.db)
|
||||
if matches!(unbound_ty, Type::Unbound)
|
||||
&& Some(self.scope) != builtins_scope(self.db)
|
||||
{
|
||||
Some(unbound_ty.replace_unbound_with(
|
||||
self.db,
|
||||
builtins_symbol_ty_by_name(self.db, id),
|
||||
))
|
||||
} else {
|
||||
Some(unbound_ty)
|
||||
unbound_ty = builtins_symbol_ty_by_name(self.db, id);
|
||||
}
|
||||
Some(unbound_ty)
|
||||
} else {
|
||||
Some(Type::Unbound)
|
||||
}
|
||||
@@ -1759,148 +1662,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_relative_import_simple() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
("src/package/__init__.py", ""),
|
||||
("src/package/foo.py", "X = 42"),
|
||||
("src/package/bar.py", "from .foo import X"),
|
||||
])?;
|
||||
|
||||
assert_public_ty(&db, "src/package/bar.py", "X", "Literal[42]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_nonexistent_relative_import_simple() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
("src/package/__init__.py", ""),
|
||||
("src/package/bar.py", "from .foo import X"),
|
||||
])?;
|
||||
|
||||
assert_public_ty(&db, "src/package/bar.py", "X", "Unbound");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_relative_import_dotted() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
("src/package/__init__.py", ""),
|
||||
("src/package/foo/bar/baz.py", "X = 42"),
|
||||
("src/package/bar.py", "from .foo.bar.baz import X"),
|
||||
])?;
|
||||
|
||||
assert_public_ty(&db, "src/package/bar.py", "X", "Literal[42]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_relative_import_bare_to_package() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
("src/package/__init__.py", "X = 42"),
|
||||
("src/package/bar.py", "from . import X"),
|
||||
])?;
|
||||
|
||||
assert_public_ty(&db, "src/package/bar.py", "X", "Literal[42]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_nonexistent_relative_import_bare_to_package() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
db.write_files([("src/package/bar.py", "from . import X")])?;
|
||||
assert_public_ty(&db, "src/package/bar.py", "X", "Unbound");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[ignore = "TODO: Submodule imports possibly not supported right now?"]
|
||||
#[test]
|
||||
fn follow_relative_import_bare_to_module() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
("src/package/__init__.py", ""),
|
||||
("src/package/foo.py", "X = 42"),
|
||||
("src/package/bar.py", "from . import foo; y = foo.X"),
|
||||
])?;
|
||||
|
||||
assert_public_ty(&db, "src/package/bar.py", "y", "Literal[42]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[ignore = "TODO: Submodule imports possibly not supported right now?"]
|
||||
#[test]
|
||||
fn follow_nonexistent_import_bare_to_module() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
("src/package/__init__.py", ""),
|
||||
("src/package/bar.py", "from . import foo"),
|
||||
])?;
|
||||
|
||||
assert_public_ty(&db, "src/package/bar.py", "foo", "Unbound");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_relative_import_from_dunder_init() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
("src/package/__init__.py", "from .foo import X"),
|
||||
("src/package/foo.py", "X = 42"),
|
||||
])?;
|
||||
|
||||
assert_public_ty(&db, "src/package/__init__.py", "X", "Literal[42]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_nonexistent_relative_import_from_dunder_init() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
db.write_files([("src/package/__init__.py", "from .foo import X")])?;
|
||||
assert_public_ty(&db, "src/package/__init__.py", "X", "Unbound");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_very_relative_import() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
("src/package/__init__.py", ""),
|
||||
("src/package/foo.py", "X = 42"),
|
||||
(
|
||||
"src/package/subpackage/subsubpackage/bar.py",
|
||||
"from ...foo import X",
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_public_ty(
|
||||
&db,
|
||||
"src/package/subpackage/subsubpackage/bar.py",
|
||||
"X",
|
||||
"Literal[42]",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_base_class_by_name() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
@@ -2402,38 +2163,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditionally_global_or_builtin() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
if flag:
|
||||
copyright = 1
|
||||
def f():
|
||||
y = copyright
|
||||
",
|
||||
)?;
|
||||
|
||||
let file = system_path_to_file(&db, "src/a.py").expect("Expected file to exist.");
|
||||
let index = semantic_index(&db, file);
|
||||
let function_scope = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.next()
|
||||
.unwrap()
|
||||
.0
|
||||
.to_scope_id(&db, file);
|
||||
let y_ty = symbol_ty_by_name(&db, function_scope, "y");
|
||||
|
||||
assert_eq!(
|
||||
y_ty.display(&db).to_string(),
|
||||
"Literal[1] | Literal[copyright]"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Class name lookups do fall back to globals, but the public type never does.
|
||||
#[test]
|
||||
fn unbound_class_local() -> anyhow::Result<()> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -89,7 +89,7 @@ fn benchmark_incremental(criterion: &mut Criterion) {
|
||||
let Case { db, parser, .. } = case;
|
||||
let result = db.check_file(*parser).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 29);
|
||||
assert_eq!(result.len(), 402);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
@@ -104,7 +104,7 @@ fn benchmark_cold(criterion: &mut Criterion) {
|
||||
let Case { db, parser, .. } = case;
|
||||
let result = db.check_file(*parser).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 29);
|
||||
assert_eq!(result.len(), 402);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
# Errors
|
||||
@app.get("/things/{thing_id}")
|
||||
async def read_thing(query: str):
|
||||
return {"query": query}
|
||||
|
||||
|
||||
@app.get("/books/isbn-{isbn}")
|
||||
async def read_thing():
|
||||
...
|
||||
|
||||
|
||||
@app.get("/things/{thing_id:path}")
|
||||
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("/books/{author}/{title}")
|
||||
async def read_thing(author: str):
|
||||
return {"author": author}
|
||||
|
||||
|
||||
@app.get("/books/{author_name}/{title}")
|
||||
async def read_thing():
|
||||
...
|
||||
|
||||
|
||||
@app.get("/books/{author}/{title}")
|
||||
async def read_thing(author: str, title: str, /):
|
||||
return {"author": author, "title": title}
|
||||
|
||||
|
||||
@app.get("/books/{author}/{title}/{page}")
|
||||
async def read_thing(
|
||||
author: str,
|
||||
query: str,
|
||||
): ...
|
||||
|
||||
|
||||
@app.get("/books/{author}/{title}")
|
||||
async def read_thing():
|
||||
...
|
||||
|
||||
|
||||
@app.get("/books/{author}/{title}")
|
||||
async def read_thing(*, author: str):
|
||||
...
|
||||
|
||||
|
||||
@app.get("/books/{author}/{title}")
|
||||
async def read_thing(hello, /, *, author: str):
|
||||
...
|
||||
|
||||
|
||||
@app.get("/things/{thing_id}")
|
||||
async def read_thing(
|
||||
query: str,
|
||||
):
|
||||
return {"query": query}
|
||||
|
||||
|
||||
@app.get("/things/{thing_id}")
|
||||
async def read_thing(
|
||||
query: str = "default",
|
||||
):
|
||||
return {"query": query}
|
||||
|
||||
|
||||
@app.get("/things/{thing_id}")
|
||||
async def read_thing(
|
||||
*, query: str = "default",
|
||||
):
|
||||
return {"query": query}
|
||||
|
||||
|
||||
# OK
|
||||
@app.get("/things/{thing_id}")
|
||||
async def read_thing(thing_id: int, query: str):
|
||||
return {"thing_id": thing_id, "query": query}
|
||||
|
||||
|
||||
@app.get("/books/isbn-{isbn}")
|
||||
async def read_thing(isbn: str):
|
||||
return {"isbn": isbn}
|
||||
|
||||
|
||||
@app.get("/things/{thing_id:path}")
|
||||
async def read_thing(thing_id: str, query: str):
|
||||
return {"thing_id": thing_id, "query": query}
|
||||
|
||||
|
||||
@app.get("/things/{thing_id : path}")
|
||||
async def read_thing(thing_id: str, query: str):
|
||||
return {"thing_id": thing_id, "query": query}
|
||||
|
||||
|
||||
@app.get("/books/{author}/{title}")
|
||||
async def read_thing(author: str, title: str):
|
||||
return {"author": author, "title": title}
|
||||
|
||||
|
||||
@app.get("/books/{author}/{title}")
|
||||
async def read_thing(*, author: str, title: str):
|
||||
return {"author": author, "title": title}
|
||||
|
||||
|
||||
@app.get("/books/{author}/{title:path}")
|
||||
async def read_thing(*, author: str, title: str):
|
||||
return {"author": author, "title": title}
|
||||
|
||||
|
||||
# Ignored
|
||||
@app.get("/things/{thing-id}")
|
||||
async def read_thing(query: str):
|
||||
return {"query": query}
|
||||
|
||||
|
||||
@app.get("/things/{thing_id!r}")
|
||||
async def read_thing(query: str):
|
||||
return {"query": query}
|
||||
|
||||
|
||||
@app.get("/things/{thing_id=}")
|
||||
async def read_thing(query: str):
|
||||
return {"query": query}
|
||||
@@ -1,6 +1,2 @@
|
||||
import mod.CaMel as CM
|
||||
from mod import CamelCase as CC
|
||||
|
||||
|
||||
# OK depending on configured import convention
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
@@ -94,9 +94,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::FastApiNonAnnotatedDependency) {
|
||||
fastapi::rules::fastapi_non_annotated_dependency(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::FastApiUnusedPathParameter) {
|
||||
fastapi::rules::fastapi_unused_path_parameter(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::AmbiguousFunctionName) {
|
||||
if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
@@ -707,7 +704,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
if checker.enabled(Rule::CamelcaseImportedAsAcronym) {
|
||||
if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym(
|
||||
name, asname, alias, stmt, checker,
|
||||
name,
|
||||
asname,
|
||||
alias,
|
||||
stmt,
|
||||
&checker.settings.pep8_naming.ignore_names,
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -1022,7 +1023,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
asname,
|
||||
alias,
|
||||
stmt,
|
||||
checker,
|
||||
&checker.settings.pep8_naming.ignore_names,
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -920,7 +920,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
// fastapi
|
||||
(FastApi, "001") => (RuleGroup::Preview, rules::fastapi::rules::FastApiRedundantResponseModel),
|
||||
(FastApi, "002") => (RuleGroup::Preview, rules::fastapi::rules::FastApiNonAnnotatedDependency),
|
||||
(FastApi, "003") => (RuleGroup::Preview, rules::fastapi::rules::FastApiUnusedPathParameter),
|
||||
|
||||
// pydoclint
|
||||
(Pydoclint, "201") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingReturns),
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt};
|
||||
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Stmt};
|
||||
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
@@ -282,59 +282,6 @@ pub(crate) fn add_argument(
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic function to add a (regular) parameter to a function definition.
|
||||
pub(crate) fn add_parameter(parameter: &str, parameters: &Parameters, source: &str) -> Edit {
|
||||
if let Some(last) = parameters
|
||||
.args
|
||||
.iter()
|
||||
.filter(|arg| arg.default.is_none())
|
||||
.last()
|
||||
{
|
||||
// Case 1: at least one regular parameter, so append after the last one.
|
||||
Edit::insertion(format!(", {parameter}"), last.end())
|
||||
} else if parameters.args.first().is_some() {
|
||||
// Case 2: no regular parameters, but at least one keyword parameter, so add before the
|
||||
// first.
|
||||
let pos = parameters.start();
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(pos, source);
|
||||
let name = tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Name)
|
||||
.expect("Unable to find name token");
|
||||
Edit::insertion(format!("{parameter}, "), name.start())
|
||||
} else if let Some(last) = parameters.posonlyargs.last() {
|
||||
// Case 2: no regular parameter, but a positional-only parameter exists, so add after that.
|
||||
// We take care to add it *after* the `/` separator.
|
||||
let pos = last.end();
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(pos, source);
|
||||
let slash = tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Slash)
|
||||
.expect("Unable to find `/` token");
|
||||
// Try to find a comma after the slash.
|
||||
let comma = tokenizer.find(|token| token.kind == SimpleTokenKind::Comma);
|
||||
if let Some(comma) = comma {
|
||||
Edit::insertion(format!(" {parameter},"), comma.start() + TextSize::from(1))
|
||||
} else {
|
||||
Edit::insertion(format!(", {parameter}"), slash.start())
|
||||
}
|
||||
} else if parameters.kwonlyargs.first().is_some() {
|
||||
// Case 3: no regular parameter, but a keyword-only parameter exist, so add parameter before that.
|
||||
// We need to backtrack to before the `*` separator.
|
||||
// We know there is no non-keyword-only params, so we can safely assume that the `*` separator is the first
|
||||
let pos = parameters.start();
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(pos, source);
|
||||
let star = tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Star)
|
||||
.expect("Unable to find `*` token");
|
||||
Edit::insertion(format!("{parameter}, "), star.start())
|
||||
} else {
|
||||
// Case 4: no parameters at all, so add parameter after the opening parenthesis.
|
||||
Edit::insertion(
|
||||
parameter.to_string(),
|
||||
parameters.start() + TextSize::from(1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely adjust the indentation of the indented block at [`TextRange`].
|
||||
///
|
||||
/// The [`TextRange`] is assumed to represent an entire indented block, including the leading
|
||||
|
||||
@@ -15,7 +15,6 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::FastApiRedundantResponseModel, Path::new("FAST001.py"))]
|
||||
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002.py"))]
|
||||
#[test_case(Rule::FastApiUnusedPathParameter, Path::new("FAST003.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
use std::iter::Peekable;
|
||||
use std::ops::Range;
|
||||
use std::str::CharIndices;
|
||||
|
||||
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_stdlib::identifiers::is_identifier;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::add_parameter;
|
||||
use crate::rules::fastapi::rules::is_fastapi_route_decorator;
|
||||
|
||||
/// ## What it does
|
||||
/// Identifies FastAPI routes that declare path parameters in the route path
|
||||
/// that are not included in the function signature.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Path parameters are used to extract values from the URL path.
|
||||
///
|
||||
/// If a path parameter is declared in the route path but not in the function
|
||||
/// signature, it will not be accessible in the function body, which is likely
|
||||
/// a mistake.
|
||||
///
|
||||
/// If a path parameter is declared in the route path, but as a positional-only
|
||||
/// argument in the function signature, it will also not be accessible in the
|
||||
/// function body, as FastAPI will not inject the parameter.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// If the path parameter is _not_ a valid Python identifier (e.g., `user-id`, as
|
||||
/// opposed to `user_id`), FastAPI will normalize it. However, this rule simply
|
||||
/// ignores such path parameters, as FastAPI's normalization behavior is undocumented.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from fastapi import FastAPI
|
||||
///
|
||||
/// app = FastAPI()
|
||||
///
|
||||
///
|
||||
/// @app.get("/things/{thing_id}")
|
||||
/// async def read_thing(query: str): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from fastapi import FastAPI
|
||||
///
|
||||
/// app = FastAPI()
|
||||
///
|
||||
///
|
||||
/// @app.get("/things/{thing_id}")
|
||||
/// async def read_thing(thing_id: int, query: str): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as modifying a function signature can
|
||||
/// change the behavior of the code.
|
||||
#[violation]
|
||||
pub struct FastApiUnusedPathParameter {
|
||||
arg_name: String,
|
||||
function_name: String,
|
||||
is_positional: bool,
|
||||
}
|
||||
|
||||
impl Violation for FastApiUnusedPathParameter {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self {
|
||||
arg_name,
|
||||
function_name,
|
||||
is_positional,
|
||||
} = self;
|
||||
#[allow(clippy::if_not_else)]
|
||||
if !is_positional {
|
||||
format!("Parameter `{arg_name}` appears in route path, but not in `{function_name}` signature")
|
||||
} else {
|
||||
format!(
|
||||
"Parameter `{arg_name}` appears in route path, but only as a positional-only argument in `{function_name}` signature"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self {
|
||||
arg_name,
|
||||
is_positional,
|
||||
..
|
||||
} = self;
|
||||
if *is_positional {
|
||||
None
|
||||
} else {
|
||||
Some(format!("Add `{arg_name}` to function signature"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FAST003
|
||||
pub(crate) fn fastapi_unused_path_parameter(
|
||||
checker: &mut Checker,
|
||||
function_def: &ast::StmtFunctionDef,
|
||||
) {
|
||||
if !checker.semantic().seen_module(Modules::FASTAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the route path from the decorator.
|
||||
let route_decorator = function_def
|
||||
.decorator_list
|
||||
.iter()
|
||||
.find_map(|decorator| is_fastapi_route_decorator(decorator, checker.semantic()));
|
||||
|
||||
let Some(route_decorator) = route_decorator else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(path_arg) = route_decorator.arguments.args.first() else {
|
||||
return;
|
||||
};
|
||||
let diagnostic_range = path_arg.range();
|
||||
|
||||
// We can't really handle anything other than string literals.
|
||||
let path = match path_arg.as_string_literal_expr() {
|
||||
Some(path_arg) => &path_arg.value,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Extract the path parameters from the route path.
|
||||
let path_params = PathParamIterator::new(path.to_str());
|
||||
|
||||
// Extract the arguments from the function signature
|
||||
let named_args: Vec<_> = function_def
|
||||
.parameters
|
||||
.args
|
||||
.iter()
|
||||
.chain(function_def.parameters.kwonlyargs.iter())
|
||||
.map(|arg| arg.parameter.name.as_str())
|
||||
.collect();
|
||||
|
||||
// Check if any of the path parameters are not in the function signature.
|
||||
let mut diagnostics = vec![];
|
||||
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 signature, we don't need to do anything.
|
||||
if named_args.contains(&path_param) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine whether the path parameter is used as a positional-only argument. In this case,
|
||||
// the path parameter injection won't work, but we also can't fix it (yet), since we'd need
|
||||
// to make the parameter non-positional-only.
|
||||
let is_positional = function_def
|
||||
.parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.any(|arg| arg.parameter.name.as_str() == path_param);
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
FastApiUnusedPathParameter {
|
||||
arg_name: path_param.to_string(),
|
||||
function_name: function_def.name.to_string(),
|
||||
is_positional,
|
||||
},
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
diagnostic_range
|
||||
.add_start(TextSize::from(range.start as u32 + 1))
|
||||
.sub_end(TextSize::from((path.len() - range.end + 1) as u32)),
|
||||
);
|
||||
if !is_positional {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(add_parameter(
|
||||
path_param,
|
||||
&function_def.parameters,
|
||||
checker.locator().contents(),
|
||||
)));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
checker.diagnostics.extend(diagnostics);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// inclusive of curly braces.
|
||||
#[derive(Debug)]
|
||||
struct PathParamIterator<'a> {
|
||||
input: &'a str,
|
||||
chars: Peekable<CharIndices<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> PathParamIterator<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
PathParamIterator {
|
||||
input,
|
||||
chars: input.char_indices().peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 convertors
|
||||
// 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();
|
||||
|
||||
#[allow(clippy::range_plus_one)]
|
||||
return Some((param_name, start..end + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
pub(crate) use fastapi_non_annotated_dependency::*;
|
||||
pub(crate) use fastapi_redundant_response_model::*;
|
||||
pub(crate) use fastapi_unused_path_parameter::*;
|
||||
|
||||
mod fastapi_non_annotated_dependency;
|
||||
mod fastapi_redundant_response_model;
|
||||
mod fastapi_unused_path_parameter;
|
||||
|
||||
use ruff_python_ast::{Decorator, ExprCall, StmtFunctionDef};
|
||||
use ruff_python_semantic::analyze::typing::resolve_assignment;
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
---
|
||||
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
|
||||
|
||||
ℹ 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}")
|
||||
| ^^^^^^ FAST003
|
||||
13 | async def read_thing():
|
||||
14 | ...
|
||||
|
|
||||
= 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}")
|
||||
| ^^^^^^^^^^^^^^^ FAST003
|
||||
18 | async def read_thing(query: str):
|
||||
19 | 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}")
|
||||
| ^^^^^^^^^^^^^^^^^ FAST003
|
||||
23 | async def read_thing(query: str):
|
||||
24 | 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}")
|
||||
| ^^^^^^^ FAST003
|
||||
28 | async def read_thing(author: str):
|
||||
29 | 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}")
|
||||
| ^^^^^^^^^^^^^ FAST003
|
||||
33 | async def read_thing():
|
||||
34 | ...
|
||||
|
|
||||
= 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}")
|
||||
| ^^^^^^^ FAST003
|
||||
33 | async def read_thing():
|
||||
34 | ...
|
||||
|
|
||||
= 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}")
|
||||
| ^^^^^^^^ FAST003
|
||||
38 | async def read_thing(author: str, title: str, /):
|
||||
39 | 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
|
||||
|
|
||||
37 | @app.get("/books/{author}/{title}")
|
||||
| ^^^^^^^ FAST003
|
||||
38 | async def read_thing(author: str, title: str, /):
|
||||
39 | return {"author": author, "title": title}
|
||||
|
|
||||
|
||||
FAST003.py:42:27: FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature
|
||||
|
|
||||
42 | @app.get("/books/{author}/{title}/{page}")
|
||||
| ^^^^^^^ FAST003
|
||||
43 | async def read_thing(
|
||||
44 | 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}")
|
||||
| ^^^^^^ FAST003
|
||||
43 | async def read_thing(
|
||||
44 | 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}")
|
||||
| ^^^^^^^^ FAST003
|
||||
50 | async def read_thing():
|
||||
51 | ...
|
||||
|
|
||||
= 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}")
|
||||
| ^^^^^^^ FAST003
|
||||
50 | async def read_thing():
|
||||
51 | ...
|
||||
|
|
||||
= 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}")
|
||||
| ^^^^^^^ FAST003
|
||||
55 | async def read_thing(*, author: str):
|
||||
56 | ...
|
||||
|
|
||||
= 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}")
|
||||
| ^^^^^^^ FAST003
|
||||
60 | async def read_thing(hello, /, *, author: str):
|
||||
61 | ...
|
||||
|
|
||||
= 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}")
|
||||
| ^^^^^^^^^^ FAST003
|
||||
65 | async def read_thing(
|
||||
66 | 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}")
|
||||
| ^^^^^^^^^^ FAST003
|
||||
72 | async def read_thing(
|
||||
73 | 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}")
|
||||
| ^^^^^^^^^^ FAST003
|
||||
79 | async def read_thing(
|
||||
80 | *, 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 |
|
||||
@@ -32,7 +32,7 @@ use crate::rules::flake8_async::helpers::AsyncModule;
|
||||
///
|
||||
///
|
||||
/// async def main():
|
||||
/// async with asyncio.timeout(2):
|
||||
/// with asyncio.timeout(2):
|
||||
/// await long_running_task()
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -22,14 +22,14 @@ use crate::rules::flake8_async::helpers::MethodName;
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def func():
|
||||
/// async with asyncio.timeout(2):
|
||||
/// with asyncio.timeout(2):
|
||||
/// do_something()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def func():
|
||||
/// async with asyncio.timeout(2):
|
||||
/// with asyncio.timeout(2):
|
||||
/// do_something()
|
||||
/// await awaitable()
|
||||
/// ```
|
||||
|
||||
@@ -8,12 +8,11 @@ mod tests {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use rustc_hash::FxHashMap;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pep8_naming;
|
||||
use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
use crate::rules::{flake8_import_conventions, pep8_naming};
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -88,25 +87,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camelcase_imported_as_incorrect_convention() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pep8_naming").join("N817.py").as_path(),
|
||||
&settings::LinterSettings {
|
||||
flake8_import_conventions: flake8_import_conventions::settings::Settings {
|
||||
aliases: FxHashMap::from_iter([(
|
||||
"xml.etree.ElementTree".to_string(),
|
||||
"XET".to_string(),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
..settings::LinterSettings::for_rule(Rule::CamelcaseImportedAsAcronym)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classmethod_decorators() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use ruff_python_ast::{Alias, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Alias, Stmt};
|
||||
use ruff_python_stdlib::str::{self};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::pep8_naming::helpers;
|
||||
use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `CamelCase` imports that are aliased as acronyms.
|
||||
@@ -22,9 +23,6 @@ use crate::rules::pep8_naming::helpers;
|
||||
/// Note that this rule is distinct from `camelcase-imported-as-constant`
|
||||
/// to accommodate selective enforcement.
|
||||
///
|
||||
/// Also note that import aliases following an import convention according to the
|
||||
/// [`lint.flake8-import-conventions.aliases`] option are allowed.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from example import MyClassName as MCN
|
||||
@@ -36,9 +34,6 @@ use crate::rules::pep8_naming::helpers;
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-import-conventions.aliases`
|
||||
#[violation]
|
||||
pub struct CamelcaseImportedAsAcronym {
|
||||
name: String,
|
||||
@@ -59,32 +54,17 @@ pub(crate) fn camelcase_imported_as_acronym(
|
||||
asname: &str,
|
||||
alias: &Alias,
|
||||
stmt: &Stmt,
|
||||
checker: &Checker,
|
||||
ignore_names: &IgnoreNames,
|
||||
) -> Option<Diagnostic> {
|
||||
if helpers::is_camelcase(name)
|
||||
&& !str::is_cased_lowercase(asname)
|
||||
&& str::is_cased_uppercase(asname)
|
||||
&& helpers::is_acronym(name, asname)
|
||||
{
|
||||
let ignore_names = &checker.settings.pep8_naming.ignore_names;
|
||||
|
||||
// Ignore any explicitly-allowed names.
|
||||
if ignore_names.matches(name) || ignore_names.matches(asname) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ignore names that follow a community-agreed import convention.
|
||||
if checker
|
||||
.settings
|
||||
.flake8_import_conventions
|
||||
.aliases
|
||||
.get(&*alias.name)
|
||||
.map(String::as_str)
|
||||
== Some(asname)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
CamelcaseImportedAsAcronym {
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pep8_naming/mod.rs
|
||||
---
|
||||
N817.py:1:8: N817 CamelCase `CaMel` imported as acronym `CM`
|
||||
|
|
||||
1 | import mod.CaMel as CM
|
||||
| ^^^^^^^^^^^^^^^ N817
|
||||
2 | from mod import CamelCase as CC
|
||||
|
|
||||
|
||||
N817.py:2:17: N817 CamelCase `CamelCase` imported as acronym `CC`
|
||||
|
|
||||
1 | import mod.CaMel as CM
|
||||
2 | from mod import CamelCase as CC
|
||||
| ^^^^^^^^^^^^^^^ N817
|
||||
|
|
||||
|
||||
N817.py:6:8: N817 CamelCase `ElementTree` imported as acronym `ET`
|
||||
|
|
||||
5 | # OK depending on configured import convention
|
||||
6 | import xml.etree.ElementTree as ET
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ N817
|
||||
|
|
||||
@@ -34,7 +34,6 @@ use crate::registry::Rule;
|
||||
///
|
||||
/// ```python
|
||||
/// class PhotoMetadata:
|
||||
///
|
||||
/// """Metadata about a photo."""
|
||||
/// ```
|
||||
///
|
||||
@@ -126,7 +125,6 @@ impl AlwaysFixableViolation for OneBlankLineAfterClass {
|
||||
///
|
||||
/// ```python
|
||||
/// class PhotoMetadata:
|
||||
///
|
||||
/// """Metadata about a photo."""
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_literal::format::FormatSpec;
|
||||
use ruff_python_parser::parse_expression;
|
||||
use ruff_python_semantic::analyze::logging::is_logger_candidate;
|
||||
use ruff_python_semantic::analyze::logging;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -33,8 +33,6 @@ use crate::checkers::ast::Checker;
|
||||
/// 4. The string has no `{...}` expression sections, or uses invalid f-string syntax.
|
||||
/// 5. The string references variables that are not in scope, or it doesn't capture variables at all.
|
||||
/// 6. Any format specifiers in the potential f-string are invalid.
|
||||
/// 7. The string is part of a function call that is known to expect a template string rather than an
|
||||
/// evaluated f-string: for example, a `logging` call or a [`gettext`] call
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
@@ -50,9 +48,6 @@ use crate::checkers::ast::Checker;
|
||||
/// day_of_week = "Tuesday"
|
||||
/// print(f"Hello {name}! It is {day_of_week} today!")
|
||||
/// ```
|
||||
///
|
||||
/// [`logging`]: https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application
|
||||
/// [`gettext`]: https://docs.python.org/3/library/gettext.html
|
||||
#[violation]
|
||||
pub struct MissingFStringSyntax;
|
||||
|
||||
@@ -80,22 +75,11 @@ pub(crate) fn missing_fstring_syntax(checker: &mut Checker, literal: &ast::Strin
|
||||
}
|
||||
}
|
||||
|
||||
let logger_objects = &checker.settings.logger_objects;
|
||||
|
||||
// We also want to avoid:
|
||||
// - Expressions inside `gettext()` calls
|
||||
// - Expressions passed to logging calls (since the `logging` module evaluates them lazily:
|
||||
// https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application)
|
||||
// - Expressions where a method is immediately called on the string literal
|
||||
if semantic
|
||||
.current_expressions()
|
||||
.filter_map(ast::Expr::as_call_expr)
|
||||
.any(|call_expr| {
|
||||
is_method_call_on_literal(call_expr, literal)
|
||||
|| is_gettext(call_expr, semantic)
|
||||
|| is_logger_candidate(&call_expr.func, semantic, logger_objects)
|
||||
})
|
||||
{
|
||||
// We also want to avoid expressions that are intended to be translated.
|
||||
if semantic.current_expressions().any(|expr| {
|
||||
is_gettext(expr, semantic)
|
||||
|| is_logger_call(expr, semantic, &checker.settings.logger_objects)
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,6 +90,13 @@ pub(crate) fn missing_fstring_syntax(checker: &mut Checker, literal: &ast::Strin
|
||||
}
|
||||
}
|
||||
|
||||
fn is_logger_call(expr: &ast::Expr, semantic: &SemanticModel, logger_objects: &[String]) -> bool {
|
||||
let ast::Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
||||
return false;
|
||||
};
|
||||
logging::is_logger_candidate(func, semantic, logger_objects)
|
||||
}
|
||||
|
||||
/// Returns `true` if an expression appears to be a `gettext` call.
|
||||
///
|
||||
/// We want to avoid statement expressions and assignments related to aliases
|
||||
@@ -116,9 +107,12 @@ pub(crate) fn missing_fstring_syntax(checker: &mut Checker, literal: &ast::Strin
|
||||
/// and replace the original string with its translated counterpart. If the
|
||||
/// string contains variable placeholders or formatting, it can complicate the
|
||||
/// translation process, lead to errors or incorrect translations.
|
||||
fn is_gettext(call_expr: &ast::ExprCall, semantic: &SemanticModel) -> bool {
|
||||
let func = &*call_expr.func;
|
||||
let short_circuit = match func {
|
||||
fn is_gettext(expr: &ast::Expr, semantic: &SemanticModel) -> bool {
|
||||
let ast::Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let short_circuit = match func.as_ref() {
|
||||
ast::Expr::Name(ast::ExprName { id, .. }) => {
|
||||
matches!(id.as_str(), "gettext" | "ngettext" | "_")
|
||||
}
|
||||
@@ -142,21 +136,6 @@ fn is_gettext(call_expr: &ast::ExprCall, semantic: &SemanticModel) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if `call_expr` is a method call on an [`ast::ExprStringLiteral`]
|
||||
/// in which `literal` is one of the [`ast::StringLiteral`] parts.
|
||||
///
|
||||
/// For example: `expr` is a node representing the expression `"{foo}".format(foo="bar")`,
|
||||
/// and `literal` is the node representing the string literal `"{foo}"`.
|
||||
fn is_method_call_on_literal(call_expr: &ast::ExprCall, literal: &ast::StringLiteral) -> bool {
|
||||
let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = &*call_expr.func else {
|
||||
return false;
|
||||
};
|
||||
let ast::Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &**value else {
|
||||
return false;
|
||||
};
|
||||
value.as_slice().contains(literal)
|
||||
}
|
||||
|
||||
/// Returns `true` if `literal` is likely an f-string with a missing `f` prefix.
|
||||
/// See [`MissingFStringSyntax`] for the validation criteria.
|
||||
fn should_be_fstring(
|
||||
@@ -179,28 +158,55 @@ fn should_be_fstring(
|
||||
};
|
||||
|
||||
let mut arg_names = FxHashSet::default();
|
||||
for expr in semantic
|
||||
.current_expressions()
|
||||
.filter_map(ast::Expr::as_call_expr)
|
||||
{
|
||||
let ast::Arguments { keywords, args, .. } = &expr.arguments;
|
||||
for keyword in &**keywords {
|
||||
if let Some(ident) = keyword.arg.as_ref() {
|
||||
arg_names.insert(&ident.id);
|
||||
}
|
||||
}
|
||||
for arg in &**args {
|
||||
if let ast::Expr::Name(ast::ExprName { id, .. }) = arg {
|
||||
arg_names.insert(id);
|
||||
let mut last_expr: Option<&ast::Expr> = None;
|
||||
for expr in semantic.current_expressions() {
|
||||
match expr {
|
||||
ast::Expr::Call(ast::ExprCall {
|
||||
arguments: ast::Arguments { keywords, args, .. },
|
||||
func,
|
||||
..
|
||||
}) => {
|
||||
if let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() {
|
||||
match value.as_ref() {
|
||||
// if the first part of the attribute is the string literal,
|
||||
// we want to ignore this literal from the lint.
|
||||
// for example: `"{x}".some_method(...)`
|
||||
ast::Expr::StringLiteral(expr_literal)
|
||||
if expr_literal.value.as_slice().contains(literal) =>
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// if the first part of the attribute was the expression we
|
||||
// just went over in the last iteration, then we also want to pass
|
||||
// this over in the lint.
|
||||
// for example: `some_func("{x}").some_method(...)`
|
||||
value if last_expr == Some(value) => {
|
||||
return false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for keyword in &**keywords {
|
||||
if let Some(ident) = keyword.arg.as_ref() {
|
||||
arg_names.insert(ident.as_str());
|
||||
}
|
||||
}
|
||||
for arg in &**args {
|
||||
if let ast::Expr::Name(ast::ExprName { id, .. }) = arg {
|
||||
arg_names.insert(id.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
last_expr.replace(expr);
|
||||
}
|
||||
|
||||
for f_string in value.f_strings() {
|
||||
let mut has_name = false;
|
||||
for element in f_string.elements.expressions() {
|
||||
if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() {
|
||||
if arg_names.contains(id) {
|
||||
if arg_names.contains(id.as_str()) {
|
||||
return false;
|
||||
}
|
||||
if semantic
|
||||
|
||||
@@ -84,13 +84,13 @@ impl FormatNodeRule<Comprehension> for FormatComprehension {
|
||||
let (trailing_in_comments, dangling_if_comments) = dangling_comments
|
||||
.split_at(dangling_comments.partition_point(|comment| comment.start() < iter.start()));
|
||||
|
||||
let in_spacer = format_with(|f| {
|
||||
if before_in_comments.is_empty() {
|
||||
space().fmt(f)
|
||||
} else {
|
||||
soft_line_break_or_space().fmt(f)
|
||||
}
|
||||
});
|
||||
// let in_spacer = format_with(|f| {
|
||||
// // if before_in_comments.is_empty() {
|
||||
// // space().fmt(f)
|
||||
// // } else {
|
||||
// soft_line_break_or_space().fmt(f)
|
||||
// // }
|
||||
// });
|
||||
|
||||
write!(
|
||||
f,
|
||||
@@ -101,10 +101,12 @@ impl FormatNodeRule<Comprehension> for FormatComprehension {
|
||||
expression: target,
|
||||
preserve_parentheses: !target.is_tuple_expr()
|
||||
},
|
||||
ExprTupleWithoutParentheses(target),
|
||||
in_spacer,
|
||||
leading_comments(before_in_comments),
|
||||
token("in"),
|
||||
group(&format_args![
|
||||
ExprTupleWithoutParentheses(target),
|
||||
soft_line_break_or_space(),
|
||||
leading_comments(before_in_comments),
|
||||
token("in"),
|
||||
]),
|
||||
trailing_comments(trailing_in_comments),
|
||||
Spacer {
|
||||
expression: iter,
|
||||
|
||||
@@ -266,7 +266,17 @@ last_call()
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -115,7 +115,7 @@
|
||||
@@ -101,7 +101,8 @@
|
||||
{a: b * -2 for a, b in dictionary.items()}
|
||||
{
|
||||
k: v
|
||||
- for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
|
||||
+ for k, v
|
||||
+ in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
|
||||
}
|
||||
Python3 > Python2 > COBOL
|
||||
Life is Life
|
||||
@@ -115,7 +116,7 @@
|
||||
arg,
|
||||
another,
|
||||
kwarg="hey",
|
||||
@@ -383,7 +393,8 @@ str or None if (1 if True else 2) else str or bytes or None
|
||||
{a: b * -2 for a, b in dictionary.items()}
|
||||
{
|
||||
k: v
|
||||
for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
|
||||
for k, v
|
||||
in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
|
||||
}
|
||||
Python3 > Python2 > COBOL
|
||||
Life is Life
|
||||
@@ -1028,5 +1039,3 @@ bbbb >> bbbb * bbbb
|
||||
last_call()
|
||||
# standalone comment at ENDMARKER
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -373,7 +373,7 @@ for foo in ["a", "b"]:
|
||||
|
||||
func(
|
||||
[
|
||||
@@ -114,13 +140,15 @@
|
||||
@@ -114,13 +140,16 @@
|
||||
func(
|
||||
[x for x in "long line long line long line long line long line long line long line"]
|
||||
)
|
||||
@@ -386,7 +386,8 @@ for foo in ["a", "b"]:
|
||||
- for x in "long line long line long line long line long line long line long line"
|
||||
+ for x in [
|
||||
+ x
|
||||
+ for x in "long line long line long line long line long line long line long line"
|
||||
+ for x
|
||||
+ in "long line long line long line long line long line long line long line"
|
||||
+ ]
|
||||
]
|
||||
-])
|
||||
@@ -394,7 +395,7 @@ for foo in ["a", "b"]:
|
||||
|
||||
foooooooooooooooooooo(
|
||||
[{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size}
|
||||
@@ -131,10 +159,12 @@
|
||||
@@ -131,10 +160,12 @@
|
||||
)
|
||||
|
||||
nested_mapping = {
|
||||
@@ -411,7 +412,7 @@ for foo in ["a", "b"]:
|
||||
}
|
||||
explicit_exploding = [
|
||||
[
|
||||
@@ -144,24 +174,34 @@
|
||||
@@ -144,24 +175,34 @@
|
||||
],
|
||||
],
|
||||
]
|
||||
@@ -461,7 +462,7 @@ for foo in ["a", "b"]:
|
||||
|
||||
# Edge case when deciding whether to hug the brackets without inner content.
|
||||
very_very_very_long_variable = very_very_very_long_module.VeryVeryVeryVeryLongClassName(
|
||||
@@ -169,11 +209,13 @@
|
||||
@@ -169,11 +210,14 @@
|
||||
)
|
||||
|
||||
for foo in ["a", "b"]:
|
||||
@@ -478,7 +479,8 @@ for foo in ["a", "b"]:
|
||||
+ individual
|
||||
+ for
|
||||
+ # Foobar
|
||||
+ container in xs_by_y[foo]
|
||||
+ container
|
||||
+ in xs_by_y[foo]
|
||||
+ # Foobar
|
||||
+ for individual in container["nested"]
|
||||
+ ]
|
||||
@@ -635,7 +637,8 @@ func(
|
||||
x
|
||||
for x in [
|
||||
x
|
||||
for x in "long line long line long line long line long line long line long line"
|
||||
for x
|
||||
in "long line long line long line long line long line long line long line"
|
||||
]
|
||||
]
|
||||
)
|
||||
@@ -704,7 +707,8 @@ for foo in ["a", "b"]:
|
||||
individual
|
||||
for
|
||||
# Foobar
|
||||
container in xs_by_y[foo]
|
||||
container
|
||||
in xs_by_y[foo]
|
||||
# Foobar
|
||||
for individual in container["nested"]
|
||||
]
|
||||
|
||||
@@ -198,7 +198,8 @@ query = {
|
||||
|
||||
{
|
||||
a: a # a
|
||||
for c in e # for # c # in # e
|
||||
for c # for # c
|
||||
in e # in # e
|
||||
}
|
||||
|
||||
{
|
||||
@@ -232,7 +233,8 @@ query = {
|
||||
for ccccccccccccccccccccccccccccccccccccccc, ddddddddddddddddddd, [
|
||||
eeeeeeeeeeeeeeeeeeeeee,
|
||||
fffffffffffffffffffffffff,
|
||||
] in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
|
||||
]
|
||||
in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
|
||||
if fffffffffffffffffffffffffffffffffffffffffff
|
||||
< gggggggggggggggggggggggggggggggggggggggggggggg
|
||||
< hhhhhhhhhhhhhhhhhhhhhhhhhh
|
||||
@@ -263,7 +265,8 @@ query = {
|
||||
a,
|
||||
a,
|
||||
a,
|
||||
] in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
|
||||
]
|
||||
in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
|
||||
}
|
||||
{
|
||||
k: v
|
||||
@@ -288,7 +291,8 @@ query = {
|
||||
a,
|
||||
a,
|
||||
a,
|
||||
) in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
|
||||
)
|
||||
in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
|
||||
}
|
||||
|
||||
# Leading
|
||||
@@ -315,7 +319,8 @@ query = {
|
||||
a,
|
||||
a,
|
||||
a, # Trailing
|
||||
) in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension # Trailing
|
||||
)
|
||||
in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension # Trailing
|
||||
} # Trailing
|
||||
# Trailing
|
||||
|
||||
@@ -336,7 +341,8 @@ selected_choices = {
|
||||
for ( # foo
|
||||
x,
|
||||
aaaayaaaayaaaayaaaayaaaayaaaayaaaayaaaayaaaayaaaayaaaayaaaayaaaayaaaay,
|
||||
) in z
|
||||
)
|
||||
in z
|
||||
}
|
||||
|
||||
a = {
|
||||
@@ -403,6 +409,3 @@ query = {
|
||||
for key, queries in self._filters.items()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -189,7 +189,8 @@ y = [
|
||||
|
||||
[
|
||||
a # a
|
||||
for c in e # for # c # in # e
|
||||
for c # for # c
|
||||
in e # in # e
|
||||
]
|
||||
|
||||
[
|
||||
@@ -220,7 +221,8 @@ y = [
|
||||
for ccccccccccccccccccccccccccccccccccccccc, ddddddddddddddddddd, [
|
||||
eeeeeeeeeeeeeeeeeeeeee,
|
||||
fffffffffffffffffffffffff,
|
||||
] in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
|
||||
]
|
||||
in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
|
||||
if fffffffffffffffffffffffffffffffffffffffffff
|
||||
< gggggggggggggggggggggggggggggggggggggggggggggg
|
||||
< hhhhhhhhhhhhhhhhhhhhhhhhhh
|
||||
@@ -304,7 +306,8 @@ aaaaaaaaaaaaaaaaaaaaa = [
|
||||
for (
|
||||
x,
|
||||
y,
|
||||
) in z
|
||||
)
|
||||
in z
|
||||
if head_name
|
||||
]
|
||||
|
||||
@@ -326,7 +329,8 @@ y = [
|
||||
(
|
||||
# comment
|
||||
a
|
||||
) in
|
||||
)
|
||||
in
|
||||
(
|
||||
# comment
|
||||
x
|
||||
@@ -350,7 +354,8 @@ y = [
|
||||
a
|
||||
for
|
||||
# comment
|
||||
a, b in x
|
||||
a, b
|
||||
in x
|
||||
if True
|
||||
]
|
||||
|
||||
@@ -361,7 +366,8 @@ y = [
|
||||
# comment
|
||||
a,
|
||||
b,
|
||||
) in x
|
||||
)
|
||||
in x
|
||||
if True
|
||||
]
|
||||
|
||||
@@ -370,7 +376,8 @@ y = [
|
||||
a
|
||||
for
|
||||
# comment
|
||||
a in
|
||||
a
|
||||
in
|
||||
# comment
|
||||
x
|
||||
if
|
||||
@@ -388,7 +395,7 @@ y = [
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -142,24 +142,20 @@
|
||||
@@ -145,25 +145,21 @@
|
||||
# Leading expression comments:
|
||||
y = [
|
||||
a
|
||||
@@ -397,9 +404,10 @@ y = [
|
||||
+ for (
|
||||
# comment
|
||||
a
|
||||
- ) in
|
||||
)
|
||||
- in
|
||||
- (
|
||||
+ ) in (
|
||||
+ in (
|
||||
# comment
|
||||
x
|
||||
)
|
||||
|
||||
@@ -74,7 +74,8 @@ selected_choices = {
|
||||
|
||||
{
|
||||
a # a
|
||||
for c in e # for # c # in # e
|
||||
for c # for # c
|
||||
in e # in # e
|
||||
}
|
||||
|
||||
{
|
||||
@@ -105,7 +106,8 @@ selected_choices = {
|
||||
for ccccccccccccccccccccccccccccccccccccccc, ddddddddddddddddddd, [
|
||||
eeeeeeeeeeeeeeeeeeeeee,
|
||||
fffffffffffffffffffffffff,
|
||||
] in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
|
||||
]
|
||||
in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
|
||||
if fffffffffffffffffffffffffffffffffffffffffff
|
||||
< gggggggggggggggggggggggggggggggggggggggggggggg
|
||||
< hhhhhhhhhhhhhhhhhhhhhhhhhh
|
||||
@@ -123,6 +125,3 @@ selected_choices = {
|
||||
if str(v) not in self.choices.field.empty_values
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -123,7 +123,11 @@ pub(crate) fn fix_all(
|
||||
fixes.insert(
|
||||
url.clone(),
|
||||
vec![lsp_types::TextEdit {
|
||||
range: source_range.to_range(&source, &source_index, encoding),
|
||||
range: source_range.to_range(
|
||||
source_kind.source_code(),
|
||||
&source_index,
|
||||
encoding,
|
||||
),
|
||||
new_text: modified[modified_range].to_owned(),
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -78,7 +78,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.1
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -91,7 +91,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.1
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -105,7 +105,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.1
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ruff"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
|
||||
readme = "README.md"
|
||||
|
||||
1
ruff.schema.json
generated
1
ruff.schema.json
generated
@@ -3121,7 +3121,6 @@
|
||||
"FAST00",
|
||||
"FAST001",
|
||||
"FAST002",
|
||||
"FAST003",
|
||||
"FBT",
|
||||
"FBT0",
|
||||
"FBT00",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "scripts"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ KNOWN_FORMATTING_VIOLATIONS = [
|
||||
"bad-quotes-inline-string",
|
||||
"bad-quotes-multiline-string",
|
||||
"blank-line-after-decorator",
|
||||
"blank-line-before-class",
|
||||
"blank-line-between-methods",
|
||||
"blank-lines-after-function-or-class",
|
||||
"blank-lines-before-nested-definition",
|
||||
@@ -68,7 +67,6 @@ KNOWN_FORMATTING_VIOLATIONS = [
|
||||
"no-space-after-inline-comment",
|
||||
"non-empty-stub-body",
|
||||
"one-blank-line-after-class",
|
||||
"one-blank-line-before-class",
|
||||
"over-indentation",
|
||||
"over-indented",
|
||||
"pass-statement-stub-body",
|
||||
|
||||
Reference in New Issue
Block a user