Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Waygood
1a30934c33 [ty] Cleanup various APIs 2025-10-30 16:52:13 -04:00
17 changed files with 64 additions and 261 deletions

View File

@@ -1,60 +1,5 @@
# Changelog
## 0.14.3
Released on 2025-10-30.
### Preview features
- Respect `--output-format` with `--watch` ([#21097](https://github.com/astral-sh/ruff/pull/21097))
- \[`pydoclint`\] Fix false positive on explicit exception re-raising (`DOC501`, `DOC502`) ([#21011](https://github.com/astral-sh/ruff/pull/21011))
- \[`pyflakes`\] Revert to stable behavior if imports for module lie in alternate branches for `F401` ([#20878](https://github.com/astral-sh/ruff/pull/20878))
- \[`pylint`\] Implement `stop-iteration-return` (`PLR1708`) ([#20733](https://github.com/astral-sh/ruff/pull/20733))
- \[`ruff`\] Add support for additional eager conversion patterns (`RUF065`) ([#20657](https://github.com/astral-sh/ruff/pull/20657))
### Bug fixes
- Fix finding keyword range for clause header after statement ending with semicolon ([#21067](https://github.com/astral-sh/ruff/pull/21067))
- Fix syntax error false positive on nested alternative patterns ([#21104](https://github.com/astral-sh/ruff/pull/21104))
- \[`ISC001`\] Fix panic when string literals are unclosed ([#21034](https://github.com/astral-sh/ruff/pull/21034))
- \[`flake8-django`\] Apply `DJ001` to annotated fields ([#20907](https://github.com/astral-sh/ruff/pull/20907))
- \[`flake8-pyi`\] Fix `PYI034` to not trigger on metaclasses (`PYI034`) ([#20881](https://github.com/astral-sh/ruff/pull/20881))
- \[`flake8-type-checking`\] Fix `TC003` false positive with `future-annotations` ([#21125](https://github.com/astral-sh/ruff/pull/21125))
- \[`pyflakes`\] Fix false positive for `__class__` in lambda expressions within class definitions (`F821`) ([#20564](https://github.com/astral-sh/ruff/pull/20564))
- \[`pyupgrade`\] Fix false positive for `TypeVar` with default on Python \<3.13 (`UP046`,`UP047`) ([#21045](https://github.com/astral-sh/ruff/pull/21045))
### Rule changes
- Add missing docstring sections to the numpy list ([#20931](https://github.com/astral-sh/ruff/pull/20931))
- \[`airflow`\] Extend `airflow.models..Param` check (`AIR311`) ([#21043](https://github.com/astral-sh/ruff/pull/21043))
- \[`airflow`\] Warn that `airflow....DAG.create_dagrun` has been removed (`AIR301`) ([#21093](https://github.com/astral-sh/ruff/pull/21093))
- \[`refurb`\] Preserve digit separators in `Decimal` constructor (`FURB157`) ([#20588](https://github.com/astral-sh/ruff/pull/20588))
### Server
- Avoid sending an unnecessary "clear diagnostics" message for clients supporting pull diagnostics ([#21105](https://github.com/astral-sh/ruff/pull/21105))
### Documentation
- \[`flake8-bandit`\] Fix correct example for `S308` ([#21128](https://github.com/astral-sh/ruff/pull/21128))
### Other changes
- Clearer error message when `line-length` goes beyond threshold ([#21072](https://github.com/astral-sh/ruff/pull/21072))
### Contributors
- [@danparizher](https://github.com/danparizher)
- [@jvacek](https://github.com/jvacek)
- [@ntBre](https://github.com/ntBre)
- [@augustelalande](https://github.com/augustelalande)
- [@prakhar1144](https://github.com/prakhar1144)
- [@TaKO8Ki](https://github.com/TaKO8Ki)
- [@dylwil3](https://github.com/dylwil3)
- [@fatelei](https://github.com/fatelei)
- [@ShaharNaveh](https://github.com/ShaharNaveh)
- [@Lee-W](https://github.com/Lee-W)
## 0.14.2
Released on 2025-10-23.

6
Cargo.lock generated
View File

@@ -2835,7 +2835,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.3"
version = "0.14.2"
dependencies = [
"anyhow",
"argfile",
@@ -3092,7 +3092,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.3"
version = "0.14.2"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3447,7 +3447,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.3"
version = "0.14.2"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -147,8 +147,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.14.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.3/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.2/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -181,7 +181,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.14.3
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

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

View File

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

View File

@@ -212,10 +212,7 @@ pub fn completion<'db>(
offset: TextSize,
) -> Vec<Completion<'db>> {
let parsed = parsed_module(db, file).load(db);
let tokens = tokens_start_before(parsed.tokens(), offset);
if is_in_comment(tokens) || is_in_string(tokens) || is_in_definition_place(db, tokens, file) {
if is_in_comment(&parsed, offset) || is_in_string(&parsed, offset) {
return vec![];
}
@@ -832,7 +829,8 @@ fn find_typed_text(
/// Whether the given offset within the parsed module is within
/// a comment or not.
fn is_in_comment(tokens: &[Token]) -> bool {
fn is_in_comment(parsed: &ParsedModuleRef, offset: TextSize) -> bool {
let tokens = tokens_start_before(parsed.tokens(), offset);
tokens.last().is_some_and(|t| t.kind().is_comment())
}
@@ -841,7 +839,8 @@ fn is_in_comment(tokens: &[Token]) -> bool {
///
/// Note that this will return `false` when positioned within an
/// interpolation block in an f-string or a t-string.
fn is_in_string(tokens: &[Token]) -> bool {
fn is_in_string(parsed: &ParsedModuleRef, offset: TextSize) -> bool {
let tokens = tokens_start_before(parsed.tokens(), offset);
tokens.last().is_some_and(|t| {
matches!(
t.kind(),
@@ -850,29 +849,6 @@ fn is_in_string(tokens: &[Token]) -> bool {
})
}
/// If the tokens end with `class f` or `def f` we return true.
/// If the tokens end with `class` or `def`, we return false.
/// This is fine because we don't provide completions anyway.
fn is_in_definition_place(db: &dyn Db, tokens: &[Token], file: File) -> bool {
tokens
.len()
.checked_sub(2)
.and_then(|i| tokens.get(i))
.is_some_and(|t| {
if matches!(
t.kind(),
TokenKind::Def | TokenKind::Class | TokenKind::Type
) {
true
} else if t.kind() == TokenKind::Name {
let source = source_text(db, file);
&source[t.range()] == "type"
} else {
false
}
})
}
/// Order completions according to the following rules:
///
/// 1) Names with no underscore prefix
@@ -4082,83 +4058,6 @@ def f[T](x: T):
test.build().contains("__repr__");
}
#[test]
fn no_completions_in_function_def_name() {
let builder = completion_test_builder(
"\
def f<CURSOR>
",
);
builder.auto_import().build().not_contains("fabs");
}
#[test]
fn no_completions_in_function_def_empty_name() {
let builder = completion_test_builder(
"\
def <CURSOR>
",
);
builder.auto_import().build().not_contains("fabs");
}
#[test]
fn no_completions_in_class_def_name() {
let builder = completion_test_builder(
"\
class f<CURSOR>
",
);
builder.auto_import().build().not_contains("fabs");
}
#[test]
fn no_completions_in_class_def_empty_name() {
let builder = completion_test_builder(
"\
class <CURSOR>
",
);
builder.auto_import().build().not_contains("fabs");
}
#[test]
fn no_completions_in_type_def_name() {
let builder = completion_test_builder(
"\
type f<CURSOR> = int
",
);
builder.auto_import().build().not_contains("fabs");
}
#[test]
fn no_completions_in_maybe_type_def_name() {
let builder = completion_test_builder(
"\
type f<CURSOR>
",
);
builder.auto_import().build().not_contains("fabs");
}
#[test]
fn no_completions_in_type_def_empty_name() {
let builder = completion_test_builder(
"\
type <CURSOR>
",
);
builder.auto_import().build().not_contains("fabs");
}
/// A way to create a simple single-file (named `main.py`) completion test
/// builder.
///

View File

@@ -819,17 +819,6 @@ impl<'db> Type<'db> {
.is_some_and(|instance| instance.has_known_class(db, KnownClass::NoneType))
}
fn is_bool(&self, db: &'db dyn Db) -> bool {
self.as_nominal_instance()
.is_some_and(|instance| instance.has_known_class(db, KnownClass::Bool))
}
fn is_enum(&self, db: &'db dyn Db) -> bool {
self.as_nominal_instance()
.and_then(|instance| crate::types::enums::enum_metadata(db, instance.class_literal(db)))
.is_some()
}
/// Return true if this type overrides __eq__ or __ne__ methods
fn overrides_equality(&self, db: &'db dyn Db) -> bool {
let check_dunder = |dunder_name, allowed_return_value| {
@@ -1142,7 +1131,7 @@ impl<'db> Type<'db> {
#[cfg(test)]
#[track_caller]
pub(crate) fn expect_function_literal(self) -> FunctionType<'db> {
pub(crate) const fn expect_function_literal(self) -> FunctionType<'db> {
self.as_function_literal()
.expect("Expected a Type::FunctionLiteral variant")
}
@@ -1152,42 +1141,42 @@ impl<'db> Type<'db> {
}
pub(crate) fn is_union_of_single_valued(&self, db: &'db dyn Db) -> bool {
self.as_union().is_some_and(|union| {
union.elements(db).iter().all(|ty| {
ty.is_single_valued(db)
|| ty.is_bool(db)
|| ty.is_literal_string()
|| (ty.is_enum(db) && !ty.overrides_equality(db))
})
}) || self.is_bool(db)
|| self.is_literal_string()
|| (self.is_enum(db) && !self.overrides_equality(db))
match self {
Type::LiteralString => true,
Type::NominalInstance(instance) => {
instance.has_known_class(db, KnownClass::Bool)
|| (enums::enum_metadata(db, instance.class_literal(db)).is_some()
&& !self.overrides_equality(db))
}
Type::Union(union) => union.elements(db).iter().all(|element| {
element.is_single_valued(db) || element.is_union_of_single_valued(db)
}),
_ => false,
}
}
pub(crate) fn is_union_with_single_valued(&self, db: &'db dyn Db) -> bool {
self.as_union().is_some_and(|union| {
union.elements(db).iter().any(|ty| {
ty.is_single_valued(db)
|| ty.is_bool(db)
|| ty.is_literal_string()
|| (ty.is_enum(db) && !ty.overrides_equality(db))
})
}) || self.is_bool(db)
|| self.is_literal_string()
|| (self.is_enum(db) && !self.overrides_equality(db))
match self {
Type::LiteralString => true,
Type::NominalInstance(instance) => {
instance.has_known_class(db, KnownClass::Bool)
|| (enums::enum_metadata(db, instance.class_literal(db)).is_some()
&& !self.overrides_equality(db))
}
Type::Union(union) => union.elements(db).iter().any(|element| {
element.is_single_valued(db) || element.is_union_of_single_valued(db)
}),
_ => false,
}
}
pub(crate) fn as_string_literal(self) -> Option<StringLiteralType<'db>> {
pub(crate) const fn as_string_literal(self) -> Option<StringLiteralType<'db>> {
match self {
Type::StringLiteral(string_literal) => Some(string_literal),
_ => None,
}
}
pub(crate) const fn is_literal_string(&self) -> bool {
matches!(self, Type::LiteralString)
}
pub(crate) fn string_literal(db: &'db dyn Db, string: &str) -> Self {
Self::StringLiteral(StringLiteralType::new(db, string))
}
@@ -7291,20 +7280,6 @@ impl<'db> Type<'db> {
}
}
pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option<ClassLiteral<'db>> {
match self {
Type::GenericAlias(generic) => Some(generic.origin(db)),
Type::NominalInstance(instance) => {
if let ClassType::Generic(generic) = instance.class(db) {
Some(generic.origin(db))
} else {
None
}
}
_ => None,
}
}
pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool {
any_over_type(db, self, &|ty| ty == div, false)
}
@@ -11138,7 +11113,7 @@ impl<'db> TypeAliasType<'db> {
}
}
pub(crate) fn as_pep_695_type_alias(self) -> Option<PEP695TypeAliasType<'db>> {
pub(crate) const fn as_pep_695_type_alias(self) -> Option<PEP695TypeAliasType<'db>> {
match self {
TypeAliasType::PEP695(type_alias) => Some(type_alias),
TypeAliasType::ManualPEP695(_) => None,

View File

@@ -256,15 +256,14 @@ impl<'a, 'db> FromIterator<(Argument<'a>, Option<Type<'db>>)> for CallArguments<
pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
match ty {
Type::NominalInstance(instance) => {
let class = instance.class(db);
class.is_known(db, KnownClass::Bool)
instance.has_known_class(db, KnownClass::Bool)
|| instance.tuple_spec(db).is_some_and(|spec| match &*spec {
Tuple::Fixed(fixed_length_tuple) => fixed_length_tuple
.all_elements()
.any(|element| is_expandable_type(db, *element)),
Tuple::Variable(_) => false,
})
|| enum_metadata(db, class.class_literal(db).0).is_some()
|| enum_metadata(db, instance.class_literal(db)).is_some()
}
Type::Union(_) => true,
_ => false,
@@ -278,9 +277,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
// NOTE: Update `is_expandable_type` if this logic changes accordingly.
match ty {
Type::NominalInstance(instance) => {
let class = instance.class(db);
if class.is_known(db, KnownClass::Bool) {
if instance.has_known_class(db, KnownClass::Bool) {
return Some(vec![
Type::BooleanLiteral(true),
Type::BooleanLiteral(false),
@@ -315,7 +312,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
};
}
if let Some(enum_members) = enum_member_literals(db, class.class_literal(db).0, None) {
if let Some(enum_members) = enum_member_literals(db, instance.class_literal(db), None) {
return Some(enum_members.collect());
}

View File

@@ -377,10 +377,7 @@ impl<'db> ClassType<'db> {
}
pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
match self {
Self::NonGeneric(class) => class.has_pep_695_type_params(db),
Self::Generic(generic) => generic.origin(db).has_pep_695_type_params(db),
}
self.class_literal(db).0.has_pep_695_type_params(db)
}
/// Returns the class literal and specialization for this class. For a non-generic class, this
@@ -3463,11 +3460,10 @@ impl<'db> ClassLiteral<'db> {
) -> bool {
let mut result = false;
for explicit_base in class.explicit_bases(db) {
let explicit_base_class_literal = match explicit_base {
Type::ClassLiteral(class_literal) => *class_literal,
Type::GenericAlias(generic_alias) => generic_alias.origin(db),
_ => continue,
let Some(explicit_base) = explicit_base.to_class_type(db) else {
continue;
};
let explicit_base_class_literal = explicit_base.class_literal(db).0;
if !classes_on_stack.insert(explicit_base_class_literal) {
return true;
}

View File

@@ -1379,16 +1379,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
KnownClass::OrderedDict,
];
SAFE_MUTABLE_CLASSES
.iter()
.map(|class| class.to_instance(db))
.any(|safe_mutable_class| {
ty.is_equivalent_to(db, safe_mutable_class)
|| ty
.generic_origin(db)
.zip(safe_mutable_class.generic_origin(db))
.is_some_and(|(l, r)| l == r)
})
ty.as_nominal_instance().is_some_and(|instance| {
SAFE_MUTABLE_CLASSES
.iter()
.any(|known_class| instance.has_known_class(db, *known_class))
})
}
debug_assert!(

View File

@@ -614,9 +614,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
for element in lhs_union.elements(self.db) {
// Keep only the non-single-valued portion of the original type.
if !element.is_single_valued(self.db)
&& !element.is_literal_string()
&& !element.is_bool(self.db)
&& (!element.is_enum(self.db) || element.overrides_equality(self.db))
&& !element.is_union_of_single_valued(self.db)
{
builder = builder.add(*element);
}
@@ -650,9 +648,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
if let Some(lhs_union) = lhs_ty.as_union() {
for element in lhs_union.elements(self.db) {
if element.is_single_valued(self.db)
|| element.is_literal_string()
|| element.is_bool(self.db)
|| (element.is_enum(self.db) && !element.overrides_equality(self.db))
|| element.is_union_of_single_valued(self.db)
{
single_builder = single_builder.add(*element);
} else {

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.14.3-alpine
name: ghcr.io/astral-sh/ruff:0.14.2-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
@@ -106,7 +106,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.14.3
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff-check
@@ -119,7 +119,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.14.3
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff-check
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.3
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.3
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.14.3"
version = "0.14.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"

View File

@@ -1,6 +1,6 @@
[project]
name = "scripts"
version = "0.14.3"
version = "0.14.2"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]

View File

@@ -12,7 +12,7 @@ project_root="$(dirname "$script_root")"
echo "Updating metadata with rooster..."
cd "$project_root"
uvx --python 3.12 --isolated -- \
rooster@0.1.1 release "$@"
rooster@0.1.0 release "$@"
echo "Updating lockfile..."
cargo update -p ruff