Compare commits

..

1 Commits

Author SHA1 Message Date
Dhruv Manilawala
1dc0a523de Separate TOC from the navigation 2024-09-05 16:08:53 +05:30
35 changed files with 241 additions and 760 deletions

View File

@@ -1,34 +1,5 @@
# Changelog
## 0.6.4
### Preview features
- \[`flake8-builtins`\] Use dynamic builtins list based on Python version ([#13172](https://github.com/astral-sh/ruff/pull/13172))
- \[`pydoclint`\] Permit yielding `None` in `DOC402` and `DOC403` ([#13148](https://github.com/astral-sh/ruff/pull/13148))
- \[`pylint`\] Update diagnostic message for `PLW3201` ([#13194](https://github.com/astral-sh/ruff/pull/13194))
- \[`ruff`\] Implement `post-init-default` (`RUF033`) ([#13192](https://github.com/astral-sh/ruff/pull/13192))
- \[`ruff`\] Implement useless if-else (`RUF034`) ([#13218](https://github.com/astral-sh/ruff/pull/13218))
### Rule changes
- \[`flake8-pyi`\] Respect `pep8_naming.classmethod-decorators` settings when determining if a method is a classmethod in `custom-type-var-return-type` (`PYI019`) ([#13162](https://github.com/astral-sh/ruff/pull/13162))
- \[`flake8-pyi`\] Teach various rules that annotations might be stringized ([#12951](https://github.com/astral-sh/ruff/pull/12951))
- \[`pylint`\] Avoid `no-self-use` for `attrs`-style validators ([#13166](https://github.com/astral-sh/ruff/pull/13166))
- \[`pylint`\] Recurse into subscript subexpressions when searching for list/dict lookups (`PLR1733`, `PLR1736`) ([#13186](https://github.com/astral-sh/ruff/pull/13186))
- \[`pyupgrade`\] Detect `aiofiles.open` calls in `UP015` ([#13173](https://github.com/astral-sh/ruff/pull/13173))
- \[`pyupgrade`\] Mark `sys.version_info[0] < 3` and similar comparisons as outdated (`UP036`) ([#13175](https://github.com/astral-sh/ruff/pull/13175))
### CLI
- Enrich messages of SARIF results ([#13180](https://github.com/astral-sh/ruff/pull/13180))
- Handle singular case for incompatible rules warning in `ruff format` output ([#13212](https://github.com/astral-sh/ruff/pull/13212))
### Bug fixes
- \[`pydocstyle`\] Improve heuristics for detecting Google-style docstrings ([#13142](https://github.com/astral-sh/ruff/pull/13142))
- \[`refurb`\] Treat `sep` arguments with effects as unsafe removals (`FURB105`) ([#13165](https://github.com/astral-sh/ruff/pull/13165))
## 0.6.3
### Preview features

6
Cargo.lock generated
View File

@@ -2091,7 +2091,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.6.4"
version = "0.6.3"
dependencies = [
"anyhow",
"argfile",
@@ -2284,7 +2284,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.6.4"
version = "0.6.3"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2604,7 +2604,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.6.4"
version = "0.6.3"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -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.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.6.4/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.6.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.6.3/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.4
rev: v0.6.3
hooks:
# Run the linter.
- id: ruff

View File

@@ -0,0 +1,16 @@
use crate::module_name::ModuleName;
use crate::module_resolver::resolve_module;
use crate::semantic_index::global_scope;
use crate::semantic_index::symbol::ScopeId;
use crate::Db;
/// Salsa query to get the builtins scope.
///
/// Can return None if a custom typeshed is used that is missing `builtins.pyi`.
#[salsa::tracked]
pub(crate) fn builtins_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
let builtins_name =
ModuleName::new_static("builtins").expect("Expected 'builtins' to be a valid module name");
let builtins_file = resolve_module(db, builtins_name)?.file();
Some(global_scope(db, builtins_file))
}

View File

@@ -10,6 +10,7 @@ pub use python_version::PythonVersion;
pub use semantic_model::{HasTy, SemanticModel};
pub mod ast_node_ref;
mod builtins;
mod db;
mod module_name;
mod module_resolver;
@@ -19,7 +20,6 @@ mod python_version;
pub mod semantic_index;
mod semantic_model;
pub(crate) mod site_packages;
mod stdlib;
pub mod types;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;

View File

@@ -193,11 +193,8 @@ impl<'db> SemanticIndexBuilder<'db> {
countme::Count::default(),
);
let existing_definition = self
.definitions_by_node
self.definitions_by_node
.insert(definition_node.key(), definition);
debug_assert_eq!(existing_definition, None);
self.current_use_def_map_mut()
.record_definition(symbol, definition);
@@ -330,11 +327,10 @@ impl<'db> SemanticIndexBuilder<'db> {
// Insert a mapping from the parameter to the same definition.
// This ensures that calling `HasTy::ty` on the inner parameter returns
// a valid type (and doesn't panic)
let existing_definition = self.definitions_by_node.insert(
self.definitions_by_node.insert(
DefinitionNodeRef::from(AnyParameterRef::Variadic(&with_default.parameter)).key(),
definition,
);
debug_assert_eq!(existing_definition, None);
}
}
@@ -672,7 +668,6 @@ where
ForStmtDefinitionNodeRef {
iterable: &node.iter,
target: name_node,
is_async: node.is_async,
},
);
}

View File

@@ -152,7 +152,6 @@ pub(crate) struct WithItemDefinitionNodeRef<'a> {
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
pub(crate) iterable: &'a ast::Expr,
pub(crate) target: &'a ast::ExprName,
pub(crate) is_async: bool,
}
#[derive(Copy, Clone, Debug)]
@@ -207,15 +206,12 @@ impl DefinitionNodeRef<'_> {
DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => {
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
}
DefinitionNodeRef::For(ForStmtDefinitionNodeRef {
iterable,
target,
is_async,
}) => DefinitionKind::For(ForStmtDefinitionKind {
iterable: AstNodeRef::new(parsed.clone(), iterable),
target: AstNodeRef::new(parsed, target),
is_async,
}),
DefinitionNodeRef::For(ForStmtDefinitionNodeRef { iterable, target }) => {
DefinitionKind::For(ForStmtDefinitionKind {
iterable: AstNodeRef::new(parsed.clone(), iterable),
target: AstNodeRef::new(parsed, target),
})
}
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
iterable,
target,
@@ -269,7 +265,6 @@ impl DefinitionNodeRef<'_> {
Self::For(ForStmtDefinitionNodeRef {
iterable: _,
target,
is_async: _,
}) => target.into(),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
Self::Parameter(node) => match node {
@@ -393,7 +388,6 @@ impl WithItemDefinitionKind {
pub struct ForStmtDefinitionKind {
iterable: AstNodeRef<ast::Expr>,
target: AstNodeRef<ast::ExprName>,
is_async: bool,
}
impl ForStmtDefinitionKind {
@@ -404,10 +398,6 @@ impl ForStmtDefinitionKind {
pub(crate) fn target(&self) -> &ast::ExprName {
self.target.node()
}
pub(crate) fn is_async(&self) -> bool {
self.is_async
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]

View File

@@ -8,7 +8,7 @@ use crate::module_name::ModuleName;
use crate::module_resolver::{resolve_module, Module};
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::semantic_index;
use crate::types::{definition_ty, global_symbol_ty, infer_scope_types, Type};
use crate::types::{definition_ty, global_symbol_ty_by_name, infer_scope_types, Type};
use crate::Db;
pub struct SemanticModel<'db> {
@@ -40,7 +40,7 @@ impl<'db> SemanticModel<'db> {
}
pub fn global_symbol_ty(&self, module: &Module, symbol_name: &str) -> Type<'db> {
global_symbol_ty(self.db, module.file(), symbol_name)
global_symbol_ty_by_name(self.db, module.file(), symbol_name)
}
}

View File

@@ -1,77 +0,0 @@
use crate::module_name::ModuleName;
use crate::module_resolver::resolve_module;
use crate::semantic_index::global_scope;
use crate::semantic_index::symbol::ScopeId;
use crate::types::{global_symbol_ty, Type};
use crate::Db;
/// Enumeration of various core stdlib modules, for which we have dedicated Salsa queries.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CoreStdlibModule {
Builtins,
Types,
Typeshed,
}
impl CoreStdlibModule {
fn name(self) -> ModuleName {
let module_name = match self {
Self::Builtins => "builtins",
Self::Types => "types",
Self::Typeshed => "_typeshed",
};
ModuleName::new_static(module_name)
.unwrap_or_else(|| panic!("{module_name} should be a valid module name!"))
}
}
/// Lookup the type of `symbol` in a given core module
///
/// Returns `Unbound` if the given core module cannot be resolved for some reason
fn core_module_symbol_ty<'db>(
db: &'db dyn Db,
core_module: CoreStdlibModule,
symbol: &str,
) -> Type<'db> {
resolve_module(db, core_module.name())
.map(|module| global_symbol_ty(db, module.file(), symbol))
.unwrap_or(Type::Unbound)
}
/// Lookup the type of `symbol` in the builtins namespace.
///
/// Returns `Unbound` if the `builtins` module isn't available for some reason.
#[inline]
pub(crate) fn builtins_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
core_module_symbol_ty(db, CoreStdlibModule::Builtins, symbol)
}
/// Lookup the type of `symbol` in the `types` module namespace.
///
/// Returns `Unbound` if the `types` module isn't available for some reason.
#[inline]
pub(crate) fn types_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
core_module_symbol_ty(db, CoreStdlibModule::Types, symbol)
}
/// Lookup the type of `symbol` in the `_typeshed` module namespace.
///
/// Returns `Unbound` if the `_typeshed` module isn't available for some reason.
#[inline]
pub(crate) fn typeshed_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
core_module_symbol_ty(db, CoreStdlibModule::Typeshed, symbol)
}
/// Get the scope of a core stdlib module.
///
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
fn core_module_scope(db: &dyn Db, core_module: CoreStdlibModule) -> Option<ScopeId<'_>> {
resolve_module(db, core_module.name()).map(|module| global_scope(db, module.file()))
}
/// Get the `builtins` module scope.
///
/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`.
pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
core_module_scope(db, CoreStdlibModule::Builtins)
}

View File

@@ -1,7 +1,7 @@
use infer::TypeInferenceBuilder;
use ruff_db::files::File;
use ruff_python_ast as ast;
use crate::builtins::builtins_scope;
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId};
@@ -9,7 +9,6 @@ use crate::semantic_index::{
global_scope, semantic_index, symbol_table, use_def_map, DefinitionWithConstraints,
DefinitionWithConstraintsIterator,
};
use crate::stdlib::{builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty};
use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet};
@@ -41,7 +40,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
}
/// Infer the public type of a symbol (its type as seen from outside its scope).
pub(crate) fn symbol_ty_by_id<'db>(
pub(crate) fn symbol_ty<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
symbol: ScopedSymbolId,
@@ -59,17 +58,30 @@ pub(crate) fn symbol_ty_by_id<'db>(
}
/// Shorthand for `symbol_ty` that takes a symbol name instead of an ID.
pub(crate) fn symbol_ty<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Type<'db> {
pub(crate) fn symbol_ty_by_name<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
name: &str,
) -> Type<'db> {
let table = symbol_table(db, scope);
table
.symbol_id_by_name(name)
.map(|symbol| symbol_ty_by_id(db, scope, symbol))
.map(|symbol| symbol_ty(db, scope, symbol))
.unwrap_or(Type::Unbound)
}
/// Shorthand for `symbol_ty` that looks up a module-global symbol by name in a file.
pub(crate) fn global_symbol_ty<'db>(db: &'db dyn Db, file: File, name: &str) -> Type<'db> {
symbol_ty(db, global_scope(db, file), name)
pub(crate) fn global_symbol_ty_by_name<'db>(db: &'db dyn Db, file: File, name: &str) -> Type<'db> {
symbol_ty_by_name(db, global_scope(db, file), name)
}
/// Shorthand for `symbol_ty` that looks up a symbol in the builtins.
///
/// Returns `Unbound` if the builtins module isn't available for some reason.
pub(crate) fn builtins_symbol_ty_by_name<'db>(db: &'db dyn Db, name: &str) -> Type<'db> {
builtins_scope(db)
.map(|builtins| symbol_ty_by_name(db, builtins, name))
.unwrap_or(Type::Unbound)
}
/// Infer the type of a [`Definition`].
@@ -294,9 +306,13 @@ impl<'db> Type<'db> {
pub fn replace_unbound_with(&self, db: &'db dyn Db, replacement: Type<'db>) -> Type<'db> {
match self {
Type::Unbound => replacement,
Type::Union(union) => {
union.map(db, |element| element.replace_unbound_with(db, 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,
}
}
@@ -315,7 +331,7 @@ impl<'db> Type<'db> {
/// us to explicitly consider whether to handle an error or propagate
/// it up the call stack.
#[must_use]
pub fn member(&self, db: &'db dyn Db, name: &str) -> Type<'db> {
pub fn member(&self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
match self {
Type::Any => Type::Any,
Type::Never => {
@@ -332,13 +348,19 @@ impl<'db> Type<'db> {
// TODO: attribute lookup on function type
Type::Unknown
}
Type::Module(file) => global_symbol_ty(db, *file, name),
Type::Module(file) => global_symbol_ty_by_name(db, *file, name),
Type::Class(class) => class.class_member(db, name),
Type::Instance(_) => {
// TODO MRO? get_own_instance_member, get_instance_member
Type::Unknown
}
Type::Union(union) => union.map(db, |element| element.member(db, name)),
Type::Union(union) => union
.elements(db)
.iter()
.fold(UnionBuilder::new(db), |builder, element_ty| {
builder.add(element_ty.member(db, name))
})
.build(),
Type::Intersection(_) => {
// TODO perform the get_member on each type in the intersection
// TODO return the intersection of those results
@@ -393,52 +415,6 @@ impl<'db> Type<'db> {
}
}
/// Given the type of an object that is iterated over in some way,
/// return the type of objects that are yielded by that iteration.
///
/// E.g., for the following loop, given the type of `x`, infer the type of `y`:
/// ```python
/// for y in x:
/// pass
/// ```
fn iterate(&self, db: &'db dyn Db) -> IterationOutcome<'db> {
// `self` represents the type of the iterable;
// `__iter__` and `__next__` are both looked up on the class of the iterable:
let iterable_meta_type = self.to_meta_type(db);
let dunder_iter_method = iterable_meta_type.member(db, "__iter__");
if !dunder_iter_method.is_unbound() {
let Some(iterator_ty) = dunder_iter_method.call(db) else {
return IterationOutcome::NotIterable {
not_iterable_ty: *self,
};
};
let dunder_next_method = iterator_ty.to_meta_type(db).member(db, "__next__");
return dunder_next_method
.call(db)
.map(|element_ty| IterationOutcome::Iterable { element_ty })
.unwrap_or(IterationOutcome::NotIterable {
not_iterable_ty: *self,
});
}
// Although it's not considered great practice,
// classes that define `__getitem__` are also iterable,
// even if they do not define `__iter__`.
//
// TODO(Alex) this is only valid if the `__getitem__` method is annotated as
// accepting `int` or `SupportsIndex`
let dunder_get_item_method = iterable_meta_type.member(db, "__getitem__");
dunder_get_item_method
.call(db)
.map(|element_ty| IterationOutcome::Iterable { element_ty })
.unwrap_or(IterationOutcome::NotIterable {
not_iterable_ty: *self,
})
}
#[must_use]
pub fn to_instance(&self) -> Type<'db> {
match self {
@@ -448,56 +424,6 @@ impl<'db> Type<'db> {
_ => Type::Unknown, // TODO type errors
}
}
/// Given a type that is assumed to represent an instance of a class,
/// return a type that represents that class itself.
#[must_use]
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::Unbound => Type::Unbound,
Type::Never => Type::Never,
Type::Instance(class) => Type::Class(*class),
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
Type::BooleanLiteral(_) => builtins_symbol_ty(db, "bool"),
Type::BytesLiteral(_) => builtins_symbol_ty(db, "bytes"),
Type::IntLiteral(_) => builtins_symbol_ty(db, "int"),
Type::Function(_) => types_symbol_ty(db, "FunctionType"),
Type::Module(_) => types_symbol_ty(db, "ModuleType"),
Type::None => typeshed_symbol_ty(db, "NoneType"),
// TODO not accurate if there's a custom metaclass...
Type::Class(_) => builtins_symbol_ty(db, "type"),
// TODO can we do better here? `type[LiteralString]`?
Type::StringLiteral(_) | Type::LiteralString => builtins_symbol_ty(db, "str"),
// TODO: `type[Any]`?
Type::Any => Type::Any,
// TODO: `type[Unknown]`?
Type::Unknown => Type::Unknown,
// TODO intersections
Type::Intersection(_) => Type::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IterationOutcome<'db> {
Iterable { element_ty: Type<'db> },
NotIterable { not_iterable_ty: Type<'db> },
}
impl<'db> IterationOutcome<'db> {
fn unwrap_with_diagnostic(
self,
iterable_node: ast::AnyNodeRef,
inference_builder: &mut TypeInferenceBuilder<'db>,
) -> Type<'db> {
match self {
Self::Iterable { element_ty } => element_ty,
Self::NotIterable { not_iterable_ty } => {
inference_builder.not_iterable_diagnostic(iterable_node, not_iterable_ty);
Type::Unknown
}
}
}
}
#[salsa::interned]
@@ -578,7 +504,7 @@ impl<'db> ClassType<'db> {
/// Returns the class member of this class named `name`.
///
/// The member resolves to a member of the class itself or any of its bases.
pub fn class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
pub fn class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
let member = self.own_class_member(db, name);
if !member.is_unbound() {
return member;
@@ -588,12 +514,12 @@ impl<'db> ClassType<'db> {
}
/// Returns the inferred type of the class member named `name`.
pub fn own_class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
pub fn own_class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
let scope = self.body_scope(db);
symbol_ty(db, scope, name)
symbol_ty_by_name(db, scope, name)
}
pub fn inherited_class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
pub fn inherited_class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
for base in self.bases(db) {
let member = base.member(db, name);
if !member.is_unbound() {
@@ -616,21 +542,6 @@ impl<'db> UnionType<'db> {
pub fn contains(&self, db: &'db dyn Db, ty: Type<'db>) -> bool {
self.elements(db).contains(&ty)
}
/// Apply a transformation function to all elements of the union,
/// and create a new union from the resulting set of types
pub fn map(
&self,
db: &'db dyn Db,
mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>,
) -> Type<'db> {
self.elements(db)
.into_iter()
.fold(UnionBuilder::new(db), |builder, element| {
builder.add(transform_fn(element))
})
.build()
}
}
#[salsa::interned]
@@ -777,114 +688,4 @@ mod tests {
&["Object of type 'Literal[123]' is not callable"],
);
}
#[test]
fn invalid_iterable() {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
nonsense = 123
for x in nonsense:
pass
",
)
.unwrap();
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
let a_file_diagnostics = super::check_types(&db, a_file);
assert_diagnostic_messages(
&a_file_diagnostics,
&["Object of type 'Literal[123]' is not iterable"],
);
}
#[test]
fn new_iteration_protocol_takes_precedence_over_old_style() {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
class NotIterable:
def __getitem__(self, key: int) -> int:
return 42
__iter__ = None
for x in NotIterable():
pass
",
)
.unwrap();
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
let a_file_diagnostics = super::check_types(&db, a_file);
assert_diagnostic_messages(
&a_file_diagnostics,
&["Object of type 'NotIterable' is not iterable"],
);
}
#[test]
fn starred_expressions_must_be_iterable() {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
class NotIterable: pass
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
x = [*NotIterable()]
y = [*Iterable()]
",
)
.unwrap();
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
let a_file_diagnostics = super::check_types(&db, a_file);
assert_diagnostic_messages(
&a_file_diagnostics,
&["Object of type 'NotIterable' is not iterable"],
);
}
#[test]
fn yield_from_expression_must_be_iterable() {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
class NotIterable: pass
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
def generator_function():
yield from Iterable()
yield from NotIterable()
",
)
.unwrap();
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
let a_file_diagnostics = super::check_types(&db, a_file);
assert_diagnostic_messages(
&a_file_diagnostics,
&["Object of type 'NotIterable' is not iterable"],
);
}
}

View File

@@ -25,10 +25,13 @@
//! * No type in an intersection can be a supertype of any other type in the intersection (just
//! eliminate the supertype from the intersection).
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
use crate::types::{builtins_symbol_ty, IntersectionType, Type, UnionType};
use crate::types::{IntersectionType, Type, UnionType};
use crate::{Db, FxOrderSet};
use ordermap::set::MutableValues;
use super::builtins_symbol_ty_by_name;
pub(crate) struct UnionBuilder<'db> {
elements: FxOrderSet<Type<'db>>,
db: &'db dyn Db,
@@ -65,7 +68,7 @@ impl<'db> UnionBuilder<'db> {
if let Some(true_index) = self.elements.get_index_of(&Type::BooleanLiteral(true)) {
if self.elements.contains(&Type::BooleanLiteral(false)) {
*self.elements.get_index_mut2(true_index).unwrap() =
builtins_symbol_ty(self.db, "bool");
builtins_symbol_ty_by_name(self.db, "bool");
self.elements.remove(&Type::BooleanLiteral(false));
}
}
@@ -275,7 +278,7 @@ mod tests {
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::types::builtins_symbol_ty;
use crate::types::builtins_symbol_ty_by_name;
use crate::ProgramSettings;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@@ -348,7 +351,7 @@ mod tests {
#[test]
fn build_union_bool() {
let db = setup_db();
let bool_ty = builtins_symbol_ty(&db, "bool");
let bool_ty = builtins_symbol_ty_by_name(&db, "bool");
let t0 = Type::BooleanLiteral(true);
let t1 = Type::BooleanLiteral(true);

View File

@@ -236,7 +236,9 @@ mod tests {
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use crate::db::tests::TestDb;
use crate::types::{global_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionBuilder};
use crate::types::{
global_symbol_ty_by_name, BytesLiteralType, StringLiteralType, Type, UnionBuilder,
};
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
fn setup_db() -> TestDb {
@@ -281,16 +283,16 @@ mod tests {
let vec: Vec<Type<'_>> = vec![
Type::Unknown,
Type::IntLiteral(-1),
global_symbol_ty(&db, mod_file, "A"),
global_symbol_ty_by_name(&db, mod_file, "A"),
Type::StringLiteral(StringLiteralType::new(&db, Box::from("A"))),
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([0]))),
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([7]))),
Type::IntLiteral(0),
Type::IntLiteral(1),
Type::StringLiteral(StringLiteralType::new(&db, Box::from("B"))),
global_symbol_ty(&db, mod_file, "foo"),
global_symbol_ty(&db, mod_file, "bar"),
global_symbol_ty(&db, mod_file, "B"),
global_symbol_ty_by_name(&db, mod_file, "foo"),
global_symbol_ty_by_name(&db, mod_file, "bar"),
global_symbol_ty_by_name(&db, mod_file, "B"),
Type::BooleanLiteral(true),
Type::None,
];

View File

@@ -37,6 +37,7 @@ use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, UnaryOp};
use ruff_text_size::Ranged;
use crate::builtins::builtins_scope;
use crate::module_name::ModuleName;
use crate::module_resolver::{file_to_module, resolve_module};
use crate::semantic_index::ast_ids::{HasScopedAstId, HasScopedUseId, ScopedExpressionId};
@@ -45,11 +46,11 @@ use crate::semantic_index::expression::Expression;
use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
use crate::semantic_index::SemanticIndex;
use crate::stdlib::builtins_module_scope;
use crate::types::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
use crate::types::{
builtins_symbol_ty, definitions_ty, global_symbol_ty, symbol_ty, BytesLiteralType, ClassType,
FunctionType, StringLiteralType, Type, UnionBuilder,
builtins_symbol_ty_by_name, definitions_ty, global_symbol_ty_by_name, symbol_ty,
symbol_ty_by_name, BytesLiteralType, ClassType, FunctionType, StringLiteralType, Type,
UnionBuilder,
};
use crate::Db;
@@ -243,7 +244,7 @@ impl<'db> TypeInference<'db> {
/// Similarly, when we encounter a standalone-inferable expression (right-hand side of an
/// assignment, type narrowing guard), we use the [`infer_expression_types()`] query to ensure we
/// don't infer its types more than once.
pub(super) struct TypeInferenceBuilder<'db> {
struct TypeInferenceBuilder<'db> {
db: &'db dyn Db,
index: &'db SemanticIndex<'db>,
region: InferenceRegion<'db>,
@@ -394,7 +395,6 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_for_statement_definition(
for_statement_definition.target(),
for_statement_definition.iterable(),
for_statement_definition.is_async(),
definition,
);
}
@@ -1030,23 +1030,10 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_body(orelse);
}
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
pub(super) fn not_iterable_diagnostic(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) {
self.add_diagnostic(
node,
"not-iterable",
format_args!(
"Object of type '{}' is not iterable",
not_iterable_ty.display(self.db)
),
);
}
fn infer_for_statement_definition(
&mut self,
target: &ast::ExprName,
iterable: &ast::Expr,
is_async: bool,
definition: Definition<'db>,
) {
let expression = self.index.expression(iterable);
@@ -1056,14 +1043,18 @@ impl<'db> TypeInferenceBuilder<'db> {
.types
.expression_ty(iterable.scoped_ast_id(self.db, self.scope));
let loop_var_value_ty = if is_async {
// TODO(Alex): async iterables/iterators!
Type::Unknown
} else {
iterable_ty
.iterate(self.db)
.unwrap_with_diagnostic(iterable.into(), self)
};
// TODO(Alex): only a valid iterable if the *type* of `iterable_ty` has an `__iter__`
// member (dunders are never looked up on an instance)
let _dunder_iter_ty = iterable_ty.member(self.db, &ast::name::Name::from("__iter__"));
// TODO(Alex):
// - infer the return type of the `__iter__` method, which gives us the iterator
// - lookup the `__next__` method on the iterator
// - infer the return type of the iterator's `__next__` method,
// which gives us the type of the variable being bound here
// (...or the type of the object being unpacked into multiple definitions, if it's something like
// `for k, v in d.items(): ...`)
let loop_var_value_ty = Type::Unknown;
self.types
.expressions
@@ -1409,9 +1400,11 @@ impl<'db> TypeInferenceBuilder<'db> {
ast::Number::Int(n) => n
.as_i64()
.map(Type::IntLiteral)
.unwrap_or_else(|| builtins_symbol_ty(self.db, "int").to_instance()),
ast::Number::Float(_) => builtins_symbol_ty(self.db, "float").to_instance(),
ast::Number::Complex { .. } => builtins_symbol_ty(self.db, "complex").to_instance(),
.unwrap_or_else(|| builtins_symbol_ty_by_name(self.db, "int").to_instance()),
ast::Number::Float(_) => builtins_symbol_ty_by_name(self.db, "float").to_instance(),
ast::Number::Complex { .. } => {
builtins_symbol_ty_by_name(self.db, "complex").to_instance()
}
}
}
@@ -1489,11 +1482,12 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
#[allow(clippy::unused_self)]
fn infer_ellipsis_literal_expression(
&mut self,
_literal: &ast::ExprEllipsisLiteral,
) -> Type<'db> {
builtins_symbol_ty(self.db, "Ellipsis")
builtins_symbol_ty_by_name(self.db, "Ellipsis")
}
fn infer_tuple_expression(&mut self, tuple: &ast::ExprTuple) -> Type<'db> {
@@ -1509,7 +1503,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
// TODO generic
builtins_symbol_ty(self.db, "tuple").to_instance()
builtins_symbol_ty_by_name(self.db, "tuple").to_instance()
}
fn infer_list_expression(&mut self, list: &ast::ExprList) -> Type<'db> {
@@ -1524,7 +1518,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
// TODO generic
builtins_symbol_ty(self.db, "list").to_instance()
builtins_symbol_ty_by_name(self.db, "list").to_instance()
}
fn infer_set_expression(&mut self, set: &ast::ExprSet) -> Type<'db> {
@@ -1535,7 +1529,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
// TODO generic
builtins_symbol_ty(self.db, "set").to_instance()
builtins_symbol_ty_by_name(self.db, "set").to_instance()
}
fn infer_dict_expression(&mut self, dict: &ast::ExprDict) -> Type<'db> {
@@ -1547,7 +1541,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
// TODO generic
builtins_symbol_ty(self.db, "dict").to_instance()
builtins_symbol_ty_by_name(self.db, "dict").to_instance()
}
/// Infer the type of the `iter` expression of the first comprehension.
@@ -1823,10 +1817,7 @@ impl<'db> TypeInferenceBuilder<'db> {
ctx: _,
} = starred;
let iterable_ty = self.infer_expression(value);
iterable_ty
.iterate(self.db)
.unwrap_with_diagnostic(value.as_ref().into(), self);
self.infer_expression(value);
// TODO
Type::Unknown
@@ -1844,12 +1835,9 @@ impl<'db> TypeInferenceBuilder<'db> {
fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> {
let ast::ExprYieldFrom { range: _, value } = yield_from;
let iterable_ty = self.infer_expression(value);
iterable_ty
.iterate(self.db)
.unwrap_with_diagnostic(value.as_ref().into(), self);
self.infer_expression(value);
// TODO get type from `ReturnType` of generator
// TODO get type from awaitable
Type::Unknown
}
@@ -1865,17 +1853,15 @@ impl<'db> TypeInferenceBuilder<'db> {
/// Look up a name reference that isn't bound in the local scope.
fn lookup_name(&self, name: &ast::name::Name) -> Type<'db> {
let file_scope_id = self.scope.file_scope_id(self.db);
let is_defined = self
.index
.symbol_table(file_scope_id)
let symbols = self.index.symbol_table(file_scope_id);
let symbol = symbols
.symbol_by_name(name)
.expect("Symbol table should create a symbol for every Name node")
.is_defined();
.expect("Expected the symbol table to create a symbol for every Name node");
// In function-like scopes, any local variable (symbol that is defined in this
// scope) can only have a definition in this scope, or be undefined; it never references
// another scope. (At runtime, it would use the `LOAD_FAST` opcode.)
if !is_defined || !self.scope.is_function_like(self.db) {
if !symbol.is_defined() || !self.scope.is_function_like(self.db) {
// Walk up parent scopes looking for a possible enclosing scope that may have a
// definition of this name visible to us (would be `LOAD_DEREF` at runtime.)
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id) {
@@ -1898,7 +1884,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// runtime, it is the scope that creates the cell for our closure.) If the name
// isn't bound in that scope, we should get an unbound name, not continue
// falling back to other scopes / globals / builtins.
return symbol_ty(self.db, enclosing_scope_id, name);
return symbol_ty_by_name(self.db, enclosing_scope_id, name);
}
}
// No nonlocal binding, check module globals. Avoid infinite recursion if `self.scope`
@@ -1906,11 +1892,11 @@ impl<'db> TypeInferenceBuilder<'db> {
let ty = if file_scope_id.is_global() {
Type::Unbound
} else {
global_symbol_ty(self.db, self.file, name)
global_symbol_ty_by_name(self.db, self.file, name)
};
// Fallback to builtins (without infinite recursion if we're already in builtins.)
if ty.may_be_unbound(self.db) && Some(self.scope) != builtins_module_scope(self.db) {
ty.replace_unbound_with(self.db, builtins_symbol_ty(self.db, name))
if ty.may_be_unbound(self.db) && Some(self.scope) != builtins_scope(self.db) {
ty.replace_unbound_with(self.db, builtins_symbol_ty_by_name(self.db, name))
} else {
ty
}
@@ -1923,27 +1909,20 @@ impl<'db> TypeInferenceBuilder<'db> {
let ast::ExprName { range: _, id, ctx } = name;
let file_scope_id = self.scope.file_scope_id(self.db);
// if we're inferring types of deferred expressions, always treat them as public symbols
if self.is_deferred() {
let symbols = self.index.symbol_table(file_scope_id);
let symbol = symbols
.symbol_id_by_name(id)
.expect("Expected the symbol table to create a symbol for every Name node");
return symbol_ty(self.db, self.scope, symbol);
}
match ctx {
ExprContext::Load => {
let use_def = self.index.use_def_map(file_scope_id);
let symbol = self
.index
.symbol_table(file_scope_id)
.symbol_id_by_name(id)
.expect("Expected the symbol table to create a symbol for every Name node");
// if we're inferring types of deferred expressions, always treat them as public symbols
let (definitions, may_be_unbound) = if self.is_deferred() {
(
use_def.public_definitions(symbol),
use_def.public_may_be_unbound(symbol),
)
} else {
let use_id = name.scoped_use_id(self.db, self.scope);
(
use_def.use_definitions(use_id),
use_def.use_may_be_unbound(use_id),
)
};
let use_id = name.scoped_use_id(self.db, self.scope);
let may_be_unbound = use_def.use_may_be_unbound(use_id);
let unbound_ty = if may_be_unbound {
Some(self.lookup_name(id))
@@ -1951,7 +1930,7 @@ impl<'db> TypeInferenceBuilder<'db> {
None
};
definitions_ty(self.db, definitions, unbound_ty)
definitions_ty(self.db, use_def.use_definitions(use_id), unbound_ty)
}
ExprContext::Store | ExprContext::Del => Type::None,
ExprContext::Invalid => Type::Unknown,
@@ -2007,22 +1986,22 @@ impl<'db> TypeInferenceBuilder<'db> {
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => n
.checked_add(m)
.map(Type::IntLiteral)
.unwrap_or_else(|| builtins_symbol_ty(self.db, "int").to_instance()),
.unwrap_or_else(|| builtins_symbol_ty_by_name(self.db, "int").to_instance()),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Sub) => n
.checked_sub(m)
.map(Type::IntLiteral)
.unwrap_or_else(|| builtins_symbol_ty(self.db, "int").to_instance()),
.unwrap_or_else(|| builtins_symbol_ty_by_name(self.db, "int").to_instance()),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mult) => n
.checked_mul(m)
.map(Type::IntLiteral)
.unwrap_or_else(|| builtins_symbol_ty(self.db, "int").to_instance()),
.unwrap_or_else(|| builtins_symbol_ty_by_name(self.db, "int").to_instance()),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Div) => n
.checked_div(m)
.map(Type::IntLiteral)
.unwrap_or_else(|| builtins_symbol_ty(self.db, "int").to_instance()),
.unwrap_or_else(|| builtins_symbol_ty_by_name(self.db, "int").to_instance()),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mod) => n
.checked_rem(m)
@@ -2401,14 +2380,14 @@ mod tests {
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::name::Name;
use crate::builtins::builtins_scope;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::FileScopeId;
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
use crate::stdlib::builtins_module_scope;
use crate::types::{global_symbol_ty, infer_definition_types, symbol_ty};
use crate::types::{global_symbol_ty_by_name, infer_definition_types, symbol_ty_by_name};
use crate::{HasTy, ProgramSettings, SemanticModel};
use super::TypeInferenceBuilder;
@@ -2461,7 +2440,7 @@ mod tests {
fn assert_public_ty(db: &TestDb, file_name: &str, symbol_name: &str, expected: &str) {
let file = system_path_to_file(db, file_name).expect("Expected file to exist.");
let ty = global_symbol_ty(db, file, symbol_name);
let ty = global_symbol_ty_by_name(db, file, symbol_name);
assert_eq!(ty.display(db).to_string(), expected);
}
@@ -2486,7 +2465,7 @@ mod tests {
assert_eq!(scope.name(db), *expected_scope_name);
}
let ty = symbol_ty(db, scope, symbol_name);
let ty = symbol_ty_by_name(db, scope, symbol_name);
assert_eq!(ty.display(db).to_string(), expected);
}
@@ -2690,7 +2669,7 @@ mod tests {
)?;
let mod_file = system_path_to_file(&db, "src/mod.py").expect("Expected file to exist.");
let ty = global_symbol_ty(&db, mod_file, "Sub");
let ty = global_symbol_ty_by_name(&db, mod_file, "Sub");
let class = ty.expect_class();
@@ -2717,7 +2696,7 @@ mod tests {
)?;
let mod_file = system_path_to_file(&db, "src/mod.py").unwrap();
let ty = global_symbol_ty(&db, mod_file, "C");
let ty = global_symbol_ty_by_name(&db, mod_file, "C");
let class_id = ty.expect_class();
let member_ty = class_id.class_member(&db, &Name::new_static("f"));
let func = member_ty.expect_function();
@@ -2921,7 +2900,7 @@ mod tests {
db.write_file("src/a.py", "def example() -> int: return 42")?;
let mod_file = system_path_to_file(&db, "src/a.py").unwrap();
let function = global_symbol_ty(&db, mod_file, "example").expect_function();
let function = global_symbol_ty_by_name(&db, mod_file, "example").expect_function();
let returns = function.return_type(&db);
assert_eq!(returns.display(&db).to_string(), "int");
@@ -2996,108 +2975,6 @@ mod tests {
Ok(())
}
#[test]
fn basic_for_loop() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
for x in IntIterable():
pass
",
)?;
assert_public_ty(&db, "src/a.py", "x", "int");
Ok(())
}
#[test]
fn for_loop_with_old_style_iteration_protocol() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
class OldStyleIterable:
def __getitem__(self, key: int) -> int:
return 42
for x in OldStyleIterable():
pass
",
)?;
assert_public_ty(&db, "src/a.py", "x", "int");
Ok(())
}
/// This tests that we understand that `async` for loops
/// do not work according to the synchronous iteration protocol
#[test]
fn invalid_async_for_loop() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
async def foo():
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
async for x in Iterator():
pass
",
)?;
// TODO(Alex) async iterables/iterators!
assert_scope_ty(&db, "src/a.py", &["foo"], "x", "Unknown");
Ok(())
}
#[test]
fn basic_async_for_loop() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
async def foo():
class IntAsyncIterator:
async def __anext__(self) -> int:
return 42
class IntAsyncIterable:
def __aiter__(self) -> IntAsyncIterator:
return IntAsyncIterator()
async for x in IntAsyncIterable():
pass
",
)?;
// TODO(Alex) async iterables/iterators!
assert_scope_ty(&db, "src/a.py", &["foo"], "x", "Unknown");
Ok(())
}
#[test]
fn class_constructor_call_expression() -> anyhow::Result<()> {
let mut db = setup_db();
@@ -3440,7 +3317,7 @@ mod tests {
)?;
let a = system_path_to_file(&db, "src/a.py").expect("Expected file to exist.");
let c_ty = global_symbol_ty(&db, a, "C");
let c_ty = global_symbol_ty_by_name(&db, a, "C");
let c_class = c_ty.expect_class();
let mut c_bases = c_class.bases(&db);
let b_ty = c_bases.next().unwrap();
@@ -3477,8 +3354,8 @@ mod tests {
.unwrap()
.0
.to_scope_id(&db, file);
let y_ty = symbol_ty(&db, function_scope, "y");
let x_ty = symbol_ty(&db, function_scope, "x");
let y_ty = symbol_ty_by_name(&db, function_scope, "y");
let x_ty = symbol_ty_by_name(&db, function_scope, "x");
assert_eq!(y_ty.display(&db).to_string(), "Unbound");
assert_eq!(x_ty.display(&db).to_string(), "Literal[2]");
@@ -3508,8 +3385,8 @@ mod tests {
.unwrap()
.0
.to_scope_id(&db, file);
let y_ty = symbol_ty(&db, function_scope, "y");
let x_ty = symbol_ty(&db, function_scope, "x");
let y_ty = symbol_ty_by_name(&db, function_scope, "y");
let x_ty = symbol_ty_by_name(&db, function_scope, "x");
assert_eq!(x_ty.display(&db).to_string(), "Unbound");
assert_eq!(y_ty.display(&db).to_string(), "Literal[1]");
@@ -3539,7 +3416,7 @@ mod tests {
.unwrap()
.0
.to_scope_id(&db, file);
let y_ty = symbol_ty(&db, function_scope, "y");
let y_ty = symbol_ty_by_name(&db, function_scope, "y");
assert_eq!(
y_ty.display(&db).to_string(),
@@ -3573,8 +3450,8 @@ mod tests {
.unwrap()
.0
.to_scope_id(&db, file);
let y_ty = symbol_ty(&db, class_scope, "y");
let x_ty = symbol_ty(&db, class_scope, "x");
let y_ty = symbol_ty_by_name(&db, class_scope, "y");
let x_ty = symbol_ty_by_name(&db, class_scope, "x");
assert_eq!(x_ty.display(&db).to_string(), "Unbound | Literal[2]");
assert_eq!(y_ty.display(&db).to_string(), "Literal[1]");
@@ -3667,11 +3544,9 @@ mod tests {
assert_public_ty(&db, "/src/a.py", "x", "Literal[copyright]");
// imported builtins module is the same file as the implicit builtins
let file = system_path_to_file(&db, "/src/a.py").expect("Expected file to exist.");
let builtins_ty = global_symbol_ty(&db, file, "builtins");
let builtins_ty = global_symbol_ty_by_name(&db, file, "builtins");
let builtins_file = builtins_ty.expect_module();
let implicit_builtins_file = builtins_module_scope(&db)
.expect("builtins module should exist")
.file(&db);
let implicit_builtins_file = builtins_scope(&db).expect("builtins to exist").file(&db);
assert_eq!(builtins_file, implicit_builtins_file);
Ok(())
@@ -3695,24 +3570,6 @@ mod tests {
Ok(())
}
#[test]
fn deferred_annotation_builtin() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_file("/src/a.pyi", "class C(object): pass")?;
let file = system_path_to_file(&db, "/src/a.pyi").unwrap();
let ty = global_symbol_ty(&db, file, "C");
let base = ty
.expect_class()
.bases(&db)
.next()
.expect("there should be at least one base");
assert_eq!(base.display(&db).to_string(), "Literal[object]");
Ok(())
}
#[test]
fn narrow_not_none() -> anyhow::Result<()> {
let mut db = setup_db();
@@ -3993,7 +3850,7 @@ mod tests {
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol_ty(&db, a, "x");
let x_ty = global_symbol_ty_by_name(&db, a, "x");
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
@@ -4002,7 +3859,7 @@ mod tests {
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty_2 = global_symbol_ty(&db, a, "x");
let x_ty_2 = global_symbol_ty_by_name(&db, a, "x");
assert_eq!(x_ty_2.display(&db).to_string(), "Literal[20]");
@@ -4019,7 +3876,7 @@ mod tests {
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol_ty(&db, a, "x");
let x_ty = global_symbol_ty_by_name(&db, a, "x");
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
@@ -4029,7 +3886,7 @@ mod tests {
db.clear_salsa_events();
let x_ty_2 = global_symbol_ty(&db, a, "x");
let x_ty_2 = global_symbol_ty_by_name(&db, a, "x");
assert_eq!(x_ty_2.display(&db).to_string(), "Literal[10]");
@@ -4055,7 +3912,7 @@ mod tests {
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol_ty(&db, a, "x");
let x_ty = global_symbol_ty_by_name(&db, a, "x");
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
@@ -4065,7 +3922,7 @@ mod tests {
db.clear_salsa_events();
let x_ty_2 = global_symbol_ty(&db, a, "x");
let x_ty_2 = global_symbol_ty_by_name(&db, a, "x");
assert_eq!(x_ty_2.display(&db).to_string(), "Literal[10]");

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.6.4"
version = "0.6.3"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.6.4"
version = "0.6.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -94,9 +94,6 @@ class Apples:
def __mro_entries__(self, bases):
pass
# Removed with Python 3
def __unicode__(self):
pass
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
...

View File

@@ -1,11 +0,0 @@
# Valid
x = 1 if True else 2
# Invalid
x = 1 if True else 1
# Invalid
x = "a" if True else "a"
# Invalid
x = 0.1 if False else 0.1

View File

@@ -1404,9 +1404,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::IfExpInsteadOfOrOperator) {
refurb::rules::if_exp_instead_of_or_operator(checker, if_exp);
}
if checker.enabled(Rule::UselessIfElse) {
ruff::rules::useless_if_else(checker, if_exp);
}
}
Expr::ListComp(
comp @ ast::ExprListComp {

View File

@@ -961,7 +961,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript),
(Ruff, "032") => (RuleGroup::Preview, rules::ruff::rules::DecimalFromFloatLiteral),
(Ruff, "033") => (RuleGroup::Preview, rules::ruff::rules::PostInitDefault),
(Ruff, "034") => (RuleGroup::Preview, rules::ruff::rules::UselessIfElse),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),

View File

@@ -8,10 +8,10 @@ use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::is_known_dunder_method;
/// ## What it does
/// Checks for dunder methods that have no special meaning in Python 3.
/// Checks for misspelled and unknown dunder names in method definitions.
///
/// ## Why is this bad?
/// Misspelled or no longer supported dunder name methods may cause your code to not function
/// Misspelled dunder name methods may cause your code to not function
/// as expected.
///
/// Since dunder methods are associated with customizing the behavior
@@ -51,7 +51,7 @@ impl Violation for BadDunderMethodName {
#[derive_message_formats]
fn message(&self) -> String {
let BadDunderMethodName { name } = self;
format!("Dunder method `{name}` has no special meaning in Python 3")
format!("Bad or misspelled dunder method name `{name}`")
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
bad_dunder_method_name.py:5:9: PLW3201 Dunder method `_init_` has no special meaning in Python 3
bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `_init_`
|
4 | class Apples:
5 | def _init_(self): # [bad-dunder-name]
@@ -9,7 +9,7 @@ bad_dunder_method_name.py:5:9: PLW3201 Dunder method `_init_` has no special mea
6 | pass
|
bad_dunder_method_name.py:8:9: PLW3201 Dunder method `__hello__` has no special meaning in Python 3
bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__hello__`
|
6 | pass
7 |
@@ -18,7 +18,7 @@ bad_dunder_method_name.py:8:9: PLW3201 Dunder method `__hello__` has no special
9 | print("hello")
|
bad_dunder_method_name.py:11:9: PLW3201 Dunder method `__init_` has no special meaning in Python 3
bad_dunder_method_name.py:11:9: PLW3201 Bad or misspelled dunder method name `__init_`
|
9 | print("hello")
10 |
@@ -28,7 +28,7 @@ bad_dunder_method_name.py:11:9: PLW3201 Dunder method `__init_` has no special m
13 | pass
|
bad_dunder_method_name.py:15:9: PLW3201 Dunder method `_init_` has no special meaning in Python 3
bad_dunder_method_name.py:15:9: PLW3201 Bad or misspelled dunder method name `_init_`
|
13 | pass
14 |
@@ -38,7 +38,7 @@ bad_dunder_method_name.py:15:9: PLW3201 Dunder method `_init_` has no special me
17 | pass
|
bad_dunder_method_name.py:19:9: PLW3201 Dunder method `___neg__` has no special meaning in Python 3
bad_dunder_method_name.py:19:9: PLW3201 Bad or misspelled dunder method name `___neg__`
|
17 | pass
18 |
@@ -48,7 +48,7 @@ bad_dunder_method_name.py:19:9: PLW3201 Dunder method `___neg__` has no special
21 | pass
|
bad_dunder_method_name.py:23:9: PLW3201 Dunder method `__inv__` has no special meaning in Python 3
bad_dunder_method_name.py:23:9: PLW3201 Bad or misspelled dunder method name `__inv__`
|
21 | pass
22 |
@@ -58,10 +58,4 @@ bad_dunder_method_name.py:23:9: PLW3201 Dunder method `__inv__` has no special m
25 | pass
|
bad_dunder_method_name.py:98:9: PLW3201 Dunder method `__unicode__` has no special meaning in Python 3
|
97 | # Removed with Python 3
98 | def __unicode__(self):
| ^^^^^^^^^^^ PLW3201
99 | pass
|

View File

@@ -58,7 +58,6 @@ mod tests {
#[test_case(Rule::AssertWithPrintMessage, Path::new("RUF030.py"))]
#[test_case(Rule::IncorrectlyParenthesizedTupleInSubscript, Path::new("RUF031.py"))]
#[test_case(Rule::DecimalFromFloatLiteral, Path::new("RUF032.py"))]
#[test_case(Rule::UselessIfElse, Path::new("RUF034.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
#[test_case(Rule::PostInitDefault, Path::new("RUF033.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -29,7 +29,6 @@ pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
pub(crate) use unused_async::*;
pub(crate) use unused_noqa::*;
pub(crate) use useless_if_else::*;
pub(crate) use zip_instead_of_pairwise::*;
mod ambiguous_unicode_character;
@@ -67,7 +66,6 @@ mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;
mod unused_async;
mod unused_noqa;
mod useless_if_else;
mod zip_instead_of_pairwise;
#[derive(Clone, Copy)]

View File

@@ -1,55 +0,0 @@
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
/// ## What it does
/// Checks for useless if-else conditions with identical arms.
///
/// ## Why is this bad?
/// Useless if-else conditions add unnecessary complexity to the code without
/// providing any logical benefit.
///
/// Assigning the value directly is clearer and more explicit, and
/// should be preferred.
///
/// ## Example
/// ```python
/// # Bad
/// foo = x if y else x
/// ```
///
/// Use instead:
/// ```python
/// # Good
/// foo = x
/// ```
#[violation]
pub struct UselessIfElse;
impl Violation for UselessIfElse {
#[derive_message_formats]
fn message(&self) -> String {
format!("Useless if-else condition")
}
}
/// RUF031
pub(crate) fn useless_if_else(checker: &mut Checker, if_expr: &ast::ExprIf) {
let ast::ExprIf {
body,
orelse,
range,
..
} = if_expr;
// Skip if the body and orelse are not the same
if ComparableExpr::from(body) != ComparableExpr::from(orelse) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(UselessIfElse, *range));
}

View File

@@ -1,27 +0,0 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF034.py:5:5: RUF034 Useless if-else condition
|
4 | # Invalid
5 | x = 1 if True else 1
| ^^^^^^^^^^^^^^^^ RUF034
6 |
7 | # Invalid
|
RUF034.py:8:5: RUF034 Useless if-else condition
|
7 | # Invalid
8 | x = "a" if True else "a"
| ^^^^^^^^^^^^^^^^^^^^ RUF034
9 |
10 | # Invalid
|
RUF034.py:11:5: RUF034 Useless if-else condition
|
10 | # Invalid
11 | x = 0.1 if False else 0.1
| ^^^^^^^^^^^^^^^^^^^^^ RUF034
|

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.6.4"
version = "0.6.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -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.4
rev: v0.6.3
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.4
rev: v0.6.3
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.4
rev: v0.6.3
hooks:
# Run the linter.
- id: ruff

View File

@@ -109,3 +109,16 @@
user-select: none;
}
/* Omits the nav title "Ruff" entirely unless on a small screen, in which case
the nav title is needed for backwards navigation in the collapsible
nav variant.
See https://github.com/astral-sh/uv/issues/5130 */
.md-nav__title {
display: none;
}
@media screen and (max-width: 1219px) {
.md-nav__title {
display: flex ;
}
}

View File

@@ -4,16 +4,16 @@ theme:
logo: assets/bolt.svg
favicon: assets/favicon.ico
features:
- navigation.instant
- navigation.instant.prefetch
- navigation.tracking
- content.code.annotate
- toc.integrate
- toc.follow
- navigation.path
- navigation.top
- content.code.copy
- content.tabs.link
- navigation.footer
- navigation.instant
- navigation.instant.prefetch
- navigation.path
- navigation.top
- navigation.tracking
- toc.follow
palette:
# Note: Using the system theme works with the insiders version
# https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#automatic-light-dark-mode
@@ -71,6 +71,15 @@ not_in_nav: |
extra:
analytics:
provider: fathom
social:
- icon: fontawesome/brands/github
link: https://github.com/astral-sh/ruff
- icon: fontawesome/brands/discord
link: https://discord.com/invite/astral-sh
- icon: fontawesome/brands/python
link: https://pypi.org/project/ruff/
- icon: fontawesome/brands/x-twitter
link: https://x.com/astral_sh
validation:
omitted_files: warn
absolute_links: warn

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.6.4"
version = "0.6.3"
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
View File

@@ -3740,7 +3740,6 @@
"RUF031",
"RUF032",
"RUF033",
"RUF034",
"RUF1",
"RUF10",
"RUF100",

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "scripts"
version = "0.6.4"
version = "0.6.3"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]

View File

@@ -28,8 +28,16 @@ class Section(NamedTuple):
SECTIONS: list[Section] = [
Section("Overview", "index.md", generated=True),
Section("Tutorial", "tutorial.md", generated=False),
Section("Installing Ruff", "installation.md", generated=False),
Section(
"Getting Started",
"",
generated=False,
subsections=[
Section("Tutorial", "tutorial.md", generated=False),
Section("Installation", "installation.md", generated=False),
Section("Configuration", "configuration.md", generated=False),
],
),
Section("The Ruff Linter", "linter.md", generated=False),
Section("The Ruff Formatter", "formatter.md", generated=False),
Section(
@@ -44,11 +52,17 @@ SECTIONS: list[Section] = [
Section("Migrating from ruff-lsp", "editors/migration.md", generated=False),
],
),
Section("Configuring Ruff", "configuration.md", generated=False),
Section("Preview", "preview.md", generated=False),
Section("Rules", "rules.md", generated=True),
Section("Settings", "settings.md", generated=True),
Section("Versioning", "versioning.md", generated=False),
Section(
"Reference",
"",
generated=False,
subsections=[
Section("Preview", "preview.md", generated=False),
Section("Rules", "rules.md", generated=True),
Section("Settings", "settings.md", generated=True),
Section("Versioning", "versioning.md", generated=False),
],
),
Section("Integrations", "integrations.md", generated=False),
Section("FAQ", "faq.md", generated=False),
Section("Contributing", "contributing.md", generated=True),
@@ -125,8 +139,14 @@ def main() -> None:
Path("docs").mkdir(parents=True, exist_ok=True)
section_queue = SECTIONS.copy()
# Split the README.md into sections.
for title, filename, generated, _ in SECTIONS:
while section_queue:
title, filename, generated, sub_sections = section_queue.pop(0)
if sub_sections is not None:
section_queue.extend(sub_sections)
if not generated:
continue

View File

@@ -204,11 +204,6 @@ class Venv:
"--python",
self.python,
"--quiet",
# We pass `--exclude-newer` to ensure that type-checking of one of
# our projects isn't unexpectedly broken by a change in the
# annotations of one of that project's dependencies
"--exclude-newer",
"2024-09-03T00:00:00Z",
*dependencies,
]

View File

@@ -15,11 +15,7 @@ class Project(typing.NamedTuple):
revision: str
dependencies: list[str]
"""List of type checking dependencies.
Dependencies are pinned using a `--exclude-newer` flag when installing them
into the virtual environment; see the `Venv.install()` method for details.
"""
"""List of type checking dependencies"""
include: list[str] = []
"""The directories and files to check. If empty, checks the current directory"""
@@ -100,7 +96,7 @@ ALL = [
Project(
name="black",
repository="https://github.com/psf/black",
revision="ac28187bf4a4ac159651c73d3a50fe6d0f653eac",
revision="c20423249e9d8dfb8581eebbfc67a13984ee45e9",
include=["src"],
dependencies=[
"aiohttp",