diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yaml b/.github/ISSUE_TEMPLATE/1_bug_report.yaml index 1baee4b861..f2e6d56a19 100644 --- a/.github/ISSUE_TEMPLATE/1_bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/1_bug_report.yaml @@ -26,6 +26,6 @@ body: attributes: label: Version description: What version of ruff are you using? (see `ruff version`) - placeholder: e.g., ruff 0.17.2 + placeholder: e.g., ruff 0.17.3 validations: required: false diff --git a/.github/ISSUE_TEMPLATE/3_question.yaml b/.github/ISSUE_TEMPLATE/3_question.yaml index d0856b81cd..0adb5792a4 100644 --- a/.github/ISSUE_TEMPLATE/3_question.yaml +++ b/.github/ISSUE_TEMPLATE/3_question.yaml @@ -13,6 +13,6 @@ body: attributes: label: Version description: What version of ruff are you using? (see `ruff version`) - placeholder: e.g., ruff 0.17.2 + placeholder: e.g., ruff 0.17.3 validations: required: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 304a46befb..5912e3084c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 0.17.3 + +Released on 2026-10-26. + +### Preview features + +- \[`suspicious`\] Add `shared-clock-mock` ([#29074](https://github.com/astral-sh/ruff/pull/29074)) + +### Bug fixes + +- \[`pyupgrade`\] Avoid incorrect conversions to f-strings (`UP032`) ([#25485](https://github.com/astral-sh/ruff/pull/25485)) + +### Performance + +- Buffer stdout in `ruff format` ([#29631](https://github.com/astral-sh/ruff/pull/29631)) +- Specialize ASCII identifier lexing ([#29503](https://github.com/astral-sh/ruff/pull/29503)) +- Speed up identifier and keyword lexing ([#29463](https://github.com/astral-sh/ruff/pull/29463)) + +### Documentation + +- \[`flake8-bandit`\] Describe what the rule actually checks for `httpx` (`S113`) ([#27936](https://github.com/astral-sh/ruff/pull/27936)) + +### Contributors + +- [@itsvedantkumar](https://github.com/itsvedantkumar) +- [@charliermarsh](https://github.com/charliermarsh) +- [@anishgirianish](https://github.com/anishgirianish) + ## 0.17.2 Released on 2026-10-22. diff --git a/Cargo.lock b/Cargo.lock index 7a3c5d0184..84eda7b221 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3098,7 +3098,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.17.2" +version = "0.17.3" dependencies = [ "anyhow", "assert_fs", @@ -3162,7 +3162,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anstream 1.0.0", "anstyle", @@ -3206,7 +3206,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.19" +version = "0.0.20" dependencies = [ "char_str", "filetime", @@ -3220,14 +3220,14 @@ dependencies = [ [[package]] name = "ruff_command_line" -version = "0.0.19" +version = "0.0.20" dependencies = [ "argfile", ] [[package]] name = "ruff_db" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anstyle", "arc-swap", @@ -3319,7 +3319,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "is-macro", @@ -3329,7 +3329,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.19" +version = "0.0.20" dependencies = [ "drop_bomb", "ruff_cache", @@ -3345,7 +3345,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "clap", @@ -3366,7 +3366,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "ruff_macros", @@ -3376,7 +3376,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.17.2" +version = "0.17.3" dependencies = [ "aho-corasick", "anyhow", @@ -3438,7 +3438,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.19" +version = "0.0.20" dependencies = [ "heck", "itertools 0.15.0", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "ruff_markdown" -version = "0.0.19" +version = "0.0.20" dependencies = [ "insta", "regex", @@ -3482,14 +3482,14 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "rand 0.10.3", @@ -3505,14 +3505,14 @@ dependencies = [ [[package]] name = "ruff_options_metadata" -version = "0.0.19" +version = "0.0.20" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.19" +version = "0.0.20" dependencies = [ "aho-corasick", "arrayvec", @@ -3549,7 +3549,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -3561,7 +3561,7 @@ dependencies = [ [[package]] name = "ruff_python_edits" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3571,7 +3571,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "clap", @@ -3604,7 +3604,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "insta", @@ -3619,7 +3619,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3630,7 +3630,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.19" +version = "0.0.20" dependencies = [ "bitflags 2.13.2", "icu_normalizer", @@ -3641,7 +3641,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -3672,7 +3672,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.19" +version = "0.0.20" dependencies = [ "bitflags 2.13.2", "insta", @@ -3693,7 +3693,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.19" +version = "0.0.20" dependencies = [ "bitflags 2.13.2", "unicode-ident", @@ -3701,7 +3701,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.19" +version = "0.0.20" dependencies = [ "itertools 0.15.0", "memchr", @@ -3724,7 +3724,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "ruff_db", @@ -3737,7 +3737,7 @@ dependencies = [ [[package]] name = "ruff_server" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "crossbeam", @@ -3781,7 +3781,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "memchr", @@ -3791,7 +3791,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "schemars", @@ -3802,7 +3802,7 @@ dependencies = [ [[package]] name = "ruff_unicode_names2" -version = "0.0.19" +version = "0.0.20" dependencies = [ "phf 0.14.0", "rand 0.10.3", @@ -3811,7 +3811,7 @@ dependencies = [ [[package]] name = "ruff_unicode_names2_generator" -version = "0.0.19" +version = "0.0.20" dependencies = [ "phf_codegen 0.14.0", "rand 0.10.3", @@ -3819,7 +3819,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.17.2" +version = "0.17.3" dependencies = [ "console_error_panic_hook", "console_log", @@ -3846,7 +3846,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "colored", @@ -4729,7 +4729,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ordermap", "ruff_db", @@ -4810,7 +4810,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "camino", @@ -4891,7 +4891,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -4926,7 +4926,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -5027,7 +5027,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.19" +version = "0.0.20" dependencies = [ "camino", "colored", @@ -5048,7 +5048,7 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ruff_macros", ] @@ -5081,7 +5081,7 @@ dependencies = [ [[package]] name = "ty_vendored" -version = "0.0.19" +version = "0.0.20" dependencies = [ "path-slash", "ruff_db", diff --git a/Cargo.toml b/Cargo.toml index ae31696554..f5f3f6c54d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,55 +14,55 @@ license = "MIT" [workspace.dependencies] char_str = { version = "0.0.4", default-features = false, features = ["std"] } -ruff = { version = "0.17.2", path = "crates/ruff" } -ruff_annotate_snippets = { version = "0.0.19", path = "crates/ruff_annotate_snippets" } -ruff_cache = { version = "0.0.19", path = "crates/ruff_cache" } -ruff_command_line = { version = "0.0.19", path = "crates/ruff_command_line" } -ruff_db = { version = "0.0.19", path = "crates/ruff_db", default-features = false } -ruff_diagnostics = { version = "0.0.19", path = "crates/ruff_diagnostics" } -ruff_formatter = { version = "0.0.19", path = "crates/ruff_formatter" } -ruff_graph = { version = "0.0.19", path = "crates/ruff_graph" } -ruff_index = { version = "0.0.19", path = "crates/ruff_index" } -ruff_linter = { version = "0.17.2", path = "crates/ruff_linter" } -ruff_macros = { version = "0.0.19", path = "crates/ruff_macros" } -ruff_markdown = { version = "0.0.19", path = "crates/ruff_markdown" } -ruff_memory_usage = { version = "0.0.19", path = "crates/ruff_memory_usage" } -ruff_notebook = { version = "0.0.19", path = "crates/ruff_notebook" } -ruff_options_metadata = { version = "0.0.19", path = "crates/ruff_options_metadata" } -ruff_python_ast = { version = "0.0.19", path = "crates/ruff_python_ast" } -ruff_python_codegen = { version = "0.0.19", path = "crates/ruff_python_codegen" } -ruff_python_edits = { version = "0.0.19", path = "crates/ruff_python_edits" } -ruff_python_formatter = { version = "0.0.19", path = "crates/ruff_python_formatter" } -ruff_python_importer = { version = "0.0.19", path = "crates/ruff_python_importer" } -ruff_python_index = { version = "0.0.19", path = "crates/ruff_python_index" } -ruff_python_literal = { version = "0.0.19", path = "crates/ruff_python_literal" } -ruff_python_parser = { version = "0.0.19", path = "crates/ruff_python_parser" } -ruff_python_semantic = { version = "0.0.19", path = "crates/ruff_python_semantic" } -ruff_python_stdlib = { version = "0.0.19", path = "crates/ruff_python_stdlib" } -ruff_python_trivia = { version = "0.0.19", path = "crates/ruff_python_trivia" } -ruff_server = { version = "0.0.19", path = "crates/ruff_server" } -ruff_source_file = { version = "0.0.19", path = "crates/ruff_source_file" } +ruff = { version = "0.17.3", path = "crates/ruff" } +ruff_annotate_snippets = { version = "0.0.20", path = "crates/ruff_annotate_snippets" } +ruff_cache = { version = "0.0.20", path = "crates/ruff_cache" } +ruff_command_line = { version = "0.0.20", path = "crates/ruff_command_line" } +ruff_db = { version = "0.0.20", path = "crates/ruff_db", default-features = false } +ruff_diagnostics = { version = "0.0.20", path = "crates/ruff_diagnostics" } +ruff_formatter = { version = "0.0.20", path = "crates/ruff_formatter" } +ruff_graph = { version = "0.0.20", path = "crates/ruff_graph" } +ruff_index = { version = "0.0.20", path = "crates/ruff_index" } +ruff_linter = { version = "0.17.3", path = "crates/ruff_linter" } +ruff_macros = { version = "0.0.20", path = "crates/ruff_macros" } +ruff_markdown = { version = "0.0.20", path = "crates/ruff_markdown" } +ruff_memory_usage = { version = "0.0.20", path = "crates/ruff_memory_usage" } +ruff_notebook = { version = "0.0.20", path = "crates/ruff_notebook" } +ruff_options_metadata = { version = "0.0.20", path = "crates/ruff_options_metadata" } +ruff_python_ast = { version = "0.0.20", path = "crates/ruff_python_ast" } +ruff_python_codegen = { version = "0.0.20", path = "crates/ruff_python_codegen" } +ruff_python_edits = { version = "0.0.20", path = "crates/ruff_python_edits" } +ruff_python_formatter = { version = "0.0.20", path = "crates/ruff_python_formatter" } +ruff_python_importer = { version = "0.0.20", path = "crates/ruff_python_importer" } +ruff_python_index = { version = "0.0.20", path = "crates/ruff_python_index" } +ruff_python_literal = { version = "0.0.20", path = "crates/ruff_python_literal" } +ruff_python_parser = { version = "0.0.20", path = "crates/ruff_python_parser" } +ruff_python_semantic = { version = "0.0.20", path = "crates/ruff_python_semantic" } +ruff_python_stdlib = { version = "0.0.20", path = "crates/ruff_python_stdlib" } +ruff_python_trivia = { version = "0.0.20", path = "crates/ruff_python_trivia" } +ruff_server = { version = "0.0.20", path = "crates/ruff_server" } +ruff_source_file = { version = "0.0.20", path = "crates/ruff_source_file" } ruff_mdtest = { path = "crates/ruff_mdtest" } -ruff_ranged_value = { version = "0.0.19", path = "crates/ruff_ranged_value" } -ruff_text_size = { version = "0.0.19", path = "crates/ruff_text_size" } -ruff_unicode_names2 = { version = "0.0.19", path = "crates/ruff_unicode_names2" } -ruff_unicode_names2_generator = { version = "0.0.19", path = "crates/ruff_unicode_names2_generator" } -ruff_workspace = { version = "0.0.19", path = "crates/ruff_workspace" } +ruff_ranged_value = { version = "0.0.20", path = "crates/ruff_ranged_value" } +ruff_text_size = { version = "0.0.20", path = "crates/ruff_text_size" } +ruff_unicode_names2 = { version = "0.0.20", path = "crates/ruff_unicode_names2" } +ruff_unicode_names2_generator = { version = "0.0.20", path = "crates/ruff_unicode_names2_generator" } +ruff_workspace = { version = "0.0.20", path = "crates/ruff_workspace" } ty = { path = "crates/ty" } -ty_combine = { version = "0.0.19", path = "crates/ty_combine" } +ty_combine = { version = "0.0.20", path = "crates/ty_combine" } ty_completion_bench = { path = "crates/ty_completion_bench" } ty_completion_eval = { path = "crates/ty_completion_eval" } ty_ide = { path = "crates/ty_ide" } -ty_module_resolver = { version = "0.0.19", path = "crates/ty_module_resolver" } +ty_module_resolver = { version = "0.0.20", path = "crates/ty_module_resolver" } ty_project = { path = "crates/ty_project", default-features = false } -ty_python_semantic = { version = "0.0.19", path = "crates/ty_python_semantic" } -ty_python_core = { version = "0.0.19", path = "crates/ty_python_core" } +ty_python_semantic = { version = "0.0.20", path = "crates/ty_python_semantic" } +ty_python_core = { version = "0.0.20", path = "crates/ty_python_core" } ty_server = { path = "crates/ty_server" } -ty_site_packages = { version = "0.0.19", path = "crates/ty_site_packages" } -ty_static = { version = "0.0.19", path = "crates/ty_static" } +ty_site_packages = { version = "0.0.20", path = "crates/ty_site_packages" } +ty_static = { version = "0.0.20", path = "crates/ty_static" } ty_test = { path = "crates/ty_test" } -ty_vendored = { version = "0.0.19", path = "crates/ty_vendored" } +ty_vendored = { version = "0.0.20", path = "crates/ty_vendored" } mdtest = { path = "crates/mdtest" } diff --git a/README.md b/README.md index 9e55650ba4..978a8fa353 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,8 @@ Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI. Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/): ```shell -uvx ruff@0.17.2 check # Lint all files in the current directory. -uvx ruff@0.17.2 format # Format all files in the current directory. +uvx ruff@0.17.3 check # Lint all files in the current directory. +uvx ruff@0.17.3 format # Format all files in the current directory. ``` Or install Ruff with `uv` (recommended), `pip`, or `pipx`: @@ -150,8 +150,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.17.2/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.17.2/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.17.3/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.17.3/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -184,7 +184,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.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index a7cd6cddec..d88c32273f 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.17.2" +version = "0.17.3" description = "An extremely fast Python linter and code formatter" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff/README.md b/crates/ruff/README.md index f356ec25e2..50f2e04e47 100644 --- a/crates/ruff/README.md +++ b/crates/ruff/README.md @@ -10,7 +10,7 @@ See the [documentation](https://docs.astral.sh/ruff/) or This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.17.2. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff). +This is version 0.17.3. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff). The following Ruff workspace members are also available: diff --git a/crates/ruff_annotate_snippets/Cargo.toml b/crates/ruff_annotate_snippets/Cargo.toml index 749d443abc..94b2111985 100644 --- a/crates/ruff_annotate_snippets/Cargo.toml +++ b/crates/ruff_annotate_snippets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_annotate_snippets" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index 8581f8aa64..ffb39b7e5e 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cache" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/README.md b/crates/ruff_cache/README.md index f59adc01de..0b338e68ac 100644 --- a/crates/ruff_cache/README.md +++ b/crates/ruff_cache/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_cache). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_cache). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_command_line/Cargo.toml b/crates/ruff_command_line/Cargo.toml index d5c203b763..63f53d9fb0 100644 --- a/crates/ruff_command_line/Cargo.toml +++ b/crates/ruff_command_line/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_command_line" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_command_line/README.md b/crates/ruff_command_line/README.md index 9559481135..aa7d21dbd2 100644 --- a/crates/ruff_command_line/README.md +++ b/crates/ruff_command_line/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_command_line). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_command_line). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index 2b2ff249e8..3e405e0f7b 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_db" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_db/README.md b/crates/ruff_db/README.md index f1b63aea8e..fb307ffe67 100644 --- a/crates/ruff_db/README.md +++ b/crates/ruff_db/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_db). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_db). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 5ac0c2b321..fd1fd1ac69 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_diagnostics" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_diagnostics/README.md b/crates/ruff_diagnostics/README.md index 16975d745c..299a2947b9 100644 --- a/crates/ruff_diagnostics/README.md +++ b/crates/ruff_diagnostics/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_diagnostics). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_diagnostics). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index 67276fbcdd..bc42603cd4 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_formatter" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_formatter/README.md b/crates/ruff_formatter/README.md index 969ff0e7b7..77dfc5b876 100644 --- a/crates/ruff_formatter/README.md +++ b/crates/ruff_formatter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_formatter). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index 684b5bf273..93b387cbef 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_graph" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ruff_graph/README.md b/crates/ruff_graph/README.md index 48617fdddb..fc6a1eb9dd 100644 --- a/crates/ruff_graph/README.md +++ b/crates/ruff_graph/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_graph). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_graph). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index 32e4804493..d243c18223 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_index" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_index/README.md b/crates/ruff_index/README.md index ec3604a20a..4d8e43d95a 100644 --- a/crates/ruff_index/README.md +++ b/crates/ruff_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_index). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 2c62cd4417..e6c222d19e 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.17.2" +version = "0.17.3" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/README.md b/crates/ruff_linter/README.md index 5ffb4068a6..2e0646549f 100644 --- a/crates/ruff_linter/README.md +++ b/crates/ruff_linter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.17.2) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_linter). +This version (0.17.3) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_linter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs index 9add8101f6..98f003726a 100644 --- a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs +++ b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs @@ -30,6 +30,19 @@ use crate::package::PackageRoot; /// `__init__.py` file is typically meant to be a regular package, and /// the absence of the `__init__.py` file is probably an oversight. /// +/// ## Preview +/// +/// In preview, the rule also checks for packages without `__init__.py` files between two regular +/// packages, such as `bar` in the project structure below. +/// +/// ```text +/// foo/ +/// ├── __init__.py +/// └── bar/ +/// └── baz/ +/// └── __init__.py +/// ``` +/// /// ## Options /// - `namespace-packages` #[derive(ViolationMetadata)] diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs index 80d5a89103..9a8afd528a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs @@ -155,89 +155,59 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { return; } - // Generate a map from key to (index, value). - let mut seen: FxHashMap)> = - FxHashMap::with_capacity_and_hasher(dict.len(), FxBuildHasher); + let mut seen_keys = FxHashMap::with_capacity_and_hasher(dict.len(), FxBuildHasher); + let mut seen_values = FxHashMap::default(); - // Detect duplicate keys. for (i, ast::DictItem { key, value }) in dict.iter().enumerate() { let Some(key) = key else { continue; }; - let comparable_key = HashableExpr::from(key); - let comparable_value = ComparableExpr::from(value); - - match seen.entry(comparable_key) { + let (first_index, first_key) = match seen_keys.entry(HashableExpr::from(key)) { Entry::Vacant(entry) => { - entry.insert((key, FxHashSet::from_iter([comparable_value]))); + entry.insert((i, key)); + continue; } - Entry::Occupied(mut entry) => { - let (seen_key, seen_values) = entry.get_mut(); - match key { - key if is_literal_key(key) - && checker.is_rule_enabled(Rule::MultiValueRepeatedKeyLiteral) => - { - let mut diagnostic = checker.report_diagnostic( - MultiValueRepeatedKeyLiteral { - name: SourceCodeSnippet::from_str(checker.locator().slice(key)), - existing: SourceCodeSnippet::from_str( - checker.locator().slice(*seen_key), - ), - }, - key.range(), - ); - if !seen_values.insert(comparable_value) { - diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion( - parenthesized_range( - dict.value(i - 1).into(), - dict.into(), - checker.tokens(), - ) - .unwrap_or_else(|| dict.value(i - 1).range()) - .end(), - parenthesized_range( - dict.value(i).into(), - dict.into(), - checker.tokens(), - ) - .unwrap_or_else(|| dict.value(i).range()) - .end(), - ))); - } - } - Expr::Name(_) - if checker.is_rule_enabled(Rule::MultiValueRepeatedKeyVariable) => - { - let mut diagnostic = checker.report_diagnostic( - MultiValueRepeatedKeyVariable { - name: SourceCodeSnippet::from_str(checker.locator().slice(key)), - }, - key.range(), - ); - let comparable_value: ComparableExpr = dict.value(i).into(); - if !seen_values.insert(comparable_value) { - diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion( - parenthesized_range( - dict.value(i - 1).into(), - dict.into(), - checker.tokens(), - ) - .unwrap_or_else(|| dict.value(i - 1).range()) - .end(), - parenthesized_range( - dict.value(i).into(), - dict.into(), - checker.tokens(), - ) - .unwrap_or_else(|| dict.value(i).range()) - .end(), - ))); - } - } - _ => {} - } + Entry::Occupied(entry) => *entry.get(), + }; + + let mut diagnostic = match key { + key if is_literal_key(key) + && checker.is_rule_enabled(Rule::MultiValueRepeatedKeyLiteral) => + { + checker.report_diagnostic( + MultiValueRepeatedKeyLiteral { + name: SourceCodeSnippet::from_str(checker.locator().slice(key)), + existing: SourceCodeSnippet::from_str(checker.locator().slice(first_key)), + }, + key.range(), + ) } + Expr::Name(_) if checker.is_rule_enabled(Rule::MultiValueRepeatedKeyVariable) => { + checker.report_diagnostic( + MultiValueRepeatedKeyVariable { + name: SourceCodeSnippet::from_str(checker.locator().slice(key)), + }, + key.range(), + ) + } + _ => continue, + }; + + // Comparing values is only necessary to decide whether a repeated key can be fixed. + let values = seen_values.entry(first_index).or_insert_with(|| { + FxHashSet::from_iter([ComparableExpr::from(dict.value(first_index))]) + }); + if !values.insert(ComparableExpr::from(value)) { + let previous = dict.value(i - 1); + diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion( + parenthesized_range(previous.into(), dict.into(), checker.tokens()) + .unwrap_or_else(|| previous.range()) + .end(), + parenthesized_range(value.into(), dict.into(), checker.tokens()) + .unwrap_or_else(|| value.range()) + .end(), + ))); } } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/shared_clock_mock.rs b/crates/ruff_linter/src/rules/ruff/rules/shared_clock_mock.rs index bebee850ca..f07749c596 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/shared_clock_mock.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/shared_clock_mock.rs @@ -54,7 +54,7 @@ use crate::codes::Category; /// - [Python documentation: Where to patch](https://docs.python.org/3/library/unittest.mock.html#where-to-patch) /// - [pytest documentation: Monkeypatching](https://docs.pytest.org/en/stable/how-to/monkeypatch.html) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "NEXT_RUFF_VERSION", category = Category::Suspicious)] +#[violation_metadata(preview_since = "0.17.3", category = Category::Suspicious)] pub(crate) struct SharedClockMock { function: String, } diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index 77bb1bf984..9e7bd63bd4 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_macros" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_macros/README.md b/crates/ruff_macros/README.md index 437fde3a13..0c9b6f42f9 100644 --- a/crates/ruff_macros/README.md +++ b/crates/ruff_macros/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_macros). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_macros). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_markdown/Cargo.toml b/crates/ruff_markdown/Cargo.toml index 4ade56f472..93a866515c 100644 --- a/crates/ruff_markdown/Cargo.toml +++ b/crates/ruff_markdown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_markdown" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ruff_markdown/README.md b/crates/ruff_markdown/README.md index 55fe754831..5735de90f3 100644 --- a/crates/ruff_markdown/README.md +++ b/crates/ruff_markdown/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_markdown). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_markdown). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_memory_usage/Cargo.toml b/crates/ruff_memory_usage/Cargo.toml index d0805e144c..050a3aeb4d 100644 --- a/crates/ruff_memory_usage/Cargo.toml +++ b/crates/ruff_memory_usage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_memory_usage" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_memory_usage/README.md b/crates/ruff_memory_usage/README.md index cb75b8a2ab..ea55181f7c 100644 --- a/crates/ruff_memory_usage/README.md +++ b/crates/ruff_memory_usage/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_memory_usage). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_memory_usage). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/Cargo.toml b/crates/ruff_notebook/Cargo.toml index 9862f9391b..56c6e3abf8 100644 --- a/crates/ruff_notebook/Cargo.toml +++ b/crates/ruff_notebook/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_notebook" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_notebook/README.md b/crates/ruff_notebook/README.md index c1a704d690..45192e53cb 100644 --- a/crates/ruff_notebook/README.md +++ b/crates/ruff_notebook/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_notebook). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_notebook). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml index 46cdb358f6..6f5f3a3efe 100644 --- a/crates/ruff_options_metadata/Cargo.toml +++ b/crates/ruff_options_metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_options_metadata" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_options_metadata/README.md b/crates/ruff_options_metadata/README.md index 02d39007a6..e00104c21e 100644 --- a/crates/ruff_options_metadata/README.md +++ b/crates/ruff_options_metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_options_metadata). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_options_metadata). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index 678a6a648e..b782202e44 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_ast" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_ast/README.md b/crates/ruff_python_ast/README.md index 92b1b790d8..a7a05e6370 100644 --- a/crates/ruff_python_ast/README.md +++ b/crates/ruff_python_ast/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_ast). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_ast). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_codegen/Cargo.toml b/crates/ruff_python_codegen/Cargo.toml index c5fccdb9e2..56fd650454 100644 --- a/crates/ruff_python_codegen/Cargo.toml +++ b/crates/ruff_python_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_codegen" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_codegen/README.md b/crates/ruff_python_codegen/README.md index 78cbf6a256..e3be4aa5f2 100644 --- a/crates/ruff_python_codegen/README.md +++ b/crates/ruff_python_codegen/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_codegen). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_codegen). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_edits/Cargo.toml b/crates/ruff_python_edits/Cargo.toml index 3757204e3f..261518481a 100644 --- a/crates/ruff_python_edits/Cargo.toml +++ b/crates/ruff_python_edits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_edits" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_edits/README.md b/crates/ruff_python_edits/README.md index 5c5159cb0b..35ea097130 100644 --- a/crates/ruff_python_edits/README.md +++ b/crates/ruff_python_edits/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_edits). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_edits). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 750e66356e..c5b0ad0aad 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_formatter" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index c8920bad62..47093b2e8c 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -32,8 +32,8 @@ Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage i This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_formatter). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_importer/Cargo.toml b/crates/ruff_python_importer/Cargo.toml index 85a50a119e..ef028ba550 100644 --- a/crates/ruff_python_importer/Cargo.toml +++ b/crates/ruff_python_importer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_importer" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_importer/README.md b/crates/ruff_python_importer/README.md index 28f8265916..96b0570dbe 100644 --- a/crates/ruff_python_importer/README.md +++ b/crates/ruff_python_importer/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_importer). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_importer). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_index/Cargo.toml b/crates/ruff_python_index/Cargo.toml index ec5ba29309..4ba95cc01c 100644 --- a/crates/ruff_python_index/Cargo.toml +++ b/crates/ruff_python_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_index" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_index/README.md b/crates/ruff_python_index/README.md index bddfca59ec..3f8c1efa16 100644 --- a/crates/ruff_python_index/README.md +++ b/crates/ruff_python_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_index). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_literal/Cargo.toml b/crates/ruff_python_literal/Cargo.toml index baafff7e4d..72aedb4bac 100644 --- a/crates/ruff_python_literal/Cargo.toml +++ b/crates/ruff_python_literal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_literal" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_literal/README.md b/crates/ruff_python_literal/README.md index f9b487e26f..41b21591dc 100644 --- a/crates/ruff_python_literal/README.md +++ b/crates/ruff_python_literal/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_literal). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_literal). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index 36cc69fe5b..004325e51b 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_parser" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_parser/README.md b/crates/ruff_python_parser/README.md index 50b1a48dae..e97a7b8aa0 100644 --- a/crates/ruff_python_parser/README.md +++ b/crates/ruff_python_parser/README.md @@ -19,8 +19,8 @@ Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and Git This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_parser). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_parser). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py b/crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py new file mode 100644 index 0000000000..dafdd5693d --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py @@ -0,0 +1,2 @@ +try: work() +except ValueError: raise; report() diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index cf2cdeb530..be7cd0253f 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -532,7 +532,10 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Raise); let exc = match self.current_token_kind() { - TokenKind::Newline => None, + // test_ok bare_raise_before_semicolon + // try: work() + // except ValueError: raise; report() + TokenKind::Newline | TokenKind::Semi => None, TokenKind::From => { // test_err raise_stmt_from_without_exc // raise from exc diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@bare_raise_before_semicolon.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@bare_raise_before_semicolon.py.snap new file mode 100644 index 0000000000..5cf2c6b0bc --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@bare_raise_before_semicolon.py.snap @@ -0,0 +1,108 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..47, + body: [ + Try( + StmtTry { + node_index: NodeIndex(None), + range: 0..46, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 5..11, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 5..11, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 5..9, + id: Name("work"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 9..11, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 12..46, + node_index: NodeIndex(None), + type_: Some( + Name( + ExprName { + node_index: NodeIndex(None), + range: 19..29, + id: Name("ValueError"), + ctx: Load, + }, + ), + ), + name: None, + body: [ + Raise( + StmtRaise { + node_index: NodeIndex(None), + range: 31..36, + exc: None, + cause: None, + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 38..46, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 38..46, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 38..44, + id: Name("report"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 44..46, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index 8ed697eb82..a5f8c3a193 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_semantic" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_semantic/README.md b/crates/ruff_python_semantic/README.md index d267200ea0..1646250a74 100644 --- a/crates/ruff_python_semantic/README.md +++ b/crates/ruff_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_semantic). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index 95a75257bd..08e77890d6 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_stdlib" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_stdlib/README.md b/crates/ruff_python_stdlib/README.md index f103cc1fb7..25f633fed6 100644 --- a/crates/ruff_python_stdlib/README.md +++ b/crates/ruff_python_stdlib/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_stdlib). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_stdlib). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_trivia/Cargo.toml b/crates/ruff_python_trivia/Cargo.toml index 63327128d0..d3a9f66b19 100644 --- a/crates/ruff_python_trivia/Cargo.toml +++ b/crates/ruff_python_trivia/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_trivia" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_trivia/README.md b/crates/ruff_python_trivia/README.md index bd66aab07a..a8b79ad723 100644 --- a/crates/ruff_python_trivia/README.md +++ b/crates/ruff_python_trivia/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_trivia). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_trivia). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_ranged_value/Cargo.toml b/crates/ruff_ranged_value/Cargo.toml index 921a122482..91ea44a425 100644 --- a/crates/ruff_ranged_value/Cargo.toml +++ b/crates/ruff_ranged_value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_ranged_value" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_ranged_value/README.md b/crates/ruff_ranged_value/README.md index af9ff1e5f1..e8022d559e 100644 --- a/crates/ruff_ranged_value/README.md +++ b/crates/ruff_ranged_value/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_ranged_value). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_ranged_value). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index b9bd4a99a3..74bec3146f 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_server" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_server/README.md b/crates/ruff_server/README.md index 6b288166c4..96221f3436 100644 --- a/crates/ruff_server/README.md +++ b/crates/ruff_server/README.md @@ -24,8 +24,8 @@ You can also join us on [**Discord**](https://discord.com/invite/astral-sh). This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_server). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_server). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index 387d9bd2ce..c467d71b92 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_source_file" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_source_file/README.md b/crates/ruff_source_file/README.md index 426ea0432b..d7b5ec16c4 100644 --- a/crates/ruff_source_file/README.md +++ b/crates/ruff_source_file/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_source_file). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_source_file). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml index 30c059463d..f43de12e57 100644 --- a/crates/ruff_text_size/Cargo.toml +++ b/crates/ruff_text_size/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_text_size" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_text_size/README.md b/crates/ruff_text_size/README.md index 25bacd9eff..7a4a41eec9 100644 --- a/crates/ruff_text_size/README.md +++ b/crates/ruff_text_size/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_text_size). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_text_size). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_unicode_names2/Cargo.toml b/crates/ruff_unicode_names2/Cargo.toml index de183ff12b..fde607672e 100644 --- a/crates/ruff_unicode_names2/Cargo.toml +++ b/crates/ruff_unicode_names2/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_unicode_names2" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_unicode_names2_generator/Cargo.toml b/crates/ruff_unicode_names2_generator/Cargo.toml index 199a11236a..91b26275f9 100644 --- a/crates/ruff_unicode_names2_generator/Cargo.toml +++ b/crates/ruff_unicode_names2_generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_unicode_names2_generator" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_unicode_names2_generator/README.md b/crates/ruff_unicode_names2_generator/README.md index 29b92d0174..4a8acf2b6a 100644 --- a/crates/ruff_unicode_names2_generator/README.md +++ b/crates/ruff_unicode_names2_generator/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_unicode_names2_generator). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_unicode_names2_generator). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index d6bcbdd52b..64000c30ff 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.17.2" +version = "0.17.3" description = "WebAssembly bindings for Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/README.md b/crates/ruff_wasm/README.md index e8fdad13a4..ae86fc85b5 100644 --- a/crates/ruff_wasm/README.md +++ b/crates/ruff_wasm/README.md @@ -55,8 +55,8 @@ const formatted = workspace.format(exampleDocument); This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.17.2) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_wasm). +This version (0.17.3) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_wasm). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index c1ec75cd01..92dfe082d0 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_workspace" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_workspace/README.md b/crates/ruff_workspace/README.md index bc1cef1b54..40b58f3352 100644 --- a/crates/ruff_workspace/README.md +++ b/crates/ruff_workspace/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_workspace). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_workspace). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_combine/Cargo.toml b/crates/ty_combine/Cargo.toml index 6b4c71f5ba..09c33cdbbc 100644 --- a/crates/ty_combine/Cargo.toml +++ b/crates/ty_combine/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_combine" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ty_combine/README.md b/crates/ty_combine/README.md index ead8e4cb03..e886b94af9 100644 --- a/crates/ty_combine/README.md +++ b/crates/ty_combine/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_combine). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_combine). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_module_resolver/Cargo.toml b/crates/ty_module_resolver/Cargo.toml index 5f2e63b483..03cd57822f 100644 --- a/crates/ty_module_resolver/Cargo.toml +++ b/crates/ty_module_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_module_resolver" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_module_resolver/README.md b/crates/ty_module_resolver/README.md index 0d1586486c..495c8e85f6 100644 --- a/crates/ty_module_resolver/README.md +++ b/crates/ty_module_resolver/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_module_resolver). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_module_resolver). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_core/Cargo.toml b/crates/ty_python_core/Cargo.toml index b7b9aead4e..63e78018fe 100644 --- a/crates/ty_python_core/Cargo.toml +++ b/crates/ty_python_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_core" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_core/README.md b/crates/ty_python_core/README.md index f5b92893fa..0a778fb5fd 100644 --- a/crates/ty_python_core/README.md +++ b/crates/ty_python_core/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_python_core). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_python_core). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 0db46ec7c1..5cd6d85769 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -3771,7 +3771,10 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { fn visit_bool_expression(&mut self, node: &'ast ast::ExprBoolOp, context: ExpressionContext) { let ast::ExprBoolOp { values, op, .. } = node; let mut snapshots = vec![]; - let mut reachability_constraints = vec![]; + let has_named_expression = values + .iter() + .any(|value| any_over_expr(value, &ast::Expr::is_named_expr)); + let mut reachability_constraints = SmallVec::<[ScopedReachabilityConstraintId; 1]>::new(); let mut last_condition_flow_snapshots = None; for (index, value) in values.iter().enumerate() { @@ -3812,19 +3815,17 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.flow_snapshot() }; - // We first model the short-circuiting behavior. We take the short-circuit - // path here if all of the previous short-circuit paths were not taken, so - // we record all previously existing reachability constraints, and negate the - // one for the current expression. - + // This path reaches the current operand and then short-circuits. self.record_negated_reachability_constraint(reachability_constraint); snapshots.push(self.flow_snapshot()); - // Then we model the non-short-circuiting behavior. Here, we need to delay - // the application of the reachability constraint until after the expression - // has been evaluated, so we only push it onto the stack here. self.flow_restore(continuation); self.record_narrowing_constraint_id_for_places(predicate_id, &possibly_narrowed); + // The continuation carries the conditions that led to this operand. Named + // expressions can introduce new bindings that still need the earlier gates. + if !has_named_expression { + reachability_constraints.clear(); + } reachability_constraints.push(reachability_constraint); } else { last_condition_flow_snapshots = self.take_condition_flow_snapshots(value); @@ -3838,13 +3839,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { condition_flow_snapshots.into_short_circuit_and_continuation(*op); (Some(short_circuit), Some(no_short_circuit)) } else { - ( - None, - values - .iter() - .any(|value| any_over_expr(value, &ast::Expr::is_named_expr)) - .then(|| self.flow_snapshot()), - ) + (None, has_named_expression.then(|| self.flow_snapshot())) }; if let Some(last_short_circuit) = last_short_circuit { diff --git a/crates/ty_python_core/src/expression.rs b/crates/ty_python_core/src/expression.rs index c766f22e0b..7ab9a85b49 100644 --- a/crates/ty_python_core/src/expression.rs +++ b/crates/ty_python_core/src/expression.rs @@ -91,7 +91,7 @@ pub enum ExpressionContext { pub enum ExpressionKind { /// An ordinary value expression, such as `1` in `self.x: int = 1`. Normal, - /// A predicate whose truthiness is recorded in its evaluation context. + /// A predicate evaluated in value or condition context. Predicate { context: ExpressionContext, in_loop: bool, diff --git a/crates/ty_python_core/src/use_def.rs b/crates/ty_python_core/src/use_def.rs index 619df20ddd..185101117f 100644 --- a/crates/ty_python_core/src/use_def.rs +++ b/crates/ty_python_core/src/use_def.rs @@ -2193,7 +2193,8 @@ impl<'db> UseDefMapBuilder<'db> { } pub(super) fn add_place(&mut self, place: ScopedPlaceId) { - self.checkpoint_state.record_binding_change(); + // Earlier snapshots restore new places as undefined. The first definition + // changes the bindings visible to an exception handler. match place { ScopedPlaceId::Symbol(symbol) => { let new_place = self.symbol_states.push(PendingPlaceState::new( diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index af267bcdbd..36169df119 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_semantic" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_semantic/README.md b/crates/ty_python_semantic/README.md index 17791cab99..617c0f18e6 100644 --- a/crates/ty_python_semantic/README.md +++ b/crates/ty_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_python_semantic). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_semantic/resources/corpus/ty_4834_predicate_truthiness_cycle.py b/crates/ty_python_semantic/resources/corpus/ty_4834_predicate_truthiness_cycle.py new file mode 100644 index 0000000000..777246eb46 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/ty_4834_predicate_truthiness_cycle.py @@ -0,0 +1,11 @@ +# Regression test for https://github.com/astral-sh/ty/issues/4834 + +class C("", (lambda: value, (predicate := lambda: value))): + pass + +# Read the lambda before the loop adds the class as another possible value of predicate. +predicate +while True: + assert predicate + value = 0 + predicate = C diff --git a/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md b/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md index 6286b718a4..675ddad6a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md +++ b/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md @@ -319,6 +319,15 @@ def nested_impossible_operands(other: object, value: bool, marker: int): reveal_type(marker) # revealed: Never ``` +An assignment expression can rebind a name tested earlier in the chain. Reaching the final operand +requires the new value to be `None`, so the branch is unreachable. + +```py +def reassigned_operand(value: int | None, replacement: int | None, marker: int): + if value is not None and (value := replacement) is None and value is None and isinstance(value, int): + reveal_type(marker) # revealed: Never +``` + ## Conditions with aliased `Never` operands A call cannot produce a result when its return type is an alias of `Never`. Only the preceding diff --git a/crates/ty_python_semantic/resources/mdtest/external/django.md b/crates/ty_python_semantic/resources/mdtest/external/django.md index c565586f31..eeb7738afa 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/django.md +++ b/crates/ty_python_semantic/resources/mdtest/external/django.md @@ -389,4 +389,156 @@ def _(article: NewsArticle): reveal_type(Article.get_previous_by_published(article)) # revealed: NewsArticle ``` +## Abstract models + +### Basic + +An abstract model provides reusable fields without creating its own database table. Its concrete +subclasses receive an integer primary key unless they declare or inherit an explicit primary key. + +```py +from django.db import models + +class AbstractBook(models.Model): + title = models.CharField(max_length=200) + + class Meta: + abstract = True + +class Ebook(AbstractBook): + file_size = models.IntegerField() + +class Book(AbstractBook): + isbn = models.CharField(max_length=13, primary_key=True) + +def _(abstract: AbstractBook, ebook: Ebook, book: Book): + # Abstract models do not have an automatic primary key. + abstract.id # error: [unresolved-attribute] + + # Concrete models without an explicit primary key have an automatic integer key. + reveal_type(ebook.id) # revealed: int + reveal_type(ebook.pk) # revealed: int + reveal_type(ebook.title) # revealed: str + reveal_type(ebook.file_size) # revealed: int + + # The explicit ISBN primary key prevents an automatic `id`. + book.id # error: [unresolved-attribute] + reveal_type(book.pk) # revealed: str + reveal_type(book.title) # revealed: str + reveal_type(book.isbn) # revealed: str +``` + +### Annotated abstract setting + +A bare annotation for `Meta.abstract` does not make a model abstract. + +```py +from django.db import models + +class Book(models.Model): + class Meta: + abstract: bool + +def _(book: Book): + reveal_type(book.id) # revealed: int + reveal_type(book.pk) # revealed: int +``` + +When an assignment follows the annotation, its value determines whether the model is abstract. + +```py +class AbstractBook(models.Model): + class Meta: + abstract: bool + abstract = True + +def _(abstract: AbstractBook): + abstract.id # error: [unresolved-attribute] +``` + +### Inherited Meta configuration + +Django resets an abstract model's `Meta.abstract` to `False`. A child that subclasses this `Meta` is +concrete unless it explicitly sets `abstract = True` again. + +```py +from django.db import models + +class AbstractBook(models.Model): + class Meta: + abstract = True + +class NotAbstract(AbstractBook): + # error: [invalid-attribute-override] "Meta" + class Meta(AbstractBook.Meta): + ordering = ["id"] + +class Abstract(AbstractBook): + # error: [invalid-attribute-override] "Meta" + class Meta(AbstractBook.Meta): + abstract = True + +def _(not_abstract: NotAbstract, abstract: Abstract): + # concrete models have an automatic primary key + not_abstract.id + + # abstract models do not: + abstract.id # error: [unresolved-attribute] +``` + +Configuration inherited from an ordinary class retains its `abstract` setting. + +```py +class Options: + abstract = True + +class InheritedAbstract(models.Model): + class Meta(Options): + pass + +def _(abstract: InheritedAbstract): + abstract.id # error: [unresolved-attribute] +``` + +### Multiple inheritance + +An abstract base without a primary key does not shadow an inherited `id`, regardless of the base +class order. Regression test for . + +```py +from uuid import uuid4 +from django.db import models + +class AbstractHasTitle(models.Model): + title = models.CharField(max_length=200) + + class Meta: + abstract = True + +class AbstractHasId(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4) + + class Meta: + abstract = True + +# Django allows separate `Meta` configurations, but ty currently reports a conflict between them. +# error: [invalid-attribute-override] "Incompatible inherited attribute `Meta`" +class Book1(AbstractHasTitle, AbstractHasId): + pass + +# (see above) +# error: [invalid-attribute-override] "Incompatible inherited attribute `Meta`" +class Book2(AbstractHasId, AbstractHasTitle): + pass + +def _(book1: Book1, book2: Book2): + reveal_type(book1.id) # revealed: UUID + reveal_type(book1.pk) # revealed: UUID + reveal_type(book1.title) # revealed: str + + reveal_type(book2.id) # revealed: UUID + reveal_type(book2.pk) # revealed: UUID + reveal_type(book2.title) # revealed: str +``` + [django-types]: https://github.com/sbdchd/django-types diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 63c54a9a37..9a239c8ab8 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -1943,6 +1943,21 @@ for _ in range(1_000_000): x = 2 ``` +The condition can depend on an earlier iteration. Here `y` starts at `2` and only ever receives `1` +or `2` from `x`, so the loop never decrements `x`. + +```py +def f(n: int): + x = 1 + y = 2 + for _ in range(n): + if not y: + x, y = x - 1, x + else: + x, y = 2, x + reveal_type(x) # revealed: Literal[1, 2] +``` + ### Large reachability constraint graphs fall back to `Unknown` ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 147373c3f4..e32441a6ca 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -864,6 +864,29 @@ def loop_back(): value = Integer.Y ``` +The comparison narrows a `str | int` to the matching builtin type. A plain string can equal +`String.X` and reach the `.name` access. + +```py +def compare_with_enum_member(value: str | int): + if value == String.X: + reveal_type(value) # revealed: str + value.name # error: [unresolved-attribute] + + if Integer.X == value: + reveal_type(value) # revealed: int +``` + +Members of other `IntEnum` classes can also compare equal to `Integer.X`. + +```py +def compare_int_enums(value: IntEnum): + if value != Integer.X: + reveal_type(value) # revealed: IntEnum & ~Literal[Integer.X] + else: + reveal_type(value) # revealed: IntEnum +``` + A custom `__new__` can replace the value declared in an `IntEnum` class body. We can still narrow the members of `Foo`, whose runtime values are known, but must preserve all of `Shifted` because its members' runtime values cannot be determined statically: @@ -1501,6 +1524,27 @@ def correlated_materialized_pattern(left: Top[MaterializedT], right: Materialize return 1 ``` +## `LiteralString` and string literals + +A comparison can narrow a `LiteralString` operand to a particular value or exclude that value. The +concrete string keeps its type in either branch. + +```py +from typing_extensions import LiteralString + +def compare(other: LiteralString): + value = "a" + if value != other: + reveal_type(value) # revealed: Literal["a"] + reveal_type(other) # revealed: LiteralString & ~Literal["a"] + value + 1 # error: [unsupported-operator] + + if other == value: + reveal_type(other) # revealed: Literal["a"] + else: + reveal_type(value) # revealed: Literal["a"] +``` + ## `LiteralString` and string-valued enums `LiteralString` can be narrowed by comparison with a string-valued enum member that inherits `str`'s @@ -1517,6 +1561,7 @@ from typing_extensions import LiteralString class Color(StrEnum): RED = "red" + BLUE = "blue" def narrow_literal_string_with_enum(value: LiteralString | None): if value == Color.RED: @@ -1530,6 +1575,15 @@ def narrow_literal_string_with_enum(value: LiteralString | None): reveal_type(value) # revealed: Literal["red"] ``` +The enum member keeps its type when its value differs from the `LiteralString`. + +```py +def compare_member(other: LiteralString): + color = Color.RED + if color != other: + reveal_type(color) # revealed: Literal[Color.RED] +``` + ## Module literals Modules compare equal only to the same module object: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index c814fdc39c..fe04661a36 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -1442,6 +1442,19 @@ def _(colors: list[Color]): reveal_type(inline) # revealed: list[Color] ``` +Membership succeeds for the plain integer `1` as well as for the enum member `Code.OK`. + +```py +from enum import IntEnum + +class Code(IntEnum): + OK = 1 + +def check_membership(value: int): + if value in (Code.OK,): + reveal_type(value) # revealed: int +``` + An enum that can have additional runtime members can still be narrowed by a membership test against an explicit member. The other branch excludes that member without assuming that the declared members are exhaustive. diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index f4a7008462..3636bb95e3 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -2183,6 +2183,32 @@ def test_match_mapping_uses_get(value: CustomGet) -> None: reveal_type(item) # revealed: object ``` +Narrowing a mapping preserves the type of its entries when it still uses the standard `get` method. +A truthiness check can exclude empty dictionaries, and an `isinstance` check can add another class +to the subject's type. + +```py +def match_nonempty_dict(value: dict[str, int]) -> None: + if value: + match value: + case {"item": item}: + reveal_type(item) # revealed: int + +class Tagged: ... + +def match_tagged_mapping(value: Mapping[str, bytes]) -> None: + if isinstance(value, Tagged): + match value: + case {"item": item}: + reveal_type(item) # revealed: bytes + +def match_nonempty_custom_get(value: CustomGet) -> None: + if value: + match value: + case {"item": item}: + reveal_type(item) # revealed: object +``` + A callable stored on the instance can also provide `get`. The captured value still has type `object`. @@ -4032,6 +4058,12 @@ class Color(StrEnum): GREEN = "g" BLUE = "b" +def match_string(value: str): + match value: + case Color.GREEN as captured: + reveal_type(value) # revealed: str + reveal_type(captured) # revealed: str + def test_literal_as_enum(x: Literal["g"]) -> None: match x: case Color.RED: diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index cf7c465df9..902c624089 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -399,6 +399,31 @@ static_assert(is_assignable_to(type[Any], type[Arbitrary])) static_assert(is_assignable_to(type[Any], type[object])) ``` +### Narrowed instances with a gradual base + +A class with a gradual base can match a union arm outside its known inheritance hierarchy, even +after narrowing rules out another type. + +```py +from typing import Any +from missing import UnknownBase # error: [unresolved-import] +from ty_extensions import Intersection, Not, static_assert +from ty_extensions._internal import is_assignable_to + +class A: ... +class B: ... +class FromAny(Any): ... +class Mixed(A, UnknownBase): ... + +static_assert(is_assignable_to(Intersection[FromAny, Not[int]], A | B)) +``` + +The first arm excludes `Mixed` itself, but its unknown base can still match `B`. + +```py +static_assert(is_assignable_to(Intersection[Mixed, Not[int]], Intersection[A, Not[Mixed]] | B)) +``` + ## Heterogeneous tuple types ```py diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index 1f24d98bfc..1b60c67d1b 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -349,6 +349,124 @@ def f( reveal_type(h) # revealed: tuple[Any | str, ...] ``` +## Tuple unions with narrowed built-in elements + +A tuple alternative is redundant when its element type is covered by another alternative, including +when that element is a built-in type narrowed by an exclusion. The `float` annotation includes +`int`, so it also covers `int & ~bool`: + +```py +from ty_extensions import Intersection, Not + +def numeric(value: tuple[Intersection[int, Not[bool]]] | tuple[float]): + reveal_type(value) # revealed: tuple[float] +``` + +## Tuple unions with built-in exclusions + +Matching the positive class is not enough to establish redundancy: its exclusions also matter. For +example, a subclass of both `Marker` and `str` can inhabit the first tuple alternative below, but +neither branch of the second: + +```py +from ty_extensions import Intersection, Not + +class Marker: ... + +def different_exclusions( + value: tuple[Intersection[Marker, Not[int]]] | tuple[Intersection[Marker, Not[str]] | bytes], +): + reveal_type(value) # revealed: tuple[Marker & ~int] | tuple[(Marker & ~str) | bytes] +``` + +## Tuple unions with different exclusions on the same base + +`Base & ~Y` covers `Child & ~Y`, so adding the child's tuple leaves the union unchanged. Excluding a +third class, or starting from an unrelated class, leaves an additional tuple in the union. + +```py +from ty_extensions import Intersection, Not + +class Base: ... +class Child(Base): ... +class X: ... +class Y: ... +class Z: ... + +def compare( + flag: bool, + target: tuple[Intersection[Base, Not[X]] | Intersection[Base, Not[Y]]], + covered: tuple[Intersection[Child, Not[Y]]], + different_exclusion: tuple[Intersection[Child, Not[Z]]], + unrelated: tuple[Intersection[Z, Not[X]]], +): + reveal_type(covered if flag else target) # revealed: tuple[(Base & ~X) | (Base & ~Y)] + reveal_type(different_exclusion if flag else target) # revealed: tuple[Child & ~Z] | tuple[(Base & ~X) | (Base & ~Y)] + reveal_type(unrelated if flag else target) # revealed: tuple[Z & ~X] | tuple[(Base & ~X) | (Base & ~Y)] +``` + +## Tuple unions with multiple inheritance + +Either base of `Both` can cover the narrower tuple, depending on which type its element excludes. A +`Left` that excludes `str` can still be an `int` outside `Right`, so its tuple stays in the union. + +```py +from ty_extensions import Intersection, Not + +class Left: ... +class Right: ... +class Both(Left, Right): ... + +def compare( + flag: bool, + target: tuple[Intersection[Left, Not[int]] | Intersection[Right, Not[str]]], + via_left: tuple[Intersection[Both, Not[int]]], + via_right: tuple[Intersection[Both, Not[str]]], + left_excluding_str: tuple[Intersection[Left, Not[str]]], +): + reveal_type(via_left if flag else target) # revealed: tuple[(Left & ~int) | (Right & ~str)] + reveal_type(via_right if flag else target) # revealed: tuple[(Left & ~int) | (Right & ~str)] + reveal_type(left_excluding_str if flag else target) # revealed: tuple[Left & ~str] | tuple[(Left & ~int) | (Right & ~str)] +``` + +## Tuple unions with a base of `Any` + +Inheriting from `Any` preserves the distinction between a known subclass and an unrelated class when +simplifying tuple unions. + +```py +from typing import Any +from ty_extensions import Intersection, Not + +class Base(Any): ... +class Child(Base): ... + +def compare( + unrelated: tuple[Intersection[Base, Not[int]]] | tuple[str | bytes], + inherited: tuple[Intersection[Child, Not[int]]] | tuple[Intersection[Base, Not[int]] | bytes], +): + reveal_type(unrelated) # revealed: tuple[Base & ~int] | tuple[str | bytes] + reveal_type(inherited) # revealed: tuple[(Base & ~int) | bytes] +``` + +## Tuple unions with a recursive generic base + +`Node & ~Excluded` covers `Child & ~Excluded` even when `Node` appears in its own base class +specialization. + +```py +from ty_extensions import Intersection, Not + +class Node(list["Node"]): ... +class Child(Node): ... +class Excluded: ... + +def recursive( + value: tuple[Intersection[Child, Not[Excluded]]] | tuple[Intersection[Node, Not[Excluded]] | str], +): + reveal_type(value) # revealed: tuple[(Node & ~Excluded) | str] +``` + ## Unions of other generic containers ```toml diff --git a/crates/ty_python_semantic/src/types/dedicated/django.rs b/crates/ty_python_semantic/src/types/dedicated/django.rs index e0a09b0692..0473f5ba96 100644 --- a/crates/ty_python_semantic/src/types/dedicated/django.rs +++ b/crates/ty_python_semantic/src/types/dedicated/django.rs @@ -3,10 +3,11 @@ use ruff_python_ast::name::Name; use ty_module_resolver::KnownModule; use ty_python_core::{ definition::{Definition, DefinitionKind}, - place_table, use_def_map, + place_table, semantic_index, use_def_map, }; -use crate::place::known_module_symbol; +use crate::place::{known_module_symbol, place_from_bindings}; +use crate::types::infer::nearest_enclosing_class; use crate::types::member::class_member; use crate::types::{ BindingContext, BoundTypeVarInstance, ClassBase, GenericContext, KnownClass, Parameter, @@ -149,6 +150,71 @@ fn own_model_fields<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> Box .collect() } +/// Metadata extracted from a Django model's `Meta` configuration. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +struct ModelMetadata { + is_abstract: bool, +} + +#[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] +fn model_metadata<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> ModelMetadata { + let env = ProgramEnvironment::from_scope(class.body_scope(db)); + // Subclasses do not inherit `abstract = True`, so only inspect this model's own Meta. + let Some(meta) = own_meta(db, class) else { + return ModelMetadata::default(); + }; + + for meta_base in meta.iter_mro(db, None).filter_map(ClassBase::into_class) { + let Some((meta_base, _)) = meta_base.static_class_literal(db) else { + return ModelMetadata::default(); + }; + // Django resets an abstract model's Meta.abstract to False during model creation. + // A child can inherit other options from that Meta, but must set abstract=True itself. + let enclosing_scope = meta_base.definition(db).scope(db); + if let Some(owner) = nearest_enclosing_class( + db, + semantic_index(db, enclosing_scope.program_file(db)), + enclosing_scope, + ) && owner != class + && is_model(db, owner) + && own_meta(db, owner) == Some(meta_base) + { + return ModelMetadata::default(); + } + + let scope = meta_base.body_scope(db); + let Some(symbol) = place_table(db, scope).symbol_id("abstract") else { + continue; + }; + // The public type is widened to `bool`, so read the bindings' literal values instead. + let Some(abstract_flag) = place_from_bindings( + db, + &env, + use_def_map(db, scope).end_of_scope_symbol_bindings(symbol), + ) + .place + .ignore_possibly_undefined() else { + continue; + }; + return ModelMetadata { + is_abstract: abstract_flag == Type::bool_literal(true), + }; + } + ModelMetadata::default() +} + +fn own_meta<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, +) -> Option> { + let meta = class_member(db, class.body_scope(db), "Meta") + .ignore_possibly_undefined()? + .as_class_literal()? + .as_static()?; + (meta.name(db).as_str() == "Meta" && meta.definition(db).scope(db) == class.body_scope(db)) + .then_some(meta) +} + /// Provides synthesized attributes and methods for Django models. pub(in crate::types) fn synthesized_member<'db>( db: &'db dyn Db, @@ -226,6 +292,11 @@ fn primary_key_member<'db>( } } + // Abstract models do not have an automatic primary key. + if model_metadata(db, class).is_abstract { + return None; + } + known_module_symbol(db, env, KnownModule::DjangoDbModelsFields, "AutoField") .place .ignore_possibly_undefined()? diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index f5a1cf5566..30ca71d9d0 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -54,7 +54,7 @@ enum ComparisonResult<'db> { /// The comparison allows the operand being constrained to be narrowed to the wrapped type. /// /// For example, if an object of type `LiteralString` compares equal to an object of type - /// `Literal["foo"]`, the equality branch can safely narrow either operand to `Literal["foo"]`. + /// `Literal["foo"]`, the equality branch narrows the `LiteralString` operand to `Literal["foo"]`. CanNarrow(Type<'db>), /// The comparison may evaluate to true or false, depending on runtime values. @@ -743,10 +743,8 @@ fn evaluate_structural_comparison<'db>( None => narrow_literal_comparison( db, env, - left, - right, - left_literal.kind(), - right_literal.kind(), + left_literal, + right_literal, operator.condition_expects_equality(branch), ), } @@ -1434,30 +1432,22 @@ fn finite_alternatives<'db>( /// Return a constraint for literal pairs whose equality cannot be decided statically. /// -/// This primarily handles `LiteralString`, which can be constrained by a concrete string literal -/// or a string-valued enum member without having a single statically known runtime value. +/// A concrete string or string-valued enum member can constrain a `LiteralString` operand. +/// In the reverse direction, the unknown string value places no constraint on the concrete operand. fn narrow_literal_comparison<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, - left: Type<'db>, - right: Type<'db>, - left_literal: LiteralValueTypeKind<'db>, - right_literal: LiteralValueTypeKind<'db>, + left: LiteralValueType<'db>, + right: LiteralValueType<'db>, equality_is_positive: bool, ) -> ComparisonResult<'db> { - match (left_literal, right_literal) { + match (left.kind(), right.kind()) { (LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::String(_)) => { - ComparisonResult::CanNarrow(right.negate_if(db, env, !equality_is_positive)) - } - (LiteralValueTypeKind::String(_), LiteralValueTypeKind::LiteralString) => { - ComparisonResult::CanNarrow(left.negate_if(db, env, !equality_is_positive)) + ComparisonResult::CanNarrow(Type::from(right).negate_if(db, env, !equality_is_positive)) } (LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::Enum(enum_literal)) => { narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive) } - (LiteralValueTypeKind::Enum(enum_literal), LiteralValueTypeKind::LiteralString) => { - narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive) - } _ => ComparisonResult::Ambiguous, } } @@ -1540,7 +1530,20 @@ fn compare_literal_to_other<'db>( }; let condition_expects_equality = operator.condition_expects_equality(branch); - // Treat broad builtin types as if only the literal itself can compare equal. This is + let literal_constraint = || { + if !condition_expects_equality { + ComparisonResult::CanNarrow(literal_type.negate(db, env)) + } else if matches!(literal, LiteralValueTypeKind::Enum(_)) + && literal_semantics != KnownComparisonSemantics::Object + { + // A plain string can compare equal to a `StrEnum` member. + ComparisonResult::Ambiguous + } else { + ComparisonResult::CanNarrow(literal_type) + } + }; + + // Treat broad builtin types as if only the builtin literal itself can compare equal. This is // intentionally unsafe: subclasses, including `bool` for `int`, can compare equal without // inhabiting the literal type. Explicitly typed subclasses do not take this path. if evaluator.soundness_policy.allow_unsafe_equality @@ -1549,7 +1552,7 @@ fn compare_literal_to_other<'db>( && let Some(other_semantics) = unsafe_narrowable_builtin_semantics(db, other) { return if literal_semantics == other_semantics { - ComparisonResult::CanNarrow(literal_type) + literal_constraint() } else { operator.result_from_equality(false) }; @@ -1574,17 +1577,12 @@ fn compare_literal_to_other<'db>( if literal_operand == LiteralOperand::Other && !other.is_disjoint_from(db, env, literal_type) => { - ComparisonResult::CanNarrow(literal_type.negate_if( - db, - env, - !condition_expects_equality, - )) + literal_constraint() } - Some(_) => ComparisonResult::Ambiguous, None if literal_operand == LiteralOperand::Other && !condition_expects_equality => { - ComparisonResult::CanNarrow(literal_type.negate(db, env)) + literal_constraint() } - None => ComparisonResult::Ambiguous, + _ => ComparisonResult::Ambiguous, } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index af62421e12..87f3aed618 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -45,7 +45,7 @@ use crate::ProgramEnvironment; use itertools::Either; -use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashMap; @@ -585,7 +585,7 @@ pub(crate) fn infer_expression_types<'db>( } /// A loop predicate can depend on bindings it guards in the previous iteration. -/// Reading inference directly keeps the extra truthiness query out of that cycle. +/// Evaluate it in the caller's query to keep an extra query out of that cycle. pub(crate) fn infer_predicate_truthiness<'db>( db: &'db dyn Db, expression: Expression<'db>, @@ -594,24 +594,38 @@ pub(crate) fn infer_predicate_truthiness<'db>( expression.kind(db), ExpressionKind::Predicate { in_loop: true, .. } ) { - recorded_predicate_truthiness(db, expression) + evaluate_predicate_truthiness(db, expression) } else { cached_predicate_truthiness(db, expression) } } -/// Keep reachability queries cached when an operand's type changes but its truthiness stays the same. +/// Reachability walks can revisit the same predicate for many different bindings. #[salsa::tracked( returns(copy), cycle_initial = |_, _, _| Truthiness::Ambiguous, heap_size = get_size2::GetSize::get_heap_size )] fn cached_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness { - recorded_predicate_truthiness(db, expression) + evaluate_predicate_truthiness(db, expression) } -fn recorded_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness { - infer_expression_types_impl(db, InferExpression::Bare(expression)).predicate_truthiness +/// A widened operand can require a new metaclass lookup, so evaluate truthiness after +/// expression inference has normalized the operand types. +fn evaluate_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness { + let ExpressionKind::Predicate { context, .. } = expression.kind(db) else { + return Truthiness::Ambiguous; + }; + let inference = infer_expression_types(db, expression, TypeContext::default()); + let env = ProgramEnvironment::from_scope(expression.scope(db)); + let module = parsed_module(db, expression.python_file(db)).load(db); + TruthinessAnalyzer::new( + db, + &env, + |node| inference.expression_type(node), + |node| inference.comparison_truthiness(node), + ) + .truthiness(expression.node_ref(db).node(&module), context) } #[salsa::tracked( @@ -620,7 +634,7 @@ fn recorded_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'d cycle_fn=|db, cycle, previous: &ExpressionInference<'db>, inference: ExpressionInference<'db>, input: InferExpression<'db>| { let (expression, _) = input.into_inner(db); let env = ProgramEnvironment::from_scope(expression.scope(db)); - inference.cycle_normalized(db, &env, previous, cycle, input) + inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -645,7 +659,7 @@ pub(super) fn infer_expression_types_impl<'db>( let env = ProgramEnvironment::from_file(program_file); - let mut inference = TypeInferenceBuilder::new( + TypeInferenceBuilder::new( db, &env, InferenceRegion::Expression(expression, tcx), @@ -654,9 +668,7 @@ pub(super) fn infer_expression_types_impl<'db>( index, &module, ) - .finish_expression(); - inference.record_predicate_truthiness(db, &env, input, Some(module)); - inference + .finish_expression() } fn expression_cycle_initial<'db>( @@ -2102,9 +2114,6 @@ pub(crate) struct ExpressionInference<'db> { /// The types of every expression in this region. expressions: FrozenMap>, - /// The predicate's possible outcomes in its evaluation context. - predicate_truthiness: Truthiness, - extra: Option>>, /// The scope this region is part of. @@ -2173,31 +2182,6 @@ struct ExpressionInferenceExtra<'db> { } impl<'db> ExpressionInference<'db> { - /// Reachability always asks for a predicate's truthiness without an expected type. - fn record_predicate_truthiness( - &mut self, - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - input: InferExpression<'db>, - module: Option, - ) { - let InferExpression::Bare(expression) = input else { - return; - }; - let ExpressionKind::Predicate { context, .. } = expression.kind(db) else { - return; - }; - let module = - module.unwrap_or_else(|| parsed_module(db, expression.python_file(db)).load(db)); - self.predicate_truthiness = TruthinessAnalyzer::new( - db, - env, - |node| self.expression_type(node), - |node| self.comparison_truthiness(node), - ) - .truthiness(expression.node_ref(db).node(&module), context); - } - fn cycle_initial(scope: ScopeId<'db>, cycle_recovery: Type<'db>) -> Self { let _ = scope; Self { @@ -2205,7 +2189,6 @@ impl<'db> ExpressionInference<'db> { cycle_recovery: Some(cycle_recovery), ..ExpressionInferenceExtra::default() })), - predicate_truthiness: Truthiness::Ambiguous, expressions: FrozenMap::default(), #[cfg(debug_assertions)] scope, @@ -2218,7 +2201,6 @@ impl<'db> ExpressionInference<'db> { env: &ProgramEnvironment<'db>, previous: &ExpressionInference<'db>, cycle: &salsa::Cycle, - input: InferExpression<'db>, ) -> ExpressionInference<'db> { if let Some(extra) = self.extra.as_mut() { for (binding, binding_ty) in &mut extra.bindings { @@ -2244,15 +2226,6 @@ impl<'db> ExpressionInference<'db> { *ty = ty.cycle_normalized(db, env, previous_ty, cycle); } - // Normalizing the operands can change whether the predicate completes. Once the first - // provisional results have cleared, retain earlier outcomes so the predicate converges. - self.record_predicate_truthiness(db, env, input, None); - if cycle.iteration() > crate::TAINTED_CYCLES { - self.predicate_truthiness = previous - .predicate_truthiness - .union(self.predicate_truthiness); - } - if cycle.iteration() > crate::TAINTED_CYCLES && let Some(previous_extra) = previous.extra.as_deref() && !previous_extra.collection_use_constraints.is_empty() diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6efdfd4313..8cbd494a19 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -12981,7 +12981,6 @@ impl<'db> FullExpressionCacheEntry<'db> { }); ExpressionInference { - predicate_truthiness: Truthiness::Ambiguous, expressions: FrozenMap::from(self.expressions), extra, #[cfg(debug_assertions)] diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 65bf0ed577..d2acdc23f7 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -574,7 +574,6 @@ fn comparison_truthiness_widens_across_sparse_cycle_results() -> anyhow::Result< let inference = |ty, truthiness: Option| { ( ExpressionInference { - predicate_truthiness: Truthiness::Ambiguous, expressions: [(expression, ty)].into_iter().collect(), extra: truthiness.map(|truthiness| { Box::new(ExpressionInferenceExtra { diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 77c126fe2e..abe4919f34 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -2880,7 +2880,7 @@ impl<'db, 'pattern> PatternSuccessAnalyzer<'db, 'pattern> { else { return Some(Type::unknown()); }; - let default_ty = if self.mapping_pattern_uses_standard_get(subject_ty) { + let default_ty = if self.is_standard_mapping_get(get_method) { mapping_value_ty } else { Type::object() @@ -2897,40 +2897,21 @@ impl<'db, 'pattern> PatternSuccessAnalyzer<'db, 'pattern> { ) } - fn mapping_pattern_uses_standard_get(&self, subject_ty: Type<'db>) -> bool { + /// Identify `dict.get` and `Mapping.get` by their definitions, even when the receiver + /// has been narrowed to an intersection and no longer has a single nominal class. + fn is_standard_mapping_get(&self, get_method: Type<'db>) -> bool { let db = self.db; - let Some(class) = subject_ty.nominal_class(db, &self.env) else { + let Type::BoundMethod(method) = get_method else { return false; }; - for base in class.iter_mro(db) { - let class = match base { - ClassBase::Class(class) => class, - ClassBase::Generic | ClassBase::Protocol => continue, - ClassBase::Any - | ClassBase::Dynamic(_) - | ClassBase::Divergent(_) - | ClassBase::TypedDict(_) => { - return false; - } - }; - if !class - .own_instance_member(db, &self.env, "get") - .is_undefined() - { - return false; - } - if class - .own_class_member(db, &self.env, None, "get") - .is_undefined() - { - continue; - } - return matches!( - class.known(db), - Some(KnownClass::Dict | KnownClass::Mapping) - ); - } - false + let Some(function) = method.function(db) else { + return false; + }; + function.name(db) == "get" + && [KnownClass::Dict, KnownClass::Mapping] + .into_iter() + .filter_map(|class| class.try_to_class_literal(db, &self.env)) + .any(|class| class.body_scope(db) == function.definition(db).scope(db)) } fn mapping_pattern_key_types(&self, kind: &MappingPatternPredicateKind<'db>) -> Vec> { diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 512b8faf71..6693361fc6 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use std::cell::RefCell; use itertools::Itertools; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use ty_python_core::definition::Definition; use crate::place::{DefinedPlace, Place}; @@ -35,7 +35,7 @@ use crate::types::{ TypeVarBoundOrConstraints, UnionType, UpcastPolicy, }; use crate::{ - Db, FxOrderSet, + Db, FxOrderSet, Program, types::{ ErrorContext, ErrorContextTree, Type, TypePair, constraints::ConstraintSet, typevar::TypeVarSet, @@ -254,6 +254,71 @@ fn has_missing_generic_nominal_bound<'db>( }) } +/// Return the class of a nominal instance with no generic specialization or tuple +/// specification of its own, provided the class is defined by a `class` statement. +fn non_generic_nominal_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + let Type::NominalInstance(instance) = ty else { + return None; + }; + if instance.own_tuple_spec(db).is_some() { + return None; + } + let ClassType::NonGeneric(class @ ClassLiteral::Static(_)) = instance.class(db, env) else { + return None; + }; + Some(class) +} + +/// Recognize a single nominal positive and nominal exclusions, without alias, type-variable, or +/// enum-literal operands. These classes can still inherit specialized or recursive generic bases. +fn nominal_intersection_bound<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + let positive = if let Type::Intersection(intersection) = ty { + if intersection.positive(db).len() != 1 + || intersection + .negative(db) + .iter() + .any(|&negative| non_generic_nominal_class(db, env, negative).is_none()) + { + return None; + } + *intersection.positive(db).iter().next()? + } else { + ty + }; + non_generic_nominal_class(db, env, positive) +} + +/// Index union alternatives by their required nominal class before checking their exclusions. +#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] +fn nominal_union_index<'db>( + db: &'db dyn Db, + program: Program<'db>, + union: UnionType<'db>, +) -> Option, Box<[Type<'db>]>>> { + let env = ProgramEnvironment::from_program(program); + let mut index: FxHashMap<_, Vec<_>> = FxHashMap::default(); + for &element in union.elements(db) { + index + .entry(nominal_intersection_bound(db, &env, element)?) + .or_default() + .push(element); + } + let mut index: FxHashMap<_, _> = index + .into_iter() + .map(|(class, elements)| (class, elements.into_boxed_slice())) + .collect(); + index.shrink_to_fit(); + Some(index) +} + #[salsa::tracked] impl<'db> Type<'db> { /// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for @@ -1400,6 +1465,43 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { return self.check_type_pair(db, alternatives, target); } + // Fast path for pattern narrowing, which can compare many intersections against the + // same wide union. If the source intersection and every arm require a single nominal + // class and have only nominal exclusions, the cached index lets us skip arms the + // source cannot match. Keep the full traversal when collecting error context for each arm. + if source.is_intersection() + && !self.is_context_collection_enabled() + && let Some(source_class) = nominal_intersection_bound(db, self.env, source) + && let Some(index) = nominal_union_index(db, self.env.program(db), union) + // A gradual base can make the source assignable to a class outside its known MRO. + && !(self.relation.is_assignability() + && source_class.iter_mro(db).any(|base| { + matches!(base, ClassBase::Any | ClassBase::Dynamic(_) | ClassBase::Divergent(_)) + })) + { + // A successful alternative needs the same nominal class or one of its bases. + // Check class identity separately: an unspecialized generic class acquires default + // arguments in its MRO. The full relation still checks each alternative's exclusions. + let mut matching = std::iter::once(source_class) + .chain(source_class.iter_mro(db).filter_map(|base| match base { + ClassBase::Class(ClassType::NonGeneric(class)) if class != source_class => { + Some(class) + } + _ => None, + })) + .filter_map(|class| index.get(&class)); + let Some(elements) = matching.next() else { + return self.never(); + }; + // One group preserves the target union's comparison order. If several bounds match, + // use the original traversal so recursive inherited specializations keep that order. + if matching.next().is_none() { + return elements.iter().when_any(db, self.constraints, |&element| { + self.check_type_pair(db, source, element) + }); + } + } + let check_expanded_source = || { // Normally non-unions cannot directly contain unions in our model due to the fact that // we enforce a DNF structure on our set-theoretic types. However, it *is* possible for diff --git a/crates/ty_site_packages/Cargo.toml b/crates/ty_site_packages/Cargo.toml index ec7b126e45..e6d8fca7bb 100644 --- a/crates/ty_site_packages/Cargo.toml +++ b/crates/ty_site_packages/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_site_packages" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_site_packages/README.md b/crates/ty_site_packages/README.md index ab9b82730a..9e44eaad38 100644 --- a/crates/ty_site_packages/README.md +++ b/crates/ty_site_packages/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_site_packages). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_site_packages). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_static/Cargo.toml b/crates/ty_static/Cargo.toml index d8f9429cd5..ccf1c2debd 100644 --- a/crates/ty_static/Cargo.toml +++ b/crates/ty_static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_static" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ty_static/README.md b/crates/ty_static/README.md index d193bd0415..80533cc7b2 100644 --- a/crates/ty_static/README.md +++ b/crates/ty_static/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_static). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_static). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_vendored/Cargo.toml b/crates/ty_vendored/Cargo.toml index 70365fdbdf..b32733a925 100644 --- a/crates/ty_vendored/Cargo.toml +++ b/crates/ty_vendored/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_vendored" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index 8d55abc602..e0c59f243e 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -303,7 +303,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.17.2 + rev: v0.17.3 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/installation.md b/docs/installation.md index 7bd8a1c1c4..f5a774fafd 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,8 +5,8 @@ Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI. Ruff can be invoked directly with [`uvx`](https://docs.astral.sh/uv/): ```shell -uvx ruff@0.17.2 check # Lint all files in the current directory. -uvx ruff@0.17.2 format # Format all files in the current directory. +uvx ruff@0.17.3 check # Lint all files in the current directory. +uvx ruff@0.17.3 format # Format all files in the current directory. ``` Or installed with `uv` (recommended), `pip`, or `pipx`: diff --git a/docs/integrations.md b/docs/integrations.md index e14bedc2ac..138d71e48a 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -81,7 +81,7 @@ Add the following configuration to `.gitlab-ci.yml` to run `ruff format` in para stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.17.2-alpine + name: ghcr.io/astral-sh/ruff:0.17.3-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -107,7 +107,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.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check @@ -120,7 +120,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.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check @@ -134,7 +134,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.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 7629122ecc..2cd131b5d4 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -378,7 +378,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.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 8fa2170a36..0ade15d0c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.17.2" +version = "0.17.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" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 247973da6c..3b2341ca72 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.17.2" +version = "0.17.3" description = "" authors = ["Charles Marsh "] diff --git a/uv.lock b/uv.lock index 15aed65c4a..bd29f44d71 100644 --- a/uv.lock +++ b/uv.lock @@ -1529,7 +1529,7 @@ wheels = [ [[package]] name = "ruff" -version = "0.17.2" +version = "0.17.3" source = { editable = "." } [package.dev-dependencies]