Compare commits

..

35 Commits

Author SHA1 Message Date
David Peter
b19177cd6d Experiment: allow functions to be redefined (same signature) 2025-05-23 15:08:14 +02:00
InSync
a1399656c9 [ty] Fix binary intersection comparison inference logic (#18266)
## Summary

Resolves https://github.com/astral-sh/ty/issues/485.

`infer_binary_intersection_type_comparison()` now checks for all
positive members before concluding that an operation is unsupported for
a given intersection type.

## Test Plan

Markdown tests.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-05-23 12:55:17 +02:00
David Peter
6392dccd24 [ty] Add warning that docs are autogenerated (#18270)
## Summary

This is a practice I followed on previous projects. Should hopefully
further help developers who want to update the documentation.

The big downside is that it's annoying to see this *as a user of the
documentation* if you don't open the Markdown file in the browser. But
I'd argue that those files don't really follow the original Markdown
spirit anyway with all the inline HTML.
2025-05-23 09:58:16 +00:00
David Peter
93ac0934dd [ty] Type compendium (#18263)
## Summary

This is something I wrote a few months ago, and continued to update from
time to time. It was mostly written for my own education. I found a few
bugs while writing it at the time (there are still one or two TODOs in
the test assertions that are probably bugs). Our other tests are fairly
comprehensive, but they are usually structured around a certain
functionality or operation (subtyping, assignability, narrowing). The
idea here was to focus on individual *types and their properties*.

closes #197 (added `JustFloat` and `JustComplex` to `ty_extensions`).
2025-05-23 11:41:31 +02:00
David Peter
aae4482c55 [ty] Replace remaining knot.toml reference (#18269)
## Summary

Fix remaining `knot.toml` reference and replace it with `ty.toml`. This
change was probably still in flight while we renamed things.

## Test Plan

Added a second assertion which ensures that the config file has any
effect.
2025-05-23 10:44:46 +02:00
Alex Waygood
d02c9ada5d [ty] Do not carry the generic context of Protocol or Generic in the ClassBase enum (#17989)
## Summary

It doesn't seem to be necessary for our generics implementation to carry
the `GenericContext` in the `ClassBase` variants. Removing it simplifies
the code, fixes many TODOs about `Generic` or `Protocol` appearing
multiple times in MROs when each should only appear at most once, and
allows us to more accurately detect runtime errors that occur due to
`Generic` or `Protocol` appearing multiple times in a class's bases.

In order to remove the `GenericContext` from the `ClassBase` variant, it
turns out to be necessary to emulate
`typing._GenericAlias.__mro_entries__`, or we end up with a large number
of false-positive `inconsistent-mro` errors. This PR therefore also does
that.

Lastly, this PR fixes the inferred MROs of PEP-695 generic classes,
which implicitly inherit from `Generic` even if they have no explicit
bases.

## Test Plan

mdtests
2025-05-22 21:37:03 -04:00
Dylan
6c0a59ea78 Fix insider docs requirement syntax (#18265)
Attempting to fix the `mkdocs` workflow (maybe `uv` is more forgiving
than `pip` for the syntax in `requirements.txt`?)
2025-05-22 16:21:51 -05:00
Carl Meyer
0b181bc2ad Fix instance vs callable subtyping/assignability (#18260)
## Summary

Fix some issues with subtying/assignability for instances vs callables.
We need to look up dunders on the class, not the instance, and we should
limit our logic here to delegating to the type of `__call__`, so it
doesn't get out of sync with the calls we allow.

Also, we were just entirely missing assignability handling for
`__call__` implemented as anything other than a normal bound method
(though we had it for subtyping.)

A first step towards considering what else we want to change in
https://github.com/astral-sh/ty/issues/491

## Test Plan

mdtests

---------

Co-authored-by: med <medioqrity@gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-05-22 19:47:05 +00:00
Dylan
0397682f1f Bump 0.11.11 (#18259) 2025-05-22 13:09:44 -05:00
InSync
bcefa459f4 [ty] Rename call-possibly-unbound-method to possibly-unbound-implicit-call (#18017) 2025-05-22 15:25:51 +00:00
Brandt Bucher
91b7a570c2 [ty] Implement Python's floor division semantics for Literal ints (#18249)
Division works differently in Python than in Rust. If the result is
negative and there is a remainder, the division rounds down (instead of
towards zero). The remainder needs to be adjusted to compensate so that
`(lhs // rhs) * rhs + (lhs % rhs) == lhs`.

Fixes astral-sh/ty#481.
2025-05-22 10:42:29 -04:00
Micha Reiser
98da200d45 [ty] Fix server panic when calling system_mut (#18252) 2025-05-22 16:10:07 +02:00
Sumana Harihareswara
029085fa72 [ty] Clarify ty check output default in documentation. (#18246)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-22 15:24:58 +02:00
Denys Kyslytsyn
6df10c638e [pylint] Fix docs example that produced different output (PLW0603) (#18216) 2025-05-22 07:55:37 +02:00
Max Mynter
bdf488462a Preserve tuple parentheses in case patterns (#18147) 2025-05-22 07:52:21 +02:00
justin
01eeb2f0d6 [ty] Support frozen dataclasses (#17974)
## Summary
https://github.com/astral-sh/ty/issues/111

This PR adds support for `frozen` dataclasses. It will emit a diagnostic
with a similar message to mypy

Note: This does not include emitting a diagnostic if `__setattr__` or
`__delattr__` are defined on the object as per the
[spec](https://docs.python.org/3/library/dataclasses.html#module-contents)

## Test Plan
mdtest

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-22 00:20:34 -04:00
Alex Waygood
cb04343b3b [ty] Split invalid-base error code into two error codes (#18245) 2025-05-21 18:02:39 -04:00
Alex Waygood
02394b8049 [ty] Improve invalid-type-form diagnostic where a module-literal type is used in a type expression and the module has a member which would be valid in a type expression (#18244) 2025-05-21 15:38:56 -04:00
Alex Waygood
41463396cf [ty] Add a subdiagnostic if invalid-return-type is emitted on a method with an empty body on a non-protocol subclass of a protocol class (#18243) 2025-05-21 17:38:07 +00:00
David Peter
da4be789ef [ty] Ignore ClassVar declarations when resolving instance members (#18241)
## Summary

Make sure that the following definitions all lead to the same outcome
(bug originally noticed by @AlexWaygood)

```py
from typing import ClassVar

class Descriptor:
    def __get__(self, instance, owner) -> int:
        return 42

class C:
    a: ClassVar[Descriptor]
    b: Descriptor = Descriptor()
    c: ClassVar[Descriptor] = Descriptor()

reveal_type(C().a)  # revealed: int  (previously: int | Descriptor)
reveal_type(C().b)  # revealed: int
reveal_type(C().c)  # revealed: int
```

## Test Plan

New Markdown tests
2025-05-21 19:23:35 +02:00
Max Mynter
02fd48132c [ty] Don't warn yield not in function when yield is in function (#18008) 2025-05-21 18:16:25 +02:00
Alex Waygood
d37592175f [ty] Tell the user why we inferred the Python version we inferred (#18082) 2025-05-21 11:06:27 -04:00
Micha Reiser
cb9e66927e Run mypy primer on Cargo.lock changes (#18239) 2025-05-21 13:21:38 +02:00
Micha Reiser
76ab77fe01 [ty] Support import <namespace> and from <namespace> import module (#18137) 2025-05-21 07:28:33 +00:00
Carl Meyer
7b253100f8 switch the playground repo button to ty repo (#18228)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-21 06:35:13 +00:00
Carl Meyer
d098118e37 [ty] disable division-by-zero by default (#18220)
## Summary

I think `division-by-zero` is a low-value diagnostic in general; most
real division-by-zero errors (especially those that are less obvious to
the human eye) will occur on values typed as `int`, in which case we
don't issue the diagnostic anyway. Mypy and pyright do not emit this
diagnostic.

Currently the diagnostic is prone to false positives because a) we do
not silence it in unreachable code, and b) we do not implement narrowing
of literals from inequality checks. We will probably fix (a) regardless,
but (b) is low priority apart from division-by-zero.

I think we have many more important things to do and should not allow
false positives on a low-value diagnostic to be a distraction. Not
opposed to re-enabling this diagnostic in future when we can prioritize
reducing its false positives.

References https://github.com/astral-sh/ty/issues/443

## Test Plan

Existing tests.
2025-05-20 14:47:56 -04:00
Ramil Aleskerov
7917269d9a [ty] Add support for PyPy virtual environments (#18203)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-05-20 14:46:50 -04:00
Alex Waygood
e8d4f6d891 [ty] Ensure that a function-literal type is always equivalent to itself (#18227) 2025-05-20 14:11:03 -04:00
Alex Waygood
60b486abce [ty] Deeply normalize many types (#18222) 2025-05-20 11:41:26 -04:00
Dhruv Manilawala
32403dfb28 [ty] Avoid panicking when there are multiple workspaces (#18151)
## Summary

This PR updates the language server to avoid panicking when there are
multiple workspace folders passed during initialization. The server
currently picks up the first workspace folder and provides a warning and
a log message.

## Test Plan

<img width="1724" alt="Screenshot 2025-05-17 at 11 43 09"
src="https://github.com/user-attachments/assets/1a7ddbc3-198d-4191-a28f-9b69321e8f99"
/>
2025-05-20 20:53:23 +05:30
InSync
76ab3425d3 [ty] Integer indexing into bytes returns int (#18218)
## Summary

Resolves [#461](https://github.com/astral-sh/ty/issues/461).

ty was hardcoded to infer `BytesLiteral` types for integer indexing into
`BytesLiteral`. It will now infer `IntLiteral` types instead.

## Test Plan

Markdown tests.
2025-05-20 16:44:12 +02:00
हिमांशु
90ca0a4c13 add full option name in formatter warning (#18217) 2025-05-20 16:26:47 +02:00
Brent Westbrook
15dbfad265 Remove Checker::report_diagnostics (#18206)
Summary
--

I thought that emitting multiple diagnostics at once would be difficult
to port to a diagnostic construction model closer to ty's
`InferContext::report_lint`, so as a first step toward that, this PR
removes `Checker::report_diagnostics`.

In many cases I was able to do some related refactoring to avoid
allocating a `Vec<Diagnostic>` at all, often by adding a `Checker` field
to a `Visitor` or by passing a `Checker` instead of a `&mut
Vec<Diagnostic>`.

In other cases, I had to fall back on something like

```rust
for diagnostic in diagnostics {
    checker.report_diagnostic(diagnostic);
}
```

which I guess is a bit worse than the `extend` call in
`report_diagnostics`, but hopefully it won't make too much of a
difference.

I'm still not quite sure what to do with the remaining loop cases. The
two main use cases for collecting a sequence of diagnostics before
emitting any of them are:

1. Applying a single `Fix` to a group of diagnostics
2. Avoiding an earlier diagnostic if something goes wrong later

I was hoping we could get away with just a `DiagnosticGuard` that
reported a `Diagnostic` on drop, but I guess we will still need a
`DiagnosticGuardBuilder` that can be collected in these cases and
produce a `DiagnosticGuard` once we know we actually want the
diagnostics.

Test Plan
--

Existing tests
2025-05-20 10:00:06 -04:00
Vasco Schiavo
4f8a005f8f [flake8-simplify] enable fix in preview mode (SIM117) (#18208)
The PR add the `fix safety` section for rule `SIM117` (#15584 ), and
enable a fix in preview mode.
2025-05-20 08:34:50 -05:00
Micha Reiser
3b56c7ca3d Update salsa (#18212) 2025-05-20 09:19:34 +02:00
164 changed files with 4986 additions and 1391 deletions

View File

@@ -5,3 +5,4 @@
[rules]
possibly-unresolved-reference = "warn"
unused-ignore-comment = "warn"
division-by-zero = "warn"

View File

@@ -11,6 +11,7 @@ on:
- "crates/ruff_python_parser"
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"
- "Cargo.lock"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}

View File

@@ -1,5 +1,30 @@
# Changelog
## 0.11.11
### Preview features
- \[`airflow`\] Add autofixes for `AIR302` and `AIR312` ([#17942](https://github.com/astral-sh/ruff/pull/17942))
- \[`airflow`\] Move rules from `AIR312` to `AIR302` ([#17940](https://github.com/astral-sh/ruff/pull/17940))
- \[`airflow`\] Update `AIR301` and `AIR311` with the latest Airflow implementations ([#17985](https://github.com/astral-sh/ruff/pull/17985))
- \[`flake8-simplify`\] Enable fix in preview mode (`SIM117`) ([#18208](https://github.com/astral-sh/ruff/pull/18208))
### Bug fixes
- Fix inconsistent formatting of match-case on `[]` and `_` ([#18147](https://github.com/astral-sh/ruff/pull/18147))
- \[`pylint`\] Fix `PLW1514` not recognizing the `encoding` positional argument of `codecs.open` ([#18109](https://github.com/astral-sh/ruff/pull/18109))
### CLI
- Add full option name in formatter warning ([#18217](https://github.com/astral-sh/ruff/pull/18217))
### Documentation
- Fix rendering of admonition in docs ([#18163](https://github.com/astral-sh/ruff/pull/18163))
- \[`flake8-print`\] Improve print/pprint docs for `T201` and `T203` ([#18130](https://github.com/astral-sh/ruff/pull/18130))
- \[`flake8-simplify`\] Add fix safety section (`SIM110`,`SIM210`) ([#18114](https://github.com/astral-sh/ruff/pull/18114),[#18100](https://github.com/astral-sh/ruff/pull/18100))
- \[`pylint`\] Fix docs example that produced different output (`PLW0603`) ([#18216](https://github.com/astral-sh/ruff/pull/18216))
## 0.11.10
### Preview features

14
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "adler2"
@@ -2485,7 +2485,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.10"
version = "0.11.11"
dependencies = [
"anyhow",
"argfile",
@@ -2725,7 +2725,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.10"
version = "0.11.11"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3061,7 +3061,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.10"
version = "0.11.11"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3180,7 +3180,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
source = "git+https://github.com/salsa-rs/salsa.git?rev=4818b15f3b7516555d39f5a41cb75970448bee4c#4818b15f3b7516555d39f5a41cb75970448bee4c"
dependencies = [
"boxcar",
"compact_str",
@@ -3203,12 +3203,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
source = "git+https://github.com/salsa-rs/salsa.git?rev=4818b15f3b7516555d39f5a41cb75970448bee4c#4818b15f3b7516555d39f5a41cb75970448bee4c"
[[package]]
name = "salsa-macros"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
source = "git+https://github.com/salsa-rs/salsa.git?rev=4818b15f3b7516555d39f5a41cb75970448bee4c#4818b15f3b7516555d39f5a41cb75970448bee4c"
dependencies = [
"heck",
"proc-macro2",

View File

@@ -129,7 +129,7 @@ regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "7edce6e248f35c8114b4b021cdb474a3fb2813b3" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "4818b15f3b7516555d39f5a41cb75970448bee4c" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

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

View File

@@ -1,6 +1,7 @@
doc-valid-idents = [
"..",
"CodeQL",
"CPython",
"FastAPI",
"IPython",
"LangChain",
@@ -14,7 +15,7 @@ doc-valid-idents = [
"SNMPv1",
"SNMPv2",
"SNMPv3",
"PyFlakes"
"PyFlakes",
]
ignore-interior-mutability = [

View File

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

View File

@@ -822,11 +822,11 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
rule_names.sort();
if let [rule] = rule_names.as_slice() {
warn_user_once!(
"The following rule may cause conflicts when used with the formatter: {rule}. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration."
"The following rule may cause conflicts when used with the formatter: {rule}. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration."
);
} else {
warn_user_once!(
"The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.",
"The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `lint.select` or `lint.extend-select` configuration, or adding them to the `lint.ignore` configuration.",
rule_names.join(", ")
);
}

View File

@@ -862,7 +862,7 @@ if condition:
print('Should change quotes')
----- stderr -----
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration.
"#);
Ok(())
}
@@ -999,7 +999,7 @@ def say_hy(name: str):
1 file reformatted
----- stderr -----
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration.
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`.
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
@@ -1059,7 +1059,7 @@ def say_hy(name: str):
print(f"Hy {name}")
----- stderr -----
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration.
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
@@ -1199,7 +1199,7 @@ def say_hy(name: str):
----- stderr -----
warning: `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `incorrect-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration.
");
Ok(())
}

View File

@@ -275,7 +275,12 @@ impl fmt::Debug for Files {
impl std::panic::RefUnwindSafe for Files {}
/// A file that's either stored on the host system's file system or in the vendored file system.
///
/// # Ordering
/// Ordering is based on the file's salsa-assigned id and not on its values.
/// The id may change between runs.
#[salsa::input]
#[derive(PartialOrd, Ord)]
pub struct File {
/// The path of the file (immutable).
#[returns(ref)]

View File

@@ -80,6 +80,7 @@ fn generate() -> String {
let mut parents = Vec::new();
output.push_str("<!-- WARNING: This file is auto-generated (cargo dev generate-all). Edit the doc comments in 'crates/ty/src/args.rs' if you want to change anything here. -->\n\n");
output.push_str("# CLI Reference\n\n");
generate_command(&mut output, &ty, &mut parents);

View File

@@ -25,6 +25,10 @@ pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
let file_name = "crates/ty/docs/configuration.md";
let markdown_path = PathBuf::from(ROOT_DIR).join(file_name);
output.push_str(
"<!-- WARNING: This file is auto-generated (cargo dev generate-all). Update the doc comments on the 'Options' struct in 'crates/ty_project/src/metadata/options.rs' if you want to change anything here. -->\n\n",
);
generate_set(
&mut output,
Set::Toplevel(Options::metadata()),

View File

@@ -56,6 +56,10 @@ fn generate_markdown() -> String {
let mut output = String::new();
let _ = writeln!(
&mut output,
"<!-- WARNING: This file is auto-generated (cargo dev generate-all). Edit the lint-declarations in 'crates/ty_python_semantic/src/types/diagnostic.rs' if you want to change anything here. -->\n"
);
let _ = writeln!(&mut output, "# Rules\n");
let mut lints: Vec<_> = registry.lints().iter().collect();

View File

@@ -9,8 +9,8 @@ use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::PythonVersion;
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{
Db, Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
default_lint_registry,
Db, Program, ProgramSettings, PythonPath, PythonPlatform, PythonVersionSource,
PythonVersionWithSource, SearchPathSettings, default_lint_registry,
};
static EMPTY_VENDORED: std::sync::LazyLock<VendoredFileSystem> = std::sync::LazyLock::new(|| {
@@ -44,7 +44,10 @@ impl ModuleDb {
Program::from_settings(
&db,
ProgramSettings {
python_version,
python_version: PythonVersionWithSource {
version: python_version,
source: PythonVersionSource::default(),
},
python_platform: PythonPlatform::default(),
search_paths,
},

View File

@@ -19,19 +19,20 @@ impl<'a> Resolver<'a> {
pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
match import {
CollectedImport::Import(import) => {
resolve_module(self.db, &import).map(|module| module.file().path(self.db))
let module = resolve_module(self.db, &import)?;
Some(module.file()?.path(self.db))
}
CollectedImport::ImportFrom(import) => {
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
let parent = import.parent();
resolve_module(self.db, &import)
.map(|module| module.file().path(self.db))
.or_else(|| {
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
let module = resolve_module(self.db, &import).or_else(|| {
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
resolve_module(self.db, &parent?).map(|module| module.file().path(self.db))
})
resolve_module(self.db, &parent?)
})?;
Some(module.file()?.path(self.db))
}
}
}

View File

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

View File

@@ -9,3 +9,11 @@ class Foo:
yield 3
yield from 3
await f()
def _():
# Invalid yield scopes; but not outside a function
type X[T: (yield 1)] = int
type Y = (yield 2)
# Valid yield scope
yield 3

View File

@@ -83,11 +83,7 @@ pub(crate) fn bindings(checker: &Checker) {
}
}
if !checker.source_type.is_stub() && checker.enabled(Rule::UnquotedTypeAlias) {
if let Some(diagnostics) =
flake8_type_checking::rules::unquoted_type_alias(checker, binding)
{
checker.report_diagnostics(diagnostics);
}
flake8_type_checking::rules::unquoted_type_alias(checker, binding);
}
if checker.enabled(Rule::UnsortedDunderSlots) {
if let Some(diagnostic) = ruff::rules::sort_dunder_slots(checker, binding) {

View File

@@ -137,11 +137,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
&checker.semantic,
)
}) {
checker.report_diagnostics(flake8_annotations::rules::definition(
checker,
definition,
*visibility,
));
flake8_annotations::rules::definition(checker, definition, *visibility);
}
overloaded_name =
flake8_annotations::helpers::overloaded_name(definition, &checker.semantic);

View File

@@ -385,15 +385,6 @@ impl<'a> Checker<'a> {
diagnostics.push(diagnostic);
}
/// Extend the collection of [`Diagnostic`] objects in the [`Checker`]
pub(crate) fn report_diagnostics<I>(&self, diagnostics: I)
where
I: IntoIterator<Item = Diagnostic>,
{
let mut checker_diagnostics = self.diagnostics.borrow_mut();
checker_diagnostics.extend(diagnostics);
}
/// Adds a [`TextRange`] to the set of ranges of variable names
/// flagged in `flake8-bugbear` violations so far.
///
@@ -690,6 +681,17 @@ impl SemanticSyntaxContext for Checker<'_> {
false
}
fn in_yield_allowed_context(&self) -> bool {
for scope in self.semantic.current_scopes() {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Generator { .. } => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Module | ScopeKind::Type => {}
}
}
false
}
fn in_sync_comprehension(&self) -> bool {
for scope in self.semantic.current_scopes() {
if let ScopeKind::Generator {

View File

@@ -127,6 +127,10 @@ pub(crate) const fn is_check_file_level_directives_enabled(settings: &LinterSett
}
// https://github.com/astral-sh/ruff/pull/17644
pub(crate) const fn is_readlines_in_for_fix_safe(settings: &LinterSettings) -> bool {
pub(crate) const fn is_readlines_in_for_fix_safe_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
pub(crate) const fn multiple_with_statements_fix_safe_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -190,24 +190,12 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
match qualified_name.segments() {
["airflow", .., "DAG" | "dag"] => {
// with replacement
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"fail_stop",
Some("fail_fast"),
));
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"schedule_interval",
Some("schedule"),
));
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"timetable",
Some("schedule"),
));
diagnostic_for_argument(checker, arguments, "fail_stop", Some("fail_fast"));
diagnostic_for_argument(checker, arguments, "schedule_interval", Some("schedule"));
diagnostic_for_argument(checker, arguments, "timetable", Some("schedule"));
// without replacement
checker.report_diagnostics(diagnostic_for_argument(arguments, "default_view", None));
checker.report_diagnostics(diagnostic_for_argument(arguments, "orientation", None));
diagnostic_for_argument(checker, arguments, "default_view", None);
diagnostic_for_argument(checker, arguments, "orientation", None);
}
segments => {
if is_airflow_auth_manager(segments) {
@@ -223,17 +211,14 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
));
}
} else if is_airflow_task_handler(segments) {
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"filename_template",
None,
));
diagnostic_for_argument(checker, arguments, "filename_template", None);
} else if is_airflow_builtin_or_provider(segments, "operators", "Operator") {
checker.report_diagnostics(diagnostic_for_argument(
diagnostic_for_argument(
checker,
arguments,
"task_concurrency",
Some("max_active_tis_per_dag"),
));
);
match segments {
[
"airflow",
@@ -242,11 +227,12 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
"trigger_dagrun",
"TriggerDagRunOperator",
] => {
checker.report_diagnostics(diagnostic_for_argument(
diagnostic_for_argument(
checker,
arguments,
"execution_date",
Some("logical_date"),
));
);
}
[
"airflow",
@@ -263,11 +249,12 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
"BranchDayOfWeekOperator",
]
| ["airflow", .., "sensors", "weekday", "DayOfWeekSensor"] => {
checker.report_diagnostics(diagnostic_for_argument(
diagnostic_for_argument(
checker,
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
);
}
_ => {}
}
@@ -1057,11 +1044,14 @@ fn check_airflow_plugin_extension(
/// Check if the `deprecated` keyword argument is being used and create a diagnostic if so along
/// with a possible `replacement`.
fn diagnostic_for_argument(
checker: &Checker,
arguments: &Arguments,
deprecated: &str,
replacement: Option<&'static str>,
) -> Option<Diagnostic> {
let keyword = arguments.find_keyword(deprecated)?;
) {
let Some(keyword) = arguments.find_keyword(deprecated) else {
return;
};
let mut diagnostic = Diagnostic::new(
Airflow3Removal {
deprecated: deprecated.to_string(),
@@ -1083,7 +1073,7 @@ fn diagnostic_for_argument(
)));
}
Some(diagnostic)
checker.report_diagnostic(diagnostic);
}
/// Check whether the symbol is coming from the `secrets` builtin or provider module which ends

View File

@@ -112,11 +112,14 @@ pub(crate) fn airflow_3_0_suggested_update_expr(checker: &Checker, expr: &Expr)
/// Check if the `deprecated` keyword argument is being used and create a diagnostic if so along
/// with a possible `replacement`.
fn diagnostic_for_argument(
checker: &Checker,
arguments: &Arguments,
deprecated: &str,
replacement: Option<&'static str>,
) -> Option<Diagnostic> {
let keyword = arguments.find_keyword(deprecated)?;
) {
let Some(keyword) = arguments.find_keyword(deprecated) else {
return;
};
let mut diagnostic = Diagnostic::new(
Airflow3SuggestedUpdate {
deprecated: deprecated.to_string(),
@@ -138,7 +141,7 @@ fn diagnostic_for_argument(
)));
}
Some(diagnostic)
checker.report_diagnostic(diagnostic);
}
/// Check whether a removed Airflow argument is passed.
///
@@ -152,15 +155,11 @@ fn diagnostic_for_argument(
fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, arguments: &Arguments) {
match qualified_name.segments() {
["airflow", .., "DAG" | "dag"] => {
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"sla_miss_callback",
None,
));
diagnostic_for_argument(checker, arguments, "sla_miss_callback", None);
}
segments => {
if is_airflow_builtin_or_provider(segments, "operators", "Operator") {
checker.report_diagnostics(diagnostic_for_argument(arguments, "sla", None));
diagnostic_for_argument(checker, arguments, "sla", None);
}
}
}

View File

@@ -604,9 +604,9 @@ pub(crate) fn definition(
checker: &Checker,
definition: &Definition,
visibility: visibility::Visibility,
) -> Vec<Diagnostic> {
) {
let Some(function) = definition.as_function_def() else {
return vec![];
return;
};
let ast::StmtFunctionDef {
@@ -899,13 +899,10 @@ pub(crate) fn definition(
}
}
if !checker.settings.flake8_annotations.ignore_fully_untyped {
return diagnostics;
}
// If settings say so, don't report any of the
// diagnostics gathered here if there were no type annotations at all.
if has_any_typed_arg
if !checker.settings.flake8_annotations.ignore_fully_untyped
|| has_any_typed_arg
|| has_typed_return
|| (is_method
&& !visibility::is_staticmethod(decorator_list, checker.semantic())
@@ -915,8 +912,8 @@ pub(crate) fn definition(
.or_else(|| parameters.args.first())
.is_some_and(|first_param| first_param.annotation().is_some()))
{
diagnostics
} else {
vec![]
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
}
}

View File

@@ -52,17 +52,21 @@ impl Violation for HardcodedPasswordFuncArg {
/// S106
pub(crate) fn hardcoded_password_func_arg(checker: &Checker, keywords: &[Keyword]) {
checker.report_diagnostics(keywords.iter().filter_map(|keyword| {
string_literal(&keyword.value).filter(|string| !string.is_empty())?;
let arg = keyword.arg.as_ref()?;
if !matches_password_name(arg) {
return None;
for keyword in keywords {
if string_literal(&keyword.value).is_none_or(str::is_empty) {
continue;
}
Some(Diagnostic::new(
let Some(arg) = &keyword.arg else {
continue;
};
if !matches_password_name(arg) {
continue;
}
checker.report_diagnostic(Diagnostic::new(
HardcodedPasswordFuncArg {
name: arg.to_string(),
},
keyword.range(),
))
}));
));
}
}

View File

@@ -76,16 +76,20 @@ pub(crate) fn compare_to_hardcoded_password_string(
left: &Expr,
comparators: &[Expr],
) {
checker.report_diagnostics(comparators.iter().filter_map(|comp| {
string_literal(comp).filter(|string| !string.is_empty())?;
let name = password_target(left)?;
Some(Diagnostic::new(
for comp in comparators {
if string_literal(comp).is_none_or(str::is_empty) {
continue;
}
let Some(name) = password_target(left) else {
continue;
};
checker.report_diagnostic(Diagnostic::new(
HardcodedPasswordString {
name: name.to_string(),
},
comp.range(),
))
}));
));
}
}
/// S105

View File

@@ -7,7 +7,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing::{
is_immutable_annotation, is_immutable_func, is_immutable_newtype_call, is_mutable_func,
};
@@ -83,20 +82,15 @@ impl Violation for FunctionCallInDefaultArgument {
}
struct ArgumentDefaultVisitor<'a, 'b> {
semantic: &'a SemanticModel<'b>,
checker: &'a Checker<'b>,
extend_immutable_calls: &'a [QualifiedName<'b>],
diagnostics: Vec<Diagnostic>,
}
impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> {
fn new(
semantic: &'a SemanticModel<'b>,
extend_immutable_calls: &'a [QualifiedName<'b>],
) -> Self {
fn new(checker: &'a Checker<'b>, extend_immutable_calls: &'a [QualifiedName<'b>]) -> Self {
Self {
semantic,
checker,
extend_immutable_calls,
diagnostics: Vec::new(),
}
}
}
@@ -105,13 +99,21 @@ impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> {
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Call(ast::ExprCall { func, .. }) => {
if !is_mutable_func(func, self.semantic)
&& !is_immutable_func(func, self.semantic, self.extend_immutable_calls)
if !is_mutable_func(func, self.checker.semantic())
&& !is_immutable_func(
func,
self.checker.semantic(),
self.extend_immutable_calls,
)
&& !func.as_name_expr().is_some_and(|name| {
is_immutable_newtype_call(name, self.semantic, self.extend_immutable_calls)
is_immutable_newtype_call(
name,
self.checker.semantic(),
self.extend_immutable_calls,
)
})
{
self.diagnostics.push(Diagnostic::new(
self.checker.report_diagnostic(Diagnostic::new(
FunctionCallInDefaultArgument {
name: UnqualifiedName::from_expr(func).map(|name| name.to_string()),
},
@@ -139,7 +141,7 @@ pub(crate) fn function_call_in_argument_default(checker: &Checker, parameters: &
.map(|target| QualifiedName::from_dotted_name(target))
.collect();
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), &extend_immutable_calls);
let mut visitor = ArgumentDefaultVisitor::new(checker, &extend_immutable_calls);
for parameter in parameters.iter_non_variadic_params() {
if let Some(default) = parameter.default() {
if !parameter.annotation().is_some_and(|expr| {
@@ -149,6 +151,4 @@ pub(crate) fn function_call_in_argument_default(checker: &Checker, parameters: &
}
}
}
checker.report_diagnostics(visitor.diagnostics);
}

View File

@@ -109,5 +109,7 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &Checker, expr: &'a Expr) {
}
}
checker.report_diagnostics(diagnostics);
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
}

View File

@@ -139,7 +139,9 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
}
// Add all diagnostics to the checker
checker.report_diagnostics(diagnostics);
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@@ -210,23 +210,23 @@ impl Violation for PytestUnittestAssertion {
/// Visitor that tracks assert statements and checks if they reference
/// the exception name.
struct ExceptionHandlerVisitor<'a> {
struct ExceptionHandlerVisitor<'a, 'b> {
exception_name: &'a str,
current_assert: Option<&'a Stmt>,
errors: Vec<Diagnostic>,
checker: &'a Checker<'b>,
}
impl<'a> ExceptionHandlerVisitor<'a> {
const fn new(exception_name: &'a str) -> Self {
impl<'a, 'b> ExceptionHandlerVisitor<'a, 'b> {
const fn new(checker: &'a Checker<'b>, exception_name: &'a str) -> Self {
Self {
exception_name,
current_assert: None,
errors: Vec::new(),
checker,
}
}
}
impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a> {
impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a, '_> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::Assert(_) => {
@@ -243,7 +243,7 @@ impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a> {
Expr::Name(ast::ExprName { id, .. }) => {
if let Some(current_assert) = self.current_assert {
if id.as_str() == self.exception_name {
self.errors.push(Diagnostic::new(
self.checker.report_diagnostic(Diagnostic::new(
PytestAssertInExcept {
name: id.to_string(),
},
@@ -257,13 +257,12 @@ impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a> {
}
}
fn check_assert_in_except(name: &str, body: &[Stmt]) -> Vec<Diagnostic> {
fn check_assert_in_except(checker: &Checker, name: &str, body: &[Stmt]) {
// Walk body to find assert statements that reference the exception name
let mut visitor = ExceptionHandlerVisitor::new(name);
let mut visitor = ExceptionHandlerVisitor::new(checker, name);
for stmt in body {
visitor.visit_stmt(stmt);
}
visitor.errors
}
/// PT009
@@ -596,15 +595,13 @@ pub(crate) fn assert_falsy(checker: &Checker, stmt: &Stmt, test: &Expr) {
/// PT017
pub(crate) fn assert_in_exception_handler(checker: &Checker, handlers: &[ExceptHandler]) {
checker.report_diagnostics(handlers.iter().flat_map(|handler| match handler {
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) => {
if let Some(name) = name {
check_assert_in_except(name, body)
} else {
Vec::new()
}
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) =
handler;
if let Some(name) = name {
check_assert_in_except(checker, name, body);
}
}));
}
}
#[derive(Copy, Clone)]

View File

@@ -9,7 +9,6 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::Locator;
use crate::checkers::ast::Checker;
use crate::rules::flake8_quotes;
use crate::settings::LinterSettings;
/// ## What it does
/// Checks for strings that include escaped quotes, and suggests changing
@@ -61,11 +60,7 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
return;
}
let mut rule_checker = AvoidableEscapedQuoteChecker::new(
checker.locator(),
checker.settings,
checker.target_version(),
);
let mut rule_checker = AvoidableEscapedQuoteChecker::new(checker, checker.target_version());
for part in string_like.parts() {
match part {
@@ -78,59 +73,45 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
ast::StringLikePart::FString(f_string) => rule_checker.visit_f_string(f_string),
}
}
checker.report_diagnostics(rule_checker.into_diagnostics());
}
/// Checks for `Q003` violations using the [`Visitor`] implementation.
#[derive(Debug)]
struct AvoidableEscapedQuoteChecker<'a> {
locator: &'a Locator<'a>,
struct AvoidableEscapedQuoteChecker<'a, 'b> {
checker: &'a Checker<'b>,
quotes_settings: &'a flake8_quotes::settings::Settings,
supports_pep701: bool,
diagnostics: Vec<Diagnostic>,
}
impl<'a> AvoidableEscapedQuoteChecker<'a> {
fn new(
locator: &'a Locator<'a>,
settings: &'a LinterSettings,
target_version: PythonVersion,
) -> Self {
impl<'a, 'b> AvoidableEscapedQuoteChecker<'a, 'b> {
fn new(checker: &'a Checker<'b>, target_version: PythonVersion) -> Self {
Self {
locator,
quotes_settings: &settings.flake8_quotes,
checker,
quotes_settings: &checker.settings.flake8_quotes,
supports_pep701: target_version.supports_pep_701(),
diagnostics: vec![],
}
}
/// Consumes the checker and returns a vector of [`Diagnostic`] found during the visit.
fn into_diagnostics(self) -> Vec<Diagnostic> {
self.diagnostics
}
}
impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_> {
fn visit_string_literal(&mut self, string_literal: &'_ ast::StringLiteral) {
impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_, '_> {
fn visit_string_literal(&mut self, string_literal: &ast::StringLiteral) {
if let Some(diagnostic) = check_string_or_bytes(
self.locator,
self.checker.locator(),
self.quotes_settings,
string_literal.range(),
AnyStringFlags::from(string_literal.flags),
) {
self.diagnostics.push(diagnostic);
self.checker.report_diagnostic(diagnostic);
}
}
fn visit_bytes_literal(&mut self, bytes_literal: &'_ ast::BytesLiteral) {
fn visit_bytes_literal(&mut self, bytes_literal: &ast::BytesLiteral) {
if let Some(diagnostic) = check_string_or_bytes(
self.locator,
self.checker.locator(),
self.quotes_settings,
bytes_literal.range(),
AnyStringFlags::from(bytes_literal.flags),
) {
self.diagnostics.push(diagnostic);
self.checker.report_diagnostic(diagnostic);
}
}
@@ -203,8 +184,10 @@ impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_> {
.literals()
.any(|literal| contains_quote(literal, opposite_quote_char))
{
if let Some(diagnostic) = check_f_string(self.locator, self.quotes_settings, f_string) {
self.diagnostics.push(diagnostic);
if let Some(diagnostic) =
check_f_string(self.checker.locator(), self.quotes_settings, f_string)
{
self.checker.report_diagnostic(diagnostic);
}
}

View File

@@ -59,6 +59,7 @@ mod tests {
}
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))]
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -8,10 +8,10 @@ use ruff_python_ast::{self as ast, Stmt, WithItem};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange};
use super::fix_with;
use crate::checkers::ast::Checker;
use crate::fix::edits::fits;
use super::fix_with;
use crate::preview::multiple_with_statements_fix_safe_enabled;
/// ## What it does
/// Checks for the unnecessary nesting of multiple consecutive context
@@ -45,8 +45,15 @@ use super::fix_with;
/// pass
/// ```
///
/// # Fix safety
///
/// This fix is marked as always unsafe unless [preview] mode is enabled, in which case it is always
/// marked as safe. Note that the fix is unavailable if it would remove comments (in either case).
///
/// ## References
/// - [Python documentation: The `with` statement](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct MultipleWithStatements;
@@ -188,7 +195,11 @@ pub(crate) fn multiple_with_statements(
checker.settings.tab_size,
)
}) {
Ok(Some(Fix::unsafe_edit(edit)))
if multiple_with_statements_fix_safe_enabled(checker.settings) {
Ok(Some(Fix::safe_edit(edit)))
} else {
Ok(Some(Fix::unsafe_edit(edit)))
}
} else {
Ok(None)
}

View File

@@ -0,0 +1,339 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM117.py:2:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
1 | # SIM117
2 | / with A() as a:
3 | | with B() as b:
| |__________________^ SIM117
4 | print("hello")
|
= help: Combine `with` statements
Safe fix
1 1 | # SIM117
2 |-with A() as a:
3 |- with B() as b:
4 |- print("hello")
2 |+with A() as a, B() as b:
3 |+ print("hello")
5 4 |
6 5 | # SIM117
7 6 | with A():
SIM117.py:7:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
6 | # SIM117
7 | / with A():
8 | | with B():
| |_____________^ SIM117
9 | with C():
10 | print("hello")
|
= help: Combine `with` statements
Safe fix
4 4 | print("hello")
5 5 |
6 6 | # SIM117
7 |-with A():
8 |- with B():
9 |- with C():
10 |- print("hello")
7 |+with A(), B():
8 |+ with C():
9 |+ print("hello")
11 10 |
12 11 | # SIM117
13 12 | with A() as a:
SIM117.py:13:1: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
12 | # SIM117
13 | / with A() as a:
14 | | # Unfixable due to placement of this comment.
15 | | with B() as b:
| |__________________^ SIM117
16 | print("hello")
|
= help: Combine `with` statements
SIM117.py:19:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
18 | # SIM117
19 | / with A() as a:
20 | | with B() as b:
| |__________________^ SIM117
21 | # Fixable due to placement of this comment.
22 | print("hello")
|
= help: Combine `with` statements
Safe fix
16 16 | print("hello")
17 17 |
18 18 | # SIM117
19 |-with A() as a:
20 |- with B() as b:
21 |- # Fixable due to placement of this comment.
22 |- print("hello")
19 |+with A() as a, B() as b:
20 |+ # Fixable due to placement of this comment.
21 |+ print("hello")
23 22 |
24 23 | # OK
25 24 | with A() as a:
SIM117.py:47:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
46 | # SIM117
47 | / async with A() as a:
48 | | async with B() as b:
| |________________________^ SIM117
49 | print("hello")
|
= help: Combine `with` statements
Safe fix
44 44 | print("hello")
45 45 |
46 46 | # SIM117
47 |-async with A() as a:
48 |- async with B() as b:
49 |- print("hello")
47 |+async with A() as a, B() as b:
48 |+ print("hello")
50 49 |
51 50 | while True:
52 51 | # SIM117
SIM117.py:53:5: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
51 | while True:
52 | # SIM117
53 | / with A() as a:
54 | | with B() as b:
| |______________________^ SIM117
55 | """this
56 | is valid"""
|
= help: Combine `with` statements
Safe fix
50 50 |
51 51 | while True:
52 52 | # SIM117
53 |- with A() as a:
54 |- with B() as b:
55 |- """this
53 |+ with A() as a, B() as b:
54 |+ """this
56 55 | is valid"""
57 56 |
58 |- """the indentation on
57 |+ """the indentation on
59 58 | this line is significant"""
60 59 |
61 |- "this is" \
60 |+ "this is" \
62 61 | "allowed too"
63 62 |
64 |- ("so is"
63 |+ ("so is"
65 64 | "this for some reason")
66 65 |
67 66 | # SIM117
SIM117.py:68:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
67 | # SIM117
68 | / with (
69 | | A() as a,
70 | | B() as b,
71 | | ):
72 | | with C() as c:
| |__________________^ SIM117
73 | print("hello")
|
= help: Combine `with` statements
Safe fix
67 67 | # SIM117
68 68 | with (
69 69 | A() as a,
70 |- B() as b,
70 |+ B() as b,C() as c
71 71 | ):
72 |- with C() as c:
73 |- print("hello")
72 |+ print("hello")
74 73 |
75 74 | # SIM117
76 75 | with A() as a:
SIM117.py:76:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
75 | # SIM117
76 | / with A() as a:
77 | | with (
78 | | B() as b,
79 | | C() as c,
80 | | ):
| |______^ SIM117
81 | print("hello")
|
= help: Combine `with` statements
Safe fix
73 73 | print("hello")
74 74 |
75 75 | # SIM117
76 |-with A() as a:
77 |- with (
78 |- B() as b,
79 |- C() as c,
80 |- ):
81 |- print("hello")
76 |+with (
77 |+ A() as a, B() as b,
78 |+ C() as c,
79 |+):
80 |+ print("hello")
82 81 |
83 82 | # SIM117
84 83 | with (
SIM117.py:84:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
83 | # SIM117
84 | / with (
85 | | A() as a,
86 | | B() as b,
87 | | ):
88 | | with (
89 | | C() as c,
90 | | D() as d,
91 | | ):
| |______^ SIM117
92 | print("hello")
|
= help: Combine `with` statements
Safe fix
83 83 | # SIM117
84 84 | with (
85 85 | A() as a,
86 |- B() as b,
86 |+ B() as b,C() as c,
87 |+ D() as d,
87 88 | ):
88 |- with (
89 |- C() as c,
90 |- D() as d,
91 |- ):
92 |- print("hello")
89 |+ print("hello")
93 90 |
94 91 | # SIM117 (auto-fixable)
95 92 | with A("01ß9💣28901ß9💣28901ß9💣289") as a:
SIM117.py:95:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
94 | # SIM117 (auto-fixable)
95 | / with A("01ß9💣28901ß9💣28901ß9💣289") as a:
96 | | with B("01ß9💣28901ß9💣28901ß9💣289") as b:
| |__________________________________________________^ SIM117
97 | print("hello")
|
= help: Combine `with` statements
Safe fix
92 92 | print("hello")
93 93 |
94 94 | # SIM117 (auto-fixable)
95 |-with A("01ß9💣28901ß9💣28901ß9💣289") as a:
96 |- with B("01ß9💣28901ß9💣28901ß9💣289") as b:
97 |- print("hello")
95 |+with A("01ß9💣28901ß9💣28901ß9💣289") as a, B("01ß9💣28901ß9💣28901ß9💣289") as b:
96 |+ print("hello")
98 97 |
99 98 | # SIM117 (not auto-fixable too long)
100 99 | with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
SIM117.py:100:1: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
99 | # SIM117 (not auto-fixable too long)
100 | / with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
101 | | with B("01ß9💣28901ß9💣28901ß9💣289") as b:
| |__________________________________________________^ SIM117
102 | print("hello")
|
= help: Combine `with` statements
SIM117.py:106:5: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
104 | # From issue #3025.
105 | async def main():
106 | / async with A() as a: # SIM117.
107 | | async with B() as b:
| |____________________________^ SIM117
108 | print("async-inside!")
|
= help: Combine `with` statements
SIM117.py:126:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
125 | # SIM117
126 | / with A() as a:
127 | | with B() as b:
| |__________________^ SIM117
128 | type ListOrSet[T] = list[T] | set[T]
|
= help: Combine `with` statements
Safe fix
123 123 | f(b2, c2, d2)
124 124 |
125 125 | # SIM117
126 |-with A() as a:
127 |- with B() as b:
128 |- type ListOrSet[T] = list[T] | set[T]
126 |+with A() as a, B() as b:
127 |+ type ListOrSet[T] = list[T] | set[T]
129 128 |
130 |- class ClassA[T: str]:
131 |- def method1(self) -> T:
132 |- ...
129 |+ class ClassA[T: str]:
130 |+ def method1(self) -> T:
131 |+ ...
133 132 |
134 |- f" something { my_dict["key"] } something else "
133 |+ f" something { my_dict["key"] } something else "
135 134 |
136 |- f"foo {f"bar {x}"} baz"
135 |+ f"foo {f"bar {x}"} baz"
137 136 |
138 137 | # Allow cascading for some statements.
139 138 | import anyio
SIM117.py:163:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
162 | # Do not suppress combination, if a context manager is already combined with another.
163 | / async with asyncio.timeout(1), A():
164 | | async with B():
| |___________________^ SIM117
165 | pass
|
= help: Combine `with` statements
Safe fix
160 160 | pass
161 161 |
162 162 | # Do not suppress combination, if a context manager is already combined with another.
163 |-async with asyncio.timeout(1), A():
164 |- async with B():
165 |- pass
163 |+async with asyncio.timeout(1), A(), B():
164 |+ pass

View File

@@ -142,26 +142,26 @@ impl AlwaysFixableViolation for QuotedTypeAlias {
}
/// TC007
pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) -> Option<Vec<Diagnostic>> {
pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) {
if binding.context.is_typing() {
return None;
return;
}
if !binding.is_annotated_type_alias() {
return None;
return;
}
let Some(Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(expr), ..
})) = binding.statement(checker.semantic())
else {
return None;
return;
};
let mut names = Vec::new();
collect_typing_references(checker, expr, &mut names);
if names.is_empty() {
return None;
return;
}
// We generate a diagnostic for every name that needs to be quoted
@@ -178,14 +178,13 @@ pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) -> Optio
checker.locator(),
checker.default_string_flags(),
);
let mut diagnostics = Vec::with_capacity(names.len());
for name in names {
let mut diagnostic = Diagnostic::new(UnquotedTypeAlias, name.range());
diagnostic.set_parent(parent);
diagnostic.set_fix(Fix::unsafe_edit(edit.clone()));
diagnostics.push(diagnostic);
checker.report_diagnostic(
Diagnostic::new(UnquotedTypeAlias, name.range())
.with_parent(parent)
.with_fix(Fix::unsafe_edit(edit.clone())),
);
}
Some(diagnostics)
}
/// Traverses the type expression and collects `[Expr::Name]` nodes that are

View File

@@ -304,19 +304,21 @@ fn call<'a>(
) {
let semantic = checker.semantic();
let dummy_variable_rgx = &checker.settings.dummy_variable_rgx;
checker.report_diagnostics(parameters.filter_map(|arg| {
let binding = scope
for arg in parameters {
let Some(binding) = scope
.get(arg.name())
.map(|binding_id| semantic.binding(binding_id))?;
.map(|binding_id| semantic.binding(binding_id))
else {
continue;
};
if binding.kind.is_argument()
&& binding.is_unused()
&& !dummy_variable_rgx.is_match(arg.name())
{
Some(argumentable.check_for(arg.name.to_string(), binding.range()))
} else {
None
checker
.report_diagnostic(argumentable.check_for(arg.name.to_string(), binding.range()));
}
}));
}
}
/// Returns `true` if a function appears to be a base class stub. In other

View File

@@ -431,5 +431,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
}
}
checker.report_diagnostics(diagnostics);
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
}

View File

@@ -873,8 +873,6 @@ pub(crate) fn check_docstring(
section_contexts: &SectionContexts,
convention: Option<Convention>,
) {
let mut diagnostics = Vec::new();
// Only check function docstrings.
let Some(function_def) = definition.as_function_def() else {
return;
@@ -927,7 +925,7 @@ pub(crate) fn check_docstring(
semantic,
)
{
diagnostics.push(Diagnostic::new(
checker.report_diagnostic(Diagnostic::new(
DocstringMissingReturns,
docstring.range(),
));
@@ -938,8 +936,10 @@ pub(crate) fn check_docstring(
.iter()
.any(|entry| !entry.is_none_return()) =>
{
diagnostics
.push(Diagnostic::new(DocstringMissingReturns, docstring.range()));
checker.report_diagnostic(Diagnostic::new(
DocstringMissingReturns,
docstring.range(),
));
}
_ => {}
}
@@ -958,12 +958,16 @@ pub(crate) fn check_docstring(
|arguments| arguments.first().is_none_or(Expr::is_none_literal_expr),
) =>
{
diagnostics
.push(Diagnostic::new(DocstringMissingYields, docstring.range()));
checker.report_diagnostic(Diagnostic::new(
DocstringMissingYields,
docstring.range(),
));
}
None if body_entries.yields.iter().any(|entry| !entry.is_none_yield) => {
diagnostics
.push(Diagnostic::new(DocstringMissingYields, docstring.range()));
checker.report_diagnostic(Diagnostic::new(
DocstringMissingYields,
docstring.range(),
));
}
_ => {}
}
@@ -996,7 +1000,7 @@ pub(crate) fn check_docstring(
},
docstring.range(),
);
diagnostics.push(diagnostic);
checker.report_diagnostic(diagnostic);
}
}
}
@@ -1011,7 +1015,7 @@ pub(crate) fn check_docstring(
|| body_entries.returns.iter().all(ReturnEntry::is_implicit)
{
let diagnostic = Diagnostic::new(DocstringExtraneousReturns, docstring.range());
diagnostics.push(diagnostic);
checker.report_diagnostic(diagnostic);
}
}
}
@@ -1021,7 +1025,7 @@ pub(crate) fn check_docstring(
if docstring_sections.yields.is_some() {
if body_entries.yields.is_empty() {
let diagnostic = Diagnostic::new(DocstringExtraneousYields, docstring.range());
diagnostics.push(diagnostic);
checker.report_diagnostic(diagnostic);
}
}
}
@@ -1047,11 +1051,9 @@ pub(crate) fn check_docstring(
},
docstring.range(),
);
diagnostics.push(diagnostic);
checker.report_diagnostic(diagnostic);
}
}
}
}
checker.report_diagnostics(diagnostics);
}

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F704.py:6:5: F704 `yield` statement outside of a function
|
@@ -31,4 +30,6 @@ F704.py:11:1: F704 `await` statement outside of a function
10 | yield from 3
11 | await f()
| ^^^^^^^^^ F704
12 |
13 | def _():
|

View File

@@ -158,7 +158,9 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB
Some(diagnostic)
});
checker.report_diagnostics(diagnostics);
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
}
/// Checks whether two compare expressions are simplifiable

View File

@@ -31,8 +31,9 @@ use crate::checkers::ast::Checker;
///
///
/// def foo():
/// var = 10
/// print(var)
/// return 10
/// return var
///
///
/// var = foo()

View File

@@ -379,15 +379,13 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) {
_ => panic!("redefined_loop_name called on Statement that is not a `With` or `For`"),
};
let mut diagnostics = Vec::new();
for outer_assignment_target in &outer_assignment_targets {
for inner_assignment_target in &inner_assignment_targets {
// Compare the targets structurally.
if ComparableExpr::from(outer_assignment_target.expr)
.eq(&(ComparableExpr::from(inner_assignment_target.expr)))
{
diagnostics.push(Diagnostic::new(
checker.report_diagnostic(Diagnostic::new(
RedefinedLoopName {
name: checker.generator().expr(outer_assignment_target.expr),
outer_kind: outer_assignment_target.binding_kind,
@@ -398,6 +396,4 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) {
}
}
}
checker.report_diagnostics(diagnostics);
}

View File

@@ -1,6 +1,6 @@
use std::hash::Hash;
use ruff_python_semantic::{SemanticModel, analyze::class::iter_super_class};
use ruff_python_semantic::analyze::class::iter_super_class;
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
@@ -66,11 +66,9 @@ pub(crate) fn redefined_slots_in_subclass(checker: &Checker, class_def: &ast::St
return;
}
let semantic = checker.semantic();
let diagnostics = class_slots
.iter()
.filter_map(|slot| check_super_slots(class_def, semantic, slot));
checker.report_diagnostics(diagnostics);
for slot in class_slots {
check_super_slots(checker, class_def, &slot);
}
}
#[derive(Clone, Debug)]
@@ -103,25 +101,18 @@ impl Ranged for Slot<'_> {
}
}
fn check_super_slots(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
slot: &Slot,
) -> Option<Diagnostic> {
iter_super_class(class_def, semantic)
.skip(1)
.find_map(&|super_class: &ast::StmtClassDef| {
if slots_members(&super_class.body).contains(slot) {
return Some(Diagnostic::new(
RedefinedSlotsInSubclass {
base: super_class.name.to_string(),
slot_name: slot.name.to_string(),
},
slot.range(),
));
}
None
})
fn check_super_slots(checker: &Checker, class_def: &ast::StmtClassDef, slot: &Slot) {
for super_class in iter_super_class(class_def, checker.semantic()).skip(1) {
if slots_members(&super_class.body).contains(slot) {
checker.report_diagnostic(Diagnostic::new(
RedefinedSlotsInSubclass {
base: super_class.name.to_string(),
slot_name: slot.name.to_string(),
},
slot.range(),
));
}
}
}
fn slots_members(body: &[Stmt]) -> FxHashSet<Slot> {

View File

@@ -43,7 +43,6 @@ pub(crate) fn self_assignment(checker: &Checker, assign: &ast::StmtAssign) {
if checker.semantic().current_scope().kind.is_class() {
return;
}
let mut diagnostics = Vec::new();
for (left, right) in assign
.targets
@@ -51,9 +50,8 @@ pub(crate) fn self_assignment(checker: &Checker, assign: &ast::StmtAssign) {
.chain(std::iter::once(assign.value.as_ref()))
.tuple_combinations()
{
visit_assignments(left, right, &mut diagnostics);
visit_assignments(checker, left, right);
}
checker.report_diagnostics(diagnostics);
}
/// PLW0127
@@ -67,23 +65,21 @@ pub(crate) fn self_annotated_assignment(checker: &Checker, assign: &ast::StmtAnn
if checker.semantic().current_scope().kind.is_class() {
return;
}
let mut diagnostics = Vec::new();
visit_assignments(&assign.target, value, &mut diagnostics);
checker.report_diagnostics(diagnostics);
visit_assignments(checker, &assign.target, value);
}
fn visit_assignments(left: &Expr, right: &Expr, diagnostics: &mut Vec<Diagnostic>) {
fn visit_assignments(checker: &Checker, left: &Expr, right: &Expr) {
match (left, right) {
(Expr::Tuple(lhs), Expr::Tuple(rhs)) if lhs.len() == rhs.len() => lhs
.iter()
.zip(rhs)
.for_each(|(lhs_elem, rhs_elem)| visit_assignments(lhs_elem, rhs_elem, diagnostics)),
.for_each(|(lhs_elem, rhs_elem)| visit_assignments(checker, lhs_elem, rhs_elem)),
(
Expr::Name(ast::ExprName { id: lhs_name, .. }),
Expr::Name(ast::ExprName { id: rhs_name, .. }),
) if lhs_name == rhs_name => {
diagnostics.push(Diagnostic::new(
checker.report_diagnostic(Diagnostic::new(
SelfAssigningVariable {
name: lhs_name.to_string(),
},

View File

@@ -64,49 +64,26 @@ pub(crate) fn read_whole_file(checker: &Checker, with: &ast::StmtWith) {
}
// Then we need to match each `open` operation with exactly one `read` call.
let matches = {
let mut matcher = ReadMatcher::new(candidates);
visitor::walk_body(&mut matcher, &with.body);
matcher.into_matches()
};
// All the matched operations should be reported.
let diagnostics: Vec<Diagnostic> = matches
.iter()
.map(|open| {
Diagnostic::new(
ReadWholeFile {
filename: SourceCodeSnippet::from_str(&checker.generator().expr(open.filename)),
suggestion: make_suggestion(open, checker.generator()),
},
open.item.range(),
)
})
.collect();
checker.report_diagnostics(diagnostics);
let mut matcher = ReadMatcher::new(checker, candidates);
visitor::walk_body(&mut matcher, &with.body);
}
/// AST visitor that matches `open` operations with the corresponding `read` calls.
#[derive(Debug)]
struct ReadMatcher<'a> {
struct ReadMatcher<'a, 'b> {
checker: &'a Checker<'b>,
candidates: Vec<FileOpen<'a>>,
matches: Vec<FileOpen<'a>>,
}
impl<'a> ReadMatcher<'a> {
fn new(candidates: Vec<FileOpen<'a>>) -> Self {
impl<'a, 'b> ReadMatcher<'a, 'b> {
fn new(checker: &'a Checker<'b>, candidates: Vec<FileOpen<'a>>) -> Self {
Self {
checker,
candidates,
matches: vec![],
}
}
fn into_matches(self) -> Vec<FileOpen<'a>> {
self.matches
}
}
impl<'a> Visitor<'a> for ReadMatcher<'a> {
impl<'a> Visitor<'a> for ReadMatcher<'a, '_> {
fn visit_expr(&mut self, expr: &'a Expr) {
if let Some(read_from) = match_read_call(expr) {
if let Some(open) = self
@@ -114,7 +91,16 @@ impl<'a> Visitor<'a> for ReadMatcher<'a> {
.iter()
.position(|open| open.is_ref(read_from))
{
self.matches.push(self.candidates.remove(open));
let open = self.candidates.remove(open);
self.checker.report_diagnostic(Diagnostic::new(
ReadWholeFile {
filename: SourceCodeSnippet::from_str(
&self.checker.generator().expr(open.filename),
),
suggestion: make_suggestion(&open, self.checker.generator()),
},
open.item.range(),
));
}
return;
}

View File

@@ -1,4 +1,4 @@
use crate::preview::is_readlines_in_for_fix_safe;
use crate::preview::is_readlines_in_for_fix_safe_enabled;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{Comprehension, Expr, StmtFor};
@@ -86,7 +86,7 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
}
let mut diagnostic = Diagnostic::new(ReadlinesInFor, expr_call.range());
diagnostic.set_fix(if is_readlines_in_for_fix_safe(checker.settings) {
diagnostic.set_fix(if is_readlines_in_for_fix_safe_enabled(checker.settings) {
Fix::safe_edit(Edit::range_deletion(
expr_call.range().add_start(expr_attr.value.range().len()),
))

View File

@@ -86,48 +86,42 @@ pub(crate) fn repeated_append(checker: &Checker, stmt: &Stmt) {
return;
}
// group borrows from checker, so we can't directly push into checker.diagnostics
let diagnostics: Vec<Diagnostic> = group_appends(appends)
.iter()
.filter_map(|group| {
// Groups with just one element are fine, and shouldn't be replaced by `extend`.
if group.appends.len() <= 1 {
return None;
}
for group in group_appends(appends) {
// Groups with just one element are fine, and shouldn't be replaced by `extend`.
if group.appends.len() <= 1 {
continue;
}
let replacement = make_suggestion(group, checker.generator());
let replacement = make_suggestion(&group, checker.generator());
let mut diagnostic = Diagnostic::new(
RepeatedAppend {
name: group.name().to_string(),
replacement: SourceCodeSnippet::new(replacement.clone()),
},
group.range(),
);
let mut diagnostic = Diagnostic::new(
RepeatedAppend {
name: group.name().to_string(),
replacement: SourceCodeSnippet::new(replacement.clone()),
},
group.range(),
);
// We only suggest a fix when all appends in a group are clumped together. If they're
// non-consecutive, fixing them is much more difficult.
//
// Avoid fixing if there are comments in between the appends:
//
// ```python
// a.append(1)
// # comment
// a.append(2)
// ```
if group.is_consecutive && !checker.comment_ranges().intersects(group.range()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::replacement(
replacement,
group.start(),
group.end(),
)));
}
// We only suggest a fix when all appends in a group are clumped together. If they're
// non-consecutive, fixing them is much more difficult.
//
// Avoid fixing if there are comments in between the appends:
//
// ```python
// a.append(1)
// # comment
// a.append(2)
// ```
if group.is_consecutive && !checker.comment_ranges().intersects(group.range()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::replacement(
replacement,
group.start(),
group.end(),
)));
}
Some(diagnostic)
})
.collect();
checker.report_diagnostics(diagnostics);
checker.report_diagnostic(diagnostic);
}
}
#[derive(Debug, Clone)]

View File

@@ -65,54 +65,28 @@ pub(crate) fn write_whole_file(checker: &Checker, with: &ast::StmtWith) {
}
// Then we need to match each `open` operation with exactly one `write` call.
let (matches, contents) = {
let mut matcher = WriteMatcher::new(candidates);
visitor::walk_body(&mut matcher, &with.body);
matcher.finish()
};
// All the matched operations should be reported.
let diagnostics: Vec<Diagnostic> = matches
.iter()
.zip(contents)
.map(|(open, content)| {
Diagnostic::new(
WriteWholeFile {
filename: SourceCodeSnippet::from_str(&checker.generator().expr(open.filename)),
suggestion: make_suggestion(open, content, checker.generator()),
},
open.item.range(),
)
})
.collect();
checker.report_diagnostics(diagnostics);
let mut matcher = WriteMatcher::new(checker, candidates);
visitor::walk_body(&mut matcher, &with.body);
}
/// AST visitor that matches `open` operations with the corresponding `write` calls.
#[derive(Debug)]
struct WriteMatcher<'a> {
struct WriteMatcher<'a, 'b> {
checker: &'a Checker<'b>,
candidates: Vec<FileOpen<'a>>,
matches: Vec<FileOpen<'a>>,
contents: Vec<&'a Expr>,
loop_counter: u32,
}
impl<'a> WriteMatcher<'a> {
fn new(candidates: Vec<FileOpen<'a>>) -> Self {
impl<'a, 'b> WriteMatcher<'a, 'b> {
fn new(checker: &'a Checker<'b>, candidates: Vec<FileOpen<'a>>) -> Self {
Self {
checker,
candidates,
matches: vec![],
contents: vec![],
loop_counter: 0,
}
}
fn finish(self) -> (Vec<FileOpen<'a>>, Vec<&'a Expr>) {
(self.matches, self.contents)
}
}
impl<'a> Visitor<'a> for WriteMatcher<'a> {
impl<'a> Visitor<'a> for WriteMatcher<'a, '_> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
if matches!(stmt, ast::Stmt::While(_) | ast::Stmt::For(_)) {
self.loop_counter += 1;
@@ -131,8 +105,16 @@ impl<'a> Visitor<'a> for WriteMatcher<'a> {
.position(|open| open.is_ref(write_to))
{
if self.loop_counter == 0 {
self.matches.push(self.candidates.remove(open));
self.contents.push(content);
let open = self.candidates.remove(open);
self.checker.report_diagnostic(Diagnostic::new(
WriteWholeFile {
filename: SourceCodeSnippet::from_str(
&self.checker.generator().expr(open.filename),
),
suggestion: make_suggestion(&open, content, self.checker.generator()),
},
open.item.range(),
));
} else {
self.candidates.remove(open);
}

View File

@@ -211,7 +211,9 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
context,
checker.settings,
);
checker.report_diagnostics(diagnostics);
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
}
ast::StringLikePart::Bytes(_) => {}
ast::StringLikePart::FString(f_string) => {
@@ -225,7 +227,9 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
context,
checker.settings,
);
checker.report_diagnostics(diagnostics);
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
}
}
}

View File

@@ -108,7 +108,6 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
}
let mut stopped_fixes = false;
let mut diagnostics = vec![];
for parameter in function_def.parameters.iter_non_variadic_params() {
let Some(default) = parameter.default() else {
@@ -132,10 +131,8 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
stopped_fixes |= diagnostic.fix.is_none();
}
diagnostics.push(diagnostic);
checker.report_diagnostic(diagnostic);
}
checker.report_diagnostics(diagnostics);
}
/// Generate a [`Fix`] to transform a `__post_init__` default argument into a

View File

@@ -40,33 +40,30 @@ impl Violation for UselessTryExcept {
/// TRY203 (previously TRY302)
pub(crate) fn useless_try_except(checker: &Checker, handlers: &[ExceptHandler]) {
if let Some(diagnostics) = handlers
.iter()
.map(|handler| {
let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) =
handler;
let Some(Stmt::Raise(ast::StmtRaise {
exc, cause: None, ..
})) = &body.first()
else {
return None;
};
if let Some(expr) = exc {
// E.g., `except ... as e: raise e`
if let Expr::Name(ast::ExprName { id, .. }) = expr.as_ref() {
if name.as_ref().is_some_and(|name| name.as_str() == id) {
return Some(Diagnostic::new(UselessTryExcept, handler.range()));
}
if handlers.iter().all(|handler| {
let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) = handler;
let Some(Stmt::Raise(ast::StmtRaise {
exc, cause: None, ..
})) = &body.first()
else {
return false;
};
if let Some(expr) = exc {
// E.g., `except ... as e: raise e`
if let Expr::Name(ast::ExprName { id, .. }) = expr.as_ref() {
if name.as_ref().is_some_and(|name| name.as_str() == id) {
return true;
}
None
} else {
// E.g., `except ...: raise`
Some(Diagnostic::new(UselessTryExcept, handler.range()))
}
})
.collect::<Option<Vec<_>>>()
{
false
} else {
// E.g., `except ...: raise`
true
}
}) {
// Require that all handlers are useless, but create one diagnostic per handler.
checker.report_diagnostics(diagnostics);
for handler in handlers {
checker.report_diagnostic(Diagnostic::new(UselessTryExcept, handler.range()));
}
}
}

View File

@@ -288,5 +288,13 @@ match x:
]:
pass
match a, b:
case [], []:
...
case [], _:
...
case _, []:
...
case _, _:
...

View File

@@ -0,0 +1,8 @@
# Ruff in some cases added brackets around tuples in some cases; deviating from Black.
# Ensure we don't revert already-applied formats with the fix.
# See https://github.com/astral-sh/ruff/pull/18147
match a, b:
case [[], []]:
...
case [[], _]:
...

View File

@@ -79,9 +79,26 @@ pub(crate) enum SequenceType {
impl SequenceType {
pub(crate) fn from_pattern(pattern: &PatternMatchSequence, source: &str) -> SequenceType {
if source[pattern.range()].starts_with('[') {
let before_first_pattern = &source[TextRange::new(
pattern.start(),
pattern
.patterns
.first()
.map(Ranged::start)
.unwrap_or(pattern.end()),
)];
let after_last_patttern = &source[TextRange::new(
pattern.start(),
pattern
.patterns
.first()
.map(Ranged::end)
.unwrap_or(pattern.end()),
)];
if before_first_pattern.starts_with('[') && !after_last_patttern.ends_with(',') {
SequenceType::List
} else if source[pattern.range()].starts_with('(') {
} else if before_first_pattern.starts_with('(') {
// If the pattern is empty, it must be a parenthesized tuple with no members. (This
// branch exists to differentiate between a tuple with and without its own parentheses,
// but a tuple without its own parentheses must have at least one member.)

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/pattern_matching_trailing_comma.py
snapshot_kind: text
---
## Input
@@ -27,7 +26,7 @@ match more := (than, one), indeed,:
```diff
--- Black
+++ Ruff
@@ -8,13 +8,16 @@
@@ -8,7 +8,10 @@
pass
@@ -38,15 +37,7 @@ match more := (than, one), indeed,:
+):
case _, (5, 6):
pass
- case (
+ case [
[[5], (6)],
[7],
- ):
+ ]:
pass
case _:
pass
case (
```
## Ruff Output
@@ -68,10 +59,10 @@ match (
):
case _, (5, 6):
pass
case [
case (
[[5], (6)],
[7],
]:
):
pass
case _:
pass

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py
snapshot_kind: text
---
## Input
```python
@@ -295,7 +294,15 @@ match x:
]:
pass
match a, b:
case [], []:
...
case [], _:
...
case _, []:
...
case _, _:
...
```
@@ -592,4 +599,14 @@ match x:
ccccccccccccccccccccccccccccccccc,
]:
pass
match a, b:
case [], []:
...
case [], _:
...
case _, []:
...
case _, _:
...
```

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern_match_regression_brackets.py
---
## Input
```python
# Ruff in some cases added brackets around tuples in some cases; deviating from Black.
# Ensure we don't revert already-applied formats with the fix.
# See https://github.com/astral-sh/ruff/pull/18147
match a, b:
case [[], []]:
...
case [[], _]:
...
```
## Output
```python
# Ruff in some cases added brackets around tuples in some cases; deviating from Black.
# Ensure we don't revert already-applied formats with the fix.
# See https://github.com/astral-sh/ruff/pull/18147
match a, b:
case [[], []]:
...
case [[], _]:
...
```

View File

@@ -769,16 +769,21 @@ impl SemanticSyntaxChecker {
// We are intentionally not inspecting the async status of the scope for now to mimic F704.
// await-outside-async is PLE1142 instead, so we'll end up emitting both syntax errors for
// cases that trigger F704
if ctx.in_function_scope() {
return;
}
if kind.is_await() {
if ctx.in_await_allowed_context() {
return;
}
// `await` is allowed at the top level of a Jupyter notebook.
// See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html.
if ctx.in_module_scope() && ctx.in_notebook() {
return;
}
} else if ctx.in_function_scope() {
if ctx.in_await_allowed_context() {
return;
}
} else if ctx.in_yield_allowed_context() {
return;
}
@@ -1719,6 +1724,35 @@ pub trait SemanticSyntaxContext {
/// See the trait-level documentation for more details.
fn in_await_allowed_context(&self) -> bool;
/// Returns `true` if the visitor is currently in a context where `yield` and `yield from`
/// expressions are allowed.
///
/// Yield expressions are allowed only in:
/// 1. Function definitions
/// 2. Lambda expressions
///
/// Unlike `await`, yield is not allowed in:
/// - Comprehensions (list, set, dict)
/// - Generator expressions
/// - Class definitions
///
/// This method should traverse parent scopes to check if the closest relevant scope
/// is a function or lambda, and that no disallowed context (class, comprehension, generator)
/// intervenes. For example:
///
/// ```python
/// def f():
/// yield 1 # okay, in a function
/// lambda: (yield 1) # okay, in a lambda
///
/// [(yield 1) for x in range(3)] # error, in a comprehension
/// ((yield 1) for x in range(3)) # error, in a generator expression
/// class C:
/// yield 1 # error, in a class within a function
/// ```
///
fn in_yield_allowed_context(&self) -> bool;
/// Returns `true` if the visitor is currently inside of a synchronous comprehension.
///
/// This method is necessary because `in_async_context` only checks for the nearest, enclosing

View File

@@ -556,6 +556,10 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
true
}
fn in_yield_allowed_context(&self) -> bool {
true
}
fn in_generator_scope(&self) -> bool {
true
}

View File

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

4
crates/ty/docs/cli.md generated
View File

@@ -1,3 +1,5 @@
<!-- WARNING: This file is auto-generated (cargo dev generate-all). Edit the doc comments in 'crates/ty/src/args.rs' if you want to change anything here. -->
# CLI Reference
## ty
@@ -51,7 +53,7 @@ ty check [OPTIONS] [PATH]...
</dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p>
<p>Possible values:</p>
<ul>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints</li>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints [default]</li>
<li><code>concise</code>: Print diagnostics concisely, one per line</li>
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>

View File

@@ -1,3 +1,5 @@
<!-- WARNING: This file is auto-generated (cargo dev generate-all). Update the doc comments on the 'Options' struct in 'crates/ty_project/src/metadata/options.rs' if you want to change anything here. -->
# Configuration
#### `respect-ignore-files`

250
crates/ty/docs/rules.md generated
View File

@@ -1,3 +1,5 @@
<!-- WARNING: This file is auto-generated (cargo dev generate-all). Edit the lint-declarations in 'crates/ty_python_semantic/src/types/diagnostic.rs' if you want to change anything here. -->
# Rules
## `byte-string-type-annotation`
@@ -50,7 +52,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L87)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L91)
</details>
## `conflicting-argument-forms`
@@ -81,7 +83,7 @@ f(int) # error
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L118)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L135)
</details>
## `conflicting-declarations`
@@ -111,7 +113,7 @@ a = 1
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L161)
</details>
## `conflicting-metaclass`
@@ -142,7 +144,7 @@ class C(A, B): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L186)
</details>
## `cyclic-class-definition`
@@ -173,30 +175,7 @@ class B(A): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L195)
</details>
## `division-by-zero`
**Default level**: error
<details>
<summary>detects division by zero</summary>
### What it does
It detects division by zero.
### Why is this bad?
Dividing by zero raises a `ZeroDivisionError` at runtime.
### Examples
```python
5 / 0
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L212)
</details>
## `duplicate-base`
@@ -222,7 +201,7 @@ class B(A, A): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L239)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256)
</details>
## `escape-character-in-forward-annotation`
@@ -359,7 +338,7 @@ TypeError: multiple bases have instance lay-out conflict
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L277)
</details>
## `inconsistent-mro`
@@ -388,7 +367,7 @@ class C(A, B): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L346)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L363)
</details>
## `index-out-of-bounds`
@@ -413,7 +392,7 @@ t[3] # IndexError: tuple index out of range
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L370)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L387)
</details>
## `invalid-argument-type`
@@ -439,7 +418,7 @@ func("foo") # error: [invalid-argument-type]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L390)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L407)
</details>
## `invalid-assignment`
@@ -466,7 +445,7 @@ a: int = ''
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L447)
</details>
## `invalid-attribute-access`
@@ -499,7 +478,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1336)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1395)
</details>
## `invalid-base`
@@ -507,13 +486,22 @@ C.instance_var = 3 # error: Cannot assign to instance variable
**Default level**: error
<details>
<summary>detects invalid bases in class definitions</summary>
<summary>detects class bases that will cause the class definition to raise an exception at runtime</summary>
TODO #14889
### What it does
Checks for class definitions that have bases which are not instances of `type`.
### Why is this bad?
Class definitions with bases like this will lead to `TypeError` being raised at runtime.
### Examples
```python
class A(42): ... # error: [invalid-base]
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L452)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L469)
</details>
## `invalid-context-manager`
@@ -539,7 +527,7 @@ with 1:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L461)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L520)
</details>
## `invalid-declaration`
@@ -567,7 +555,7 @@ a: str
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L482)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541)
</details>
## `invalid-exception-caught`
@@ -608,7 +596,7 @@ except ZeroDivisionError:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L505)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L564)
</details>
## `invalid-generic-class`
@@ -639,7 +627,7 @@ class C[U](Generic[T]): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L600)
</details>
## `invalid-legacy-type-variable`
@@ -672,7 +660,7 @@ def f(t: TypeVar("U")): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626)
</details>
## `invalid-metaclass`
@@ -704,7 +692,7 @@ class B(metaclass=f): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L616)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L675)
</details>
## `invalid-overload`
@@ -752,7 +740,7 @@ def foo(x: int) -> int: ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L643)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702)
</details>
## `invalid-parameter-default`
@@ -777,7 +765,7 @@ def f(a: int = ''): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L686)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L745)
</details>
## `invalid-protocol`
@@ -810,7 +798,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L318)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L335)
</details>
## `invalid-raise`
@@ -858,7 +846,7 @@ def g():
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L706)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765)
</details>
## `invalid-return-type`
@@ -882,7 +870,7 @@ def func() -> int:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L411)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L428)
</details>
## `invalid-super-argument`
@@ -926,7 +914,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)`
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L749)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L808)
</details>
## `invalid-syntax-in-forward-annotation`
@@ -966,7 +954,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L595)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654)
</details>
## `invalid-type-checking-constant`
@@ -995,7 +983,7 @@ TYPE_CHECKING = ''
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L847)
</details>
## `invalid-type-form`
@@ -1024,7 +1012,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L812)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L871)
</details>
## `invalid-type-variable-constraints`
@@ -1058,7 +1046,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L836)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L895)
</details>
## `missing-argument`
@@ -1082,7 +1070,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L924)
</details>
## `no-matching-overload`
@@ -1110,7 +1098,7 @@ func("string") # error: [no-matching-overload]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L884)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L943)
</details>
## `non-subscriptable`
@@ -1133,7 +1121,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L907)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L966)
</details>
## `not-iterable`
@@ -1158,7 +1146,7 @@ for i in 34: # TypeError: 'int' object is not iterable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L925)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L984)
</details>
## `parameter-already-assigned`
@@ -1184,7 +1172,7 @@ f(1, x=2) # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L976)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1035)
</details>
## `raw-string-type-annotation`
@@ -1243,7 +1231,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371)
</details>
## `subclass-of-final-class`
@@ -1271,7 +1259,7 @@ class B(A): ... # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1067)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1126)
</details>
## `too-many-positional-arguments`
@@ -1297,7 +1285,7 @@ f("foo") # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1112)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1171)
</details>
## `type-assertion-failure`
@@ -1324,7 +1312,7 @@ def _(x: int):
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1090)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1149)
</details>
## `unavailable-implicit-super-arguments`
@@ -1368,7 +1356,7 @@ class A:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1133)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1192)
</details>
## `unknown-argument`
@@ -1394,7 +1382,7 @@ f(x=1, y=2) # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1190)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1249)
</details>
## `unresolved-attribute`
@@ -1421,7 +1409,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1211)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1270)
</details>
## `unresolved-import`
@@ -1445,7 +1433,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1233)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1292)
</details>
## `unresolved-reference`
@@ -1469,7 +1457,7 @@ print(x) # NameError: name 'x' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1252)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1311)
</details>
## `unsupported-bool-conversion`
@@ -1505,7 +1493,7 @@ b1 < b2 < b1 # exception raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L945)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1004)
</details>
## `unsupported-operator`
@@ -1532,7 +1520,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1330)
</details>
## `zero-stepsize-in-slice`
@@ -1556,25 +1544,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1293)
</details>
## `call-possibly-unbound-method`
**Default level**: warn
<details>
<summary>detects calls to possibly unbound methods</summary>
### What it does
Checks for calls to possibly unbound methods.
### Why is this bad?
Calling an unbound method will raise an `AttributeError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-possibly-unbound-method)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L105)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1352)
</details>
## `invalid-ignore-comment`
@@ -1630,7 +1600,38 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L997)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1056)
</details>
## `possibly-unbound-implicit-call`
**Default level**: warn
<details>
<summary>detects implicit calls to possibly unbound methods</summary>
### What it does
Checks for implicit calls to possibly unbound methods.
### Why is this bad?
Expressions such as `x[y]` and `x * y` call methods
under the hood (`__getitem__` and `__mul__` respectively).
Calling an unbound method will raise an `AttributeError` at runtime.
### Examples
```python
import datetime
class A:
if datetime.date.today().weekday() != 6:
def __getitem__(self, v): ...
A()[0] # TypeError: 'A' object is not subscriptable
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L109)
</details>
## `possibly-unbound-import`
@@ -1661,7 +1662,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1019)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1078)
</details>
## `redundant-cast`
@@ -1687,7 +1688,7 @@ cast(int, f()) # Redundant
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1364)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1423)
</details>
## `undefined-reveal`
@@ -1710,7 +1711,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1172)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1231)
</details>
## `unknown-rule`
@@ -1743,6 +1744,67 @@ a = 20 / 0 # ty: ignore[division-by-zero]
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L40)
</details>
## `unsupported-base`
**Default level**: warn
<details>
<summary>detects class bases that are unsupported as ty could not feasibly calculate the class's MRO</summary>
### What it does
Checks for class definitions that have bases which are unsupported by ty.
### Why is this bad?
If a class has a base that is an instance of a complex type such as a union type,
ty will not be able to resolve the [method resolution order] (MRO) for the class.
This will lead to an inferior understanding of your codebase and unpredictable
type-checking behavior.
### Examples
```python
import datetime
class A: ...
class B: ...
if datetime.date.today().weekday() != 6:
C = A
else:
C = B
class D(C): ... # error: [unsupported-base]
```
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L487)
</details>
## `division-by-zero`
**Default level**: ignore
<details>
<summary>detects division by zero</summary>
### What it does
It detects division by zero.
### Why is this bad?
Dividing by zero raises a `ZeroDivisionError` at runtime.
### Examples
```python
5 / 0
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L238)
</details>
## `possibly-unresolved-reference`
**Default level**: ignore
@@ -1767,7 +1829,7 @@ print(x) # NameError: name 'x' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1045)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1104)
</details>
## `unused-ignore-comment`

View File

@@ -283,7 +283,7 @@ impl clap::Args for RulesArg {
/// The diagnostic output format.
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum OutputFormat {
/// Print diagnostics verbosely, with context and helpful hints.
/// Print diagnostics verbosely, with context and helpful hints \[default\].
///
/// Diagnostic messages may include additional context and
/// annotations on the input to help understand the message.

View File

@@ -241,6 +241,73 @@ fn config_override_python_platform() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn config_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-version = "3.8"
"#,
),
(
"test.py",
r#"
aiter
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:2:1
|
2 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types
--> pyproject.toml:3:18
|
2 | [tool.ty.environment]
3 | python-version = "3.8"
| ^^^^^ Python 3.8 assumed due to this configuration setting
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:2:1
|
2 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.9 was assumed when resolving types because it was specified on the command line
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// Paths specified on the CLI are relative to the current working directory and not the project root.
///
/// We test this by adding an extra search path from the CLI to the libs directory when
@@ -387,22 +454,11 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error)
// and a `division-by-zero` diagnostic (error).
assert_cmd_snapshot!(case.command(), @r"
// Assert that there's an `unresolved-reference` diagnostic (error).
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: rule `division-by-zero` is enabled by default
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:7:1
|
@@ -413,17 +469,17 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
"###);
case.write_file(
"pyproject.toml",
r#"
[tool.ty.rules]
division-by-zero = "warn" # demote to warn
division-by-zero = "warn" # promote to warn
unresolved-reference = "ignore"
"#,
)?;
@@ -468,9 +524,9 @@ fn cli_rule_severity() -> anyhow::Result<()> {
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error),
// a `division-by-zero` (error) and a unresolved-import (error) diagnostic by default.
assert_cmd_snapshot!(case.command(), @r"
// Assert that there's an `unresolved-reference` diagnostic (error)
// and an unresolved-import (error) diagnostic by default.
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -485,18 +541,6 @@ fn cli_rule_severity() -> anyhow::Result<()> {
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
info: rule `division-by-zero` is enabled by default
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:9:1
|
@@ -507,11 +551,11 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
info: rule `unresolved-reference` is enabled by default
Found 3 diagnostics
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
"###);
assert_cmd_snapshot!(
case
@@ -575,22 +619,11 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
"#,
)?;
// Assert that there's a `unresolved-reference` diagnostic (error)
// and a `division-by-zero` (error) by default.
assert_cmd_snapshot!(case.command(), @r"
// Assert that there's a `unresolved-reference` diagnostic (error) by default.
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: rule `division-by-zero` is enabled by default
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:7:1
|
@@ -601,11 +634,11 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
"###);
assert_cmd_snapshot!(
case
@@ -614,7 +647,6 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
.arg("unresolved-reference")
.arg("--warn")
.arg("division-by-zero")
// Override the error severity with warning
.arg("--ignore")
.arg("unresolved-reference"),
@r"
@@ -1103,18 +1135,10 @@ fn check_specific_paths() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command(),
@r"
@r###"
success: false
exit_code: 1
----- stdout -----
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> project/main.py:2:5
|
2 | y = 4 / 0 # error: division-by-zero
| ^^^^^
|
info: rule `division-by-zero` is enabled by default
error[unresolved-import]: Cannot resolve imported module `main2`
--> project/other.py:2:6
|
@@ -1135,11 +1159,11 @@ fn check_specific_paths() -> anyhow::Result<()> {
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
Found 3 diagnostics
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"
"###
);
// Now check only the `tests` and `other.py` files.
@@ -1430,10 +1454,10 @@ fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
}
#[test]
fn cli_config_args_overrides_knot_toml() -> anyhow::Result<()> {
fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> {
let case = TestCase::with_files(vec![
(
"knot.toml",
"ty.toml",
r#"
[terminal]
error-on-warning = true
@@ -1441,6 +1465,27 @@ fn cli_config_args_overrides_knot_toml() -> anyhow::Result<()> {
),
("test.py", r"print(x) # [unresolved-reference]"),
])?;
// Exit code of 1 due to the setting in `ty.toml`
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Exit code of 0 because the `ty.toml` setting is overwritten by `--config`
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r"
success: true
exit_code: 0

View File

@@ -1444,13 +1444,11 @@ mod unix {
)
.expect("Expected bar.baz to exist in site-packages.");
let baz_project = case.project_path("bar/baz.py");
let baz_file = baz.file().unwrap();
assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
assert_eq!(
source_text(case.db(), baz.file()).as_str(),
"def baz(): ..."
);
assert_eq!(
baz.file().path(case.db()).as_system_path(),
baz_file.path(case.db()).as_system_path(),
Some(&*baz_project)
);
@@ -1465,7 +1463,7 @@ mod unix {
case.apply_changes(changes);
assert_eq!(
source_text(case.db(), baz.file()).as_str(),
source_text(case.db(), baz_file).as_str(),
"def baz(): print('Version 2')"
);
@@ -1478,7 +1476,7 @@ mod unix {
case.apply_changes(changes);
assert_eq!(
source_text(case.db(), baz.file()).as_str(),
source_text(case.db(), baz_file).as_str(),
"def baz(): print('Version 3')"
);
@@ -1524,6 +1522,7 @@ mod unix {
&ModuleName::new_static("bar.baz").unwrap(),
)
.expect("Expected bar.baz to exist in site-packages.");
let baz_file = baz.file().unwrap();
let bar_baz = case.project_path("bar/baz.py");
let patched_bar_baz = case.project_path("patched/bar/baz.py");
@@ -1534,11 +1533,8 @@ mod unix {
"def baz(): ..."
);
assert_eq!(
source_text(case.db(), baz.file()).as_str(),
"def baz(): ..."
);
assert_eq!(baz.file().path(case.db()).as_system_path(), Some(&*bar_baz));
assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
assert_eq!(baz_file.path(case.db()).as_system_path(), Some(&*bar_baz));
case.assert_indexed_project_files([patched_bar_baz_file]);
@@ -1567,7 +1563,7 @@ mod unix {
let patched_baz_text = source_text(case.db(), patched_bar_baz_file);
let did_update_patched_baz = patched_baz_text.as_str() == "def baz(): print('Version 2')";
let bar_baz_text = source_text(case.db(), baz.file());
let bar_baz_text = source_text(case.db(), baz_file);
let did_update_bar_baz = bar_baz_text.as_str() == "def baz(): print('Version 2')";
assert!(
@@ -1650,7 +1646,7 @@ mod unix {
"def baz(): ..."
);
assert_eq!(
baz.file().path(case.db()).as_system_path(),
baz.file().unwrap().path(case.db()).as_system_path(),
Some(&*baz_original)
);

View File

@@ -158,9 +158,9 @@ mod tests {
use crate::db::tests::TestDb;
use ruff_db::system::{DbWithWritableSystem, SystemPathBuf};
use ruff_python_ast::PythonVersion;
use ty_python_semantic::{
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
Program, ProgramSettings, PythonPath, PythonPlatform, PythonVersionWithSource,
SearchPathSettings,
};
pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest {
@@ -191,7 +191,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::latest_ty(),
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],

View File

@@ -13,11 +13,10 @@ pub use hover::hover;
pub use inlay_hints::inlay_hints;
pub use markup::MarkupKind;
use rustc_hash::FxHashSet;
use std::ops::{Deref, DerefMut};
use ruff_db::files::{File, FileRange};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::ops::{Deref, DerefMut};
use ty_python_semantic::types::{Type, TypeDefinition};
/// Information associated with a text range.
@@ -188,7 +187,10 @@ impl HasNavigationTargets for Type<'_> {
impl HasNavigationTargets for TypeDefinition<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
let full_range = self.full_range(db.upcast());
let Some(full_range) = self.full_range(db.upcast()) else {
return NavigationTargets::empty();
};
NavigationTargets::single(NavigationTarget {
file: full_range.file(),
focus_range: self.focus_range(db.upcast()).unwrap_or(full_range).range(),
@@ -205,10 +207,10 @@ mod tests {
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
use ruff_db::files::{File, system_path_to_file};
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion;
use ruff_text_size::TextSize;
use ty_python_semantic::{
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
Program, ProgramSettings, PythonPath, PythonPlatform, PythonVersionWithSource,
SearchPathSettings,
};
pub(super) fn cursor_test(source: &str) -> CursorTest {
@@ -228,7 +230,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::latest_ty(),
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],

View File

@@ -25,9 +25,17 @@ pub trait Db: SemanticDb + Upcast<dyn SemanticDb> {
#[derive(Clone)]
pub struct ProjectDatabase {
project: Option<Project>,
storage: salsa::Storage<ProjectDatabase>,
files: Files,
// IMPORTANT: Never return clones of `system` outside `ProjectDatabase` (only return references)
// or the "trick" to get a mutable `Arc` in `Self::system_mut` is no longer guaranteed to work.
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
// IMPORTANT: This field must be the last because we use `zalsa_mut` (drops all other storage references)
// to drop all other references to the database, which gives us exclusive access to other `Arc`s stored on this db.
// However, for this to work it's important that the `storage` is dropped AFTER any `Arc` that
// we try to mutably borrow using `Arc::get_mut` (like `system`).
storage: salsa::Storage<ProjectDatabase>,
}
impl ProjectDatabase {

View File

@@ -663,10 +663,11 @@ mod tests {
use ruff_db::source::source_text;
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::PythonVersion;
use ruff_python_ast::name::Name;
use ty_python_semantic::types::check_types;
use ty_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
use ty_python_semantic::{
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
};
#[test]
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
@@ -677,7 +678,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
},

View File

@@ -10,7 +10,10 @@ use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use thiserror::Error;
use ty_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
use ty_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
use ty_python_semantic::{
ProgramSettings, PythonPath, PythonPlatform, PythonVersionSource, PythonVersionWithSource,
SearchPathSettings,
};
use super::settings::{Settings, TerminalSettings};
@@ -93,8 +96,17 @@ impl Options {
let python_version = self
.environment
.as_ref()
.and_then(|env| env.python_version.as_deref().copied())
.unwrap_or(PythonVersion::latest_ty());
.and_then(|env| env.python_version.as_ref())
.map(|ranged_version| PythonVersionWithSource {
version: **ranged_version,
source: match ranged_version.source() {
ValueSource::Cli => PythonVersionSource::Cli,
ValueSource::File(path) => {
PythonVersionSource::File(path.clone(), ranged_version.range())
}
},
})
.unwrap_or_default();
let python_platform = self
.environment
.as_ref()

View File

@@ -113,3 +113,35 @@ def _(
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
```
## Diagnostics for common errors
<!-- snapshot-diagnostics -->
### Module-literal used when you meant to use a class from that module
It's pretty common in Python to accidentally use a module-literal type in a type expression when you
*meant* to use a class by the same name that comes from that module. We emit a nice subdiagnostic
for this case:
`foo.py`:
```py
import datetime
def f(x: datetime): ... # error: [invalid-type-form]
```
`PIL/Image.py`:
```py
class Image: ...
```
`bar.py`:
```py
from PIL import Image
def g(x: Image): ... # error: [invalid-type-form]
```

View File

@@ -81,23 +81,22 @@ import typing
class ListSubclass(typing.List): ...
# revealed: tuple[<class 'ListSubclass'>, <class 'list[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'ListSubclass'>, <class 'list[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# TODO: should not have multiple `Generic[]` elements
# revealed: tuple[<class 'DictSubclass'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
# revealed: tuple[<class 'DictSubclass'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# revealed: tuple[<class 'SetSubclass'>, <class 'set[Unknown]'>, <class 'MutableSet[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'SetSubclass'>, <class 'set[Unknown]'>, <class 'MutableSet[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(FrozenSetSubclass.__mro__)
####################
@@ -106,30 +105,26 @@ reveal_type(FrozenSetSubclass.__mro__)
class ChainMapSubclass(typing.ChainMap): ...
# TODO: should not have multiple `Generic[]` elements
# revealed: tuple[<class 'ChainMapSubclass'>, <class 'ChainMap[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
# revealed: tuple[<class 'ChainMapSubclass'>, <class 'ChainMap[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
# TODO: Should have one `Generic[]` element, not three(!)
# revealed: tuple[<class 'CounterSubclass'>, <class 'Counter[Unknown]'>, <class 'dict[Unknown, int]'>, <class 'MutableMapping[Unknown, int]'>, <class 'Mapping[Unknown, int]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], typing.Generic[_T], <class 'object'>]
# revealed: tuple[<class 'CounterSubclass'>, <class 'Counter[Unknown]'>, <class 'dict[Unknown, int]'>, <class 'MutableMapping[Unknown, int]'>, <class 'Mapping[Unknown, int]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(CounterSubclass.__mro__)
class DefaultDictSubclass(typing.DefaultDict): ...
# TODO: Should not have multiple `Generic[]` elements
# revealed: tuple[<class 'DefaultDictSubclass'>, <class 'defaultdict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
# revealed: tuple[<class 'DefaultDictSubclass'>, <class 'defaultdict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# revealed: tuple[<class 'DequeSubclass'>, <class 'deque[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'DequeSubclass'>, <class 'deque[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# TODO: Should not have multiple `Generic[]` elements
# revealed: tuple[<class 'OrderedDictSubclass'>, <class 'OrderedDict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
# revealed: tuple[<class 'OrderedDictSubclass'>, <class 'OrderedDict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(OrderedDictSubclass.__mro__)
```

View File

@@ -16,8 +16,7 @@ Alias: TypeAlias = int
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
# TODO: this is not correct. At runtime, this is `type` (`Alias` is just `<class 'int'>`).
reveal_type(Alias) # revealed: typing.TypeAliasType
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
def g() -> TypeGuard[int]: ...
def h() -> TypeIs[int]: ...

View File

@@ -777,6 +777,30 @@ reveal_type(C.variable_with_class_default1) # revealed: str
reveal_type(c_instance.variable_with_class_default1) # revealed: str
```
#### Descriptor attributes as class variables
Whether they are explicitly qualified as `ClassVar`, or just have a class level default, we treat
descriptor attributes as class variables. This test mainly makes sure that we do *not* treat them as
instance variables. This would lead to a different outcome, since the `__get__` method would not be
called (the descriptor protocol is not invoked for instance variables).
```py
from typing import ClassVar
class Descriptor:
def __get__(self, instance, owner) -> int:
return 42
class C:
a: ClassVar[Descriptor]
b: Descriptor = Descriptor()
c: ClassVar[Descriptor] = Descriptor()
reveal_type(C().a) # revealed: int
reveal_type(C().b) # revealed: int
reveal_type(C().c) # revealed: int
```
### Inheritance of class/instance attributes
#### Instance variable defined in a base class
@@ -1736,9 +1760,9 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
```py
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
reveal_type(b"foo".join)
# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
reveal_type(b"foo".endswith)
```

View File

@@ -313,7 +313,8 @@ reveal_type(A() + "foo") # revealed: A
reveal_type("foo" + A()) # revealed: A
reveal_type(A() + b"foo") # revealed: A
reveal_type(b"foo" + A()) # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(A() + ()) # revealed: A
reveal_type(() + A()) # revealed: A

View File

@@ -54,8 +54,10 @@ reveal_type(2**largest_u32) # revealed: int
def variable(x: int):
reveal_type(x**2) # revealed: int
reveal_type(2**x) # revealed: Any
reveal_type(x**x) # revealed: Any
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(2**x) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(x**x) # revealed: int
```
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
@@ -70,6 +72,34 @@ reveal_type(2 ** (-1)) # revealed: float
reveal_type((-1) ** (-1)) # revealed: float
```
## Division and Modulus
Division works differently in Python than in Rust. If the result is negative and there is a
remainder, the division rounds down (instead of towards zero). The remainder needs to be adjusted to
compensate so that `(lhs // rhs) * rhs + (lhs % rhs) == lhs`:
```py
reveal_type(256 % 129) # revealed: Literal[127]
reveal_type(-256 % 129) # revealed: Literal[2]
reveal_type(256 % -129) # revealed: Literal[-2]
reveal_type(-256 % -129) # revealed: Literal[-127]
reveal_type(129 % 16) # revealed: Literal[1]
reveal_type(-129 % 16) # revealed: Literal[15]
reveal_type(129 % -16) # revealed: Literal[-15]
reveal_type(-129 % -16) # revealed: Literal[-1]
reveal_type(10 // 8) # revealed: Literal[1]
reveal_type(-10 // 8) # revealed: Literal[-2]
reveal_type(10 // -8) # revealed: Literal[-2]
reveal_type(-10 // -8) # revealed: Literal[1]
reveal_type(10 // 6) # revealed: Literal[1]
reveal_type(-10 // 6) # revealed: Literal[-2]
reveal_type(10 // -6) # revealed: Literal[-2]
reveal_type(-10 // -6) # revealed: Literal[1]
```
## Division by Zero
This error is really outside the current Python type system, because e.g. `int.__truediv__` and

View File

@@ -158,10 +158,10 @@ def _(flag: bool) -> None:
def __new__(cls):
return object.__new__(cls)
# error: [call-possibly-unbound-method]
# error: [possibly-unbound-implicit-call]
reveal_type(Foo()) # revealed: Foo
# error: [call-possibly-unbound-method]
# error: [possibly-unbound-implicit-call]
# error: [too-many-positional-arguments]
reveal_type(Foo(1)) # revealed: Foo
```

View File

@@ -112,7 +112,7 @@ def _(flag: bool):
this_fails = ThisFails()
# error: [call-possibly-unbound-method]
# error: [possibly-unbound-implicit-call]
reveal_type(this_fails[0]) # revealed: Unknown | str
```
@@ -236,6 +236,40 @@ def _(flag: bool):
return str(key)
c = C()
# error: [call-possibly-unbound-method]
# error: [possibly-unbound-implicit-call]
reveal_type(c[0]) # revealed: str
```
## Dunder methods cannot be looked up on instances
Class-level annotations with no value assigned are considered instance-only, and aren't available as
dunder methods:
```py
from typing import Callable
class C:
__call__: Callable[..., None]
# error: [call-non-callable]
C()()
# error: [invalid-assignment]
_: Callable[..., None] = C()
```
And of course the same is true if we have only an implicit assignment inside a method:
```py
from typing import Callable
class C:
def __init__(self):
self.__call__ = lambda *a, **kw: None
# error: [call-non-callable]
C()()
# error: [invalid-assignment]
_: Callable[..., None] = C()
```

View File

@@ -109,23 +109,50 @@ def _(o: object):
### Unsupported operators for positive contributions
Raise an error if any of the positive contributions to the intersection type are unsupported for the
given operator:
Raise an error if the given operator is unsupported for all positive contributions to the
intersection type:
```py
class NonContainer1: ...
class NonContainer2: ...
def _(x: object):
if isinstance(x, NonContainer1):
if isinstance(x, NonContainer2):
reveal_type(x) # revealed: NonContainer1 & NonContainer2
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer1`"
reveal_type(2 in x) # revealed: bool
```
Do not raise an error if at least one of the positive contributions to the intersection type support
the operator:
```py
class Container:
def __contains__(self, x) -> bool:
return False
class NonContainer: ...
def _(x: object):
if isinstance(x, Container):
if isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & NonContainer
if isinstance(x, NonContainer1):
if isinstance(x, Container):
if isinstance(x, NonContainer2):
reveal_type(x) # revealed: NonContainer1 & Container & NonContainer2
reveal_type(2 in x) # revealed: bool
```
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
reveal_type(2 in x) # revealed: bool
Do also raise an error if the intersection has no positive contributions at all, unless the operator
is supported on `object`:
```py
def _(x: object):
if not isinstance(x, NonContainer1):
reveal_type(x) # revealed: ~NonContainer1
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `object`, in comparing `Literal[2]` with `~NonContainer1`"
reveal_type(2 in x) # revealed: bool
reveal_type(2 is x) # revealed: bool
```
### Unsupported operators for negative contributions

View File

@@ -369,7 +369,65 @@ To do
### `frozen`
To do
If true (the default is False), assigning to fields will generate a diagnostic.
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int
frozen_instance = MyFrozenClass(1)
frozen_instance.x = 2 # error: [invalid-assignment]
```
If `__setattr__()` or `__delattr__()` is defined in the class, we should emit a diagnostic.
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int
# TODO: Emit a diagnostic here
def __setattr__(self, name: str, value: object) -> None: ...
# TODO: Emit a diagnostic here
def __delattr__(self, name: str) -> None: ...
```
This also works for generic dataclasses:
```toml
[environment]
python-version = "3.12"
```
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenGeneric[T]:
x: T
frozen_instance = MyFrozenGeneric[int](1)
frozen_instance.x = 2 # error: [invalid-assignment]
```
When attempting to mutate an unresolved attribute on a frozen dataclass, only `unresolved-attribute`
is emitted:
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass: ...
frozen = MyFrozenClass()
frozen.x = 2 # error: [unresolved-attribute]
```
### `match_args`

View File

@@ -298,25 +298,25 @@ python-version = "3.12"
```
```py
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound"
# error: [invalid-syntax] "`yield` statement outside of a function"
type X[T: (yield 1)] = int
def _():
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound"
type X[T: (yield 1)] = int
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a type alias"
# error: [invalid-syntax] "`yield` statement outside of a function"
type Y = (yield 1)
def _():
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a type alias"
type Y = (yield 1)
# error: [invalid-type-form] "Named expressions are not allowed in type expressions"
# error: [invalid-syntax] "named expression cannot be used within a generic definition"
def f[T](x: int) -> (y := 3):
return x
# error: [invalid-syntax] "`yield from` statement outside of a function"
# error: [invalid-syntax] "yield expression cannot be used within a generic definition"
class C[T]((yield from [object])):
pass
def _():
# error: [invalid-syntax] "yield expression cannot be used within a generic definition"
class C[T]((yield from [object])):
pass
```
## `await` outside async function

View File

@@ -397,3 +397,19 @@ async def i() -> typing.AsyncIterable:
async def j() -> str: # error: [invalid-return-type]
yield 42
```
## Diagnostics for `invalid-return-type` on non-protocol subclasses of protocol classes
<!-- snapshot-diagnostics -->
We emit a nice subdiagnostic in this situation explaining the probable error here:
```py
from typing_extensions import Protocol
class Abstract(Protocol):
def method(self) -> str: ...
class Concrete(Abstract):
def method(self) -> str: ... # error: [invalid-return-type]
```

View File

@@ -24,9 +24,7 @@ class:
```py
class Bad(Generic[T], Generic[T]): ... # error: [duplicate-base]
# TODO: should emit an error (fails at runtime)
class AlsoBad(Generic[T], Generic[S]): ...
class AlsoBad(Generic[T], Generic[S]): ... # error: [duplicate-base]
```
You cannot use the same typevar more than once.

View File

@@ -29,8 +29,10 @@ import parent.child.two
`from.py`
```py
# TODO: This should not be an error
from parent.child import one, two # error: [unresolved-import]
from parent.child import one, two
reveal_type(one) # revealed: <module 'parent.child.one'>
reveal_type(two) # revealed: <module 'parent.child.two'>
```
## Regular package in namespace package
@@ -105,3 +107,42 @@ reveal_type(x) # revealed: Unknown | Literal["module"]
import foo.bar # error: [unresolved-import]
```
## `from` import with namespace package
Regression test for <https://github.com/astral-sh/ty/issues/363>
`google/cloud/pubsub_v1/__init__.py`:
```py
class PublisherClient: ...
```
```py
from google.cloud import pubsub_v1
reveal_type(pubsub_v1.PublisherClient) # revealed: <class 'PublisherClient'>
```
## `from` root importing sub-packages
Regresssion test for <https://github.com/astral-sh/ty/issues/375>
`opentelemetry/trace/__init__.py`:
```py
class Trace: ...
```
`opentelemetry/metrics/__init__.py`:
```py
class Metric: ...
```
```py
from opentelemetry import trace, metrics
reveal_type(trace) # revealed: <module 'opentelemetry.trace'>
reveal_type(metrics) # revealed: <module 'opentelemetry.metrics'>
```

View File

@@ -173,7 +173,7 @@ if hasattr(DoesNotExist, "__mro__"):
if not isinstance(DoesNotExist, type):
reveal_type(DoesNotExist) # revealed: Unknown & ~type
class Foo(DoesNotExist): ... # error: [invalid-base]
class Foo(DoesNotExist): ... # error: [unsupported-base]
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
```
@@ -232,11 +232,15 @@ reveal_type(AA.__mro__) # revealed: tuple[<class 'AA'>, <class 'Z'>, Unknown, <
## `__bases__` includes a `Union`
<!-- snapshot-diagnostics -->
We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we
find a union type in a class's bases, we infer the class's `__mro__` as being
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
```py
from typing_extensions import reveal_type
def returns_bool() -> bool:
return True
@@ -250,7 +254,7 @@ else:
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
class Foo(x): ...
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
@@ -259,8 +263,8 @@ reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'obje
## `__bases__` is a union of a dynamic type and valid bases
If a dynamic type such as `Any` or `Unknown` is one of the elements in the union, and all other
types *would be* valid class bases, we do not emit an `invalid-base` diagnostic and use the dynamic
type as a base to prevent further downstream errors.
types *would be* valid class bases, we do not emit an `invalid-base` or `unsupported-base`
diagnostic, and we use the dynamic type as a base to prevent further downstream errors.
```py
from typing import Any
@@ -299,8 +303,8 @@ else:
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
reveal_type(y) # revealed: <class 'C'> | <class 'D'>
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 14 [invalid-base] "Invalid class base with type `<class 'C'> | <class 'D'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
# error: 14 [unsupported-base] "Unsupported class base with type `<class 'C'> | <class 'D'>`"
class Foo(x, y): ...
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
@@ -321,7 +325,7 @@ if returns_bool():
else:
foo = object
# error: 21 [invalid-base] "Invalid class base with type `<class 'Y'> | <class 'object'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 21 [unsupported-base] "Unsupported class base with type `<class 'Y'> | <class 'object'>`"
class PossibleError(foo, X): ...
reveal_type(PossibleError.__mro__) # revealed: tuple[<class 'PossibleError'>, Unknown, <class 'object'>]
@@ -339,12 +343,47 @@ else:
# revealed: tuple[<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>] | tuple[<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>]
reveal_type(B.__mro__)
# error: 12 [invalid-base] "Invalid class base with type `<class 'B'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'B'> | <class 'B'>`"
class Z(A, B): ...
reveal_type(Z.__mro__) # revealed: tuple[<class 'Z'>, Unknown, <class 'object'>]
```
## `__bases__` lists that include objects that are not instances of `type`
<!-- snapshot-diagnostics -->
```py
class Foo(2): ... # error: [invalid-base]
```
A base that is not an instance of `type` but does have an `__mro_entries__` method will not raise an
exception at runtime, so we issue `unsupported-base` rather than `invalid-base`:
```py
class Foo:
def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
return ()
class Bar(Foo()): ... # error: [unsupported-base]
```
But for objects that have badly defined `__mro_entries__`, `invalid-base` is emitted rather than
`unsupported-base`:
```py
class Bad1:
def __mro_entries__(self, bases, extra_arg):
return ()
class Bad2:
def __mro_entries__(self, bases) -> int:
return 42
class BadSub1(Bad1()): ... # error: [invalid-base]
class BadSub2(Bad2()): ... # error: [invalid-base]
```
## `__bases__` lists with duplicate bases
<!-- snapshot-diagnostics -->
@@ -488,6 +527,45 @@ reveal_type(unknown_object) # revealed: Unknown
reveal_type(unknown_object.__mro__) # revealed: Unknown
```
## MROs of classes that use multiple inheritance with generic aliases and subscripted `Generic`
```py
from typing import Generic, TypeVar, Iterator
T = TypeVar("T")
class peekable(Generic[T], Iterator[T]): ...
# revealed: tuple[<class 'peekable[Unknown]'>, <class 'Iterator[T]'>, <class 'Iterable[T]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(peekable.__mro__)
class peekable2(Iterator[T], Generic[T]): ...
# revealed: tuple[<class 'peekable2[Unknown]'>, <class 'Iterator[T]'>, <class 'Iterable[T]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(peekable2.__mro__)
class Base: ...
class Intermediate(Base, Generic[T]): ...
class Sub(Intermediate[T], Base): ...
# revealed: tuple[<class 'Sub[Unknown]'>, <class 'Intermediate[T]'>, <class 'Base'>, typing.Generic, <class 'object'>]
reveal_type(Sub.__mro__)
```
## Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases
<!-- snapshot-diagnostics -->
```py
from typing_extensions import Protocol, TypeVar, Generic
T = TypeVar("T")
class Foo(Protocol): ...
class Bar(Protocol[T]): ...
class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
```
## Classes that inherit from themselves
These are invalid, but we need to be able to handle them gracefully without panicking.

View File

@@ -146,11 +146,13 @@ def _(flag: bool):
def _(flag: bool):
x = 1 if flag else "a"
# error: [invalid-argument-type]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1, "a"]
# error: [invalid-argument-type]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1, "a"]
```

View File

@@ -214,7 +214,8 @@ def flag() -> bool:
t = int if flag() else str
# error: [invalid-argument-type]
# TODO: this should cause us to emit a diagnostic during
# type checking
if issubclass(t, "str"):
reveal_type(t) # revealed: <class 'int'> | <class 'str'>

View File

@@ -28,7 +28,7 @@ def f() -> None:
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__value__) # revealed: Any
reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`)
```
## Invalid assignment

View File

@@ -67,12 +67,10 @@ It's an error to include both bare `Protocol` and subscripted `Protocol[]` in th
simultaneously:
```py
# TODO: should emit a `[duplicate-bases]` error here:
class DuplicateBases(Protocol, Protocol[T]):
class DuplicateBases(Protocol, Protocol[T]): # error: [duplicate-base]
x: T
# TODO: should not have `Protocol` or `Generic` multiple times
# revealed: tuple[<class 'DuplicateBases[Unknown]'>, typing.Protocol, typing.Generic, typing.Protocol[T], typing.Generic[T], <class 'object'>]
# revealed: tuple[<class 'DuplicateBases[Unknown]'>, Unknown, <class 'object'>]
reveal_type(DuplicateBases.__mro__)
```
@@ -243,8 +241,7 @@ def f(
Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime:
```py
# TODO: Should be `Literal[True]`, but `bool` is also fine
# error: [invalid-argument-type]
# Could also be `Literal[True]`, but `bool` is fine:
reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
```

View File

@@ -27,8 +27,7 @@ error[unsupported-operator]: Operator `|` is unsupported between objects of type
| ^^^^^^^^^
|
info: Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later
info: The inferred target version of your project is Python 3.9
info: If using a pyproject.toml file, consider adjusting the `project.requires-python` or `tool.ty.environment.python-version` field
info: Python 3.9 was assumed when resolving types because it was specified on the command line
info: rule `unsupported-operator` is enabled by default
```

View File

@@ -0,0 +1,62 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Module-literal used when you meant to use a class from that module
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
---
# Python source files
## foo.py
```
1 | import datetime
2 |
3 | def f(x: datetime): ... # error: [invalid-type-form]
```
## PIL/Image.py
```
1 | class Image: ...
```
## bar.py
```
1 | from PIL import Image
2 |
3 | def g(x: Image): ... # error: [invalid-type-form]
```
# Diagnostics
```
error[invalid-type-form]: Variable of type `<module 'datetime'>` is not allowed in a type expression
--> src/foo.py:3:10
|
1 | import datetime
2 |
3 | def f(x: datetime): ... # error: [invalid-type-form]
| ^^^^^^^^
|
info: Did you mean to use the module's member `datetime.datetime` instead?
info: rule `invalid-type-form` is enabled by default
```
```
error[invalid-type-form]: Variable of type `<module 'PIL.Image'>` is not allowed in a type expression
--> src/bar.py:3:10
|
1 | from PIL import Image
2 |
3 | def g(x: Image): ... # error: [invalid-type-form]
| ^^^^^
|
info: Did you mean to use the module's member `Image.Image` instead?
info: rule `invalid-type-form` is enabled by default
```

View File

@@ -0,0 +1,37 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol, TypeVar, Generic
2 |
3 | T = TypeVar("T")
4 |
5 | class Foo(Protocol): ...
6 | class Bar(Protocol[T]): ...
7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
```
# Diagnostics
```
error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO) for class `Baz` with bases list `[typing.Protocol[T], <class 'Foo'>, <class 'Bar[T]'>]`
--> src/mdtest_snippet.py:7:1
|
5 | class Foo(Protocol): ...
6 | class Bar(Protocol[T]): ...
7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info: rule `inconsistent-mro` is enabled by default
```

View File

@@ -0,0 +1,78 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - `__bases__` includes a `Union`
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def returns_bool() -> bool:
4 | return True
5 |
6 | class A: ...
7 | class B: ...
8 |
9 | if returns_bool():
10 | x = A
11 | else:
12 | x = B
13 |
14 | reveal_type(x) # revealed: <class 'A'> | <class 'B'>
15 |
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
17 | class Foo(x): ...
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
```
# Diagnostics
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:14:13
|
12 | x = B
13 |
14 | reveal_type(x) # revealed: <class 'A'> | <class 'B'>
| ^ `<class 'A'> | <class 'B'>`
15 |
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
|
```
```
warning[unsupported-base]: Unsupported class base with type `<class 'A'> | <class 'B'>`
--> src/mdtest_snippet.py:17:11
|
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
17 | class Foo(x): ...
| ^
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
info: ty cannot resolve a consistent MRO for class `Foo` due to this base
info: Only class objects or `Any` are supported as class bases
info: rule `unsupported-base` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:19:13
|
17 | class Foo(x): ...
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
| ^^^^^^^^^^^ `tuple[<class 'Foo'>, Unknown, <class 'object'>]`
|
```

View File

@@ -0,0 +1,97 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - `__bases__` lists that include objects that are not instances of `type`
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | class Foo(2): ... # error: [invalid-base]
2 | class Foo:
3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
4 | return ()
5 |
6 | class Bar(Foo()): ... # error: [unsupported-base]
7 | class Bad1:
8 | def __mro_entries__(self, bases, extra_arg):
9 | return ()
10 |
11 | class Bad2:
12 | def __mro_entries__(self, bases) -> int:
13 | return 42
14 |
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
```
# Diagnostics
```
error[invalid-base]: Invalid class base with type `Literal[2]`
--> src/mdtest_snippet.py:1:11
|
1 | class Foo(2): ... # error: [invalid-base]
| ^
2 | class Foo:
3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
|
info: Definition of class `Foo` will raise `TypeError` at runtime
info: rule `invalid-base` is enabled by default
```
```
warning[unsupported-base]: Unsupported class base with type `Foo`
--> src/mdtest_snippet.py:6:11
|
4 | return ()
5 |
6 | class Bar(Foo()): ... # error: [unsupported-base]
| ^^^^^
7 | class Bad1:
8 | def __mro_entries__(self, bases, extra_arg):
|
info: ty cannot resolve a consistent MRO for class `Bar` due to this base
info: Only class objects or `Any` are supported as class bases
info: rule `unsupported-base` is enabled by default
```
```
error[invalid-base]: Invalid class base with type `Bad1`
--> src/mdtest_snippet.py:15:15
|
13 | return 42
14 |
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
| ^^^^^^
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
|
info: Definition of class `BadSub1` will raise `TypeError` at runtime
info: An instance type is only a valid class base if it has a valid `__mro_entries__` method
info: Type `Bad1` has an `__mro_entries__` method, but it cannot be called with the expected arguments
info: Expected a signature at least as permissive as `def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]`
info: rule `invalid-base` is enabled by default
```
```
error[invalid-base]: Invalid class base with type `Bad2`
--> src/mdtest_snippet.py:16:15
|
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
| ^^^^^^
|
info: Definition of class `BadSub2` will raise `TypeError` at runtime
info: An instance type is only a valid class base if it has a valid `__mro_entries__` method
info: Type `Bad2` has an `__mro_entries__` method, but it does not return a tuple of types
info: rule `invalid-base` is enabled by default
```

View File

@@ -0,0 +1,48 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: return_type.md - Function return type - Diagnostics for `invalid-return-type` on non-protocol subclasses of protocol classes
mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol
2 |
3 | class Abstract(Protocol):
4 | def method(self) -> str: ...
5 |
6 | class Concrete(Abstract):
7 | def method(self) -> str: ... # error: [invalid-return-type]
```
# Diagnostics
```
error[invalid-return-type]: Function can implicitly return `None`, which is not assignable to return type `str`
--> src/mdtest_snippet.py:7:25
|
6 | class Concrete(Abstract):
7 | def method(self) -> str: ... # error: [invalid-return-type]
| ^^^
|
info: Only functions in stub files, methods on protocol classes, or methods with `@abstractmethod` are permitted to have empty bodies
info: Class `Concrete` has `typing.Protocol` in its MRO, but it is not a protocol class
info: Only classes that directly inherit from `typing.Protocol` or `typing_extensions.Protocol` are considered protocol classes
--> src/mdtest_snippet.py:6:7
|
4 | def method(self) -> str: ...
5 |
6 | class Concrete(Abstract):
| ^^^^^^^^^^^^^^^^^^ `Protocol` not present in `Concrete`'s immediate bases
7 | def method(self) -> str: ... # error: [invalid-return-type]
|
info: See https://typing.python.org/en/latest/spec/protocol.html#
info: rule `invalid-return-type` is enabled by default
```

View File

@@ -25,8 +25,7 @@ error[unresolved-reference]: Name `aiter` used when not defined
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: The inferred target version of your project is Python 3.9
info: If using a pyproject.toml file, consider adjusting the `project.requires-python` or `tool.ty.environment.python-version` field
info: Python 3.9 was assumed when resolving types because it was specified on the command line
info: rule `unresolved-reference` is enabled by default
```

View File

@@ -16,7 +16,7 @@ class Foo[T]: ...
class Bar(Foo[Bar]): ...
reveal_type(Bar) # revealed: <class 'Bar'>
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, <class 'Foo[Bar]'>, <class 'object'>]
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, <class 'Foo[Bar]'>, typing.Generic, <class 'object'>]
```
## Access to attributes declared in stubs

View File

@@ -5,16 +5,16 @@
```py
b = b"\x00abc\xff"
reveal_type(b[0]) # revealed: Literal[b"\x00"]
reveal_type(b[1]) # revealed: Literal[b"a"]
reveal_type(b[4]) # revealed: Literal[b"\xff"]
reveal_type(b[0]) # revealed: Literal[0]
reveal_type(b[1]) # revealed: Literal[97]
reveal_type(b[4]) # revealed: Literal[255]
reveal_type(b[-1]) # revealed: Literal[b"\xff"]
reveal_type(b[-2]) # revealed: Literal[b"c"]
reveal_type(b[-5]) # revealed: Literal[b"\x00"]
reveal_type(b[-1]) # revealed: Literal[255]
reveal_type(b[-2]) # revealed: Literal[99]
reveal_type(b[-5]) # revealed: Literal[0]
reveal_type(b[False]) # revealed: Literal[b"\x00"]
reveal_type(b[True]) # revealed: Literal[b"a"]
reveal_type(b[False]) # revealed: Literal[0]
reveal_type(b[True]) # revealed: Literal[97]
x = b[5] # error: [index-out-of-bounds] "Index 5 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
reveal_type(x) # revealed: Unknown

Some files were not shown because too many files have changed in this diff Show More