Compare commits

..

45 Commits

Author SHA1 Message Date
Micha Reiser
e02fe815e8 Add DisplayType trait 2024-12-12 20:57:15 +01:00
Alex Waygood
45b565cbb5 [red-knot] Any cannot be parameterized (#14933) 2024-12-12 11:50:34 +00:00
Micha Reiser
82faa9bb62 Add tests demonstrating f-strings with debug expressions in replacements that contain escaped characters (#14929) 2024-12-12 09:33:20 +00:00
w0nder1ng
2eac00c60f [perflint] fix invalid hoist in perf401 (#14369)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-12 09:11:09 +01:00
Alex Waygood
033ecf5a4b Also have zizmor check for low-severity security issues (#14893)
## Summary

This PR changes our zizmor configuration to also flag low-severity
security issues in our GitHub Actions workflows. It's a followup to
https://github.com/astral-sh/ruff/pull/14844. The issues being fixed
here were all flagged by [zizmor's `template-injection`
rule](https://woodruffw.github.io/zizmor/audits/#template-injection):

> Detects potential sources of code injection via template expansion.
>
> GitHub Actions allows workflows to define template expansions, which
occur within special `${{ ... }}` delimiters. These expansions happen
before workflow and job execution, meaning the expansion of a given
expression appears verbatim in whatever context it was performed in.
>
> Template expansions aren't syntax-aware, meaning that they can result
in unintended shell injection vectors. This is especially true when
they're used with attacker-controllable expression contexts, such as
`github.event.issue.title` (which the attacker can fully control by
supplying a new issue title).

[...]

> To fully remediate the vulnerability, you should not use `${{
env.VARNAME }}`, since that is still a template expansion. Instead, you
should use `${VARNAME}` to ensure that the shell itself performs the
variable expansion.

## Test Plan

I tested that this passes all zizmore warnings by running `pre-commit
run -a zizmor` locally. The other test is obviously to check that the
workflows all still run correctly in CI 😄
2024-12-12 07:43:17 +00:00
Peter Tripp
5509a3d7ae Add LSP settings example for Zed editor (#14894)
## Summary

Add Zed settings examples to in addition to NeoVim and VSCode.

<img width="373" alt="Screenshot 2024-12-11 at 9 54 57"
src="https://github.com/user-attachments/assets/2f00cd8b-b23e-4ecb-8a0d-732ec275ee6b"
/>

## Test Plan

[*] Tested locally. No errors. Looks fine to me.
2024-12-12 12:44:56 +05:30
InSync
e4885a2fb2 [red-knot] Understand typing.Tuple (#14927)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-12 00:58:06 +00:00
David Peter
a7e5e42b88 [red-knot] Make attributes.md test future-proof (#14923)
## Summary

Using `typing.LiteralString` breaks as soon as we understand
`sys.version_info` branches, as it's only available in 3.11 and later.

## Test Plan

Made sure it didn't fail on my #14759 branch anymore.
2024-12-11 20:46:24 +01:00
Alex Waygood
c361cf66ad [red-knot] Precise inference for __class__ attributes on objects of all types (#14921) 2024-12-11 17:30:34 +00:00
Alex Waygood
a54353392f [red-knot] Add failing test for use of type[] as a base class (#14913)
We support using `typing.Type[]` as a base class (and we have tests for
it), but not yet `builtins.type[]`. At some point we should fix that,
but I don't think it';s worth spending much time on now (and it might be
easier once we've implemented generics?). This PR just adds a failing
test with a TODO.
2024-12-11 17:08:00 +00:00
Alex Waygood
ef153a0cce [red-knot] Remove an unnecessary branch and a confusing TODO comment (#14915) 2024-12-11 16:57:40 +00:00
InSync
0c85023cd9 Fix a typo in if_key_in_dict_del.rs (#14920)
(Accidentally introduced in #14553).
2024-12-11 16:28:27 +00:00
Alex Waygood
7135a49aea [red-knot] Record the TODO message in ClassBase::Todo, same as in Type::Todo (#14919) 2024-12-11 15:17:56 +00:00
Micha Reiser
6e11086c98 Support lint:<rule> in mdtests (#14914)
## Summary

Fixes a small scoping issue in `DiagnosticId::matches`

Note: I don't think we should use `lint:id` in mdtests just yet. I worry
that it could lead to many unnecessary churns if we decide **not** to
use `lint:<id>` as the format (e.g., `lint/id`).

The reason why users even see `lint:<rule>` is because the mdtest
framework uses the diagnostic infrastructure

Closes #14910

## Test Plan

Added tests
2024-12-11 14:37:12 +01:00
Micha Reiser
28653c7c47 Strip lint: prefix from mdtest diagnostics 2024-12-11 14:33:31 +01:00
Alex Waygood
1d91dae11f [red-knot] Minor simplifications to mro.rs (#14912) 2024-12-11 13:14:12 +00:00
Micha Reiser
01d16e8941 Fix fuzzer build (#14911) 2024-12-11 13:06:51 +00:00
Micha Reiser
881375a8d9 [red-knot] Lint registry and rule selection (#14874)
## Summary

This is the third and last PR in this stack that adds support for
toggling lints at a per-rule level.

This PR introduces a new `LintRegistry`, a central index of known lints.
The registry is required because we want to support lint rules from many
different crates but need a way to look them up by name, e.g., when
resolving a lint from a name in the configuration or analyzing a
suppression comment.

Adding a lint now requires two steps:

1. Declare the lint with `declare_lint`
2. Register the lint in the registry inside the `register_lints`
function.

I considered some more involved macros to avoid changes in two places.
Still, I ultimately decided against it because a) it's just two places
and b) I'd expect that registering a type checker lint will differ from
registering a lint that runs as a rule in the linter. I worry that any
more opinionated design could limit our options when working on the
linter, so I kept it simple.

The second part of this PR is the `RuleSelection`. It stores which lints
are enabled and what severity they should use for created diagnostics.
For now, the `RuleSelection` always gets initialized with all known
lints and it uses their default level.

## Linter crates

Each crate that defines lints should export a `register_lints` function
that accepts a `&mut LintRegistryBuilder` to register all its known
lints in the registry. This should make registering all known lints in a
top-level crate easy: Just call `register_lints` of every crate that
defines lint rules.

I considered defining a `LintCollection` trait and even some fancy
macros to accomplish the same but decided to go for this very simplistic
approach for now. We can add more abstraction once needed.

## Lint rules

This is a bit hand-wavy. I don't have a good sense for how our linter
infrastructure will look like, but I expect we'll need a way to register
the rules that should run as part of the red knot linter. One way is to
keep doing what Ruff does by having one massive `checker` and each lint
rule adds a call to itself in the relevant AST visitor methods. An
alternative is that we have a `LintRule` trait that provides common
hooks and implementations will be called at the "right time". Such a
design would need a way to register all known lint implementations,
possibly with the lint. This is where we'd probably want a dedicated
`register_rule` method. A third option is that lint rules are handled
separately from the `LintRegistry` and are specific to the linter crate.

The current design should be flexible enough to support the three
options.


## Documentation generation

The documentation for all known lints can be generated by creating a
factory, registering all lints by calling the `register_lints` methods,
and then querying the registry for the metadata.

## Deserialization and Schema generation

I haven't fully decided what the best approach is when it comes to
deserializing lint rule names:

* Reject invalid names in the deserializer. This gives us error messages
with line and column numbers (by serde)
* Don't validate lint rule names during deserialization; defer the
validation until the configuration is resolved. This gives us more
control over handling the error, e.g. emit a warning diagnostic instead
of aborting when a rule isn't known.

One technical challenge for both deserialization and schema generation
is that the `Deserialize` and `JSONSchema` traits do not allow passing
the `LintRegistry`, which is required to look up the lints by name. I
suggest that we either rely on the salsa db being set for the current
thread (`salsa::Attach`) or build our own thread-local storage for the
`LintRegistry`. It's the caller's responsibility to make the lint
registry available before calling `Deserialize` or `JSONSchema`.


## CLI support

I prefer deferring adding support for enabling and disabling lints from
the CLI for now because I think it will be easier
to add once I've figured out how to handle configurations. 

## Bitset optimization

Ruff tracks the enabled rules using a cheap copyable `Bitset` instead of
a hash map. This helped improve performance by a few percent (see
https://github.com/astral-sh/ruff/pull/3606). However, this approach is
no longer possible because lints have no "cheap" way to compute their
index inside the registry (other than using a hash map).

We could consider doing something similar to Salsa where each
`LintMetadata` stores a `LazyLintIndex`.

```
pub struct LazyLintIndex {
	cached: OnceLock<(Nonce, LintIndex)>
}

impl LazyLintIndex {
	pub fn get(registry: &LintRegistry, lint: &'static LintMetadata) {
	
	let (nonce, index) = self.cached.get_or_init(|| registry.lint_index(lint));

	if registry.nonce() == nonce {
		index
	} else {
		registry.lint_index(lint)
	}
}
```

Each registry keeps a map from `LintId` to `LintIndex` where `LintIndex`
is in the range of `0...registry.len()`. The `LazyLintIndex` is based on
the assumption that every program has exactly **one** registry. This
assumption allows to cache the `LintIndex` directly on the
`LintMetadata`. The implementation falls back to the "slow" path if
there is more than one registry at runtime.

I was very close to implementing this optimization because it's kind of
fun to implement. I ultimately decided against it because it adds
complexity and I don't think it's worth doing in Red Knot today:

* Red Knot only queries the rule selection when deciding whether or not
to emit a diagnostic. It is rarely used to detect if a certain code
block should run. This is different from Ruff where the rule selection
is queried many times for every single AST node to determine which rules
*should* run.
* I'm not sure if a 2-3% performance improvement is worth the complexity

I suggest revisiting this decision when working on the linter where a
fast path for deciding if a rule is enabled might be more important (but
that depends on how lint rules are implemented)


## Test Plan

I removed a lint from the default rule registry, and the MD tests
started failing because the diagnostics were no longer emitted.
2024-12-11 13:25:19 +01:00
InSync
6f8d8fa36b [ruff] if k in d: del d[k] (RUF051) (#14553)
## Summary

Resolves #7537.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-11 11:12:23 +00:00
InSync
f30227c436 [red-knot] Understand typing.Type (#14904)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-11 11:01:38 +00:00
InSync
c8d505c8ea [pyupgrade] Do not report when a UTF-8 comment is followed by a non-UTF-8 one (UP009) (#14728)
## Summary

Resolves #14704.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-11 10:30:41 +00:00
Dimitri Papadopoulos Orfanos
a55722e740 Revert disjointness->disjointedness (#14906)
## Summary

Partially revert #14880. While `disjointness` is missing from the
[OED](https://www.oed.com/search/dictionary/?q=disjointness) and [SCOWL
(And
Friends)](http://app.aspell.net/lookup?dict=en_US-large;words=disjointness),
it is commonly used in mathematics to describe disjoint sets.

## Test Plan

CI tests.
2024-12-11 08:26:45 +00:00
Dylan
a3bb0cd5ec Raise syntax error for mixing except and except* (#14895)
This PR adds a syntax error if the parser encounters a `TryStmt` that
has except clauses both with and without a star.

The displayed error points to each except clause that contradicts the
original except clause kind. So, for example,

```python
try:
    ....
except:     #<-- we assume this is the desired except kind
    ....
except*:    #<---  error will point here
    ....
except*:    #<--- and here
    ....
```

Closes #14860
2024-12-10 17:50:55 -06:00
Douglas Creager
d4126f6049 Handle type[Any] correctly (#14876)
This adds support for `type[Any]`, which represents an unknown type (not
an instance of an unknown type), and `type`, which we are choosing to
interpret as `type[object]`.

Closes #14546
2024-12-10 16:12:37 -05:00
Carl Meyer
03fb2e5ac1 [red-knot] split call-outcome enums to their own submodule (#14898)
## Summary

This is already several hundred lines of code, and it will get more
complex with call-signature checking.

## Test Plan

This is a pure code move; the moved code wasn't changed, just imports.
Existing tests pass.
2024-12-10 12:03:29 -08:00
David Peter
1a3c311ac5 [red-knot] Property tests: account non-fully-static types (#14897)
## Summary

Add a `is_fully_static` premise to the equivalence on subtyping property tests.

## Test Plan

```
cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable
```
2024-12-10 19:55:45 +00:00
Andrew Gallant
2ecd164adb ruff: add worktree support to build.rs (#14896)
Without this, `cargo insta test` re-compiles every time it is run, even
if there are no changes. With this, I can re-run `cargo insta test` (or
other `cargo build` commands) without it resulting in re-compiles.

I made an identical change to uv a while back:
https://github.com/astral-sh/uv/pull/6825
2024-12-10 14:06:59 -05:00
Micha Reiser
5fc8e5d80e [red-knot] Add infrastructure to declare lints (#14873)
## Summary

This is the second PR out of three that adds support for
enabling/disabling lint rules in Red Knot. You may want to take a look
at the [first PR](https://github.com/astral-sh/ruff/pull/14869) in this
stack to familiarize yourself with the used terminology.

This PR adds a new syntax to define a lint: 

```rust
declare_lint! {
    /// ## What it does
    /// Checks for references to names that are not defined.
    ///
    /// ## Why is this bad?
    /// Using an undefined variable will raise a `NameError` at runtime.
    ///
    /// ## Example
    ///
    /// ```python
    /// print(x)  # NameError: name 'x' is not defined
    /// ```
    pub(crate) static UNRESOLVED_REFERENCE = {
        summary: "detects references to names that are not defined",
        status: LintStatus::preview("1.0.0"),
        default_level: Level::Warn,
    }
}
```

A lint has a name and metadata about its status (preview, stable,
removed, deprecated), the default diagnostic level (unless the
configuration changes), and documentation. I use a macro here to derive
the kebab-case name and extract the documentation automatically.

This PR doesn't yet add any mechanism to discover all known lints. This
will be added in the next and last PR in this stack.


## Documentation
I documented some rules but then decided that it's probably not my best
use of time if I document all of them now (it also means that I play
catch-up with all of you forever). That's why I left some rules
undocumented (marked with TODO)

## Where is the best place to define all lints?

I'm not sure. I think what I have in this PR is fine but I also don't
love it because most lints are in a single place but not all of them. If
you have ideas, let me know.


## Why is the message not part of the lint, unlike Ruff's `Violation`

I understand that the main motivation for defining `message` on
`Violation` in Ruff is to remove the need to repeat the same message
over and over again. I'm not sure if this is an actual problem. Most
rules only emit a diagnostic in a single place and they commonly use
different messages if they emit diagnostics in different code paths,
requiring extra fields on the `Violation` struct.

That's why I'm not convinced that there's an actual need for it and
there are alternatives that can reduce the repetition when creating a
diagnostic:

* Create a helper function. We already do this in red knot with the
`add_xy` methods
* Create a custom `Diagnostic` implementation that tailors the entire
diagnostic and pre-codes e.g. the message

Avoiding an extra field on the `Violation` also removes the need to
allocate intermediate strings as it is commonly the place in Ruff.
Instead, Red Knot can use a borrowed string with `format_args`

## Test Plan

`cargo test`
2024-12-10 16:14:44 +00:00
Micha Reiser
5f548072d9 [red-knot] Typed diagnostic id (#14869)
## Summary

This PR introduces a structured `DiagnosticId` instead of using a plain
`&'static str`. It is the first of three in a stack that implements a
basic rules infrastructure for Red Knot.

`DiagnosticId` is an enum over all known diagnostic codes. A closed enum
reduces the risk of accidentally introducing two identical diagnostic
codes. It also opens the possibility of generating reference
documentation from the enum in the future (not part of this PR).

The enum isn't *fully closed* because it uses a `&'static str` for lint
names. This is because we want the flexibility to define lints in
different crates, and all names are only known in `red_knot_linter` or
above. Still, lower-level crates must already reference the lint names
to emit diagnostics. We could define all lint-names in `DiagnosticId`
but I decided against it because:

* We probably want to share the `DiagnosticId` type between Ruff and Red
Knot to avoid extra complexity in the diagnostic crate, and both tools
use different lint names.
* Lints require a lot of extra metadata beyond just the name. That's why
I think defining them close to their implementation is important.

In the long term, we may also want to support plugins, which would make
it impossible to know all lint names at compile time. The next PR in the
stack introduces extra syntax for defining lints.

A closed enum does have a few disadvantages:

* rustc can't help us detect unused diagnostic codes because the enum is
public
* Adding a new diagnostic in the workspace crate now requires changes to
at least two crates: It requires changing the workspace crate to add the
diagnostic and the `ruff_db` crate to define the diagnostic ID. I
consider this an acceptable trade. We may want to move `DiagnosticId` to
its own crate or into a shared `red_knot_diagnostic` crate.


## Preventing duplicate diagnostic identifiers

One goal of this PR is to make it harder to introduce ambiguous
diagnostic IDs, which is achieved by defining a closed enum. However,
the enum isn't fully "closed" because it doesn't explicitly list the IDs
for all lint rules. That leaves the possibility that a lint rule and a
diagnostic ID share the same name.

I made the names unambiguous in this PR by separating them into
different namespaces by using `lint/<rule>` for lint rule codes. I don't
mind the `lint` prefix in a *Ruff next* context, but it is a bit weird
for a standalone type checker. I'd like to not overfocus on this for now
because I see a few different options:

* We remove the `lint` prefix and add a unit test in a top-level crate
that iterates over all known lint rules and diagnostic IDs to ensure the
names are non-overlapping.
* We only render `[lint]` as the error code and add a note to the
diagnostic mentioning the lint rule. This is similar to clippy and has
the advantage that the header line remains short
(`lint/some-long-rule-name` is very long ;))
* Any other form of adjusting the diagnostic rendering to make the
distinction clear

I think we can defer this decision for now because the `DiagnosticId`
contains all the relevant information to change the rendering
accordingly.


## Why `Lint` and not `LintRule`

I see three kinds of diagnostics in Red Knot:

* Non-suppressable: Reveal type, IO errors, configuration errors, etc.
(any `DiagnosticId`)
* Lints: code-related diagnostics that are suppressable. 
* Lint rules: The same as lints, but they can be enabled or disabled in
the configuration. The majority of lints in Red Knot and the Ruff
linter.

Our current implementation doesn't distinguish between lints and Lint
rules because we aren't aware of a suppressible code-related lint that
can't be configured in the configuration. The only lint that comes to my
mind is maybe `division-by-zero` if we're 99.99% sure that it is always
right. However, I want to keep the door open to making this distinction
in the future if it proves useful.

Another reason why I chose lint over lint rule (or just rule) is that I
want to leave room for a future lint rule and lint phase concept:

* lint is the *what*: a specific code smell, pattern, or violation 
* the lint rule is the *how*: I could see a future `LintRule` trait in
`red_knot_python_linter` that provides the necessary hooks to run as
part of the linter. A lint rule produces diagnostics for exactly one
lint. A lint rule differs from all lints in `red_knot_python_semantic`
because they don't run as "rules" in the Ruff sense. Instead, they're a
side-product of type inference.
* the lint phase is a different form of *how*: A lint phase can produce
many different lints in a single pass. This is a somewhat common pattern
in Ruff where running one analysis collects the necessary information
for finding many different lints
* diagnostic is the *presentation*: Unlike a lint, the diagnostic isn't
the what, but how a specific lint gets presented. I expect that many
lints can use one generic `LintDiagnostic`, but a few lints might need
more flexibility and implement their custom diagnostic rendering (at
least custom `Diagnostic` implementation).


## Test Plan

`cargo test`
2024-12-10 15:58:07 +00:00
Karthikeyan Singaravelan
dc0d944608 [airflow] Add fix to remove deprecated keyword arguments (AIR302) (#14887)
## Summary

Add replacement fixes to deprecated arguments of a DAG.

Ref #14582 #14626

## Test Plan

Diff was verified and snapshots were updated.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-12-10 18:49:28 +05:30
InSync
15fe540251 Improve mdtests style (#14884)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-10 13:05:51 +00:00
Micha Reiser
7a0e9b34d0 Reference suppress-dummy-regex-options in documentation of rules supporting it (#14888)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/14663
2024-12-10 09:53:53 +00:00
InSync
4b8c815b27 [flake8-bugbear] itertools.batched() without explicit strict (B911) (#14408)
## Summary

Resolves #14387.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-10 08:39:46 +00:00
Alex Waygood
e3f34b8f5b [ruff] Mark autofix for RUF052 as always unsafe (#14824) 2024-12-09 23:11:44 +00:00
Alex Waygood
ab26d9cf9a [red-knot] Improve type inference for except handlers (#14838) 2024-12-09 22:49:58 +00:00
Dimitri Papadopoulos Orfanos
64944f2cf5 More typos found by codespell (#14880) 2024-12-09 22:47:34 +00:00
Carl Meyer
533e8a6ee6 [red-knot] move standalone expression_ty to TypeInferenceBuilder::file_expression_ty (#14879)
## Summary

Per suggestion in
https://github.com/astral-sh/ruff/pull/14802#discussion_r1875455417

This is a bit less error-prone and allows us to handle both expressions
in the current scope or a different scope. Also, there's currently no
need for this method outside of `TypeInferenceBuilder`, so no reason to
expose it in `types.rs`.

## Test Plan

Pure refactor, no functional change; existing tests pass.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-09 17:02:14 +00:00
InSync
c62ba48ad4 [ruff] Do not simplify round() calls (RUF046) (#14832)
## Summary

Part 1 of the big change introduced in #14828. This temporarily causes
all fixes for `round(...)` to be considered unsafe, but they will
eventually be enhanced.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-12-09 16:51:27 +01:00
Dimitri Papadopoulos Orfanos
68eb0a2511 Stop referring to early ruff versions (#14862)
## Summary

Referring to old versions has become more distracting than useful.

## Test Plan

—
2024-12-09 16:47:26 +01:00
InSync
0f4350e10e Fix a typo in class.rs (#14877)
(Accidentally introduced in #14801.)
2024-12-09 15:36:42 +00:00
InSync
aa6b812a73 [flake8-pyi] Also remove self and cls's annotation (PYI034) (#14801)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-09 14:59:12 +00:00
Harutaka Kawamura
0e9427255f [pyupgrade] Remove unreachable code in UP015 implementation (#14871) 2024-12-09 14:54:57 +00:00
Harutaka Kawamura
9c3c59aca9 [flake8-bugbear] Skip B028 if warnings.warn is called with *args or **kwargs (#14870) 2024-12-09 14:32:37 +00:00
Harutaka Kawamura
172143ae77 [flake8-bugbear] Fix B028 to allow stacklevel to be explicitly assigned as a positional argument (#14868) 2024-12-09 13:15:43 +00:00
InSync
3865fb6641 [red-knot] Understanding type[Union[A, B]] (#14858)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-09 12:47:14 +00:00
164 changed files with 7735 additions and 3693 deletions

View File

@@ -53,9 +53,9 @@ jobs:
args: --out dist
- name: "Test sdist"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
pip install dist/${PACKAGE_NAME}-*.tar.gz --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload sdist"
uses: actions/upload-artifact@v4
with:
@@ -125,7 +125,7 @@ jobs:
args: --release --locked --out dist
- name: "Test wheel - aarch64"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
ruff --help
python -m ruff --help
- name: "Upload wheels"
@@ -186,9 +186,9 @@ jobs:
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash
run: |
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
python -m pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
@@ -236,9 +236,9 @@ jobs:
- name: "Test wheel"
if: ${{ startsWith(matrix.target, 'x86_64') }}
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:

View File

@@ -87,9 +87,10 @@ jobs:
outputs: type=image,name=${{ env.RUFF_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
- name: Export digests
env:
digest: ${{ steps.build.outputs.digest }}
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digests
@@ -143,7 +144,7 @@ jobs:
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
$(printf '${RUFF_BASE_IMG}@sha256:%s ' *)
docker-publish-extra:
name: Publish additional Docker image based on ${{ matrix.image-mapping }}
@@ -182,7 +183,7 @@ jobs:
# Generate Dockerfile content
cat <<EOF > Dockerfile
FROM ${BASE_IMAGE}
COPY --from=${{ env.RUFF_BASE_IMG }}:latest /ruff /usr/local/bin/ruff
COPY --from=${RUFF_BASE_IMG}:latest /ruff /usr/local/bin/ruff
ENTRYPOINT []
CMD ["/usr/local/bin/ruff"]
EOF
@@ -288,4 +289,4 @@ jobs:
docker buildx imagetools create \
"${annotations[@]}" \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
$(printf '${RUFF_BASE_IMG}@sha256:%s ' *)

View File

@@ -49,13 +49,11 @@ jobs:
- name: "Set branch name"
run: |
version="${{ env.version }}"
display_name="${{ env.display_name }}"
timestamp="$(date +%s)"
# create branch_display_name from display_name by replacing all
# characters disallowed in git branch names with hyphens
branch_display_name="$(echo "$display_name" | tr -c '[:alnum:]._' '-' | tr -s '-')"
branch_display_name="$(echo "${display_name}" | tr -c '[:alnum:]._' '-' | tr -s '-')"
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> $GITHUB_ENV
echo "timestamp=$timestamp" >> $GITHUB_ENV
@@ -93,9 +91,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.public.yml
- name: "Clone docs repo"
run: |
version="${{ env.version }}"
git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
- name: "Copy docs"
run: rm -rf astral-docs/site/ruff && mkdir -p astral-docs/site && cp -r site/ruff astral-docs/site/
@@ -103,12 +99,10 @@ jobs:
- name: "Commit docs"
working-directory: astral-docs
run: |
branch_name="${{ env.branch_name }}"
git config user.name "astral-docs-bot"
git config user.email "176161322+astral-docs-bot@users.noreply.github.com"
git checkout -b $branch_name
git checkout -b "${branch_name}"
git add site/ruff
git commit -m "Update ruff documentation for $version"
@@ -117,12 +111,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
run: |
version="${{ env.version }}"
display_name="${{ env.display_name }}"
branch_name="${{ env.branch_name }}"
# set the PR title
pull_request_title="Update ruff documentation for $display_name"
pull_request_title="Update ruff documentation for "${display_name}""
# Delete any existing pull requests that are open for this version
# by checking against pull_request_title because the new PR will
@@ -131,12 +121,12 @@ jobs:
xargs -I {} gh pr close {}
# push the branch to GitHub
git push origin $branch_name
git push origin "${branch_name}"
# create the PR
gh pr create --base main --head $branch_name \
gh pr create --base main --head "${branch_name}" \
--title "$pull_request_title" \
--body "Automated documentation update for $display_name" \
--body "Automated documentation update for "${display_name}"" \
--label "documentation"
- name: "Merge Pull Request"
@@ -145,9 +135,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
run: |
branch_name="${{ env.branch_name }}"
# auto-merge the PR if the build was triggered by a release. Manual builds should be reviewed by a human.
# give the PR a few seconds to be created before trying to auto-merge it
sleep 10
gh pr merge --squash $branch_name
gh pr merge --squash "${branch_name}"

View File

@@ -31,7 +31,7 @@ jobs:
with:
repository: python/typeshed
path: typeshed
persist-credentials: true
persist-credentials: false
- name: Setup git
run: |
git config --global user.name typeshedbot

View File

@@ -95,8 +95,6 @@ repos:
# `release.yml` is autogenerated by `dist`; security issues need to be fixed there
# (https://opensource.axo.dev/cargo-dist/)
exclude: .github/workflows/release.yml
# We could consider enabling the low-severity warnings, but they're noisy
args: [--min-severity=medium]
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.30.0

View File

@@ -34,8 +34,7 @@ If you define your own class named `Any`, using that in a type expression refers
isn't a spelling of the Any type.
```py
class Any:
pass
class Any: ...
x: Any
@@ -59,8 +58,7 @@ assignable to `int`.
```py
from typing import Any
class Subclass(Any):
pass
class Subclass(Any): ...
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
@@ -68,8 +66,18 @@ x: Subclass = 1 # error: [invalid-assignment]
# TODO: no diagnostic
y: int = Subclass() # error: [invalid-assignment]
def f() -> Subclass:
pass
reveal_type(f()) # revealed: Subclass
def _(s: Subclass):
reveal_type(s) # revealed: Subclass
```
## Invalid
`Any` cannot be parameterized:
```py
from typing import Any
# error: [invalid-type-parameter] "Type `typing.Any` expected no type parameter"
def f(x: Any[int]):
reveal_type(x) # revealed: Unknown
```

View File

@@ -89,28 +89,26 @@ vice versa.
```py
from typing_extensions import Literal, LiteralString
def coinflip() -> bool:
return True
def _(flag: bool):
foo_1: Literal["foo"] = "foo"
bar_1: LiteralString = foo_1 # fine
foo_1: Literal["foo"] = "foo"
bar_1: LiteralString = foo_1 # fine
foo_2 = "foo" if flag else "bar"
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
bar_2: LiteralString = foo_2 # fine
foo_2 = "foo" if coinflip() else "bar"
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
bar_2: LiteralString = foo_2 # fine
foo_3: LiteralString = "foo" * 1_000_000_000
bar_3: str = foo_2 # fine
foo_3: LiteralString = "foo" * 1_000_000_000
bar_3: str = foo_2 # fine
baz_1: str = str()
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
baz_1: str = str()
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
baz_2: LiteralString = "baz" * 1_000_000_000
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
baz_2: LiteralString = "baz" * 1_000_000_000
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
baz_3 = "foo" if coinflip() else 1
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
baz_3 = "foo" if flag else 1
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
```
### Narrowing

View File

@@ -3,75 +3,56 @@
## Simple
```py
def f() -> "int":
return 1
reveal_type(f()) # revealed: int
def f(v: "int"):
reveal_type(v) # revealed: int
```
## Nested
```py
def f() -> "'int'":
return 1
reveal_type(f()) # revealed: int
def f(v: "'int'"):
reveal_type(v) # revealed: int
```
## Type expression
```py
def f1() -> "int | str":
return 1
def f2() -> "tuple[int, str]":
return 1
reveal_type(f1()) # revealed: int | str
reveal_type(f2()) # revealed: tuple[int, str]
def f1(v: "int | str", w: "tuple[int, str]"):
reveal_type(v) # revealed: int | str
reveal_type(w) # revealed: tuple[int, str]
```
## Partial
```py
def f() -> tuple[int, "str"]:
return 1
reveal_type(f()) # revealed: tuple[int, str]
def f(v: tuple[int, "str"]):
reveal_type(v) # revealed: tuple[int, str]
```
## Deferred
```py
def f() -> "Foo":
return Foo()
def f(v: "Foo"):
reveal_type(v) # revealed: Foo
class Foo:
pass
reveal_type(f()) # revealed: Foo
class Foo: ...
```
## Deferred (undefined)
```py
# error: [unresolved-reference]
def f() -> "Foo":
pass
reveal_type(f()) # revealed: Unknown
def f(v: "Foo"):
reveal_type(v) # revealed: Unknown
```
## Partial deferred
```py
def f() -> int | "Foo":
return 1
def f(v: int | "Foo"):
reveal_type(v) # revealed: int | Foo
class Foo:
pass
reveal_type(f()) # revealed: int | Foo
class Foo: ...
```
## `typing.Literal`
@@ -79,65 +60,43 @@ reveal_type(f()) # revealed: int | Foo
```py
from typing import Literal
def f1() -> Literal["Foo", "Bar"]:
return "Foo"
def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'):
reveal_type(v) # revealed: Literal["Foo", "Bar"]
reveal_type(w) # revealed: Literal["Foo", "Bar"]
def f2() -> 'Literal["Foo", "Bar"]':
return "Foo"
class Foo:
pass
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
class Foo: ...
```
## Various string kinds
```py
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
def f1() -> r"int":
return 1
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
def f2() -> f"int":
return 1
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
def f3() -> b"int":
return 1
def f4() -> "int":
return 1
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
def f5() -> "in" "t":
return 1
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
def f6() -> "\N{LATIN SMALL LETTER I}nt":
return 1
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
def f7() -> "\x69nt":
return 1
def f8() -> """int""":
return 1
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
def f9() -> "b'int'":
return 1
reveal_type(f1()) # revealed: Unknown
reveal_type(f2()) # revealed: Unknown
reveal_type(f3()) # revealed: Unknown
reveal_type(f4()) # revealed: int
reveal_type(f5()) # revealed: Unknown
reveal_type(f6()) # revealed: Unknown
reveal_type(f7()) # revealed: Unknown
reveal_type(f8()) # revealed: int
reveal_type(f9()) # revealed: Unknown
def f1(
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
a: r"int",
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
b: f"int",
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
c: b"int",
d: "int",
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
e: "in" "t",
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
f: "\N{LATIN SMALL LETTER I}nt",
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
g: "\x69nt",
h: """int""",
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
i: "b'int'",
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: int
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: int
reveal_type(i) # revealed: Unknown
```
## Various string kinds in `typing.Literal`
@@ -145,10 +104,8 @@ reveal_type(f9()) # revealed: Unknown
```py
from typing import Literal
def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]:
return "normal"
reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
```
## Class variables
@@ -175,8 +132,7 @@ c: "Foo"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
d: "Foo" = 1
class Foo:
pass
class Foo: ...
c = Foo()

View File

@@ -78,20 +78,10 @@ c: tuple[str | int, str] = ([], "foo")
## PEP-604 annotations are supported
```py
def foo() -> str | int | None:
return None
reveal_type(foo()) # revealed: str | int | None
def bar() -> str | str | None:
return None
reveal_type(bar()) # revealed: str | None
def baz() -> str | str:
return "Hello, world!"
reveal_type(baz()) # revealed: str
def foo(v: str | int | None, w: str | str | None, x: str | str):
reveal_type(v) # revealed: str | int | None
reveal_type(w) # revealed: str | None
reveal_type(x) # revealed: str
```
## Attribute expressions in type annotations are understood
@@ -118,8 +108,7 @@ from __future__ import annotations
x: Foo
class Foo:
pass
class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
@@ -130,8 +119,7 @@ reveal_type(x) # revealed: Foo
```pyi path=main.pyi
x: Foo
class Foo:
pass
class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo

View File

@@ -49,134 +49,116 @@ reveal_type(x) # revealed: int
## Method union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Foo:
if flag:
def __iadd__(self, other: int) -> str:
return "Hello, world!"
else:
def __iadd__(self, other: int) -> int:
return 42
flag = bool_instance()
f = Foo()
f += 12
class Foo:
if bool_instance():
def __iadd__(self, other: int) -> str:
return "Hello, world!"
else:
def __iadd__(self, other: int) -> int:
return 42
f = Foo()
f += 12
reveal_type(f) # revealed: str | int
reveal_type(f) # revealed: str | int
```
## Partially bound `__iadd__`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Foo:
if flag:
def __iadd__(self, other: str) -> int:
return 42
class Foo:
if bool_instance():
def __iadd__(self, other: str) -> int:
return 42
f = Foo()
f = Foo()
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
# that `Foo.__iadd__` may be unbound as additional context.
f += "Hello, world!"
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
# that `Foo.__iadd__` may be unbound as additional context.
f += "Hello, world!"
reveal_type(f) # revealed: int | Unknown
reveal_type(f) # revealed: int | Unknown
```
## Partially bound with `__add__`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Foo:
def __add__(self, other: str) -> str:
return "Hello, world!"
if flag:
def __iadd__(self, other: str) -> int:
return 42
class Foo:
def __add__(self, other: str) -> str:
return "Hello, world!"
if bool_instance():
def __iadd__(self, other: str) -> int:
return 42
f = Foo()
f += "Hello, world!"
f = Foo()
f += "Hello, world!"
reveal_type(f) # revealed: int | str
reveal_type(f) # revealed: int | str
```
## Partially bound target union
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
class Foo:
def __add__(self, other: int) -> str:
return "Hello, world!"
if flag1:
def __iadd__(self, other: int) -> int:
return 42
class Foo:
def __add__(self, other: int) -> str:
return "Hello, world!"
if bool_instance():
def __iadd__(self, other: int) -> int:
return 42
if flag2:
f = Foo()
else:
f = 42.0
f += 12
if bool_instance():
f = Foo()
else:
f = 42.0
f += 12
reveal_type(f) # revealed: int | str | float
reveal_type(f) # revealed: int | str | float
```
## Target union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Foo:
def __iadd__(self, other: int) -> str:
return "Hello, world!"
flag = bool_instance()
if flag:
f = Foo()
else:
f = 42.0
f += 12
class Foo:
def __iadd__(self, other: int) -> str:
return "Hello, world!"
if flag:
f = Foo()
else:
f = 42.0
f += 12
reveal_type(f) # revealed: str | float
reveal_type(f) # revealed: str | float
```
## Partially bound target union with `__add__`
```py
def bool_instance() -> bool:
return True
def f(flag: bool, flag2: bool):
class Foo:
def __add__(self, other: int) -> str:
return "Hello, world!"
if flag:
def __iadd__(self, other: int) -> int:
return 42
flag = bool_instance()
class Bar:
def __add__(self, other: int) -> bytes:
return b"Hello, world!"
class Foo:
def __add__(self, other: int) -> str:
return "Hello, world!"
if bool_instance():
def __iadd__(self, other: int) -> int:
return 42
def __iadd__(self, other: int) -> float:
return 42.0
class Bar:
def __add__(self, other: int) -> bytes:
return b"Hello, world!"
if flag2:
f = Foo()
else:
f = Bar()
f += 12
def __iadd__(self, other: int) -> float:
return 42.0
if flag:
f = Foo()
else:
f = Bar()
f += 12
reveal_type(f) # revealed: int | str | float
reveal_type(f) # revealed: int | str | float
```

View File

@@ -3,27 +3,23 @@
## Union of attributes
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
class C1:
x = 1
else:
class C1:
x = 2
class C2:
def _(flag: bool):
if flag:
x = 3
else:
x = 4
class C1:
x = 1
reveal_type(C1.x) # revealed: Literal[1, 2]
reveal_type(C2.x) # revealed: Literal[3, 4]
else:
class C1:
x = 2
class C2:
if flag:
x = 3
else:
x = 4
reveal_type(C1.x) # revealed: Literal[1, 2]
reveal_type(C2.x) # revealed: Literal[3, 4]
```
## Inherited attributes
@@ -68,24 +64,19 @@ reveal_type(A.X) # revealed: Literal[42]
In this example, the `x` attribute is not defined in the `C2` element of the union:
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
class C1:
x = 1
class C1:
x = 1
class C2: ...
class C2: ...
class C3:
x = 3
class C3:
x = 3
C = C1 if flag1 else C2 if flag2 else C3
flag1 = bool_instance()
flag2 = bool_instance()
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[1, 3]
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[1, 3]
```
### Possibly-unbound within a class
@@ -94,26 +85,21 @@ We raise the same diagnostic if the attribute is possibly-unbound in at least on
union:
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag1: bool, flag2: bool):
class C1:
x = 1
class C1:
x = 1
class C2:
if flag:
x = 2
class C2:
if bool_instance():
x = 2
class C3:
x = 3
class C3:
x = 3
C = C1 if flag1 else C2 if flag2 else C3
flag1 = bool_instance()
flag2 = bool_instance()
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[1, 2, 3]
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[1, 2, 3]
```
## Unions with all paths unbound
@@ -121,16 +107,51 @@ reveal_type(C.x) # revealed: Literal[1, 2, 3]
If the symbol is unbound in all elements of the union, we detect that:
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class C1: ...
class C2: ...
C = C1 if flag else C2
class C1: ...
class C2: ...
flag = bool_instance()
C = C1 if flag else C2
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
reveal_type(C.x) # revealed: Unknown
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
reveal_type(C.x) # revealed: Unknown
```
## Objects of all types have a `__class__` method
```py
import typing_extensions
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
a = 42
reveal_type(a.__class__) # revealed: Literal[int]
b = "42"
reveal_type(b.__class__) # revealed: Literal[str]
c = b"42"
reveal_type(c.__class__) # revealed: Literal[bytes]
d = True
reveal_type(d.__class__) # revealed: Literal[bool]
e = (42, 42)
reveal_type(e.__class__) # revealed: Literal[tuple]
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
reveal_type(a.__class__) # revealed: type[int]
reveal_type(b.__class__) # revealed: Literal[str]
reveal_type(c.__class__) # revealed: type[int] | type[str]
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
# It would be incorrect to infer `Literal[type]` here,
# as `c` could be some subclass of `str` with a custom metaclass.
# All we know is that the metaclass must be a (non-strict) subclass of `type`.
reveal_type(d.__class__) # revealed: type[type]
reveal_type(f.__class__) # revealed: Literal[FunctionType]
class Foo: ...
reveal_type(Foo.__class__) # revealed: Literal[type]
```

View File

@@ -281,20 +281,12 @@ reveal_type(42 + 4.2) # revealed: int
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3 + 3j) # revealed: int
def returns_int() -> int:
return 42
def _(x: bool, y: int):
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: float
def returns_bool() -> bool:
return True
x = returns_bool()
y = returns_int()
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int
```
## With literal types

View File

@@ -7,29 +7,25 @@ Similarly, in `and` expressions, if the left-hand side is falsy, the right-hand
evaluated.
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag or (x := 1):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if bool_instance() or (x := 1):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if bool_instance() and (x := 1):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if flag and (x := 1):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
```
## First expression is always evaluated
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if (x := 1) or flag:
reveal_type(x) # revealed: Literal[1]
if (x := 1) or bool_instance():
reveal_type(x) # revealed: Literal[1]
if (x := 1) and bool_instance():
reveal_type(x) # revealed: Literal[1]
if (x := 1) and flag:
reveal_type(x) # revealed: Literal[1]
```
## Statically known truthiness
@@ -49,30 +45,26 @@ if True and (x := 1):
## Later expressions can always use variables from earlier expressions
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
flag or (x := 1) or reveal_type(x) # revealed: Literal[1]
bool_instance() or (x := 1) or reveal_type(x) # revealed: Literal[1]
# error: [unresolved-reference]
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unknown
# error: [unresolved-reference]
flag or reveal_type(y) or (y := 1) # revealed: Unknown
```
## Nested expressions
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
if flag1 or ((x := 1) and flag2):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if ((y := 1) and flag1) or flag2:
reveal_type(y) # revealed: Literal[1]
if bool_instance() or ((x := 1) and bool_instance()):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if ((y := 1) and bool_instance()) or bool_instance():
reveal_type(y) # revealed: Literal[1]
# error: [possibly-unresolved-reference]
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Literal[1]
# error: [possibly-unresolved-reference]
reveal_type(z) # revealed: Literal[1]
if (flag1 and (z := 1)) or reveal_type(z): # revealed: Literal[1]
# error: [possibly-unresolved-reference]
reveal_type(z) # revealed: Literal[1]
```

View File

@@ -22,29 +22,27 @@ reveal_type(b) # revealed: Unknown
## Possibly unbound `__call__` method
```py
def flag() -> bool: ...
def _(flag: bool):
class PossiblyNotCallable:
if flag:
def __call__(self) -> int: ...
class PossiblyNotCallable:
if flag():
def __call__(self) -> int: ...
a = PossiblyNotCallable()
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
reveal_type(result) # revealed: int
a = PossiblyNotCallable()
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
reveal_type(result) # revealed: int
```
## Possibly unbound callable
```py
def flag() -> bool: ...
def _(flag: bool):
if flag:
class PossiblyUnbound:
def __call__(self) -> int: ...
if flag():
class PossiblyUnbound:
def __call__(self) -> int: ...
# error: [possibly-unresolved-reference]
a = PossiblyUnbound()
reveal_type(a()) # revealed: int
# error: [possibly-unresolved-reference]
a = PossiblyUnbound()
reveal_type(a()) # revealed: int
```
## Non-callable `__call__`
@@ -61,15 +59,14 @@ reveal_type(a()) # revealed: Unknown
## Possibly non-callable `__call__`
```py
def flag() -> bool: ...
def _(flag: bool):
class NonCallable:
if flag:
__call__ = 1
else:
def __call__(self) -> int: ...
class NonCallable:
if flag():
__call__ = 1
else:
def __call__(self) -> int: ...
a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
```

View File

@@ -57,12 +57,10 @@ x = nonsense() # error: "Object of type `Literal[123]` is not callable"
## Potentially unbound function
```py
def flag() -> bool: ...
if flag():
def foo() -> int:
return 42
# error: [possibly-unresolved-reference]
reveal_type(foo()) # revealed: int
def _(flag: bool):
if flag:
def foo() -> int:
return 42
# error: [possibly-unresolved-reference]
reveal_type(foo()) # revealed: int
```

View File

@@ -3,22 +3,14 @@
## Union of return types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
def f() -> int:
return 1
else:
def f() -> str:
return "foo"
reveal_type(f()) # revealed: int | str
def _(flag: bool):
if flag:
def f() -> int:
return 1
else:
def f() -> str:
return "foo"
reveal_type(f()) # revealed: int | str
```
## Calling with an unknown union
@@ -26,13 +18,10 @@ reveal_type(f()) # revealed: int | str
```py
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
def f() -> int:
return 1
@@ -44,20 +33,14 @@ reveal_type(f()) # revealed: Unknown | int
Calling a union with a non-callable element should emit a diagnostic.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
f = 1
else:
def f() -> int:
return 1
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
reveal_type(x) # revealed: Unknown | int
def _(flag: bool):
if flag:
f = 1
else:
def f() -> int:
return 1
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
reveal_type(x) # revealed: Unknown | int
```
## Multiple non-callable elements in a union
@@ -65,23 +48,17 @@ reveal_type(x) # revealed: Unknown | int
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
if flag:
f = 1
elif flag2:
f = "foo"
else:
def f() -> int:
return 1
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
def _(flag: bool, flag2: bool):
if flag:
f = 1
elif flag2:
f = "foo"
else:
def f() -> int:
return 1
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
```
## All non-callable union elements
@@ -89,16 +66,12 @@ reveal_type(f())
Calling a union with no callable elements can emit a simpler diagnostic.
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
f = 1
else:
f = "foo"
flag = bool_instance()
if flag:
f = 1
else:
f = "foo"
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
reveal_type(x) # revealed: Unknown
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```

View File

@@ -3,38 +3,31 @@
```py
class A: ...
def get_a() -> A: ...
def get_object() -> object: ...
def _(a1: A, a2: A, o: object):
n1 = None
n2 = None
a1 = get_a()
a2 = get_a()
reveal_type(a1 is a1) # revealed: bool
reveal_type(a1 is a2) # revealed: bool
n1 = None
n2 = None
reveal_type(n1 is n1) # revealed: Literal[True]
reveal_type(n1 is n2) # revealed: Literal[True]
o = get_object()
reveal_type(a1 is n1) # revealed: Literal[False]
reveal_type(n1 is a1) # revealed: Literal[False]
reveal_type(a1 is a1) # revealed: bool
reveal_type(a1 is a2) # revealed: bool
reveal_type(a1 is o) # revealed: bool
reveal_type(n1 is o) # revealed: bool
reveal_type(n1 is n1) # revealed: Literal[True]
reveal_type(n1 is n2) # revealed: Literal[True]
reveal_type(a1 is not a1) # revealed: bool
reveal_type(a1 is not a2) # revealed: bool
reveal_type(a1 is n1) # revealed: Literal[False]
reveal_type(n1 is a1) # revealed: Literal[False]
reveal_type(n1 is not n1) # revealed: Literal[False]
reveal_type(n1 is not n2) # revealed: Literal[False]
reveal_type(a1 is o) # revealed: bool
reveal_type(n1 is o) # revealed: bool
reveal_type(a1 is not n1) # revealed: Literal[True]
reveal_type(n1 is not a1) # revealed: Literal[True]
reveal_type(a1 is not a1) # revealed: bool
reveal_type(a1 is not a2) # revealed: bool
reveal_type(n1 is not n1) # revealed: Literal[False]
reveal_type(n1 is not n2) # revealed: Literal[False]
reveal_type(a1 is not n1) # revealed: Literal[True]
reveal_type(n1 is not a1) # revealed: Literal[True]
reveal_type(a1 is not o) # revealed: bool
reveal_type(n1 is not o) # revealed: bool
reveal_type(a1 is not o) # revealed: bool
reveal_type(n1 is not o) # revealed: bool
```

View File

@@ -312,17 +312,9 @@ reveal_type(1 <= 2j) # revealed: bool
reveal_type(1 > 2j) # revealed: bool
reveal_type(1 >= 2j) # revealed: bool
def bool_instance() -> bool:
return True
def int_instance() -> int:
return 42
x = bool_instance()
y = int_instance()
reveal_type(x < y) # revealed: bool
reveal_type(y < x) # revealed: bool
reveal_type(4.2 < x) # revealed: bool
reveal_type(x < 4.2) # revealed: bool
def f(x: bool, y: int):
reveal_type(x < y) # revealed: bool
reveal_type(y < x) # revealed: bool
reveal_type(4.2 < x) # revealed: bool
reveal_type(x < 4.2) # revealed: bool
```

View File

@@ -20,10 +20,8 @@ reveal_type(1 <= "" and 0 < 1) # revealed: bool
```py
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
def int_instance() -> int:
return 42
reveal_type(1 == int_instance()) # revealed: bool
reveal_type(9 < int_instance()) # revealed: bool
reveal_type(int_instance() < int_instance()) # revealed: bool
def _(a: int, b: int):
reveal_type(1 == a) # revealed: bool
reveal_type(9 < a) # revealed: bool
reveal_type(a < b) # revealed: bool
```

View File

@@ -14,21 +14,19 @@ class Child1(Base):
class Child2(Base): ...
def get_base() -> Base: ...
def _(x: Base):
c1 = Child1()
x = get_base()
c1 = Child1()
# Create an intersection type through narrowing:
if isinstance(x, Child1):
if isinstance(x, Child2):
reveal_type(x) # revealed: Child1 & Child2
# Create an intersection type through narrowing:
if isinstance(x, Child1):
if isinstance(x, Child2):
reveal_type(x) # revealed: Child1 & Child2
reveal_type(x == 1) # revealed: Literal[True]
reveal_type(x == 1) # revealed: Literal[True]
# Other comparison operators fall back to the base type:
reveal_type(x > 1) # revealed: bool
reveal_type(x is c1) # revealed: bool
# Other comparison operators fall back to the base type:
reveal_type(x > 1) # revealed: bool
reveal_type(x is c1) # revealed: bool
```
## Negative contributions
@@ -73,18 +71,15 @@ if x != "abc":
#### Integers
```py
def get_int() -> int: ...
def _(x: int):
if x != 1:
reveal_type(x) # revealed: int & ~Literal[1]
x = get_int()
reveal_type(x != 1) # revealed: Literal[True]
reveal_type(x != 2) # revealed: bool
if x != 1:
reveal_type(x) # revealed: int & ~Literal[1]
reveal_type(x != 1) # revealed: Literal[True]
reveal_type(x != 2) # revealed: bool
reveal_type(x == 1) # revealed: Literal[False]
reveal_type(x == 2) # revealed: bool
reveal_type(x == 1) # revealed: Literal[False]
reveal_type(x == 2) # revealed: bool
```
### Identity comparisons
@@ -92,18 +87,15 @@ if x != 1:
```py
class A: ...
def get_object() -> object: ...
def _(o: object):
a = A()
n = None
o = object()
if o is not None:
reveal_type(o) # revealed: object & ~None
a = A()
n = None
if o is not None:
reveal_type(o) # revealed: object & ~None
reveal_type(o is n) # revealed: Literal[False]
reveal_type(o is not n) # revealed: Literal[True]
reveal_type(o is n) # revealed: Literal[False]
reveal_type(o is not n) # revealed: Literal[True]
```
## Diagnostics
@@ -119,16 +111,13 @@ class Container:
class NonContainer: ...
def get_object() -> object: ...
def _(x: object):
if isinstance(x, Container):
if isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & NonContainer
x = get_object()
if isinstance(x, Container):
if isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & NonContainer
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
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
```
### Unsupported operators for negative contributions
@@ -142,14 +131,11 @@ class Container:
class NonContainer: ...
def get_object() -> object: ...
def _(x: object):
if isinstance(x, Container):
if not isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & ~NonContainer
x = get_object()
if isinstance(x, Container):
if not isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & ~NonContainer
# No error here!
reveal_type(2 in x) # revealed: bool
# No error here!
reveal_type(2 in x) # revealed: bool
```

View File

@@ -3,18 +3,17 @@
## String literals
```py
def str_instance() -> str: ...
def _(x: str):
reveal_type("abc" == "abc") # revealed: Literal[True]
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
reveal_type("abc" in "ab cd") # revealed: Literal[False]
reveal_type("" not in "hello") # revealed: Literal[False]
reveal_type("--" is "--") # revealed: bool
reveal_type("A" is "B") # revealed: Literal[False]
reveal_type("--" is not "--") # revealed: bool
reveal_type("A" is not "B") # revealed: Literal[True]
reveal_type(x < "...") # revealed: bool
reveal_type("abc" == "abc") # revealed: Literal[True]
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
reveal_type("abc" in "ab cd") # revealed: Literal[False]
reveal_type("" not in "hello") # revealed: Literal[False]
reveal_type("--" is "--") # revealed: bool
reveal_type("A" is "B") # revealed: Literal[False]
reveal_type("--" is not "--") # revealed: bool
reveal_type("A" is not "B") # revealed: Literal[True]
reveal_type(str_instance() < "...") # revealed: bool
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
```

View File

@@ -58,28 +58,23 @@ reveal_type(c >= d) # revealed: Literal[True]
#### Results with Ambiguity
```py
def bool_instance() -> bool:
return True
def _(x: bool, y: int):
a = (x,)
b = (y,)
def int_instance() -> int:
return 42
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
a = (bool_instance(),)
b = (int_instance(),)
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
```
#### Comparison Unsupported
@@ -197,7 +192,7 @@ reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
> Example: `(<int instance>, "foo") == (<int instance>, "bar")`
`Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons.
Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can
@@ -208,42 +203,38 @@ In contrast, with operators like `<` and `>`, the comparison must consider each
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
```py
def str_instance() -> str:
return "hello"
def _(x: str, y: int):
reveal_type("foo" == "bar") # revealed: Literal[False]
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
reveal_type((y, "foo") == (y, "bar")) # revealed: Literal[False]
def int_instance() -> int:
return 42
a = (x, y, "foo")
reveal_type("foo" == "bar") # revealed: Literal[False]
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
reveal_type((int_instance(), "foo") == (int_instance(), "bar")) # revealed: Literal[False]
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
a = (str_instance(), int_instance(), "foo")
b = (x, y, "bar")
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
b = (str_instance(), int_instance(), "bar")
c = (x, y, "foo", "different_length")
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
c = (str_instance(), int_instance(), "foo", "different_length")
reveal_type(a == c) # revealed: Literal[False]
reveal_type(a != c) # revealed: Literal[True]
reveal_type(a < c) # revealed: bool
reveal_type(a <= c) # revealed: bool
reveal_type(a > c) # revealed: bool
reveal_type(a >= c) # revealed: bool
reveal_type(a == c) # revealed: Literal[False]
reveal_type(a != c) # revealed: Literal[True]
reveal_type(a < c) # revealed: bool
reveal_type(a <= c) # revealed: bool
reveal_type(a > c) # revealed: bool
reveal_type(a >= c) # revealed: bool
```
#### Error Propagation
@@ -252,42 +243,36 @@ Errors occurring within a tuple comparison should propagate outward. However, if
comparison can clearly conclude before encountering an error, the error should not be raised.
```py
def int_instance() -> int:
return 42
def _(n: int, s: str):
class A: ...
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
A() < A()
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
A() <= A()
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
A() > A()
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
A() >= A()
def str_instance() -> str:
return "hello"
a = (0, n, A())
class A: ...
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a < a) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a <= a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a > a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a >= a) # revealed: Unknown
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
A() < A()
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
A() <= A()
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
A() > A()
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
A() >= A()
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
# and should terminate immediately.
b = (99999, n, A())
a = (0, int_instance(), A())
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a < a) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a <= a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a > a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a >= a) # revealed: Unknown
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
# and should terminate immediately.
b = (99999, int_instance(), A())
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
```
### Membership Test Comparisons
@@ -295,22 +280,20 @@ reveal_type(a >= b) # revealed: Literal[False]
"Membership Test Comparisons" refers to the operators `in` and `not in`.
```py
def int_instance() -> int:
return 42
def _(n: int):
a = (1, 2)
b = ((3, 4), (1, 2))
c = ((1, 2, 3), (4, 5, 6))
d = ((n, n), (n, n))
a = (1, 2)
b = ((3, 4), (1, 2))
c = ((1, 2, 3), (4, 5, 6))
d = ((int_instance(), int_instance()), (int_instance(), int_instance()))
reveal_type(a in b) # revealed: Literal[True]
reveal_type(a not in b) # revealed: Literal[False]
reveal_type(a in b) # revealed: Literal[True]
reveal_type(a not in b) # revealed: Literal[False]
reveal_type(a in c) # revealed: Literal[False]
reveal_type(a not in c) # revealed: Literal[True]
reveal_type(a in c) # revealed: Literal[False]
reveal_type(a not in c) # revealed: Literal[True]
reveal_type(a in d) # revealed: bool
reveal_type(a not in d) # revealed: bool
reveal_type(a in d) # revealed: bool
reveal_type(a not in d) # revealed: bool
```
### Identity Comparisons

View File

@@ -5,49 +5,46 @@
Comparisons on union types need to consider all possible cases:
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
one_or_two = 1 if flag else 2
flag = bool_instance()
one_or_two = 1 if flag else 2
reveal_type(one_or_two <= 2) # revealed: Literal[True]
reveal_type(one_or_two <= 1) # revealed: bool
reveal_type(one_or_two <= 0) # revealed: Literal[False]
reveal_type(one_or_two <= 2) # revealed: Literal[True]
reveal_type(one_or_two <= 1) # revealed: bool
reveal_type(one_or_two <= 0) # revealed: Literal[False]
reveal_type(2 >= one_or_two) # revealed: Literal[True]
reveal_type(1 >= one_or_two) # revealed: bool
reveal_type(0 >= one_or_two) # revealed: Literal[False]
reveal_type(2 >= one_or_two) # revealed: Literal[True]
reveal_type(1 >= one_or_two) # revealed: bool
reveal_type(0 >= one_or_two) # revealed: Literal[False]
reveal_type(one_or_two < 1) # revealed: Literal[False]
reveal_type(one_or_two < 2) # revealed: bool
reveal_type(one_or_two < 3) # revealed: Literal[True]
reveal_type(one_or_two < 1) # revealed: Literal[False]
reveal_type(one_or_two < 2) # revealed: bool
reveal_type(one_or_two < 3) # revealed: Literal[True]
reveal_type(one_or_two > 0) # revealed: Literal[True]
reveal_type(one_or_two > 1) # revealed: bool
reveal_type(one_or_two > 2) # revealed: Literal[False]
reveal_type(one_or_two > 0) # revealed: Literal[True]
reveal_type(one_or_two > 1) # revealed: bool
reveal_type(one_or_two > 2) # revealed: Literal[False]
reveal_type(one_or_two == 3) # revealed: Literal[False]
reveal_type(one_or_two == 1) # revealed: bool
reveal_type(one_or_two == 3) # revealed: Literal[False]
reveal_type(one_or_two == 1) # revealed: bool
reveal_type(one_or_two != 3) # revealed: Literal[True]
reveal_type(one_or_two != 1) # revealed: bool
reveal_type(one_or_two != 3) # revealed: Literal[True]
reveal_type(one_or_two != 1) # revealed: bool
a_or_ab = "a" if flag else "ab"
a_or_ab = "a" if flag else "ab"
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
reveal_type("a" in a_or_ab) # revealed: Literal[True]
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
reveal_type("a" in a_or_ab) # revealed: Literal[True]
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
reveal_type("b" in a_or_ab) # revealed: bool
reveal_type("b" not in a_or_ab) # revealed: bool
reveal_type("b" in a_or_ab) # revealed: bool
reveal_type("b" not in a_or_ab) # revealed: bool
one_or_none = 1 if flag else None
one_or_none = 1 if flag else None
reveal_type(one_or_none is None) # revealed: bool
reveal_type(one_or_none is not None) # revealed: bool
reveal_type(one_or_none is None) # revealed: bool
reveal_type(one_or_none is not None) # revealed: bool
```
## Union on both sides of the comparison
@@ -56,18 +53,15 @@ With unions on both sides, we need to consider the full cross product of options
resulting (union) type:
```py
def bool_instance() -> bool:
return True
def _(flag_s: bool, flag_l: bool):
small = 1 if flag_s else 2
large = 2 if flag_l else 3
flag_s, flag_l = bool_instance(), bool_instance()
small = 1 if flag_s else 2
large = 2 if flag_l else 3
reveal_type(small <= large) # revealed: Literal[True]
reveal_type(small >= large) # revealed: bool
reveal_type(small <= large) # revealed: Literal[True]
reveal_type(small >= large) # revealed: bool
reveal_type(small < large) # revealed: bool
reveal_type(small > large) # revealed: Literal[False]
reveal_type(small < large) # revealed: bool
reveal_type(small > large) # revealed: Literal[False]
```
## Unsupported operations
@@ -77,12 +71,9 @@ back to `bool` for the result type instead of trying to infer something more pre
(supported) variants:
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = [1, 2] if flag else 1
flag = bool_instance()
x = [1, 2] if flag else 1
result = 1 in x # error: "Operator `in` is not supported"
reveal_type(result) # revealed: bool
result = 1 in x # error: "Operator `in` is not supported"
reveal_type(result) # revealed: bool
```

View File

@@ -1,42 +1,38 @@
# Comparison: Unsupported operators
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag1: bool, flag2: bool):
class A: ...
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
reveal_type(a) # revealed: bool
class A: ...
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
reveal_type(b) # revealed: bool
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
reveal_type(a) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `object` and `int`")
c = object() < 5
# TODO: should be Unknown, once operand type check is implemented
reveal_type(c) # revealed: bool
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
reveal_type(b) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `int` and `object`")
d = 5 < object()
# TODO: should be Unknown, once operand type check is implemented
reveal_type(d) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `object` and `int`")
c = object() < 5
# TODO: should be Unknown, once operand type check is implemented
reveal_type(c) # revealed: bool
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `int` and `object`")
d = 5 < object()
# TODO: should be Unknown, once operand type check is implemented
reveal_type(d) # revealed: bool
# TODO: should error, need to check if __lt__ signature is valid for right operand
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
f = (1, 2) < (1, "hello")
# TODO: should be Unknown, once operand type check is implemented
reveal_type(f) # revealed: bool
flag = bool_instance()
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool
# TODO: should error, need to check if __lt__ signature is valid for right operand
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
f = (1, 2) < (1, "hello")
# TODO: should be Unknown, once operand type check is implemented
reveal_type(f) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
g = (bool_instance(), A()) < (bool_instance(), A())
reveal_type(g) # revealed: Unknown
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
g = (flag1, A()) < (flag2, A())
reveal_type(g) # revealed: Unknown
```

View File

@@ -3,47 +3,35 @@
## Simple if-expression
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else 2
reveal_type(x) # revealed: Literal[1, 2]
def _(flag: bool):
x = 1 if flag else 2
reveal_type(x) # revealed: Literal[1, 2]
```
## If-expression with walrus operator
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
y = 0
z = 0
x = (y := 1) if flag else (z := 2)
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(y) # revealed: Literal[0, 1]
reveal_type(z) # revealed: Literal[0, 2]
def _(flag: bool):
y = 0
z = 0
x = (y := 1) if flag else (z := 2)
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(y) # revealed: Literal[0, 1]
reveal_type(z) # revealed: Literal[0, 2]
```
## Nested if-expression
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
x = 1 if flag else 2 if flag2 else 3
reveal_type(x) # revealed: Literal[1, 2, 3]
def _(flag: bool, flag2: bool):
x = 1 if flag else 2 if flag2 else 3
reveal_type(x) # revealed: Literal[1, 2, 3]
```
## None
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else None
reveal_type(x) # revealed: Literal[1] | None
def _(flag: bool):
x = 1 if flag else None
reveal_type(x) # revealed: Literal[1] | None
```

View File

@@ -3,128 +3,115 @@
## Simple if
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
y = 1
y = 2
flag = bool_instance()
y = 1
y = 2
if flag:
y = 3
if flag:
y = 3
reveal_type(y) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[2, 3]
```
## Simple if-elif-else
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 1
y = 2
flag, flag2 = bool_instance(), bool_instance()
y = 1
y = 2
if flag:
y = 3
elif flag2:
y = 4
else:
r = y
y = 5
s = y
x = y
if flag:
y = 3
elif flag2:
y = 4
else:
r = y
y = 5
s = y
x = y
reveal_type(x) # revealed: Literal[3, 4, 5]
reveal_type(x) # revealed: Literal[3, 4, 5]
# revealed: Literal[2]
# error: [possibly-unresolved-reference]
reveal_type(r)
# revealed: Literal[2]
# error: [possibly-unresolved-reference]
reveal_type(r)
# revealed: Literal[5]
# error: [possibly-unresolved-reference]
reveal_type(s)
# revealed: Literal[5]
# error: [possibly-unresolved-reference]
reveal_type(s)
```
## Single symbol across if-elif-else
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
if flag:
y = 1
elif flag2:
y = 2
else:
y = 3
flag, flag2 = bool_instance(), bool_instance()
if flag:
y = 1
elif flag2:
y = 2
else:
y = 3
reveal_type(y) # revealed: Literal[1, 2, 3]
reveal_type(y) # revealed: Literal[1, 2, 3]
```
## if-elif-else without else assignment
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 0
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
y = 1
elif flag2:
y = 2
else:
pass
reveal_type(y) # revealed: Literal[0, 1, 2]
if flag:
y = 1
elif flag2:
y = 2
else:
pass
reveal_type(y) # revealed: Literal[0, 1, 2]
```
## if-elif-else with intervening assignment
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 0
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
y = 1
z = 3
elif flag2:
y = 2
else:
pass
reveal_type(y) # revealed: Literal[0, 1, 2]
if flag:
y = 1
z = 3
elif flag2:
y = 2
else:
pass
reveal_type(y) # revealed: Literal[0, 1, 2]
```
## Nested if statement
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 0
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
if flag2:
y = 1
reveal_type(y) # revealed: Literal[0, 1]
if flag:
if flag2:
y = 1
reveal_type(y) # revealed: Literal[0, 1]
```
## if-elif without else
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 1
y = 2
flag, flag2 = bool_instance(), bool_instance()
y = 1
y = 2
if flag:
y = 3
elif flag2:
y = 4
if flag:
y = 3
elif flag2:
y = 4
reveal_type(y) # revealed: Literal[2, 3, 4]
reveal_type(y) # revealed: Literal[2, 3, 4]
```

View File

@@ -31,6 +31,7 @@ reveal_type(y)
```py
y = 1
y = 2
match 0:
case 1:
y = 3

View File

@@ -10,42 +10,35 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t
## Incompatible declarations
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
x: str
else:
x: int
flag = bool_instance()
if flag:
x: str
else:
x: int
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
```
## Partial declarations
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
x: int
flag = bool_instance()
if flag:
x: int
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
```
## Incompatible declarations with bad assignment
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
x: str
else:
x: int
flag = bool_instance()
if flag:
x: str
else:
x: int
# error: [conflicting-declarations]
# error: [invalid-assignment]
x = b"foo"
# error: [conflicting-declarations]
# error: [invalid-assignment]
x = b"foo"
```

View File

@@ -49,12 +49,44 @@ def foo(
try:
help()
except x as e:
# TODO: should be `AttributeError`
reveal_type(e) # revealed: @Todo(exception type)
reveal_type(e) # revealed: AttributeError
except y as f:
# TODO: should be `OSError | RuntimeError`
reveal_type(f) # revealed: @Todo(exception type)
reveal_type(f) # revealed: OSError | RuntimeError
except z as g:
# TODO: should be `BaseException`
reveal_type(g) # revealed: @Todo(exception type)
reveal_type(g) # revealed: @Todo(full tuple[...] support)
```
## Invalid exception handlers
```py
try:
pass
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
except 3 as e:
reveal_type(e) # revealed: Unknown
try:
pass
# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
except (ValueError, OSError, "foo", b"bar") as e:
reveal_type(e) # revealed: ValueError | OSError | Unknown
def foo(
x: type[str],
y: tuple[type[OSError], type[RuntimeError], int],
z: tuple[type[str], ...],
):
try:
help()
# error: [invalid-exception-caught]
except x as e:
reveal_type(e) # revealed: Unknown
# error: [invalid-exception-caught]
except y as f:
reveal_type(f) # revealed: OSError | RuntimeError | Unknown
except z as g:
# TODO: should emit a diagnostic here:
reveal_type(g) # revealed: @Todo(full tuple[...] support)
```

View File

@@ -1,30 +1,59 @@
# Except star
# `except*`
## Except\* with BaseException
## `except*` with `BaseException`
```py
try:
help()
except* BaseException as e:
# TODO: should be `BaseExceptionGroup[BaseException]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```
## Except\* with specific exception
## `except*` with specific exception
```py
try:
help()
except* OSError as e:
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
# (needs homogenous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
```
## Except\* with multiple exceptions
## `except*` with multiple exceptions
```py
try:
help()
except* (TypeError, AttributeError) as e:
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
# (needs homogenous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
```
## `except*` with mix of `Exception`s and `BaseException`s
```py
try:
help()
except* (KeyboardInterrupt, AttributeError) as e:
# TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```
## Invalid `except*` handlers
```py
try:
help()
except* 3 as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
try:
help()
except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```

View File

@@ -9,5 +9,4 @@ try:
print
except as e: # error: [invalid-syntax]
reveal_type(e) # revealed: Unknown
```

View File

@@ -3,26 +3,25 @@
## Boundness
```py
def flag() -> bool: ...
def _(flag: bool):
class A:
always_bound = 1
class A:
always_bound = 1
if flag:
union = 1
else:
union = "abc"
if flag():
union = 1
else:
union = "abc"
if flag:
possibly_unbound = "abc"
if flag():
possibly_unbound = "abc"
reveal_type(A.always_bound) # revealed: Literal[1]
reveal_type(A.always_bound) # revealed: Literal[1]
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
reveal_type(A.non_existent) # revealed: Unknown
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
reveal_type(A.non_existent) # revealed: Unknown
```

View File

@@ -3,54 +3,45 @@
## OR
```py
def foo() -> str:
pass
reveal_type(True or False) # revealed: Literal[True]
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
reveal_type("" or "y" or "z") # revealed: Literal["y"]
reveal_type(False or "z") # revealed: Literal["z"]
reveal_type(False or True) # revealed: Literal[True]
reveal_type(False or False) # revealed: Literal[False]
reveal_type(foo() or False) # revealed: str | Literal[False]
reveal_type(foo() or True) # revealed: str | Literal[True]
def _(foo: str):
reveal_type(True or False) # revealed: Literal[True]
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
reveal_type("" or "y" or "z") # revealed: Literal["y"]
reveal_type(False or "z") # revealed: Literal["z"]
reveal_type(False or True) # revealed: Literal[True]
reveal_type(False or False) # revealed: Literal[False]
reveal_type(foo or False) # revealed: str | Literal[False]
reveal_type(foo or True) # revealed: str | Literal[True]
```
## AND
```py
def foo() -> str:
pass
reveal_type(True and False) # revealed: Literal[False]
reveal_type(False and True) # revealed: Literal[False]
reveal_type(foo() and False) # revealed: str | Literal[False]
reveal_type(foo() and True) # revealed: str | Literal[True]
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
reveal_type("x" and "y" and "") # revealed: Literal[""]
reveal_type("" and "y") # revealed: Literal[""]
def _(foo: str):
reveal_type(True and False) # revealed: Literal[False]
reveal_type(False and True) # revealed: Literal[False]
reveal_type(foo and False) # revealed: str | Literal[False]
reveal_type(foo and True) # revealed: str | Literal[True]
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
reveal_type("x" and "y" and "") # revealed: Literal[""]
reveal_type("" and "y") # revealed: Literal[""]
```
## Simple function calls to bool
```py
def returns_bool() -> bool:
return True
def _(flag: bool):
if flag:
x = True
else:
x = False
if returns_bool():
x = True
else:
x = False
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: bool
```
## Complex
```py
def foo() -> str:
pass
reveal_type("x" and "y" or "z") # revealed: Literal["y"]
reveal_type("x" or "y" and "z") # revealed: Literal["x"]
reveal_type("" and "y" or "z") # revealed: Literal["z"]

View File

@@ -3,10 +3,8 @@
## Union
```py
def bool_instance() -> bool:
return True
reveal_type(1 if bool_instance() else 2) # revealed: Literal[1, 2]
def _(flag: bool):
reveal_type(1 if flag else 2) # revealed: Literal[1, 2]
```
## Statically known branches
@@ -30,14 +28,12 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2]
The test inside an if expression should not affect code outside of the expression.
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x: Literal[42, "hello"] = 42 if flag else "hello"
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
_ = ... if isinstance(x, str) else ...
_ = ... if isinstance(x, str) else ...
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
```

View File

@@ -211,8 +211,7 @@ reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
### No `__len__`
```py
class NoDunderLen:
pass
class NoDunderLen: ...
# TODO: Emit a diagnostic
reveal_type(len(NoDunderLen())) # revealed: int

View File

@@ -3,11 +3,10 @@
## Maybe unbound
```py path=maybe_unbound.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
y = 3
x = y # error: [possibly-unresolved-reference]
@@ -31,13 +30,12 @@ reveal_type(y) # revealed: Literal[3]
## Maybe unbound annotated
```py path=maybe_unbound_annotated.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
y: int = 3
x = y # error: [possibly-unresolved-reference]
# revealed: Literal[3]
@@ -63,10 +61,10 @@ reveal_type(y) # revealed: int
Importing a possibly undeclared name still gives us its declared type:
```py path=maybe_undeclared.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
if bool_instance():
if coinflip():
x: int
```
@@ -83,14 +81,12 @@ def f(): ...
```
```py path=b.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
from c import f
else:
def f(): ...
```
@@ -111,11 +107,10 @@ x: int
```
```py path=b.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
from c import x
else:
x = 1

View File

@@ -106,23 +106,19 @@ reveal_type(x)
## With non-callable iterator
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class NotIterable:
if flag:
__iter__ = 1
else:
__iter__ = None
flag = bool_instance()
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass
class NotIterable:
if flag:
__iter__ = 1
else:
__iter__ = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass
# revealed: Unknown
# error: [possibly-unresolved-reference]
reveal_type(x)
# revealed: Unknown
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## Invalid iterable
@@ -160,13 +156,9 @@ class Test2:
def __iter__(self) -> TestIter:
return TestIter()
def bool_instance() -> bool:
return True
flag = bool_instance()
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int
def _(flag: bool):
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int
```
## Union type as iterator
@@ -215,13 +207,9 @@ class Test2:
def __iter__(self) -> TestIter3 | TestIter4:
return TestIter3()
def bool_instance() -> bool:
return True
flag = bool_instance()
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
def _(flag: bool):
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
```
## Union type as iterable where one union element has no `__iter__` method
@@ -235,12 +223,10 @@ class Test:
def __iter__(self) -> TestIter:
return TestIter()
def coinflip() -> bool:
return True
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
for x in Test() if coinflip() else 42:
reveal_type(x) # revealed: int
def _(flag: bool):
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
for x in Test() if flag else 42:
reveal_type(x) # revealed: int
```
## Union type as iterable where one union element has invalid `__iter__` method
@@ -258,12 +244,10 @@ class Test2:
def __iter__(self) -> int:
return 42
def coinflip() -> bool:
return True
# error: "Object of type `Test | Test2` is not iterable"
for x in Test() if coinflip() else Test2():
reveal_type(x) # revealed: Unknown
def _(flag: bool):
# error: "Object of type `Test | Test2` is not iterable"
for x in Test() if flag else Test2():
reveal_type(x) # revealed: Unknown
```
## Union type as iterator where one union element has no `__next__` method

View File

@@ -3,54 +3,45 @@
## Basic While Loop
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1
while flag:
x = 2
flag = bool_instance()
x = 1
while flag:
x = 2
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(x) # revealed: Literal[1, 2]
```
## While with else (no break)
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1
while flag:
x = 2
else:
reveal_type(x) # revealed: Literal[1, 2]
x = 3
flag = bool_instance()
x = 1
while flag:
x = 2
else:
reveal_type(x) # revealed: Literal[1, 2]
x = 3
reveal_type(x) # revealed: Literal[3]
reveal_type(x) # revealed: Literal[3]
```
## While with Else (may break)
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
x = 1
y = 0
while flag:
x = 2
if flag2:
y = 4
break
else:
y = x
x = 3
flag, flag2 = bool_instance(), bool_instance()
x = 1
y = 0
while flag:
x = 2
if flag2:
y = 4
break
else:
y = x
x = 3
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[1, 2, 4]
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[1, 2, 4]
```
## Nested while loops

View File

@@ -1,32 +1,32 @@
## Narrowing for `bool(..)` checks
```py
def flag() -> bool: ...
def _(flag: bool):
x = 1 if flag() else None
x = 1 if flag else None
# valid invocation, positive
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None):
reveal_type(x) # revealed: Literal[1]
# valid invocation, negative
reveal_type(x) # revealed: Literal[1] | None
if not bool(x is not None):
reveal_type(x) # revealed: None
# no args/narrowing
reveal_type(x) # revealed: Literal[1] | None
if not bool():
# valid invocation, positive
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None):
reveal_type(x) # revealed: Literal[1]
# invalid invocation, too many positional args
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, 5): # TODO diagnostic
# valid invocation, negative
reveal_type(x) # revealed: Literal[1] | None
if not bool(x is not None):
reveal_type(x) # revealed: None
# invalid invocation, too many kwargs
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, y=5): # TODO diagnostic
# no args/narrowing
reveal_type(x) # revealed: Literal[1] | None
if not bool():
reveal_type(x) # revealed: Literal[1] | None
# invalid invocation, too many positional args
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, 5): # TODO diagnostic
reveal_type(x) # revealed: Literal[1] | None
# invalid invocation, too many kwargs
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, y=5): # TODO diagnostic
reveal_type(x) # revealed: Literal[1] | None
```

View File

@@ -9,85 +9,67 @@ Similarly, in `and` expressions, the right-hand side is evaluated only if the le
## Narrowing in `or`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
x: A | None = A() if flag else None
class A: ...
x: A | None = A() if bool_instance() else None
isinstance(x, A) or reveal_type(x) # revealed: None
x is None or reveal_type(x) # revealed: A
reveal_type(x) # revealed: A | None
isinstance(x, A) or reveal_type(x) # revealed: None
x is None or reveal_type(x) # revealed: A
reveal_type(x) # revealed: A | None
```
## Narrowing in `and`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
x: A | None = A() if flag else None
class A: ...
x: A | None = A() if bool_instance() else None
isinstance(x, A) and reveal_type(x) # revealed: A
x is None and reveal_type(x) # revealed: None
reveal_type(x) # revealed: A | None
isinstance(x, A) and reveal_type(x) # revealed: A
x is None and reveal_type(x) # revealed: None
reveal_type(x) # revealed: A | None
```
## Multiple `and` arms
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
class A: ...
x: A | None = A() if flag1 else None
class A: ...
x: A | None = A() if bool_instance() else None
bool_instance() and isinstance(x, A) and reveal_type(x) # revealed: A
isinstance(x, A) and bool_instance() and reveal_type(x) # revealed: A
reveal_type(x) and isinstance(x, A) and bool_instance() # revealed: A | None
flag2 and isinstance(x, A) and reveal_type(x) # revealed: A
isinstance(x, A) and flag2 and reveal_type(x) # revealed: A
reveal_type(x) and isinstance(x, A) and flag3 # revealed: A | None
```
## Multiple `or` arms
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
class A: ...
x: A | None = A() if flag1 else None
class A: ...
x: A | None = A() if bool_instance() else None
bool_instance() or isinstance(x, A) or reveal_type(x) # revealed: None
isinstance(x, A) or bool_instance() or reveal_type(x) # revealed: None
reveal_type(x) or isinstance(x, A) or bool_instance() # revealed: A | None
flag2 or isinstance(x, A) or reveal_type(x) # revealed: None
isinstance(x, A) or flag3 or reveal_type(x) # revealed: None
reveal_type(x) or isinstance(x, A) or flag4 # revealed: A | None
```
## Multiple predicates
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
class A: ...
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
class A: ...
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
```
## Mix of `and` and `or`
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
class A: ...
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
class A: ...
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
```

View File

@@ -6,15 +6,11 @@
class A: ...
class B: ...
def instance() -> A | B:
return A()
x = instance()
if isinstance(x, A) and isinstance(x, B):
reveal_type(x) # revealed: A & B
else:
reveal_type(x) # revealed: B & ~A | A & ~B
def _(x: A | B):
if isinstance(x, A) and isinstance(x, B):
reveal_type(x) # revealed: A & B
else:
reveal_type(x) # revealed: B & ~A | A & ~B
```
## Arms might not add narrowing constraints
@@ -23,25 +19,18 @@ else:
class A: ...
class B: ...
def bool_instance() -> bool:
return True
def _(flag: bool, x: A | B):
if isinstance(x, A) and flag:
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
def instance() -> A | B:
return A()
if flag and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
x = instance()
if isinstance(x, A) and bool_instance():
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
if bool_instance() and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
reveal_type(x) # revealed: A | B
```
## Statically known arms
@@ -50,39 +39,35 @@ reveal_type(x) # revealed: A | B
class A: ...
class B: ...
def instance() -> A | B:
return A()
def _(x: A | B):
if isinstance(x, A) and True:
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
x = instance()
if True and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if isinstance(x, A) and True:
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if False and isinstance(x, A):
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
if True and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if False or isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if True or isinstance(x, A):
reveal_type(x) # revealed: A | B
else:
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: B & ~A
if False and isinstance(x, A):
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
if False or isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if True or isinstance(x, A):
reveal_type(x) # revealed: A | B
else:
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: B & ~A
reveal_type(x) # revealed: A | B
```
## The type of multiple symbols can be narrowed down
@@ -91,22 +76,17 @@ reveal_type(x) # revealed: A | B
class A: ...
class B: ...
def instance() -> A | B:
return A()
def _(x: A | B, y: A | B):
if isinstance(x, A) and isinstance(y, B):
reveal_type(x) # revealed: A
reveal_type(y) # revealed: B
else:
# No narrowing: Only-one or both checks might have failed
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
x = instance()
y = instance()
if isinstance(x, A) and isinstance(y, B):
reveal_type(x) # revealed: A
reveal_type(y) # revealed: B
else:
# No narrowing: Only-one or both checks might have failed
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
```
## Narrowing in `or` conditional
@@ -116,15 +96,11 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, A) or isinstance(x, B):
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: C & ~A & ~B
def _(x: A | B | C):
if isinstance(x, A) or isinstance(x, B):
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: C & ~A & ~B
```
## In `or`, all arms should add constraint in order to narrow
@@ -134,18 +110,11 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
def bool_instance() -> bool:
return True
x = instance()
if isinstance(x, A) or isinstance(x, B) or bool_instance():
reveal_type(x) # revealed: A | B | C
else:
reveal_type(x) # revealed: C & ~A & ~B
def _(flag: bool, x: A | B | C):
if isinstance(x, A) or isinstance(x, B) or flag:
reveal_type(x) # revealed: A | B | C
else:
reveal_type(x) # revealed: C & ~A & ~B
```
## in `or`, all arms should narrow the same set of symbols
@@ -155,28 +124,23 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
def _(x: A | B | C, y: A | B | C):
if isinstance(x, A) or isinstance(y, A):
# The predicate might be satisfied by the right side, so the type of `x` cant be narrowed down here.
reveal_type(x) # revealed: A | B | C
# The same for `y`
reveal_type(y) # revealed: A | B | C
else:
reveal_type(x) # revealed: B & ~A | C & ~A
reveal_type(y) # revealed: B & ~A | C & ~A
x = instance()
y = instance()
if isinstance(x, A) or isinstance(y, A):
# The predicate might be satisfied by the right side, so the type of `x` cant be narrowed down here.
reveal_type(x) # revealed: A | B | C
# The same for `y`
reveal_type(y) # revealed: A | B | C
else:
reveal_type(x) # revealed: B & ~A | C & ~A
reveal_type(y) # revealed: B & ~A | C & ~A
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
else:
reveal_type(x) # revealed: A | B | C
reveal_type(y) # revealed: A | B | C
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
else:
reveal_type(x) # revealed: A | B | C
reveal_type(y) # revealed: A | B | C
```
## mixing `and` and `not`
@@ -186,16 +150,12 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, B) and not isinstance(x, C):
reveal_type(x) # revealed: B & ~C
else:
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
reveal_type(x) # revealed: A & ~B | C
def _(x: A | B | C):
if isinstance(x, B) and not isinstance(x, C):
reveal_type(x) # revealed: B & ~C
else:
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
reveal_type(x) # revealed: A & ~B | C
```
## mixing `or` and `not`
@@ -205,15 +165,11 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | A & ~C
else:
reveal_type(x) # revealed: C & ~B
def _(x: A | B | C):
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | A & ~C
else:
reveal_type(x) # revealed: C & ~B
```
## `or` with nested `and`
@@ -223,16 +179,12 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
reveal_type(x) # revealed: A | B & ~C
else:
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
reveal_type(x) # revealed: C & ~A
def _(x: A | B | C):
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
reveal_type(x) # revealed: A | B & ~C
else:
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
reveal_type(x) # revealed: C & ~A
```
## `and` with nested `or`
@@ -242,41 +194,32 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
# A & (B | ~C) -> (A & B) | (A & ~C)
reveal_type(x) # revealed: A & B | A & ~C
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
# (~A | ~B) & (~A | C) ->
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
# ~A | (~A & C) | (~B & C) ->
# ~A | (C & ~B) ->
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
def _(x: A | B | C):
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
# A & (B | ~C) -> (A & B) | (A & ~C)
reveal_type(x) # revealed: A & B | A & ~C
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
# (~A | ~B) & (~A | C) ->
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
# ~A | (~A & C) | (~B & C) ->
# ~A | (C & ~B) ->
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
```
## Boolean expression internal narrowing
```py
def optional_string() -> str | None:
return None
def _(x: str | None, y: str | None):
if x is None and y is not x:
reveal_type(y) # revealed: str
x = optional_string()
y = optional_string()
# Neither of the conditions alone is sufficient for narrowing y's type:
if x is None:
reveal_type(y) # revealed: str | None
if x is None and y is not x:
reveal_type(y) # revealed: str
# Neither of the conditions alone is sufficient for narrowing y's type:
if x is None:
reveal_type(y) # revealed: str | None
if y is not x:
reveal_type(y) # revealed: str | None
if y is not x:
reveal_type(y) # revealed: str | None
```

View File

@@ -3,55 +3,47 @@
## Positive contributions become negative in elif-else blocks
```py
def int_instance() -> int:
return 42
x = int_instance()
if x == 1:
# cannot narrow; could be a subclass of `int`
reveal_type(x) # revealed: int
elif x == 2:
reveal_type(x) # revealed: int & ~Literal[1]
elif x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
def _(x: int):
if x == 1:
# cannot narrow; could be a subclass of `int`
reveal_type(x) # revealed: int
elif x == 2:
reveal_type(x) # revealed: int & ~Literal[1]
elif x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
```
## Positive contributions become negative in elif-else blocks, with simplification
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
x = 1 if bool_instance() else 2 if bool_instance() else 3
if x == 1:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
elif x == 2:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[2, 3]
else:
reveal_type(x) # revealed: Literal[3]
if x == 1:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
elif x == 2:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[2, 3]
else:
reveal_type(x) # revealed: Literal[3]
```
## Multiple negative contributions using elif, with simplification
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
x = 1 if bool_instance() else 2 if bool_instance() else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
elif x != 2:
# TODO should be `Literal[1]`
reveal_type(x) # revealed: Literal[1, 3]
elif x == 3:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2]
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
elif x != 2:
# TODO should be `Literal[1]`
reveal_type(x) # revealed: Literal[1, 3]
elif x == 3:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2]
```

View File

@@ -3,77 +3,64 @@
## `is None`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
flag = bool_instance()
x = None if flag else 1
if x is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1]
if x is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: None | Literal[1]
reveal_type(x) # revealed: None | Literal[1]
```
## `is` for other types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
x = A()
y = x if flag else None
flag = bool_instance()
if y is x:
reveal_type(y) # revealed: A
else:
reveal_type(y) # revealed: A | None
class A: ...
x = A()
y = x if flag else None
if y is x:
reveal_type(y) # revealed: A
else:
reveal_type(y) # revealed: A | None
reveal_type(y) # revealed: A | None
```
## `is` in chained comparisons
```py
def bool_instance() -> bool:
return True
def _(x_flag: bool, y_flag: bool):
x = True if x_flag else False
y = True if y_flag else False
x_flag, y_flag = bool_instance(), bool_instance()
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is x is False: # Interpreted as `(y is x) and (x is False)`
reveal_type(x) # revealed: Literal[False]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is not x) or (x is not False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is x is False: # Interpreted as `(y is x) and (x is False)`
reveal_type(x) # revealed: Literal[False]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is not x) or (x is not False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
```
## `is` in elif clause
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = None if flag1 else (1 if flag2 else True)
x = None if bool_instance() else (1 if bool_instance() else True)
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
if x is None:
reveal_type(x) # revealed: None
elif x is True:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
if x is None:
reveal_type(x) # revealed: None
elif x is True:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -5,34 +5,28 @@
The type guard removes `None` from the union type:
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
flag = bool_instance()
x = None if flag else 1
if x is not None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
if x is not None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
reveal_type(x) # revealed: None | Literal[1]
reveal_type(x) # revealed: None | Literal[1]
```
## `is not` for other singleton types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = True if flag else False
reveal_type(x) # revealed: bool
flag = bool_instance()
x = True if flag else False
reveal_type(x) # revealed: bool
if x is not False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
if x is not False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
```
## `is not` for non-singleton types
@@ -53,20 +47,17 @@ else:
## `is not` for other types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
x = A()
y = x if flag else None
class A: ...
if y is not x:
reveal_type(y) # revealed: A | None
else:
reveal_type(y) # revealed: A
x = A()
y = x if bool_instance() else None
if y is not x:
reveal_type(y) # revealed: A | None
else:
reveal_type(y) # revealed: A
reveal_type(y) # revealed: A | None
```
## `is not` in chained comparisons
@@ -74,23 +65,20 @@ reveal_type(y) # revealed: A | None
The type guard removes `False` from the union type of the tested value only.
```py
def bool_instance() -> bool:
return True
x_flag, y_flag = bool_instance(), bool_instance()
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
reveal_type(x) # revealed: Literal[True]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is x) or (x is False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
def _(x_flag: bool, y_flag: bool):
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
reveal_type(x) # revealed: Literal[True]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is x) or (x is False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
```

View File

@@ -3,54 +3,45 @@
## Multiple negative contributions
```py
def int_instance() -> int:
return 42
x = int_instance()
if x != 1:
if x != 2:
if x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
def _(x: int):
if x != 1:
if x != 2:
if x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
```
## Multiple negative contributions with simplification
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
flag1, flag2 = bool_instance(), bool_instance()
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x != 2:
reveal_type(x) # revealed: Literal[3]
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x != 2:
reveal_type(x) # revealed: Literal[3]
```
## elif-else blocks
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
x = 1 if bool_instance() else 2 if bool_instance() else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x == 2:
# TODO should be `Literal[2]`
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
elif x == 3:
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: Never
if x == 2:
# TODO should be `Literal[2]`
reveal_type(x) # revealed: Literal[2, 3]
elif x == 3:
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: Never
elif x != 2:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 3]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
elif x != 2:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 3]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
```

View File

@@ -5,29 +5,25 @@ The `not` operator negates a constraint.
## `not is None`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
x = None if bool_instance() else 1
if not x is None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
if not x is None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
reveal_type(x) # revealed: None | Literal[1]
reveal_type(x) # revealed: None | Literal[1]
```
## `not isinstance`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
x = 1 if bool_instance() else "a"
if not isinstance(x, (int)):
reveal_type(x) # revealed: Literal["a"]
else:
reveal_type(x) # revealed: Literal[1]
if not isinstance(x, (int)):
reveal_type(x) # revealed: Literal["a"]
else:
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -3,82 +3,66 @@
## `x != None`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
flag = bool_instance()
x = None if flag else 1
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
# TODO should be None
reveal_type(x) # revealed: None | Literal[1]
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
# TODO should be None
reveal_type(x) # revealed: None | Literal[1]
```
## `!=` for other singleton types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = True if flag else False
flag = bool_instance()
x = True if flag else False
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
# TODO should be Literal[False]
reveal_type(x) # revealed: bool
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
# TODO should be Literal[False]
reveal_type(x) # revealed: bool
```
## `x != y` where `y` is of literal type
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else 2
flag = bool_instance()
x = 1 if flag else 2
if x != 1:
reveal_type(x) # revealed: Literal[2]
if x != 1:
reveal_type(x) # revealed: Literal[2]
```
## `x != y` where `y` is a single-valued type
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
class B: ...
C = A if flag else B
flag = bool_instance()
class A: ...
class B: ...
C = A if flag else B
if C != A:
reveal_type(C) # revealed: Literal[B]
else:
# TODO should be Literal[A]
reveal_type(C) # revealed: Literal[A, B]
if C != A:
reveal_type(C) # revealed: Literal[B]
else:
# TODO should be Literal[A]
reveal_type(C) # revealed: Literal[A, B]
```
## `x != y` where `y` has multiple single-valued options
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2
y = 2 if flag2 else 3
x = 1 if bool_instance() else 2
y = 2 if bool_instance() else 3
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[1, 2]
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[1, 2]
```
## `!=` for non-single-valued types
@@ -86,34 +70,22 @@ else:
Only single-valued types should narrow the type:
```py
def bool_instance() -> bool:
return True
def _(flag: bool, a: int, y: int):
x = a if flag else None
def int_instance() -> int:
return 42
flag = bool_instance()
x = int_instance() if flag else None
y = int_instance()
if x != y:
reveal_type(x) # revealed: int | None
if x != y:
reveal_type(x) # revealed: int | None
```
## Mix of single-valued and non-single-valued types
```py
def int_instance() -> int:
return 42
def _(flag1: bool, flag2: bool, a: int):
x = 1 if flag1 else 2
y = 2 if flag2 else a
def bool_instance() -> bool:
return True
x = 1 if bool_instance() else 2
y = 2 if bool_instance() else int_instance()
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2]
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2]
```

View File

@@ -5,23 +5,19 @@ Narrowing for `isinstance(object, classinfo)` expressions.
## `classinfo` is a single type
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
if isinstance(x, str):
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, int):
reveal_type(x) # revealed: Never
reveal_type(x) # revealed: Literal[1]
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
if isinstance(x, str):
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, int):
reveal_type(x) # revealed: Never
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## `classinfo` is a tuple of types
@@ -30,56 +26,48 @@ Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tup
The former is equivalent to `isinstance(x, int | str)`:
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag1: bool, flag2: bool):
x = 1 if flag else "a"
flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance()
if isinstance(x, (int, str)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
else:
reveal_type(x) # revealed: Never
x = 1 if flag else "a"
if isinstance(x, (int, bytes)):
reveal_type(x) # revealed: Literal[1]
if isinstance(x, (int, str)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
else:
reveal_type(x) # revealed: Never
if isinstance(x, (bytes, str)):
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, (int, bytes)):
reveal_type(x) # revealed: Literal[1]
# No narrowing should occur if a larger type is also
# one of the possibilities:
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
else:
reveal_type(x) # revealed: Never
if isinstance(x, (bytes, str)):
reveal_type(x) # revealed: Literal["a"]
y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
reveal_type(y) # revealed: Literal[1] | Literal["a"]
# No narrowing should occur if a larger type is also
# one of the possibilities:
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
else:
reveal_type(x) # revealed: Never
if isinstance(y, (int, bytes)):
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
reveal_type(y) # revealed: Literal[1] | Literal["a"]
if isinstance(y, (int, bytes)):
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
if isinstance(y, (str, bytes)):
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
if isinstance(y, (str, bytes)):
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
```
## `classinfo` is a nested tuple of types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
if isinstance(x, (bool, (bytes, int))):
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, (bool, (bytes, int))):
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Literal["a"]
```
## Class types
@@ -89,9 +77,7 @@ class A: ...
class B: ...
class C: ...
def get_object() -> object: ...
x = get_object()
x = object()
if isinstance(x, A):
reveal_type(x) # revealed: A
@@ -112,50 +98,40 @@ else:
## No narrowing for instances of `builtins.type`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
t = type("t", (), {})
flag = bool_instance()
# This isn't testing what we want it to test if we infer anything more precise here:
reveal_type(t) # revealed: type
t = type("t", (), {})
x = 1 if flag else "foo"
# This isn't testing what we want it to test if we infer anything more precise here:
reveal_type(t) # revealed: type
x = 1 if flag else "foo"
if isinstance(x, t):
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
if isinstance(x, t):
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
```
## Do not use custom `isinstance` for narrowing
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
def isinstance(x, t):
return True
x = 1 if flag else "a"
flag = bool_instance()
def isinstance(x, t):
return True
x = 1 if flag else "a"
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## Do support narrowing if `isinstance` is aliased
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
isinstance_alias = isinstance
flag = bool_instance()
x = 1 if flag else "a"
isinstance_alias = isinstance
x = 1 if flag else "a"
if isinstance_alias(x, int):
reveal_type(x) # revealed: Literal[1]
if isinstance_alias(x, int):
reveal_type(x) # revealed: Literal[1]
```
## Do support narrowing if `isinstance` is imported
@@ -163,46 +139,38 @@ if isinstance_alias(x, int):
```py
from builtins import isinstance as imported_isinstance
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
if imported_isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
if imported_isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
```
## Do not narrow if second argument is not a type
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## Do not narrow if there are keyword arguments
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic
# (`isinstance` has no `foo` parameter)
if isinstance(x, int, foo="bar"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# TODO: this should cause us to emit a diagnostic
# (`isinstance` has no `foo` parameter)
if isinstance(x, int, foo="bar"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```

View File

@@ -7,45 +7,43 @@ Narrowing for `issubclass(class, classinfo)` expressions.
### Basic example
```py
def flag() -> bool: ...
def _(flag: bool):
t = int if flag else str
t = int if flag() else str
if issubclass(t, bytes):
reveal_type(t) # revealed: Never
if issubclass(t, object):
reveal_type(t) # revealed: Literal[int, str]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
else:
reveal_type(t) # revealed: Literal[str]
if issubclass(t, str):
reveal_type(t) # revealed: Literal[str]
if issubclass(t, int):
if issubclass(t, bytes):
reveal_type(t) # revealed: Never
if issubclass(t, object):
reveal_type(t) # revealed: Literal[int, str]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
else:
reveal_type(t) # revealed: Literal[str]
if issubclass(t, str):
reveal_type(t) # revealed: Literal[str]
if issubclass(t, int):
reveal_type(t) # revealed: Never
```
### Proper narrowing in `elif` and `else` branches
```py
def flag() -> bool: ...
def _(flag1: bool, flag2: bool):
t = int if flag1 else str if flag2 else bytes
t = int if flag() else str if flag() else bytes
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
else:
reveal_type(t) # revealed: Literal[str, bytes]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
else:
reveal_type(t) # revealed: Literal[str, bytes]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
elif issubclass(t, str):
reveal_type(t) # revealed: Literal[str]
else:
reveal_type(t) # revealed: Literal[bytes]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
elif issubclass(t, str):
reveal_type(t) # revealed: Literal[str]
else:
reveal_type(t) # revealed: Literal[bytes]
```
### Multiple derived classes
@@ -56,29 +54,28 @@ class Derived1(Base): ...
class Derived2(Base): ...
class Unrelated: ...
def flag() -> bool: ...
def _(flag1: bool, flag2: bool, flag3: bool):
t1 = Derived1 if flag1 else Derived2
t1 = Derived1 if flag() else Derived2
if issubclass(t1, Base):
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
if issubclass(t1, Base):
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
if issubclass(t1, Derived1):
reveal_type(t1) # revealed: Literal[Derived1]
else:
reveal_type(t1) # revealed: Literal[Derived2]
if issubclass(t1, Derived1):
reveal_type(t1) # revealed: Literal[Derived1]
else:
reveal_type(t1) # revealed: Literal[Derived2]
t2 = Derived1 if flag2 else Base
t2 = Derived1 if flag() else Base
if issubclass(t2, Base):
reveal_type(t2) # revealed: Literal[Derived1, Base]
if issubclass(t2, Base):
reveal_type(t2) # revealed: Literal[Derived1, Base]
t3 = Derived1 if flag3 else Unrelated
t3 = Derived1 if flag() else Unrelated
if issubclass(t3, Base):
reveal_type(t3) # revealed: Literal[Derived1]
else:
reveal_type(t3) # revealed: Literal[Unrelated]
if issubclass(t3, Base):
reveal_type(t3) # revealed: Literal[Derived1]
else:
reveal_type(t3) # revealed: Literal[Unrelated]
```
### Narrowing for non-literals
@@ -87,16 +84,13 @@ else:
class A: ...
class B: ...
def get_class() -> type[object]: ...
t = get_class()
if issubclass(t, A):
reveal_type(t) # revealed: type[A]
if issubclass(t, B):
reveal_type(t) # revealed: type[A] & type[B]
else:
reveal_type(t) # revealed: type[object] & ~type[A]
def _(t: type[object]):
if issubclass(t, A):
reveal_type(t) # revealed: type[A]
if issubclass(t, B):
reveal_type(t) # revealed: type[A] & type[B]
else:
reveal_type(t) # revealed: type[object] & ~type[A]
```
### Handling of `None`
@@ -107,16 +101,15 @@ else:
# error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound"
from types import NoneType
def flag() -> bool: ...
def _(flag: bool):
t = int if flag else NoneType
t = int if flag() else NoneType
if issubclass(t, NoneType):
reveal_type(t) # revealed: Literal[NoneType]
if issubclass(t, NoneType):
reveal_type(t) # revealed: Literal[NoneType]
if issubclass(t, type(None)):
# TODO: this should be just `Literal[NoneType]`
reveal_type(t) # revealed: Literal[int, NoneType]
if issubclass(t, type(None)):
# TODO: this should be just `Literal[NoneType]`
reveal_type(t) # revealed: Literal[int, NoneType]
```
## `classinfo` contains multiple types
@@ -126,14 +119,13 @@ if issubclass(t, type(None)):
```py
class Unrelated: ...
def flag() -> bool: ...
def _(flag1: bool, flag2: bool):
t = int if flag1 else str if flag2 else bytes
t = int if flag() else str if flag() else bytes
if issubclass(t, (int, (Unrelated, (bytes,)))):
reveal_type(t) # revealed: Literal[int, bytes]
else:
reveal_type(t) # revealed: Literal[str]
if issubclass(t, (int, (Unrelated, (bytes,)))):
reveal_type(t) # revealed: Literal[int, bytes]
else:
reveal_type(t) # revealed: Literal[str]
```
## Special cases
@@ -148,9 +140,7 @@ to `issubclass`:
```py
class A: ...
def get_object() -> object: ...
t = get_object()
t = object()
# TODO: we should emit a diagnostic here
if issubclass(t, A):

View File

@@ -3,19 +3,16 @@
## Single `match` pattern
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
flag = bool_instance()
reveal_type(x) # revealed: None | Literal[1]
x = None if flag else 1
reveal_type(x) # revealed: None | Literal[1]
y = 0
y = 0
match x:
case None:
y = x
match x:
case None:
y = x
reveal_type(y) # revealed: Literal[0] | None
reveal_type(y) # revealed: Literal[0] | None
```

View File

@@ -3,62 +3,49 @@
## After if-else statements, narrowing has no effect if the variable is not mutated in any branch
```py
def optional_int() -> int | None: ...
def _(x: int | None):
if x is None:
pass
else:
pass
x = optional_int()
if x is None:
pass
else:
pass
reveal_type(x) # revealed: int | None
reveal_type(x) # revealed: int | None
```
## Narrowing can have a persistent effect if the variable is mutated in one branch
```py
def optional_int() -> int | None: ...
def _(x: int | None):
if x is None:
x = 10
else:
pass
x = optional_int()
if x is None:
x = 10
else:
pass
reveal_type(x) # revealed: int
reveal_type(x) # revealed: int
```
## An if statement without an explicit `else` branch is equivalent to one with a no-op `else` branch
```py
def optional_int() -> int | None: ...
def _(x: int | None, y: int | None):
if x is None:
x = 0
x = optional_int()
y = optional_int()
if y is None:
pass
if x is None:
x = 0
if y is None:
pass
reveal_type(x) # revealed: int
reveal_type(y) # revealed: int | None
reveal_type(x) # revealed: int
reveal_type(y) # revealed: int | None
```
## An if-elif without an explicit else branch is equivalent to one with an empty else branch
```py
def optional_int() -> int | None: ...
def _(x: int | None):
if x is None:
x = 0
elif x > 50:
x = 50
x = optional_int()
if x is None:
x = 0
elif x > 50:
x = 50
reveal_type(x) # revealed: int
reveal_type(x) # revealed: int
```

View File

@@ -6,18 +6,14 @@
class A: ...
class B: ...
def get_a_or_b() -> A | B:
return A()
x = get_a_or_b()
if type(x) is A:
reveal_type(x) # revealed: A
else:
# It would be wrong to infer `B` here. The type
# of `x` could be a subclass of `A`, so we need
# to infer the full union type:
reveal_type(x) # revealed: A | B
def _(x: A | B):
if type(x) is A:
reveal_type(x) # revealed: A
else:
# It would be wrong to infer `B` here. The type
# of `x` could be a subclass of `A`, so we need
# to infer the full union type:
reveal_type(x) # revealed: A | B
```
## `type(x) is not C`
@@ -26,16 +22,12 @@ else:
class A: ...
class B: ...
def get_a_or_b() -> A | B:
return A()
x = get_a_or_b()
if type(x) is not A:
# Same reasoning as above: no narrowing should occur here.
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A
def _(x: A | B):
if type(x) is not A:
# Same reasoning as above: no narrowing should occur here.
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A
```
## `type(x) == C`, `type(x) != C`
@@ -54,16 +46,12 @@ class IsEqualToEverything(type):
class A(metaclass=IsEqualToEverything): ...
class B(metaclass=IsEqualToEverything): ...
def get_a_or_b() -> A | B:
return B()
def _(x: A | B):
if type(x) == A:
reveal_type(x) # revealed: A | B
x = get_a_or_b()
if type(x) == A:
reveal_type(x) # revealed: A | B
if type(x) != A:
reveal_type(x) # revealed: A | B
if type(x) != A:
reveal_type(x) # revealed: A | B
```
## No narrowing for custom `type` callable
@@ -75,15 +63,11 @@ class B: ...
def type(x):
return int
def get_a_or_b() -> A | B:
return A()
x = get_a_or_b()
if type(x) is A:
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A | B
def _(x: A | B):
if type(x) is A:
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A | B
```
## No narrowing for multiple arguments
@@ -91,15 +75,11 @@ else:
No narrowing should occur if `type` is used to dynamically create a class:
```py
def get_str_or_int() -> str | int:
return "test"
x = get_str_or_int()
if type(x, (), {}) is str:
reveal_type(x) # revealed: str | int
else:
reveal_type(x) # revealed: str | int
def _(x: str | int):
if type(x, (), {}) is str:
reveal_type(x) # revealed: str | int
else:
reveal_type(x) # revealed: str | int
```
## No narrowing for keyword arguments
@@ -107,14 +87,10 @@ else:
`type` can't be used with a keyword argument:
```py
def get_str_or_int() -> str | int:
return "test"
x = get_str_or_int()
# TODO: we could issue a diagnostic here
if type(object=x) is str:
reveal_type(x) # revealed: str | int
def _(x: str | int):
# TODO: we could issue a diagnostic here
if type(object=x) is str:
reveal_type(x) # revealed: str | int
```
## Narrowing if `type` is aliased
@@ -125,13 +101,9 @@ class B: ...
alias_for_type = type
def get_a_or_b() -> A | B:
return A()
x = get_a_or_b()
if alias_for_type(x) is A:
reveal_type(x) # revealed: A
def _(x: A | B):
if alias_for_type(x) is A:
reveal_type(x) # revealed: A
```
## Limitations
@@ -140,13 +112,9 @@ if alias_for_type(x) is A:
class Base: ...
class Derived(Base): ...
def get_base() -> Base:
return Base()
x = get_base()
if type(x) is Base:
# Ideally, this could be narrower, but there is now way to
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
reveal_type(x) # revealed: Base
def _(x: Base):
if type(x) is Base:
# Ideally, this could be narrower, but there is now way to
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
reveal_type(x) # revealed: Base
```

View File

@@ -59,7 +59,7 @@ reveal_type(typing.__init__) # revealed: Literal[__init__]
# These come from `builtins.object`, not `types.ModuleType`:
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
reveal_type(typing.__class__) # revealed: Literal[type]
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
# TODO: needs support for attribute access on instances, properties and generics;
# should be `dict[str, Any]`

View File

@@ -5,10 +5,10 @@
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
```py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
flag = coinflip()
x = 1
class C:
@@ -24,14 +24,14 @@ reveal_type(C.y) # revealed: Literal[1]
## Possibly unbound in class and global scope
```py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
if bool_instance():
if coinflip():
x = "abc"
class C:
if bool_instance():
if coinflip():
x = 1
# error: [possibly-unresolved-reference]

View File

@@ -3,14 +3,11 @@
## Shadow after incompatible declarations is OK
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
x: str
else:
x: int
flag = bool_instance()
if flag:
x: str
else:
x: int
x: bytes = b"foo"
x: bytes = b"foo"
```

View File

@@ -22,12 +22,10 @@ reveal_type(x) # revealed: Unknown
y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
reveal_type(y) # revealed: Unknown
def int_instance() -> int:
return 42
a = b"abcde"[int_instance()]
# TODO: Support overloads... Should be `bytes`
reveal_type(a) # revealed: @Todo(return type)
def _(n: int):
a = b"abcde"[n]
# TODO: Support overloads... Should be `bytes`
reveal_type(a) # revealed: @Todo(return type)
```
## Slices
@@ -43,15 +41,13 @@ b[:4:0] # error: [zero-stepsize-in-slice]
b[0::0] # error: [zero-stepsize-in-slice]
b[::0] # error: [zero-stepsize-in-slice]
def int_instance() -> int: ...
def _(m: int, n: int):
byte_slice1 = b[m:n]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice1) # revealed: @Todo(return type)
byte_slice1 = b[int_instance() : int_instance()]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice1) # revealed: @Todo(return type)
def bytes_instance() -> bytes: ...
byte_slice2 = bytes_instance()[0:5]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice2) # revealed: @Todo(return type)
def _(s: bytes) -> bytes:
byte_slice2 = s[0:5]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice2) # revealed: @Todo(return type)
```

View File

@@ -21,77 +21,66 @@ reveal_type(Identity[0]) # revealed: str
## Class getitem union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class UnionClassGetItem:
if flag:
def __class_getitem__(cls, item: int) -> str:
return item
else:
def __class_getitem__(cls, item: int) -> int:
return item
class UnionClassGetItem:
if bool_instance():
def __class_getitem__(cls, item: int) -> str:
return item
else:
def __class_getitem__(cls, item: int) -> int:
return item
reveal_type(UnionClassGetItem[0]) # revealed: str | int
reveal_type(UnionClassGetItem[0]) # revealed: str | int
```
## Class getitem with class union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A:
def __class_getitem__(cls, item: int) -> str:
return item
class A:
def __class_getitem__(cls, item: int) -> str:
return item
class B:
def __class_getitem__(cls, item: int) -> int:
return item
class B:
def __class_getitem__(cls, item: int) -> int:
return item
x = A if flag else B
x = A if bool_instance() else B
reveal_type(x) # revealed: Literal[A, B]
reveal_type(x[0]) # revealed: str | int
reveal_type(x) # revealed: Literal[A, B]
reveal_type(x[0]) # revealed: str | int
```
## Class getitem with unbound method union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
class Spam:
def __class_getitem__(self, x: int) -> str:
return "foo"
if bool_instance():
class Spam:
def __class_getitem__(self, x: int) -> str:
return "foo"
else:
class Spam: ...
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
# revealed: str
reveal_type(Spam[42])
else:
class Spam: ...
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
# revealed: str
reveal_type(Spam[42])
```
## TODO: Class getitem non-class union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
class Eggs:
def __class_getitem__(self, x: int) -> str:
return "foo"
if bool_instance():
class Eggs:
def __class_getitem__(self, x: int) -> str:
return "foo"
else:
Eggs = 1
else:
Eggs = 1
a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method"
a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method"
# TODO: should _probably_ emit `str | Unknown`
reveal_type(a) # revealed: Unknown
# TODO: should _probably_ emit `str | Unknown`
reveal_type(a) # revealed: Unknown
```

View File

@@ -30,18 +30,14 @@ reveal_type(Identity()[0]) # revealed: int
## Getitem union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Identity:
if flag:
def __getitem__(self, index: int) -> int:
return index
else:
def __getitem__(self, index: int) -> str:
return str(index)
class Identity:
if bool_instance():
def __getitem__(self, index: int) -> int:
return index
else:
def __getitem__(self, index: int) -> str:
return str(index)
reveal_type(Identity()[0]) # revealed: int | str
reveal_type(Identity()[0]) # revealed: int | str
```

View File

@@ -19,11 +19,10 @@ reveal_type(a) # revealed: Unknown
b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
reveal_type(b) # revealed: Unknown
def int_instance() -> int: ...
a = "abcde"[int_instance()]
# TODO: Support overloads... Should be `str`
reveal_type(a) # revealed: @Todo(return type)
def _(n: int):
a = "abcde"[n]
# TODO: Support overloads... Should be `str`
reveal_type(a) # revealed: @Todo(return type)
```
## Slices
@@ -74,17 +73,14 @@ s[:4:0] # error: [zero-stepsize-in-slice]
s[0::0] # error: [zero-stepsize-in-slice]
s[::0] # error: [zero-stepsize-in-slice]
def int_instance() -> int: ...
def _(m: int, n: int, s2: str):
substring1 = s[m:n]
# TODO: Support overloads... Should be `LiteralString`
reveal_type(substring1) # revealed: @Todo(return type)
substring1 = s[int_instance() : int_instance()]
# TODO: Support overloads... Should be `LiteralString`
reveal_type(substring1) # revealed: @Todo(return type)
def str_instance() -> str: ...
substring2 = str_instance()[0:5]
# TODO: Support overloads... Should be `str`
reveal_type(substring2) # revealed: @Todo(return type)
substring2 = s2[0:5]
# TODO: Support overloads... Should be `str`
reveal_type(substring2) # revealed: @Todo(return type)
```
## Unsupported slice types

View File

@@ -67,9 +67,60 @@ t[:4:0] # error: [zero-stepsize-in-slice]
t[0::0] # error: [zero-stepsize-in-slice]
t[::0] # error: [zero-stepsize-in-slice]
def int_instance() -> int: ...
tuple_slice = t[int_instance() : int_instance()]
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: @Todo(return type)
def _(m: int, n: int):
tuple_slice = t[m:n]
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: @Todo(return type)
```
## Inheritance
```toml
[environment]
target-version = "3.9"
```
```py
# TODO:
# * `tuple.__class_getitem__` is always bound on 3.9 (`sys.version_info`)
# * `tuple[int, str]` is a valid base (generics)
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[tuple]` is possibly unbound"
# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class A(tuple[int, str]): ...
# Runtime value: `(A, tuple, object)`
# TODO: Generics
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
```
## `typing.Tuple`
### Correspondence with `tuple`
`typing.Tuple` can be used interchangeably with `tuple`:
```py
from typing import Tuple
class A: ...
def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]):
reveal_type(c) # revealed: tuple
reveal_type(d) # revealed: tuple[int, A]
reveal_type(e) # revealed: @Todo(full tuple[...] support)
```
### Inheritance
Inheriting from `Tuple` results in a MRO with `builtins.tuple` and `typing.Generic`. `Tuple` itself
is not a class.
```py
from typing import Tuple
class C(Tuple): ...
# Runtime value: `(C, tuple, typing.Generic, object)`
# TODO: Add `Generic` to the MRO
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[tuple], Unknown, Literal[object]]
```

View File

@@ -0,0 +1,53 @@
# type[Any]
## Simple
```py
def f(x: type[Any]):
reveal_type(x) # revealed: type[Any]
# TODO: could be `<object.__repr__ type> & Any`
reveal_type(x.__repr__) # revealed: Any
class A: ...
x: type[Any] = object
x: type[Any] = type
x: type[Any] = A
x: type[Any] = A() # error: [invalid-assignment]
```
## Bare type
The interpretation of bare `type` is not clear: existing wording in the spec does not match the
behavior of mypy or pyright. For now we interpret it as simply "an instance of `builtins.type`",
which is equivalent to `type[object]`. This is similar to the current behavior of mypy, and pyright
in strict mode.
```py
def f(x: type):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
class A: ...
x: type = object
x: type = type
x: type = A
x: type = A() # error: [invalid-assignment]
```
## type[object] != type[Any]
```py
def f(x: type[object]):
reveal_type(x) # revealed: type[object]
# TODO: bound method types
reveal_type(x.__repr__) # revealed: Literal[__repr__]
class A: ...
x: type[object] = object
x: type[object] = type
x: type[object] = A
x: type[object] = A() # error: [invalid-assignment]
```

View File

@@ -5,10 +5,8 @@
```py
class A: ...
def f() -> type[A]:
return A
reveal_type(f()) # revealed: type[A]
def _(c: type[A]):
reveal_type(c) # revealed: type[A]
```
## Nested class literal
@@ -17,10 +15,8 @@ reveal_type(f()) # revealed: type[A]
class A:
class B: ...
def f() -> type[A.B]:
return A.B
reveal_type(f()) # revealed: type[B]
def f(c: type[A.B]):
reveal_type(c) # revealed: type[B]
```
## Deeply nested class literal
@@ -30,10 +26,8 @@ class A:
class B:
class C: ...
def f() -> type[A.B.C]:
return A.B.C
reveal_type(f()) # revealed: type[C]
def f(c: type[A.B.C]):
reveal_type(c) # revealed: type[C]
```
## Class literal from another module
@@ -41,10 +35,8 @@ reveal_type(f()) # revealed: type[C]
```py
from a import A
def f() -> type[A]:
return A
reveal_type(f()) # revealed: type[A]
def f(c: type[A]):
reveal_type(c) # revealed: type[A]
```
```py path=a.py
@@ -56,10 +48,8 @@ class A: ...
```py
import a
def f() -> type[a.B]:
return a.B
reveal_type(f()) # revealed: type[B]
def f(c: type[a.B]):
reveal_type(c) # revealed: type[B]
```
```py path=a.py
@@ -73,12 +63,8 @@ import a.b
# TODO: no diagnostic
# error: [unresolved-attribute]
def f() -> type[a.b.C]:
# TODO: no diagnostic
# error: [unresolved-attribute]
return a.b.C
reveal_type(f()) # revealed: @Todo(unsupported type[X] special form)
def f(c: type[a.b.C]):
reveal_type(c) # revealed: @Todo(unsupported type[X] special form)
```
```py path=a/__init__.py
@@ -88,7 +74,7 @@ reveal_type(f()) # revealed: @Todo(unsupported type[X] special form)
class C: ...
```
## Union of classes
## New-style union of classes
```py
class BasicUser: ...
@@ -98,11 +84,44 @@ class A:
class B:
class C: ...
def get_user() -> type[BasicUser | ProUser | A.B.C]:
return BasicUser
def _(u: type[BasicUser | ProUser | A.B.C]):
# revealed: type[BasicUser] | type[ProUser] | type[C]
reveal_type(u)
```
# revealed: type[BasicUser] | type[ProUser] | type[C]
reveal_type(get_user())
## Old-style union of classes
```py
from typing import Union
class BasicUser: ...
class ProUser: ...
class A:
class B:
class C: ...
def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Union[BasicUser, Union[ProUser, A.B.C]]]):
reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C]
reveal_type(b) # revealed: type[str]
reveal_type(c) # revealed: type[BasicUser] | type[ProUser] | type[C]
```
## New-style and old-style unions in combination
```py
from typing import Union
class BasicUser: ...
class ProUser: ...
class A:
class B:
class C: ...
def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Union[ProUser, A.B.C | str]]]):
reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C]
reveal_type(b) # revealed: type[BasicUser] | type[ProUser] | type[C] | type[str]
```
## Illegal parameters
@@ -112,6 +131,16 @@ class A: ...
class B: ...
# error: [invalid-type-form]
def get_user() -> type[A, B]:
return A
_: type[A, B]
```
## As a base class
```py
# TODO: this is a false positive
# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Foo(type[int]): ...
# TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```

View File

@@ -0,0 +1,32 @@
# `typing.Type`
## Annotation
`typing.Type` can be used interchangeably with `type`:
```py
from typing import Type
class A: ...
def _(c: Type, d: Type[A], e: Type[A]):
reveal_type(c) # revealed: type
reveal_type(d) # revealed: type[A]
c = d # fine
d = c # fine
```
## Inheritance
Inheriting from `Type` results in a MRO with `builtins.type` and `typing.Generic`. `Type` itself is
not a class.
```py
from typing import Type
class C(Type): ...
# Runtime value: `(C, type, typing.Generic, object)`
# TODO: Add `Generic` to the MRO
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[type], Literal[object]]
```

View File

@@ -35,29 +35,25 @@ y = 1
## Union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
p = 1
q = 3.3
r = "hello"
s = "world"
t = 0
else:
p = "hello"
q = 4
r = ""
s = 0
t = ""
flag = bool_instance()
if flag:
p = 1
q = 3.3
r = "hello"
s = "world"
t = 0
else:
p = "hello"
q = 4
r = ""
s = 0
t = ""
reveal_type(not p) # revealed: Literal[False]
reveal_type(not q) # revealed: bool
reveal_type(not r) # revealed: bool
reveal_type(not s) # revealed: bool
reveal_type(not t) # revealed: Literal[True]
reveal_type(not p) # revealed: Literal[False]
reveal_type(not q) # revealed: bool
reveal_type(not r) # revealed: bool
reveal_type(not s) # revealed: bool
reveal_type(not t) # revealed: Literal[True]
```
## Integer literal

View File

@@ -21,25 +21,23 @@ with Manager() as f:
## Union context manager
```py
def coinflip() -> bool:
return True
def _(flag: bool):
class Manager1:
def __enter__(self) -> str:
return "foo"
class Manager1:
def __enter__(self) -> str:
return "foo"
def __exit__(self, exc_type, exc_value, traceback): ...
def __exit__(self, exc_type, exc_value, traceback): ...
class Manager2:
def __enter__(self) -> int:
return 42
class Manager2:
def __enter__(self) -> int:
return 42
def __exit__(self, exc_type, exc_value, traceback): ...
def __exit__(self, exc_type, exc_value, traceback): ...
context_expr = Manager1() if flag else Manager2()
context_expr = Manager1() if coinflip() else Manager2()
with context_expr as f:
reveal_type(f) # revealed: str | int
with context_expr as f:
reveal_type(f) # revealed: str | int
```
## Context manager without an `__enter__` or `__exit__` method
@@ -103,39 +101,34 @@ with Manager():
## Context expression with possibly-unbound union variants
```py
def coinflip() -> bool:
return True
def _(flag: bool):
class Manager1:
def __enter__(self) -> str:
return "foo"
class Manager1:
def __enter__(self) -> str:
return "foo"
def __exit__(self, exc_type, exc_value, traceback): ...
def __exit__(self, exc_type, exc_value, traceback): ...
class NotAContextManager: ...
context_expr = Manager1() if flag else NotAContextManager()
class NotAContextManager: ...
context_expr = Manager1() if coinflip() else NotAContextManager()
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound"
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound"
with context_expr as f:
reveal_type(f) # revealed: str
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound"
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound"
with context_expr as f:
reveal_type(f) # revealed: str
```
## Context expression with "sometimes" callable `__enter__` method
```py
def coinflip() -> bool:
return True
def _(flag: bool):
class Manager:
if flag:
def __enter__(self) -> str:
return "abcd"
class Manager:
if coinflip():
def __enter__(self) -> str:
return "abcd"
def __exit__(self, *args): ...
def __exit__(self, *args): ...
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound"
with Manager() as f:
reveal_type(f) # revealed: str
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound"
with Manager() as f:
reveal_type(f) # revealed: str
```

View File

@@ -29,12 +29,14 @@ mod util;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
/// Creates a new registry with all known semantic lints.
pub fn default_lint_registry() -> LintRegistry {
let mut registry = LintRegistryBuilder::default();
register_semantic_lints(&mut registry);
register_lints(&mut registry);
registry.build()
}
pub fn register_semantic_lints(registry: &mut LintRegistryBuilder) {
types::register_type_lints(registry);
/// Register all known semantic lints.
pub fn register_lints(registry: &mut LintRegistryBuilder) {
types::register_lints(registry);
}

View File

@@ -23,8 +23,11 @@ pub struct LintMetadata {
pub status: LintStatus,
/// Location where this lint is declared: `file_name:line`
pub source: &'static str,
/// The source file in which the lint is declared.
pub file: &'static str,
/// The 1-based line number in the source `file` where the lint is declared.
pub line: u32,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@@ -74,7 +77,7 @@ impl LintMetadata {
self.summary
}
/// Returns the documentation line by line with leading and trailing whitespace removed.
/// Returns the documentation line by line with one leading space and all trailing whitespace removed.
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
self.raw_documentation
.lines()
@@ -94,8 +97,12 @@ impl LintMetadata {
&self.status
}
pub fn source(&self) -> &str {
self.source
pub fn file(&self) -> &str {
self.file
}
pub fn line(&self) -> u32 {
self.line
}
}
@@ -107,30 +114,43 @@ pub const fn lint_metadata_defaults() -> LintMetadata {
raw_documentation: "",
default_level: Level::Error,
status: LintStatus::preview("0.0.0"),
source: "",
file: "",
line: 1,
}
}
#[derive(Copy, Clone, Debug)]
pub enum LintStatus {
/// The rule has been added to the linter, but is not yet stable.
/// The lint has been added to the linter, but is not yet stable.
Preview {
/// When the rule was added to preview
/// The version in which the lint was added.
since: &'static str,
},
/// Stable rule that was added in the version defined by `since`.
Stable { since: &'static str },
/// The rule has been deprecated [`since`] (version) and will be removed in the future.
Deprecated {
/// The lint is stable.
Stable {
/// The version in which the lint was stabilized.
since: &'static str,
},
/// The lint is deprecated and no longer recommended for use.
Deprecated {
/// The version in which the lint was deprecated.
since: &'static str,
/// The reason why the lint has been deprecated.
///
/// This should explain why the lint has been deprecated and if there's a replacement lint that users
/// can use instead.
reason: &'static str,
},
/// The rule has been removed [`since`] (version) and using it will result in an error.
/// The lint has been removed and can no longer be used.
Removed {
/// The version in which the lint was removed.
since: &'static str,
/// The reason why the lint has been removed.
reason: &'static str,
},
}
@@ -157,6 +177,31 @@ impl LintStatus {
}
}
/// Declares a lint rule with the given metadata.
///
/// ```rust
/// use red_knot_python_semantic::declare_lint;
/// use red_knot_python_semantic::lint::{LintStatus, Level};
///
/// declare_lint! {
/// /// ## What it does
/// /// Checks for references to names that are not defined.
/// ///
/// /// ## Why is this bad?
/// /// Using an undefined variable will raise a `NameError` at runtime.
/// ///
/// /// ## Example
/// ///
/// /// ```python
/// /// print(x) # NameError: name 'x' is not defined
/// /// ```
/// pub(crate) static UNRESOLVED_REFERENCE = {
/// summary: "detects references to names that are not defined",
/// status: LintStatus::preview("1.0.0"),
/// default_level: Level::Warn,
/// }
/// }
/// ```
#[macro_export]
macro_rules! declare_lint {
(
@@ -175,7 +220,8 @@ macro_rules! declare_lint {
summary: $summary,
raw_documentation: concat!($($doc,)+ "\n"),
status: $status,
source: concat!(file!(), ":", line!()),
file: file!(),
line: line!(),
$( $key: $value, )*
..$crate::lint::lint_metadata_defaults()
};
@@ -306,7 +352,7 @@ impl LintRegistry {
/// Returns an iterator over all known aliases and to their target lints.
///
/// This iterator includes aliases that point to removed lints.
pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + use<'_> {
pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + '_ {
self.by_name.iter().filter_map(|(key, value)| {
if let LintEntry::Alias(alias) = value {
Some((LintName::of(key), *alias))
@@ -317,7 +363,7 @@ impl LintRegistry {
}
/// Iterates over all removed lints.
pub fn removed(&self) -> impl Iterator<Item = LintId> + use<'_> {
pub fn removed(&self) -> impl Iterator<Item = LintId> + '_ {
self.by_name.iter().filter_map(|(_, value)| {
if let LintEntry::Removed(metadata) = value {
Some(*metadata)
@@ -384,12 +430,12 @@ impl RuleSelection {
}
/// Returns an iterator over all enabled lints.
pub fn enabled(&self) -> impl Iterator<Item = LintId> + use<'_> {
pub fn enabled(&self) -> impl Iterator<Item = LintId> + '_ {
self.lints.keys().copied()
}
/// Returns an iterator over all enabled lints and their severity.
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + use<'_> {
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + '_ {
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
}

View File

@@ -47,7 +47,7 @@
//! The **declared type** represents the code author's declaration (usually through a type
//! annotation) that a given variable should not be assigned any type outside the declared type. In
//! our model, declared types are also control-flow-sensitive; we allow the code author to
//! explicitly re-declare the same variable with a different type. So for a given binding of a
//! explicitly redeclare the same variable with a different type. So for a given binding of a
//! variable, we will want to ask which declarations of that variable can reach that binding, in
//! order to determine whether the binding is permitted, or should be a type error. For example:
//!
@@ -62,7 +62,7 @@
//! assignable to `str`. This is the purpose of declared types: they prevent accidental assignment
//! of the wrong type to a variable.
//!
//! But in some cases it is useful to "shadow" or "re-declare" a variable with a new type, and we
//! But in some cases it is useful to "shadow" or "redeclare" a variable with a new type, and we
//! permit this, as long as it is done with an explicit re-annotation. So `path: Path =
//! Path(path)`, with the explicit `: Path` annotation, is permitted.
//!

View File

@@ -2,14 +2,13 @@ use std::hash::Hash;
use indexmap::IndexSet;
use itertools::Itertools;
use ruff_db::diagnostic::{DiagnosticId, Severity};
use ruff_db::diagnostic::Severity;
use ruff_db::files::File;
use ruff_python_ast as ast;
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub(crate) use self::diagnostic::register_type_lints;
pub(crate) use self::diagnostic::register_lints;
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
};
@@ -26,12 +25,14 @@ use crate::stdlib::{
builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule,
};
use crate::symbol::{Boundness, Symbol};
use crate::types::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
use crate::types::call::{CallDunderResult, CallOutcome};
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder;
use crate::types::mro::{ClassBase, Mro, MroError, MroIterator};
use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
mod builder;
mod call;
mod diagnostic;
mod display;
mod infer;
@@ -226,18 +227,6 @@ fn definition_expression_ty<'db>(
}
}
/// Get the type of an expression from an arbitrary scope.
///
/// Can cause query cycles if used carelessly; caller must be sure that type inference isn't
/// currently in progress for the expression's scope.
fn expression_ty<'db>(db: &'db dyn Db, file: File, expression: &ast::Expr) -> Type<'db> {
let index = semantic_index(db, file);
let file_scope = index.expression_scope_id(expression);
let scope = file_scope.to_scope_id(db, file);
let expr_id = expression.scoped_expression_id(db, scope);
infer_scope_types(db, scope).expression_ty(expr_id)
}
/// Infer the combined type of an iterator of bindings.
///
/// Will return a union if there is more than one binding.
@@ -573,7 +562,11 @@ impl<'db> Type<'db> {
}
pub const fn subclass_of(class: Class<'db>) -> Self {
Self::SubclassOf(SubclassOfType { class })
Self::subclass_of_base(ClassBase::Class(class))
}
pub const fn subclass_of_base(base: ClassBase<'db>) -> Self {
Self::SubclassOf(SubclassOfType { base })
}
pub fn string_literal(db: &'db dyn Db, string: &str) -> Self {
@@ -618,8 +611,6 @@ impl<'db> Type<'db> {
return false;
}
match (self, target) {
(Type::Unknown | Type::Any | Type::Todo(_), _) => false,
(_, Type::Unknown | Type::Any | Type::Todo(_)) => false,
(Type::Never, _) => true,
(_, Type::Never) => false,
(_, Type::Instance(InstanceType { class }))
@@ -662,19 +653,31 @@ impl<'db> Type<'db> {
},
)
}
(Type::ClassLiteral(..), Type::Instance(InstanceType { class }))
if class.is_known(db, KnownClass::Type) =>
{
true
}
(Type::ClassLiteral(self_class), Type::SubclassOf(target_class)) => {
self_class.class.is_subclass_of(db, target_class.class)
}
(Type::SubclassOf(self_class), Type::SubclassOf(target_class)) => {
self_class.class.is_subclass_of(db, target_class.class)
self_class.class.is_subclass_of_base(db, target_class.base)
}
(
Type::SubclassOf(SubclassOfType { class: self_class }),
Type::Instance(InstanceType { class: self_class }),
Type::SubclassOf(target_class),
) if self_class.is_known(db, KnownClass::Type) => {
self_class.is_subclass_of_base(db, target_class.base)
}
(
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(self_class),
}),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(target_class),
}),
) => self_class.is_subclass_of(db, target_class),
// C ⊆ type
// type[C] ⊆ type
// Though note that this works regardless of which metaclass C has, not just for type.
(
Type::ClassLiteral(ClassLiteralType { class: self_class })
| Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(self_class),
}),
Type::Instance(InstanceType {
class: target_class,
}),
@@ -771,6 +774,30 @@ impl<'db> Type<'db> {
},
)
}
(
Type::SubclassOf(SubclassOfType {
base: ClassBase::Any,
}),
Type::SubclassOf(_),
) => true,
(
Type::SubclassOf(SubclassOfType {
base: ClassBase::Any,
}),
Type::Instance(target),
) if target.class.is_known(db, KnownClass::Type) => true,
(
Type::Instance(class),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Any,
}),
) if class.class.is_known(db, KnownClass::Type) => true,
(
Type::ClassLiteral(_) | Type::SubclassOf(_),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Any,
}),
) => true,
// TODO other types containing gradual forms (e.g. generics containing Any/Unknown)
_ => self.is_subtype_of(db, target),
}
@@ -786,27 +813,56 @@ impl<'db> Type<'db> {
return false;
}
// TODO: The following is a workaround that is required to unify the two different versions
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
// we understand `sys.version_info` branches.
if let (
Type::Instance(InstanceType { class: self_class }),
Type::Instance(InstanceType {
class: target_class,
}),
) = (self, other)
{
let self_known = self_class.known(db);
if matches!(
self_known,
Some(KnownClass::NoneType | KnownClass::NoDefaultType)
) && self_known == target_class.known(db)
{
return true;
}
}
// type[object] ≡ type
if let (
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(object_class),
}),
Type::Instance(InstanceType { class: type_class }),
)
| (
Type::Instance(InstanceType { class: type_class }),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(object_class),
}),
) = (self, other)
{
// This is the only case where "instance of a class" is equivalent to "subclass of a
// class", so we don't need to fall through if we're not looking at instance[type] and
// type[object] specifically.
return object_class.is_known(db, KnownClass::Object)
&& type_class.is_known(db, KnownClass::Type);
}
// TODO equivalent but not identical structural types, differently-ordered unions and
// intersections, other cases?
// TODO: Once we have support for final classes, we can establish that
// `Type::SubclassOf('FinalClass')` is equivalent to `Type::ClassLiteral('FinalClass')`.
// TODO: The following is a workaround that is required to unify the two different versions
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
// we understand `sys.version_info` branches.
// For all other cases, types are equivalent iff they have the same internal
// representation.
self == other
|| matches!((self, other),
(
Type::Instance(InstanceType { class: self_class }),
Type::Instance(InstanceType { class: target_class })
)
if {
let self_known = self_class.known(db);
matches!(self_known, Some(KnownClass::NoneType | KnownClass::NoDefaultType))
&& self_known == target_class.known(db)
}
)
}
/// Returns true if both `self` and `other` are the same gradual form
@@ -873,7 +929,7 @@ impl<'db> Type<'db> {
(Type::SubclassOf(type_class), Type::ClassLiteral(class_literal))
| (Type::ClassLiteral(class_literal), Type::SubclassOf(type_class)) => {
!class_literal.class.is_subclass_of(db, type_class.class)
!class_literal.class.is_subclass_of_base(db, type_class.base)
}
(Type::SubclassOf(_), Type::SubclassOf(_)) => false,
(Type::SubclassOf(_), Type::Instance(_)) | (Type::Instance(_), Type::SubclassOf(_)) => {
@@ -1040,7 +1096,8 @@ impl<'db> Type<'db> {
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::KnownInstance(_) => true,
Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) => {
Type::SubclassOf(SubclassOfType { base }) => matches!(base, ClassBase::Class(_)),
Type::ClassLiteral(_) | Type::Instance(_) => {
// TODO: Ideally, we would iterate over the MRO of the class, check if all
// bases are fully static, and only return `true` if that is the case.
//
@@ -1186,6 +1243,8 @@ impl<'db> Type<'db> {
| KnownClass::Set
| KnownClass::Dict
| KnownClass::Slice
| KnownClass::BaseException
| KnownClass::BaseExceptionGroup
| KnownClass::GenericAlias
| KnownClass::ModuleType
| KnownClass::FunctionType
@@ -1212,6 +1271,10 @@ impl<'db> Type<'db> {
/// as accessed from instances of the `Bar` class.
#[must_use]
pub(crate) fn member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
if name == "__class__" {
return self.to_meta_type(db).into();
}
match self {
Type::Any => Type::Any.into(),
Type::Never => {
@@ -1600,12 +1663,6 @@ impl<'db> Type<'db> {
};
}
if matches!(self, Type::Unknown | Type::Any | Type::Todo(_)) {
// Explicit handling of `Unknown` and `Any` necessary until `type[Unknown]` and
// `type[Any]` are not defined as `Todo` anymore.
return IterationOutcome::Iterable { element_ty: self };
}
let dunder_iter_result = self.call_dunder(db, "__iter__", &[self]);
match dunder_iter_result {
CallDunderResult::CallOutcome(ref call_outcome)
@@ -1663,7 +1720,10 @@ impl<'db> Type<'db> {
Type::Unknown => Type::Unknown,
Type::Never => Type::Never,
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class),
Type::SubclassOf(SubclassOfType { class }) => Type::instance(*class),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(class),
}) => Type::instance(*class),
Type::SubclassOf(_) => Type::Any,
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
// TODO: we can probably do better here: --Alex
Type::Intersection(_) => todo_type!(),
@@ -1692,7 +1752,12 @@ impl<'db> Type<'db> {
#[must_use]
pub fn in_type_expression(&self, db: &'db dyn Db) -> Type<'db> {
match self {
// In a type expression, a bare `type` is interpreted as "instance of `type`", which is
// equivalent to `type[object]`.
Type::ClassLiteral(_) | Type::SubclassOf(_) => self.to_instance(db),
// We treat `typing.Type` exactly the same as `builtins.type`:
Type::KnownInstance(KnownInstanceType::Type) => KnownClass::Type.to_instance(db),
Type::KnownInstance(KnownInstanceType::Tuple) => KnownClass::Tuple.to_instance(db),
Type::Union(union) => union.map(db, |element| element.in_type_expression(db)),
Type::Unknown => Type::Unknown,
// TODO map this to a new `Type::TypeVar` variant
@@ -1751,9 +1816,7 @@ impl<'db> Type<'db> {
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::Never => Type::Never,
Type::Instance(InstanceType { class }) => {
Type::SubclassOf(SubclassOfType { class: *class })
}
Type::Instance(InstanceType { class }) => Type::subclass_of(*class),
Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db),
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db),
@@ -1764,7 +1827,9 @@ impl<'db> Type<'db> {
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db),
Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db),
Type::SubclassOf(SubclassOfType { class }) => Type::subclass_of(
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(class),
}) => Type::subclass_of(
class
.try_metaclass(db)
.ok()
@@ -1772,10 +1837,9 @@ impl<'db> Type<'db> {
.unwrap_or_else(|| KnownClass::Type.to_class_literal(db).expect_class_literal())
.class,
),
Type::SubclassOf(_) => Type::Any,
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db),
// TODO: `type[Any]`?
Type::Any => Type::Any,
// TODO: `type[Unknown]`?
Type::Unknown => Type::Unknown,
// TODO intersections
Type::Intersection(_) => todo_type!(),
@@ -1858,6 +1922,8 @@ pub enum KnownClass {
Set,
Dict,
Slice,
BaseException,
BaseExceptionGroup,
// Types
GenericAlias,
ModuleType,
@@ -1888,6 +1954,8 @@ impl<'db> KnownClass {
Self::List => "list",
Self::Type => "type",
Self::Slice => "slice",
Self::BaseException => "BaseException",
Self::BaseExceptionGroup => "BaseExceptionGroup",
Self::GenericAlias => "GenericAlias",
Self::ModuleType => "ModuleType",
Self::FunctionType => "FunctionType",
@@ -1915,6 +1983,12 @@ impl<'db> KnownClass {
.unwrap_or(Type::Unknown)
}
pub fn to_subclass_of(self, db: &'db dyn Db) -> Option<Type<'db>> {
self.to_class_literal(db)
.into_class_literal()
.map(|ClassLiteralType { class }| Type::subclass_of(class))
}
/// Return the module in which we should look up the definition for this class
pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule {
match self {
@@ -1929,6 +2003,8 @@ impl<'db> KnownClass {
| Self::Tuple
| Self::Set
| Self::Dict
| Self::BaseException
| Self::BaseExceptionGroup
| Self::Slice => CoreStdlibModule::Builtins,
Self::VersionInfo => CoreStdlibModule::Sys,
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
@@ -1972,6 +2048,8 @@ impl<'db> KnownClass {
| Self::ModuleType
| Self::FunctionType
| Self::SpecialForm
| Self::BaseException
| Self::BaseExceptionGroup
| Self::TypeVar => false,
}
}
@@ -1993,6 +2071,8 @@ impl<'db> KnownClass {
"dict" => Self::Dict,
"list" => Self::List,
"slice" => Self::Slice,
"BaseException" => Self::BaseException,
"BaseExceptionGroup" => Self::BaseExceptionGroup,
"GenericAlias" => Self::GenericAlias,
"NoneType" => Self::NoneType,
"ModuleType" => Self::ModuleType,
@@ -2029,6 +2109,8 @@ impl<'db> KnownClass {
| Self::GenericAlias
| Self::ModuleType
| Self::VersionInfo
| Self::BaseException
| Self::BaseExceptionGroup
| Self::FunctionType => module.name() == self.canonical_module(db).as_str(),
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
@@ -2055,6 +2137,10 @@ pub enum KnownInstanceType<'db> {
Never,
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
Any,
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
Tuple,
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
Type,
/// A single instance of `typing.TypeVar`
TypeVar(TypeVarInstance<'db>),
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
@@ -2073,6 +2159,8 @@ impl<'db> KnownInstanceType<'db> {
Self::NoReturn => "NoReturn",
Self::Never => "Never",
Self::Any => "Any",
Self::Tuple => "Tuple",
Self::Type => "Type",
Self::TypeAliasType(_) => "TypeAliasType",
}
}
@@ -2088,6 +2176,8 @@ impl<'db> KnownInstanceType<'db> {
| Self::NoReturn
| Self::Never
| Self::Any
| Self::Tuple
| Self::Type
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
}
}
@@ -2102,6 +2192,8 @@ impl<'db> KnownInstanceType<'db> {
Self::NoReturn => "typing.NoReturn",
Self::Never => "typing.Never",
Self::Any => "typing.Any",
Self::Tuple => "typing.Tuple",
Self::Type => "typing.Type",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
}
@@ -2117,6 +2209,8 @@ impl<'db> KnownInstanceType<'db> {
Self::NoReturn => KnownClass::SpecialForm,
Self::Never => KnownClass::SpecialForm,
Self::Any => KnownClass::Object,
Self::Tuple => KnownClass::SpecialForm,
Self::Type => KnownClass::SpecialForm,
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
}
@@ -2143,6 +2237,8 @@ impl<'db> KnownInstanceType<'db> {
("typing" | "typing_extensions", "Union") => Some(Self::Union),
("typing" | "typing_extensions", "NoReturn") => Some(Self::NoReturn),
("typing" | "typing_extensions", "Never") => Some(Self::Never),
("typing" | "typing_extensions", "Tuple") => Some(Self::Tuple),
("typing" | "typing_extensions", "Type") => Some(Self::Type),
_ => None,
}
}
@@ -2207,313 +2303,6 @@ pub enum TypeVarBoundOrConstraints<'db> {
Constraints(TupleType<'db>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CallOutcome<'db> {
Callable {
return_ty: Type<'db>,
},
RevealType {
return_ty: Type<'db>,
revealed_ty: Type<'db>,
},
NotCallable {
not_callable_ty: Type<'db>,
},
Union {
called_ty: Type<'db>,
outcomes: Box<[CallOutcome<'db>]>,
},
PossiblyUnboundDunderCall {
called_ty: Type<'db>,
call_outcome: Box<CallOutcome<'db>>,
},
}
impl<'db> CallOutcome<'db> {
/// Create a new `CallOutcome::Callable` with given return type.
fn callable(return_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::Callable { return_ty }
}
/// Create a new `CallOutcome::NotCallable` with given not-callable type.
fn not_callable(not_callable_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::NotCallable { not_callable_ty }
}
/// Create a new `CallOutcome::RevealType` with given revealed and return types.
fn revealed(return_ty: Type<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::RevealType {
return_ty,
revealed_ty,
}
}
/// Create a new `CallOutcome::Union` with given wrapped outcomes.
fn union(
called_ty: Type<'db>,
outcomes: impl IntoIterator<Item = CallOutcome<'db>>,
) -> CallOutcome<'db> {
CallOutcome::Union {
called_ty,
outcomes: outcomes.into_iter().collect(),
}
}
/// Get the return type of the call, or `None` if not callable.
fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::Callable { return_ty } => Some(*return_ty),
Self::RevealType {
return_ty,
revealed_ty: _,
} => Some(*return_ty),
Self::NotCallable { not_callable_ty: _ } => None,
Self::Union {
outcomes,
called_ty: _,
} => outcomes
.iter()
// If all outcomes are NotCallable, we return None; if some outcomes are callable
// and some are not, we return a union including Unknown.
.fold(None, |acc, outcome| {
let ty = outcome.return_ty(db);
match (acc, ty) {
(None, None) => None,
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))),
}
})
.map(UnionBuilder::build),
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
}
}
/// Get the return type of the call, emitting default diagnostics if needed.
fn unwrap_with_diagnostic<'a>(
&self,
db: &'db dyn Db,
node: ast::AnyNodeRef,
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
) -> Type<'db> {
match self.return_ty_result(db, node, diagnostics) {
Ok(return_ty) => return_ty,
Err(NotCallableError::Type {
not_callable_ty,
return_ty,
}) => {
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable",
not_callable_ty.display(db)
),
);
return_ty
}
Err(NotCallableError::UnionElement {
not_callable_ty,
called_ty,
return_ty,
}) => {
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (due to union element `{}`)",
called_ty.display(db),
not_callable_ty.display(db),
),
);
return_ty
}
Err(NotCallableError::UnionElements {
not_callable_tys,
called_ty,
return_ty,
}) => {
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (due to union elements {})",
called_ty.display(db),
not_callable_tys.display(db),
),
);
return_ty
}
Err(NotCallableError::PossiblyUnboundDunderCall {
callable_ty: called_ty,
return_ty,
}) => {
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
called_ty.display(db)
),
);
return_ty
}
}
}
/// Get the return type of the call as a result.
fn return_ty_result<'a>(
&self,
db: &'db dyn Db,
node: ast::AnyNodeRef,
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
) -> Result<Type<'db>, NotCallableError<'db>> {
match self {
Self::Callable { return_ty } => Ok(*return_ty),
Self::RevealType {
return_ty,
revealed_ty,
} => {
diagnostics.add(
node,
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
);
Ok(*return_ty)
}
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
not_callable_ty: *not_callable_ty,
return_ty: Type::Unknown,
}),
Self::PossiblyUnboundDunderCall {
called_ty,
call_outcome,
} => Err(NotCallableError::PossiblyUnboundDunderCall {
callable_ty: *called_ty,
return_ty: call_outcome.return_ty(db).unwrap_or(Type::Unknown),
}),
Self::Union {
outcomes,
called_ty,
} => {
let mut not_callable = vec![];
let mut union_builder = UnionBuilder::new(db);
let mut revealed = false;
for outcome in outcomes {
let return_ty = match outcome {
Self::NotCallable { not_callable_ty } => {
not_callable.push(*not_callable_ty);
Type::Unknown
}
Self::RevealType {
return_ty,
revealed_ty: _,
} => {
if revealed {
*return_ty
} else {
revealed = true;
outcome.unwrap_with_diagnostic(db, node, diagnostics)
}
}
_ => outcome.unwrap_with_diagnostic(db, node, diagnostics),
};
union_builder = union_builder.add(return_ty);
}
let return_ty = union_builder.build();
match not_callable[..] {
[] => Ok(return_ty),
[elem] => Err(NotCallableError::UnionElement {
not_callable_ty: elem,
called_ty: *called_ty,
return_ty,
}),
_ if not_callable.len() == outcomes.len() => Err(NotCallableError::Type {
not_callable_ty: *called_ty,
return_ty,
}),
_ => Err(NotCallableError::UnionElements {
not_callable_tys: not_callable.into_boxed_slice(),
called_ty: *called_ty,
return_ty,
}),
}
}
}
}
}
enum CallDunderResult<'db> {
CallOutcome(CallOutcome<'db>),
PossiblyUnbound(CallOutcome<'db>),
MethodNotAvailable,
}
impl<'db> CallDunderResult<'db> {
fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::CallOutcome(outcome) => outcome.return_ty(db),
Self::PossiblyUnbound { .. } => None,
Self::MethodNotAvailable => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum NotCallableError<'db> {
/// The type is not callable.
Type {
not_callable_ty: Type<'db>,
return_ty: Type<'db>,
},
/// A single union element is not callable.
UnionElement {
not_callable_ty: Type<'db>,
called_ty: Type<'db>,
return_ty: Type<'db>,
},
/// Multiple (but not all) union elements are not callable.
UnionElements {
not_callable_tys: Box<[Type<'db>]>,
called_ty: Type<'db>,
return_ty: Type<'db>,
},
PossiblyUnboundDunderCall {
callable_ty: Type<'db>,
return_ty: Type<'db>,
},
}
impl<'db> NotCallableError<'db> {
/// The return type that should be used when a call is not callable.
fn return_ty(&self) -> Type<'db> {
match self {
Self::Type { return_ty, .. } => *return_ty,
Self::UnionElement { return_ty, .. } => *return_ty,
Self::UnionElements { return_ty, .. } => *return_ty,
Self::PossiblyUnboundDunderCall { return_ty, .. } => *return_ty,
}
}
/// The resolved type that was not callable.
///
/// For unions, returns the union type itself, which may contain a mix of callable and
/// non-callable types.
fn called_ty(&self) -> Type<'db> {
match self {
Self::Type {
not_callable_ty, ..
} => *not_callable_ty,
Self::UnionElement { called_ty, .. } => *called_ty,
Self::UnionElements { called_ty, .. } => *called_ty,
Self::PossiblyUnboundDunderCall {
callable_ty: called_ty,
..
} => *called_ty,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IterationOutcome<'db> {
Iterable {
@@ -2919,15 +2708,11 @@ impl<'db> Class<'db> {
return Type::tuple(db, &tuple_elements).into();
}
if name == "__class__" {
return self.metaclass(db).into();
}
for superclass in self.iter_mro(db) {
match superclass {
// TODO we may instead want to record the fact that we encountered dynamic, and intersect it with
// the type found on the next "real" class.
ClassBase::Any | ClassBase::Unknown | ClassBase::Todo => {
ClassBase::Any | ClassBase::Unknown | ClassBase::Todo(_) => {
return Type::from(superclass).member(db, name)
}
ClassBase::Class(class) => {
@@ -3035,12 +2820,12 @@ impl<'db> From<ClassLiteralType<'db>> for Type<'db> {
/// A type that represents `type[C]`, i.e. the class literal `C` and class literals that are subclasses of `C`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub struct SubclassOfType<'db> {
class: Class<'db>,
base: ClassBase<'db>,
}
impl<'db> SubclassOfType<'db> {
fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
self.class.class_member(db, name)
Type::from(self.base).member(db, name)
}
}
@@ -3259,6 +3044,8 @@ pub(crate) mod tests {
Union(Vec<Ty>),
Intersection { pos: Vec<Ty>, neg: Vec<Ty> },
Tuple(Vec<Ty>),
SubclassOfAny,
SubclassOfBuiltinClass(&'static str),
}
impl Ty {
@@ -3296,6 +3083,13 @@ pub(crate) mod tests {
let elements = tys.into_iter().map(|ty| ty.into_type(db));
Type::tuple(db, elements)
}
Ty::SubclassOfAny => Type::subclass_of_base(ClassBase::Any),
Ty::SubclassOfBuiltinClass(s) => Type::subclass_of(
builtins_symbol(db, s)
.expect_type()
.expect_class_literal()
.class,
),
}
}
}
@@ -3333,6 +3127,26 @@ pub(crate) mod tests {
)]
#[test_case(Ty::Tuple(vec![Ty::Todo]), Ty::Tuple(vec![Ty::IntLiteral(2)]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::Todo]))]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfAny)]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfBuiltinClass("object"))]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfBuiltinClass("str"))]
#[test_case(Ty::SubclassOfAny, Ty::BuiltinInstance("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::SubclassOfAny)]
#[test_case(
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("object")
)]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::SubclassOfAny)]
#[test_case(
Ty::SubclassOfBuiltinClass("str"),
Ty::SubclassOfBuiltinClass("object")
)]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::SubclassOfBuiltinClass("str"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfAny)]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfBuiltinClass("object"))]
#[test_case(Ty::BuiltinInstance("type"), Ty::BuiltinInstance("type"))]
fn is_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
@@ -3350,6 +3164,11 @@ pub(crate) mod tests {
Ty::Union(vec![Ty::IntLiteral(1), Ty::None]),
Ty::Union(vec![Ty::BuiltinInstance("str"), Ty::None])
)]
#[test_case(
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("str")
)]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfBuiltinClass("str"))]
fn is_not_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
@@ -3504,10 +3323,13 @@ pub(crate) mod tests {
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]),
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)])
)]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::BuiltinInstance("type"))]
fn is_equivalent_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
let from = from.into_type(&db);
let to = to.into_type(&db);
assert!(from.is_equivalent_to(&db, to));
assert!(to.is_equivalent_to(&db, from));
}
#[test_case(Ty::Any, Ty::Any)]
@@ -3518,8 +3340,10 @@ pub(crate) mod tests {
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
fn is_not_equivalent_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
let from = from.into_type(&db);
let to = to.into_type(&db);
assert!(!from.is_equivalent_to(&db, to));
assert!(!to.is_equivalent_to(&db, from));
}
#[test_case(Ty::Never, Ty::Never)]
@@ -3781,6 +3605,9 @@ pub(crate) mod tests {
#[test_case(Ty::Intersection{pos: vec![Ty::KnownClassInstance(KnownClass::Str)], neg: vec![Ty::LiteralString]})]
#[test_case(Ty::Tuple(vec![]))]
#[test_case(Ty::Tuple(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::KnownClassInstance(KnownClass::Object)]))]
#[test_case(Ty::BuiltinInstance("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("object"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"))]
fn is_fully_static(from: Ty) {
let db = setup_db();
@@ -3794,6 +3621,7 @@ pub(crate) mod tests {
#[test_case(Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Str), Ty::Unknown]))]
#[test_case(Ty::Intersection{pos: vec![Ty::Any], neg: vec![Ty::LiteralString]})]
#[test_case(Ty::Tuple(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::Any]))]
#[test_case(Ty::SubclassOfAny)]
fn is_not_fully_static(from: Ty) {
let db = setup_db();

View File

@@ -0,0 +1,312 @@
use super::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
use super::{Severity, Type, UnionBuilder};
use crate::Db;
use ruff_db::diagnostic::DiagnosticId;
use ruff_python_ast as ast;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallOutcome<'db> {
Callable {
return_ty: Type<'db>,
},
RevealType {
return_ty: Type<'db>,
revealed_ty: Type<'db>,
},
NotCallable {
not_callable_ty: Type<'db>,
},
Union {
called_ty: Type<'db>,
outcomes: Box<[CallOutcome<'db>]>,
},
PossiblyUnboundDunderCall {
called_ty: Type<'db>,
call_outcome: Box<CallOutcome<'db>>,
},
}
impl<'db> CallOutcome<'db> {
/// Create a new `CallOutcome::Callable` with given return type.
pub(super) fn callable(return_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::Callable { return_ty }
}
/// Create a new `CallOutcome::NotCallable` with given not-callable type.
pub(super) fn not_callable(not_callable_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::NotCallable { not_callable_ty }
}
/// Create a new `CallOutcome::RevealType` with given revealed and return types.
pub(super) fn revealed(return_ty: Type<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::RevealType {
return_ty,
revealed_ty,
}
}
/// Create a new `CallOutcome::Union` with given wrapped outcomes.
pub(super) fn union(
called_ty: Type<'db>,
outcomes: impl IntoIterator<Item = CallOutcome<'db>>,
) -> CallOutcome<'db> {
CallOutcome::Union {
called_ty,
outcomes: outcomes.into_iter().collect(),
}
}
/// Get the return type of the call, or `None` if not callable.
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::Callable { return_ty } => Some(*return_ty),
Self::RevealType {
return_ty,
revealed_ty: _,
} => Some(*return_ty),
Self::NotCallable { not_callable_ty: _ } => None,
Self::Union {
outcomes,
called_ty: _,
} => outcomes
.iter()
// If all outcomes are NotCallable, we return None; if some outcomes are callable
// and some are not, we return a union including Unknown.
.fold(None, |acc, outcome| {
let ty = outcome.return_ty(db);
match (acc, ty) {
(None, None) => None,
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))),
}
})
.map(UnionBuilder::build),
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
}
}
/// Get the return type of the call, emitting default diagnostics if needed.
pub(super) fn unwrap_with_diagnostic<'a>(
&self,
db: &'db dyn Db,
node: ast::AnyNodeRef,
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
) -> Type<'db> {
match self.return_ty_result(db, node, diagnostics) {
Ok(return_ty) => return_ty,
Err(NotCallableError::Type {
not_callable_ty,
return_ty,
}) => {
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable",
not_callable_ty.display(db)
),
);
return_ty
}
Err(NotCallableError::UnionElement {
not_callable_ty,
called_ty,
return_ty,
}) => {
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (due to union element `{}`)",
called_ty.display(db),
not_callable_ty.display(db),
),
);
return_ty
}
Err(NotCallableError::UnionElements {
not_callable_tys,
called_ty,
return_ty,
}) => {
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (due to union elements {})",
called_ty.display(db),
Type::display_slice(db, &not_callable_tys),
),
);
return_ty
}
Err(NotCallableError::PossiblyUnboundDunderCall {
callable_ty: called_ty,
return_ty,
}) => {
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
called_ty.display(db)
),
);
return_ty
}
}
}
/// Get the return type of the call as a result.
pub(super) fn return_ty_result<'a>(
&self,
db: &'db dyn Db,
node: ast::AnyNodeRef,
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
) -> Result<Type<'db>, NotCallableError<'db>> {
match self {
Self::Callable { return_ty } => Ok(*return_ty),
Self::RevealType {
return_ty,
revealed_ty,
} => {
diagnostics.add(
node,
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
);
Ok(*return_ty)
}
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
not_callable_ty: *not_callable_ty,
return_ty: Type::Unknown,
}),
Self::PossiblyUnboundDunderCall {
called_ty,
call_outcome,
} => Err(NotCallableError::PossiblyUnboundDunderCall {
callable_ty: *called_ty,
return_ty: call_outcome.return_ty(db).unwrap_or(Type::Unknown),
}),
Self::Union {
outcomes,
called_ty,
} => {
let mut not_callable = vec![];
let mut union_builder = UnionBuilder::new(db);
let mut revealed = false;
for outcome in outcomes {
let return_ty = match outcome {
Self::NotCallable { not_callable_ty } => {
not_callable.push(*not_callable_ty);
Type::Unknown
}
Self::RevealType {
return_ty,
revealed_ty: _,
} => {
if revealed {
*return_ty
} else {
revealed = true;
outcome.unwrap_with_diagnostic(db, node, diagnostics)
}
}
_ => outcome.unwrap_with_diagnostic(db, node, diagnostics),
};
union_builder = union_builder.add(return_ty);
}
let return_ty = union_builder.build();
match not_callable[..] {
[] => Ok(return_ty),
[elem] => Err(NotCallableError::UnionElement {
not_callable_ty: elem,
called_ty: *called_ty,
return_ty,
}),
_ if not_callable.len() == outcomes.len() => Err(NotCallableError::Type {
not_callable_ty: *called_ty,
return_ty,
}),
_ => Err(NotCallableError::UnionElements {
not_callable_tys: not_callable.into_boxed_slice(),
called_ty: *called_ty,
return_ty,
}),
}
}
}
}
}
pub(super) enum CallDunderResult<'db> {
CallOutcome(CallOutcome<'db>),
PossiblyUnbound(CallOutcome<'db>),
MethodNotAvailable,
}
impl<'db> CallDunderResult<'db> {
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::CallOutcome(outcome) => outcome.return_ty(db),
Self::PossiblyUnbound { .. } => None,
Self::MethodNotAvailable => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum NotCallableError<'db> {
/// The type is not callable.
Type {
not_callable_ty: Type<'db>,
return_ty: Type<'db>,
},
/// A single union element is not callable.
UnionElement {
not_callable_ty: Type<'db>,
called_ty: Type<'db>,
return_ty: Type<'db>,
},
/// Multiple (but not all) union elements are not callable.
UnionElements {
not_callable_tys: Box<[Type<'db>]>,
called_ty: Type<'db>,
return_ty: Type<'db>,
},
PossiblyUnboundDunderCall {
callable_ty: Type<'db>,
return_ty: Type<'db>,
},
}
impl<'db> NotCallableError<'db> {
/// The return type that should be used when a call is not callable.
pub(super) fn return_ty(&self) -> Type<'db> {
match self {
Self::Type { return_ty, .. } => *return_ty,
Self::UnionElement { return_ty, .. } => *return_ty,
Self::UnionElements { return_ty, .. } => *return_ty,
Self::PossiblyUnboundDunderCall { return_ty, .. } => *return_ty,
}
}
/// The resolved type that was not callable.
///
/// For unions, returns the union type itself, which may contain a mix of callable and
/// non-callable types.
pub(super) fn called_ty(&self) -> Type<'db> {
match self {
Self::Type {
not_callable_ty, ..
} => *not_callable_ty,
Self::UnionElement { called_ty, .. } => *called_ty,
Self::UnionElements { called_ty, .. } => *called_ty,
Self::PossiblyUnboundDunderCall {
callable_ty: called_ty,
..
} => *called_ty,
}
}
}

View File

@@ -16,7 +16,7 @@ use std::ops::Deref;
use std::sync::Arc;
/// Registers all known type check lints.
pub(crate) fn register_type_lints(registry: &mut LintRegistryBuilder) {
pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&UNRESOLVED_REFERENCE);
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
registry.register_lint(&NOT_ITERABLE);
@@ -46,6 +46,7 @@ pub(crate) fn register_type_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&UNDEFINED_REVEAL);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_TYPE_FORM);
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
// String annotations
registry.register_lint(&FSTRING_TYPE_ANNOTATION);
@@ -77,7 +78,7 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for references to names that are possibly not defined..
/// Checks for references to names that are possibly not defined.
///
/// ## Why is this bad?
/// Using an undefined variable will raise a `NameError` at runtime.
@@ -91,7 +92,7 @@ declare_lint! {
/// print(x) # NameError: name 'x' is not defined
/// ```
pub(crate) static POSSIBLY_UNRESOLVED_REFERENCE = {
summary: "detects references to possibly unresolved references",
summary: "detects references to possibly undefined names",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
@@ -111,7 +112,7 @@ declare_lint! {
/// pass
/// ```
pub(crate) static NOT_ITERABLE = {
summary: "detects objects that are not iterable",
summary: "detects iteration over an object that is not iterable",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
@@ -119,7 +120,7 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// TODO
/// TODO #14889
pub(crate) static INDEX_OUT_OF_BOUNDS = {
summary: "detects index out of bounds errors",
status: LintStatus::preview("1.0.0"),
@@ -129,7 +130,7 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for objects that do not support subscripting but are used in a context that requires them to be.
/// Checks for subscripting objects that do not support subscripting.
///
/// ## Why is this bad?
/// Subscripting an object that does not support it will raise a `TypeError` at runtime.
@@ -139,7 +140,7 @@ declare_lint! {
/// 4[1] # TypeError: 'int' object is not subscriptable
/// ```
pub(crate) static NON_SUBSCRIPTABLE = {
summary: "detects objects that do not support subscripting",
summary: "detects subscripting objects that do not support subscripting",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
@@ -159,7 +160,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static POSSIBLY_UNBOUND_IMPORT = {
summary: "detects possibly unbound imports",
status: LintStatus::preview("1.0.0"),
@@ -186,7 +187,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static INVALID_ASSIGNMENT = {
summary: "detects invalid assignments",
status: LintStatus::preview("1.0.0"),
@@ -195,7 +196,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static INVALID_DECLARATION = {
summary: "detects invalid declarations",
status: LintStatus::preview("1.0.0"),
@@ -204,7 +205,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static CONFLICTING_DECLARATIONS = {
summary: "detects conflicting declarations",
status: LintStatus::preview("1.0.0"),
@@ -250,7 +251,7 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// TODO
/// TODO #14889
pub(crate) static INVALID_TYPE_PARAMETER = {
summary: "detects invalid type parameters",
status: LintStatus::preview("1.0.0"),
@@ -259,7 +260,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static INVALID_TYPE_VARIABLE_CONSTRAINTS = {
summary: "detects invalid type variable constraints",
status: LintStatus::preview("1.0.0"),
@@ -272,7 +273,7 @@ declare_lint! {
/// Checks for class definitions with a cyclic inheritance chain.
///
/// ## Why is it bad?
/// TODO
/// TODO #14889
pub(crate) static CYCLIC_CLASS_DEFINITION = {
summary: "detects cyclic class definitions",
status: LintStatus::preview("1.0.0"),
@@ -281,7 +282,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static DUPLICATE_BASE = {
summary: "detects class definitions with duplicate bases",
status: LintStatus::preview("1.0.0"),
@@ -290,7 +291,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static INVALID_BASE = {
summary: "detects class definitions with an invalid base",
status: LintStatus::preview("1.0.0"),
@@ -299,7 +300,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static INCONSISTENT_MRO = {
summary: "detects class definitions with an inconsistent MRO",
status: LintStatus::preview("1.0.0"),
@@ -310,6 +311,8 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for invalid parameters to `typing.Literal`.
///
/// TODO #14889
pub(crate) static INVALID_LITERAL_PARAMETER = {
summary: "detects invalid literal parameters",
status: LintStatus::preview("1.0.0"),
@@ -320,6 +323,8 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for calls to possibly unbound methods.
///
/// TODO #14889
pub(crate) static CALL_POSSIBLY_UNBOUND_METHOD = {
summary: "detects calls to possibly unbound methods",
status: LintStatus::preview("1.0.0"),
@@ -330,6 +335,8 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for possibly unbound attributes.
///
/// TODO #14889
pub(crate) static POSSIBLY_UNBOUND_ATTRIBUTE = {
summary: "detects references to possibly unbound attributes",
status: LintStatus::preview("1.0.0"),
@@ -340,6 +347,8 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for unresolved attributes.
///
/// TODO #14889
pub(crate) static UNRESOLVED_ATTRIBUTE = {
summary: "detects references to unresolved attributes",
status: LintStatus::preview("1.0.0"),
@@ -348,7 +357,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static CONFLICTING_METACLASS = {
summary: "detects conflicting metaclasses",
status: LintStatus::preview("1.0.0"),
@@ -359,15 +368,17 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for binary expressions, comparisons, and unary expressions where the operands don't support the operator.
///
/// TODO #14889
pub(crate) static UNSUPPORTED_OPERATOR = {
summary: "detects binary expressions where the operands don't support the operator",
summary: "detects binary, unary, or comparison expressions where the operands don't support the operator",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static INVALID_CONTEXT_MANAGER = {
summary: "detects expressions used in with statements that don't implement the context manager protocol",
status: LintStatus::preview("1.0.0"),
@@ -381,6 +392,9 @@ declare_lint! {
///
/// ## Why is this bad?
/// Using `reveal_type` without importing it will raise a `NameError` at runtime.
///
/// ## Examples
/// TODO #14889
pub(crate) static UNDEFINED_REVEAL = {
summary: "detects usages of `reveal_type` without importing it",
status: LintStatus::preview("1.0.0"),
@@ -391,6 +405,9 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for default values that can't be assigned to the parameter's annotated type.
///
/// ## Why is this bad?
/// TODO #14889
pub(crate) static INVALID_PARAMETER_DEFAULT = {
summary: "detects default values that can't be assigned to the parameter's annotated type",
status: LintStatus::preview("1.0.0"),
@@ -400,7 +417,10 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for `type[]` usages that have too many or too few type arguments.
/// Checks for invalid type expressions.
///
/// ## Why is this bad?
/// TODO #14889
pub(crate) static INVALID_TYPE_FORM = {
summary: "detects invalid type forms",
status: LintStatus::preview("1.0.0"),
@@ -408,6 +428,41 @@ declare_lint! {
}
}
declare_lint! {
/// Checks for exception handlers that catch non-exception classes.
///
/// ## Why is this bad?
/// Catching classes that do not inherit from `BaseException` will raise a TypeError at runtime.
///
/// ## Example
/// ```python
/// try:
/// 1 / 0
/// except 1:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// try:
/// 1 / 0
/// except ZeroDivisionError:
/// ...
/// ```
///
/// ## References
/// - [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
/// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)
///
/// ## Ruff rule
/// This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes)
pub(crate) static INVALID_EXCEPTION_CAUGHT = {
summary: "detects exception handlers that catch classes that do not inherit from `BaseException`",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic {
pub(super) id: DiagnosticId,
@@ -688,6 +743,18 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
);
}
pub(super) fn add_invalid_exception_caught(&mut self, db: &dyn Db, node: &ast::Expr, ty: Type) {
self.add_lint(
&INVALID_EXCEPTION_CAUGHT,
node.into(),
format_args!(
"Cannot catch object of type `{}` in an exception handler \
(must be a `BaseException` subclass or a tuple of `BaseException` subclasses)",
ty.display(db)
),
);
}
pub(super) fn add_lint(
&mut self,
lint: &'static LintMetadata,

View File

@@ -1,11 +1,10 @@
//! Display implementations for types.
use std::fmt::{self, Display, Formatter, Write};
use ruff_db::display::FormatterJoinExtension;
use ruff_python_ast::str::Quote;
use ruff_python_literal::escape::AsciiEscape;
use std::fmt::{self, Arguments, Formatter, Write};
use crate::types::mro::ClassBase;
use crate::types::{
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
SubclassOfType, Type, UnionType,
@@ -14,25 +13,33 @@ use crate::Db;
use rustc_hash::FxHashMap;
impl<'db> Type<'db> {
pub fn display(&self, db: &'db dyn Db) -> DisplayType {
DisplayType { ty: self, db }
fn representation(self) -> Representation<'db> {
Representation { ty: self }
}
fn representation(self, db: &'db dyn Db) -> DisplayRepresentation<'db> {
DisplayRepresentation { db, ty: self }
pub fn display(self, db: &'db dyn Db) -> DisplayWrapper<'db, Type<'db>> {
DisplayWrapper::new(db, self)
}
pub fn display_slice<'types>(
db: &'db dyn Db,
types: &'types [Type<'db>],
) -> DisplayWrapper<'db, &'types [Type<'db>]> {
DisplayWrapper::new(db, types)
}
}
#[derive(Copy, Clone)]
pub struct DisplayType<'db> {
ty: &'db Type<'db>,
db: &'db dyn Db,
}
impl<'db> DisplayType<'db> for Type<'db> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
if f.visited.contains(self) {
return f.write_str("<recursion>");
}
f.visited.push(*self);
let representation = self.representation();
impl Display for DisplayType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let representation = self.ty.representation(self.db);
if matches!(
self.ty,
self,
Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
@@ -40,38 +47,38 @@ impl Display for DisplayType<'_> {
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_)
) {
write!(f, "Literal[{representation}]")
f.write_str("Literal[")?;
representation.fmt(f)?;
f.write_str("]")?;
} else {
representation.fmt(f)
representation.fmt(f)?;
}
}
}
impl fmt::Debug for DisplayType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
let removed = f.visited.pop();
debug_assert_eq!(removed, Some(*self));
Ok(())
}
}
/// Writes the string representation of a type, which is the value displayed either as
/// `Literal[<repr>]` or `Literal[<repr1>, <repr2>]` for literal types or as `<repr>` for
/// non literals
struct DisplayRepresentation<'db> {
struct Representation<'db> {
ty: Type<'db>,
db: &'db dyn Db,
}
impl Display for DisplayRepresentation<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
impl<'db> DisplayType<'db> for Representation<'db> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
match self.ty {
Type::Any => f.write_str("Any"),
Type::Never => f.write_str("Never"),
Type::Unknown => f.write_str("Unknown"),
Type::Instance(InstanceType { class }) => {
let representation = match class.known(self.db) {
let representation = match class.known(f.db()) {
Some(KnownClass::NoneType) => "None",
Some(KnownClass::NoDefaultType) => "NoDefault",
_ => class.name(self.db),
_ => class.name(f.db()),
};
f.write_str(representation)
}
@@ -79,30 +86,37 @@ impl Display for DisplayRepresentation<'_> {
// any other type
Type::Todo(todo) => write!(f, "@Todo{todo}"),
Type::ModuleLiteral(file) => {
write!(f, "<module '{:?}'>", file.path(self.db))
write!(f, "<module '{:?}'>", file.path(f.db()))
}
// TODO functions and classes should display using a fully qualified name
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
Type::SubclassOf(SubclassOfType { class }) => {
write!(f, "type[{}]", class.name(self.db))
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(f.db())),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(class),
}) => {
// Only show the bare class name here; ClassBase::display would render this as
// type[<class 'Foo'>] instead of type[Foo].
write!(f, "type[{}]", class.name(f.db()))
}
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
Type::IntLiteral(n) => n.fmt(f),
Type::SubclassOf(SubclassOfType { base }) => {
write!(f, "type[{}]", base.display(f.db()))
}
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(f.db())),
Type::FunctionLiteral(function) => f.write_str(function.name(f.db())),
Type::Union(union) => union.fmt(f),
Type::Intersection(intersection) => intersection.fmt(f),
Type::IntLiteral(n) => write!(f, "{n}"),
Type::BooleanLiteral(boolean) => f.write_str(if boolean { "True" } else { "False" }),
Type::StringLiteral(string) => string.display(self.db).fmt(f),
Type::StringLiteral(string) => string.fmt(f),
Type::LiteralString => f.write_str("LiteralString"),
Type::BytesLiteral(bytes) => {
let escape =
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
AsciiEscape::with_preferred_quote(bytes.value(f.db()).as_ref(), Quote::Double);
escape.bytes_repr().write(f)
}
Type::SliceLiteral(slice) => {
f.write_str("slice[")?;
if let Some(start) = slice.start(self.db) {
if let Some(start) = slice.start(f.db()) {
write!(f, "Literal[{start}]")?;
} else {
f.write_str("None")?;
@@ -110,13 +124,13 @@ impl Display for DisplayRepresentation<'_> {
f.write_str(", ")?;
if let Some(stop) = slice.stop(self.db) {
if let Some(stop) = slice.stop(f.db()) {
write!(f, "Literal[{stop}]")?;
} else {
f.write_str("None")?;
}
if let Some(step) = slice.step(self.db) {
if let Some(step) = slice.step(f.db()) {
write!(f, ", Literal[{step}]")?;
}
@@ -124,11 +138,11 @@ impl Display for DisplayRepresentation<'_> {
}
Type::Tuple(tuple) => {
f.write_str("tuple[")?;
let elements = tuple.elements(self.db);
let elements = tuple.elements(f.db());
if elements.is_empty() {
f.write_str("()")?;
} else {
elements.display(self.db).fmt(f)?;
elements.fmt(f)?;
}
f.write_str("]")
}
@@ -136,20 +150,9 @@ impl Display for DisplayRepresentation<'_> {
}
}
impl<'db> UnionType<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> {
DisplayUnionType { db, ty: self }
}
}
struct DisplayUnionType<'db> {
ty: &'db UnionType<'db>,
db: &'db dyn Db,
}
impl Display for DisplayUnionType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let elements = self.ty.elements(self.db);
impl<'db> DisplayType<'db> for UnionType<'db> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
let elements = self.elements(f.db());
// Group condensed-display types by kind.
let mut grouped_condensed_kinds = FxHashMap::default();
@@ -173,12 +176,11 @@ impl Display for DisplayUnionType<'_> {
if kind == CondensedDisplayTypeKind::Int {
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
}
join.entry(&DisplayLiteralGroup {
join.entry(&LiteralGroup {
literals: condensed_kind,
db: self.db,
});
} else {
join.entry(&element.display(self.db));
join.entry(element);
}
}
@@ -190,22 +192,15 @@ impl Display for DisplayUnionType<'_> {
}
}
impl fmt::Debug for DisplayUnionType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
struct DisplayLiteralGroup<'db> {
struct LiteralGroup<'db> {
literals: Vec<Type<'db>>,
db: &'db dyn Db,
}
impl Display for DisplayLiteralGroup<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
impl<'db> DisplayType<'db> for LiteralGroup<'db> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
f.write_str("Literal[")?;
f.join(", ")
.entries(self.literals.iter().map(|ty| ty.representation(self.db)))
.entries(self.literals.iter().map(|ty| ty.representation()))
.finish()?;
f.write_str("]")
}
@@ -241,106 +236,63 @@ impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
}
}
impl<'db> IntersectionType<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayIntersectionType<'db> {
DisplayIntersectionType { db, ty: self }
}
}
struct DisplayIntersectionType<'db> {
ty: &'db IntersectionType<'db>,
db: &'db dyn Db,
}
impl Display for DisplayIntersectionType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
impl<'db> DisplayType<'db> for IntersectionType<'db> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
let tys = self
.ty
.positive(self.db)
.positive(f.db())
.iter()
.map(|&ty| DisplayMaybeNegatedType {
ty,
db: self.db,
negated: false,
})
.map(|&ty| MaybeNegatedType { ty, negated: false })
.chain(
self.ty
.negative(self.db)
self.negative(f.db())
.iter()
.map(|&ty| DisplayMaybeNegatedType {
ty,
db: self.db,
negated: true,
}),
.map(|&ty| MaybeNegatedType { ty, negated: true }),
);
f.join(" & ").entries(tys).finish()
}
}
impl fmt::Debug for DisplayIntersectionType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
struct DisplayMaybeNegatedType<'db> {
struct MaybeNegatedType<'db> {
ty: Type<'db>,
db: &'db dyn Db,
negated: bool,
}
impl Display for DisplayMaybeNegatedType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
impl<'db> DisplayType<'db> for MaybeNegatedType<'db> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
if self.negated {
f.write_str("~")?;
}
self.ty.display(self.db).fmt(f)
self.ty.fmt(f)
}
}
pub(crate) trait TypeArrayDisplay<'db> {
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray;
}
impl<'db> TypeArrayDisplay<'db> for Box<[Type<'db>]> {
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
DisplayTypeArray { types: self, db }
impl<'db> DisplayType<'db> for [Type<'db>] {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
f.join(", ").entries(self.iter().copied()).finish()
}
}
impl<'db> TypeArrayDisplay<'db> for Vec<Type<'db>> {
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
DisplayTypeArray { types: self, db }
impl<'db> DisplayType<'db> for &[Type<'db>] {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
(**self).fmt(f)
}
}
pub(crate) struct DisplayTypeArray<'b, 'db> {
types: &'b [Type<'db>],
db: &'db dyn Db,
}
impl Display for DisplayTypeArray<'_, '_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.join(", ")
.entries(self.types.iter().map(|ty| ty.display(self.db)))
.finish()
impl<'db> DisplayType<'db> for Box<[Type<'db>]> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
(**self).fmt(f)
}
}
impl<'db> StringLiteralType<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayStringLiteralType<'db> {
DisplayStringLiteralType { db, ty: self }
impl<'db> DisplayType<'db> for Vec<Type<'db>> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
(**self).fmt(f)
}
}
struct DisplayStringLiteralType<'db> {
ty: &'db StringLiteralType<'db>,
db: &'db dyn Db,
}
impl Display for DisplayStringLiteralType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let value = self.ty.value(self.db);
impl<'db> DisplayType<'db> for StringLiteralType<'db> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
let value = self.value(f.db());
f.write_char('"')?;
for ch in value.chars() {
match ch {
@@ -354,6 +306,119 @@ impl Display for DisplayStringLiteralType<'_> {
}
}
struct TypeFormatter<'db, 'write> {
db: &'db dyn Db,
write: &'write mut dyn Write,
visited: Vec<Type<'db>>,
}
impl<'db, 'write> TypeFormatter<'db, 'write> {
pub(crate) fn new(db: &'db dyn Db, write: &'write mut dyn Write) -> Self {
Self {
db,
write,
visited: Vec::default(),
}
}
pub(crate) fn join<'f>(&'f mut self, separator: &'static str) -> Join<'db, 'f, 'write> {
Join {
fmt: self,
separator,
result: Ok(()),
seen_first: false,
}
}
pub(crate) fn db(&self) -> &'db dyn Db {
self.db
}
}
impl Write for TypeFormatter<'_, '_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write.write_str(s)
}
fn write_char(&mut self, c: char) -> fmt::Result {
self.write.write_char(c)
}
fn write_fmt(&mut self, args: Arguments<'_>) -> fmt::Result {
self.write.write_fmt(args)
}
}
trait DisplayType<'db> {
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result;
}
pub struct DisplayWrapper<'db, T> {
db: &'db dyn Db,
inner: T,
}
impl<'db, T> DisplayWrapper<'db, T> {
fn new(db: &'db dyn Db, inner: T) -> Self {
Self { db, inner }
}
}
impl<'db, T> DisplayType<'db> for DisplayWrapper<'db, T>
where
T: DisplayType<'db>,
{
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl<'db, T> fmt::Display for DisplayWrapper<'db, T>
where
T: DisplayType<'db>,
{
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let mut f = TypeFormatter::new(self.db, f);
DisplayType::fmt(self, &mut f)
}
}
struct Join<'db, 'f, 'write> {
fmt: &'f mut TypeFormatter<'db, 'write>,
separator: &'static str,
result: fmt::Result,
seen_first: bool,
}
impl<'db> Join<'db, '_, '_> {
fn entry(&mut self, item: &dyn DisplayType<'db>) -> &mut Self {
if self.seen_first {
self.result = self
.result
.and_then(|()| self.fmt.write_str(self.separator));
} else {
self.seen_first = true;
}
self.result = self.result.and_then(|()| item.fmt(self.fmt));
self
}
fn entries<I, F>(&mut self, items: I) -> &mut Self
where
I: IntoIterator<Item = F>,
F: DisplayType<'db>,
{
for item in items {
self.entry(&item);
}
self
}
fn finish(&mut self) -> fmt::Result {
self.result
}
}
#[cfg(test)]
mod tests {
use ruff_db::files::system_path_to_file;
@@ -398,7 +463,7 @@ mod tests {
Type::none(&db),
];
let union = UnionType::from_elements(&db, union_elements).expect_union();
let display = format!("{}", union.display(&db));
let display = format!("{}", Type::Union(union).display(&db));
assert_eq!(
display,
concat!(

View File

@@ -57,21 +57,20 @@ use crate::types::diagnostic::{
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
};
use crate::types::mro::MroErrorKind;
use crate::types::mro::{ClassBase, MroErrorKind};
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, todo_type,
typing_extensions_symbol, Boundness, Class, ClassLiteralType, FunctionType, InstanceType,
IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction,
KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol,
Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, TypeVarBoundOrConstraints,
TypeVarInstance, UnionBuilder, UnionType,
Truthiness, TupleType, Type, TypeAliasType, TypeVarBoundOrConstraints, TypeVarInstance,
UnionBuilder, UnionType,
};
use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
use super::expression_ty;
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
@@ -417,13 +416,37 @@ impl<'db> TypeInferenceBuilder<'db> {
/// Get the already-inferred type of an expression node.
///
/// PANIC if no type has been inferred for this node.
/// ## Panics
/// If the expression is not within this region, or if no type has yet been inferred for
/// this node.
#[track_caller]
fn expression_ty(&self, expr: &ast::Expr) -> Type<'db> {
self.types
.expression_ty(expr.scoped_expression_id(self.db, self.scope()))
}
/// Get the type of an expression from any scope in the same file.
///
/// If the expression is in the current scope, and we are inferring the entire scope, just look
/// up the expression in our own results, otherwise call [`infer_scope_types()`] for the scope
/// of the expression.
///
/// ## Panics
///
/// If the expression is in the current scope but we haven't yet inferred a type for it.
///
/// Can cause query cycles if the expression is from a different scope and type inference is
/// already in progress for that scope (further up the stack).
fn file_expression_ty(&self, expression: &ast::Expr) -> Type<'db> {
let file_scope = self.index.expression_scope_id(expression);
let expr_scope = file_scope.to_scope_id(self.db, self.file);
let expr_id = expression.scoped_expression_id(self.db, expr_scope);
match self.region {
InferenceRegion::Scope(scope) if scope == expr_scope => self.expression_ty(expression),
_ => infer_scope_types(self.db, expr_scope).expression_ty(expr_id),
}
}
/// Infers types in the given [`InferenceRegion`].
fn infer_region(&mut self) {
match self.region {
@@ -776,7 +799,7 @@ impl<'db> TypeInferenceBuilder<'db> {
node,
format_args!(
"Conflicting declared types for `{symbol_name}`: {}",
conflicting.display(self.db)
Type::display_slice(self.db, &conflicting)
),
);
ty
@@ -1091,9 +1114,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} = parameter_with_default;
let default_ty = default
.as_ref()
.map(|default| expression_ty(self.db, self.file, default));
.map(|default| self.file_expression_ty(default));
if let Some(annotation) = parameter.annotation.as_ref() {
let declared_ty = expression_ty(self.db, self.file, annotation);
let declared_ty = self.file_expression_ty(annotation);
let inferred_ty = if let Some(default_ty) = default_ty {
if default_ty.is_assignable_to(self.db, declared_ty) {
UnionType::from_elements(self.db, [declared_ty, default_ty])
@@ -1137,7 +1160,7 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>,
) {
if let Some(annotation) = parameter.annotation.as_ref() {
let _annotated_ty = expression_ty(self.db, self.file, annotation);
let _annotated_ty = self.file_expression_ty(annotation);
// TODO `tuple[annotated_ty, ...]`
let ty = KnownClass::Tuple.to_instance(self.db);
self.add_declaration_with_binding(parameter.into(), definition, ty, ty);
@@ -1162,7 +1185,7 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>,
) {
if let Some(annotation) = parameter.annotation.as_ref() {
let _annotated_ty = expression_ty(self.db, self.file, annotation);
let _annotated_ty = self.file_expression_ty(annotation);
// TODO `dict[str, annotated_ty]`
let ty = KnownClass::Dict.to_instance(self.db);
self.add_declaration_with_binding(parameter.into(), definition, ty, ty);
@@ -1522,40 +1545,56 @@ impl<'db> TypeInferenceBuilder<'db> {
except_handler_definition: &ExceptHandlerDefinitionKind,
definition: Definition<'db>,
) {
let node_ty = except_handler_definition
.handled_exceptions()
.map(|ty| self.infer_expression(ty))
// If there is no handled exception, it's invalid syntax;
// a diagnostic will have already been emitted
.unwrap_or(Type::Unknown);
let node = except_handler_definition.handled_exceptions();
// If there is no handled exception, it's invalid syntax;
// a diagnostic will have already been emitted
let node_ty = node.map_or(Type::Unknown, |ty| self.infer_expression(ty));
// If it's an `except*` handler, this won't actually be the type of the bound symbol;
// it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`.
let symbol_ty = if let Type::Tuple(tuple) = node_ty {
let type_base_exception = KnownClass::BaseException
.to_subclass_of(self.db)
.unwrap_or(Type::Unknown);
let mut builder = UnionBuilder::new(self.db);
for element in tuple.elements(self.db).iter().copied() {
builder = builder.add(if element.is_assignable_to(self.db, type_base_exception) {
element.to_instance(self.db)
} else {
if let Some(node) = node {
self.diagnostics
.add_invalid_exception_caught(self.db, node, element);
}
Type::Unknown
});
}
builder.build()
} else if node_ty.is_subtype_of(self.db, KnownClass::Tuple.to_instance(self.db)) {
todo_type!("Homogeneous tuple in exception handler")
} else {
let type_base_exception = KnownClass::BaseException
.to_subclass_of(self.db)
.unwrap_or(Type::Unknown);
if node_ty.is_assignable_to(self.db, type_base_exception) {
node_ty.to_instance(self.db)
} else {
if let Some(node) = node {
self.diagnostics
.add_invalid_exception_caught(self.db, node, node_ty);
}
Type::Unknown
}
};
let symbol_ty = if except_handler_definition.is_star() {
// TODO should be generic --Alex
// TODO: we should infer `ExceptionGroup` if `node_ty` is a subtype of `tuple[type[Exception], ...]`
// (needs support for homogeneous tuples).
//
// TODO should infer `ExceptionGroup` if all caught exceptions
// are subclasses of `Exception` --Alex
builtins_symbol(self.db, "BaseExceptionGroup")
.ignore_possibly_unbound()
.unwrap_or(Type::Unknown)
.to_instance(self.db)
// TODO: should be generic with `symbol_ty` as the generic parameter
KnownClass::BaseExceptionGroup.to_instance(self.db)
} else {
// TODO: anything that's a consistent subtype of
// `type[BaseException] | tuple[type[BaseException], ...]` should be valid;
// anything else is invalid and should lead to a diagnostic being reported --Alex
match node_ty {
Type::Any | Type::Unknown => node_ty,
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(class),
Type::Tuple(tuple) => UnionType::from_elements(
self.db,
tuple.elements(self.db).iter().map(|ty| {
ty.into_class_literal().map_or(
todo_type!("exception type"),
|ClassLiteralType { class }| Type::instance(class),
)
}),
),
_ => todo_type!("exception type"),
}
symbol_ty
};
self.add_binding(
@@ -4667,10 +4706,12 @@ impl<'db> TypeInferenceBuilder<'db> {
match slice {
ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
let name_ty = self.infer_expression(slice);
if let Some(ClassLiteralType { class }) = name_ty.into_class_literal() {
Type::subclass_of(class)
} else {
todo_type!("unsupported type[X] special form")
match name_ty {
Type::ClassLiteral(ClassLiteralType { class }) => Type::subclass_of(class),
Type::KnownInstance(KnownInstanceType::Any) => {
Type::subclass_of_base(ClassBase::Any)
}
_ => todo_type!("unsupported type[X] special form"),
}
}
ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => {
@@ -4694,6 +4735,33 @@ impl<'db> TypeInferenceBuilder<'db> {
);
Type::Unknown
}
ast::Expr::Subscript(ast::ExprSubscript {
value,
slice: parameters,
..
}) => {
let parameters_ty = match self.infer_expression(value) {
Type::KnownInstance(KnownInstanceType::Union) => match &**parameters {
ast::Expr::Tuple(tuple) => {
let ty = UnionType::from_elements(
self.db,
tuple
.iter()
.map(|element| self.infer_subclass_of_type_expression(element)),
);
self.store_expression_type(parameters, ty);
ty
}
_ => self.infer_subclass_of_type_expression(parameters),
},
_ => {
self.infer_type_expression(parameters);
todo_type!("unsupported nested subscript in type[X]")
}
};
self.store_expression_type(slice, parameters_ty);
parameters_ty
}
// TODO: subscripts, etc.
_ => {
self.infer_type_expression(slice);
@@ -4793,7 +4861,19 @@ impl<'db> TypeInferenceBuilder<'db> {
);
Type::Unknown
}
KnownInstanceType::Any => Type::Any,
KnownInstanceType::Type => self.infer_subclass_of_type_expression(parameters),
KnownInstanceType::Tuple => self.infer_tuple_type_expression(parameters),
KnownInstanceType::Any => {
self.diagnostics.add_lint(
&INVALID_TYPE_PARAMETER,
subscript.into(),
format_args!(
"Type `{}` expected no type parameter",
known_instance.repr(self.db)
),
);
Type::Unknown
}
}
}

View File

@@ -4,8 +4,8 @@ use std::ops::Deref;
use itertools::Either;
use rustc_hash::FxHashSet;
use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, Type};
use crate::{types::todo_type, Db};
use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, TodoType, Type};
use crate::Db;
/// The inferred method resolution order of a given class.
///
@@ -76,21 +76,14 @@ impl<'db> Mro<'db> {
// This *could* theoretically be handled by the final branch below,
// but it's a common case (i.e., worth optimizing for),
// and the `c3_merge` function requires lots of allocations.
[single_base] => {
let single_base = ClassBase::try_from_ty(*single_base).ok_or(*single_base);
single_base.map_or_else(
|invalid_base_ty| {
let bases_info = Box::from([(0, invalid_base_ty)]);
Err(MroErrorKind::InvalidBases(bases_info))
},
|single_base| {
let mro = std::iter::once(ClassBase::Class(class))
.chain(single_base.mro(db))
.collect();
Ok(mro)
},
)
}
[single_base] => ClassBase::try_from_ty(db, *single_base).map_or_else(
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|single_base| {
Ok(std::iter::once(ClassBase::Class(class))
.chain(single_base.mro(db))
.collect())
},
),
// The class has multiple explicit bases.
//
@@ -102,9 +95,9 @@ impl<'db> Mro<'db> {
let mut invalid_bases = vec![];
for (i, base) in multiple_bases.iter().enumerate() {
match ClassBase::try_from_ty(*base).ok_or(*base) {
Ok(valid_base) => valid_bases.push(valid_base),
Err(invalid_base) => invalid_bases.push((i, invalid_base)),
match ClassBase::try_from_ty(db, *base) {
Some(valid_base) => valid_bases.push(valid_base),
None => invalid_bases.push((i, *base)),
}
}
@@ -299,16 +292,16 @@ pub(super) enum MroErrorKind<'db> {
/// This is much more limited than the [`Type`] enum:
/// all types that would be invalid to have as a class base are
/// transformed into [`ClassBase::Unknown`]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum ClassBase<'db> {
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
pub enum ClassBase<'db> {
Any,
Unknown,
Todo,
Todo(TodoType),
Class(Class<'db>),
}
impl<'db> ClassBase<'db> {
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
struct Display<'db> {
base: ClassBase<'db>,
db: &'db dyn Db,
@@ -318,7 +311,7 @@ impl<'db> ClassBase<'db> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.base {
ClassBase::Any => f.write_str("Any"),
ClassBase::Todo => f.write_str("Todo"),
ClassBase::Todo(todo) => todo.fmt(f),
ClassBase::Unknown => f.write_str("Unknown"),
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
}
@@ -341,11 +334,11 @@ impl<'db> ClassBase<'db> {
/// Attempt to resolve `ty` into a `ClassBase`.
///
/// Return `None` if `ty` is not an acceptable type for a class base.
fn try_from_ty(ty: Type<'db>) -> Option<Self> {
fn try_from_ty(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
match ty {
Type::Any => Some(Self::Any),
Type::Unknown => Some(Self::Unknown),
Type::Todo(_) => Some(Self::Todo),
Type::Todo(todo) => Some(Self::Todo(todo)),
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
@@ -371,6 +364,13 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::Never
| KnownInstanceType::Optional => None,
KnownInstanceType::Any => Some(Self::Any),
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
KnownInstanceType::Type => {
ClassBase::try_from_ty(db, KnownClass::Type.to_class_literal(db))
}
KnownInstanceType::Tuple => {
ClassBase::try_from_ty(db, KnownClass::Tuple.to_class_literal(db))
}
},
}
}
@@ -392,7 +392,9 @@ impl<'db> ClassBase<'db> {
ClassBase::Unknown => {
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
}
ClassBase::Todo => Either::Left([ClassBase::Todo, ClassBase::object(db)].into_iter()),
ClassBase::Todo(todo) => {
Either::Left([ClassBase::Todo(todo), ClassBase::object(db)].into_iter())
}
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
}
}
@@ -408,7 +410,7 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
fn from(value: ClassBase<'db>) -> Self {
match value {
ClassBase::Any => Type::Any,
ClassBase::Todo => todo_type!(),
ClassBase::Todo(todo) => Type::Todo(todo),
ClassBase::Unknown => Type::Unknown,
ClassBase::Class(class) => Type::class_literal(class),
}

View File

@@ -64,6 +64,10 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
Ty::BuiltinClassLiteral("int"),
Ty::BuiltinClassLiteral("bool"),
Ty::BuiltinClassLiteral("object"),
Ty::BuiltinInstance("type"),
Ty::SubclassOfAny,
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("str"),
])
.unwrap()
.clone()
@@ -163,13 +167,13 @@ mod stable {
// `T` is equivalent to itself.
type_property_test!(
equivalent_to_is_reflexive, db,
forall types t. t.is_equivalent_to(db, t)
forall types t. t.is_fully_static(db) => t.is_equivalent_to(db, t)
);
// `T` is a subtype of itself.
type_property_test!(
subtype_of_is_reflexive, db,
forall types t. t.is_subtype_of(db, t)
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, t)
);
// `S <: T` and `T <: U` implies that `S <: U`.
@@ -214,6 +218,12 @@ mod stable {
forall types t. t.is_singleton(db) => t.is_single_valued(db)
);
// If `T` contains a gradual form, it should not participate in equivalence
type_property_test!(
non_fully_static_types_do_not_participate_in_equivalence, db,
forall types s, t. !s.is_fully_static(db) => !s.is_equivalent_to(db, t) && !t.is_equivalent_to(db, s)
);
// If `T` contains a gradual form, it should not participate in subtyping
type_property_test!(
non_fully_static_types_do_not_participate_in_subtyping, db,

View File

@@ -110,7 +110,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static INVALID_SYNTAX_IN_FORWARD_ANNOTATION = {
summary: "detects invalid syntax in forward annotations",
status: LintStatus::preview("1.0.0"),
@@ -119,7 +119,7 @@ declare_lint! {
}
declare_lint! {
/// TODO
/// TODO #14889
pub(crate) static ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION = {
summary: "detects forward type annotations with escape characters",
status: LintStatus::preview("1.0.0"),

View File

@@ -4,7 +4,7 @@ use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
use crate::db::Db;
use crate::diagnostic::SortedDiagnostics;
use colored::Colorize;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
use ruff_db::diagnostic::{Diagnostic, DiagnosticAsStrError, DiagnosticId};
use ruff_db::files::File;
use ruff_db::source::{line_index, source_text, SourceText};
use ruff_source_file::{LineIndex, OneIndexed};
@@ -161,9 +161,14 @@ where
T: Diagnostic,
{
fn unmatched(&self) -> String {
let id = self.id();
let id = id.as_str().unwrap_or_else(|error| match error {
DiagnosticAsStrError::Category { name, .. } => name,
});
maybe_add_undefined_reveal_clarification(
self,
format_args!(r#"[{}] "{}""#, self.id(), self.message()),
format_args!(r#"[{id}] "{message}""#, message = self.message()),
)
}
}
@@ -173,9 +178,14 @@ where
T: Diagnostic,
{
fn unmatched_with_column(&self, column: OneIndexed) -> String {
let id = self.id();
let id = id.as_str().unwrap_or_else(|error| match error {
DiagnosticAsStrError::Category { name, .. } => name,
});
maybe_add_undefined_reveal_clarification(
self,
format_args!(r#"{column} [{}] "{}""#, self.id(), self.message()),
format_args!(r#"{column} [{id}] "{message}""#, message = self.message()),
)
}
}
@@ -464,7 +474,7 @@ mod tests {
0,
&[
"unmatched assertion: revealed: Foo",
r#"unexpected error: 1 [lint/not-revealed-type] "Revealed type is `Foo`""#,
r#"unexpected error: 1 [not-revealed-type] "Revealed type is `Foo`""#,
],
)],
);
@@ -581,7 +591,7 @@ mod tests {
0,
&[
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
original diagnostic: [lint/undefined-reveal] \"undefined reveal message\")",
original diagnostic: [undefined-reveal] \"undefined reveal message\")",
r#"unexpected error: [revealed-type] "Revealed type is `Literal[1]`""#,
],
)],
@@ -613,7 +623,7 @@ mod tests {
&[
"unmatched assertion: error: [something-else]",
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
original diagnostic: 1 [lint/undefined-reveal] \"undefined reveal message\")",
original diagnostic: 1 [undefined-reveal] \"undefined reveal message\")",
r#"unexpected error: 13 [revealed-type] "Revealed type is `Literal[1]`""#,
],
)],
@@ -658,7 +668,7 @@ mod tests {
0,
&[
"unmatched assertion: error: [some-rule]",
r#"unexpected error: 1 [lint/anything] "Any message""#,
r#"unexpected error: 1 [anything] "Any message""#,
],
)],
);
@@ -695,7 +705,7 @@ mod tests {
0,
&[
r#"unmatched assertion: error: "contains this""#,
r#"unexpected error: 1 [lint/anything] "Any message""#,
r#"unexpected error: 1 [anything] "Any message""#,
],
)],
);
@@ -732,7 +742,7 @@ mod tests {
0,
&[
"unmatched assertion: error: 2 [rule]",
r#"unexpected error: 1 [lint/rule] "Any message""#,
r#"unexpected error: 1 [rule] "Any message""#,
],
)],
);
@@ -797,7 +807,7 @@ mod tests {
0,
&[
r#"unmatched assertion: error: 2 [some-rule] "contains this""#,
r#"unexpected error: 1 [lint/some-rule] "message contains this""#,
r#"unexpected error: 1 [some-rule] "message contains this""#,
],
)],
);
@@ -820,7 +830,7 @@ mod tests {
0,
&[
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
r#"unexpected error: 1 [lint/other-rule] "message contains this""#,
r#"unexpected error: 1 [other-rule] "message contains this""#,
],
)],
);
@@ -843,7 +853,7 @@ mod tests {
0,
&[
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
r#"unexpected error: 1 [lint/some-rule] "Any message""#,
r#"unexpected error: 1 [some-rule] "Any message""#,
],
)],
);
@@ -877,9 +887,9 @@ mod tests {
result,
&[
(1, &["unmatched assertion: error: [line-one]"]),
(2, &[r#"unexpected error: [lint/line-two] "msg""#]),
(2, &[r#"unexpected error: [line-two] "msg""#]),
(4, &["unmatched assertion: error: [line-four]"]),
(5, &[r#"unexpected error: [lint/line-five] "msg""#]),
(5, &[r#"unexpected error: [line-five] "msg""#]),
(6, &["unmatched assertion: error: [line-six]"]),
],
);
@@ -903,10 +913,7 @@ mod tests {
],
);
assert_fail(
result,
&[(2, &[r#"unexpected error: [lint/line-two] "msg""#])],
);
assert_fail(result, &[(2, &[r#"unexpected error: [line-two] "msg""#])]);
}
#[test]
@@ -972,7 +979,7 @@ mod tests {
assert_fail(
result,
&[(3, &[r#"unexpected error: 1 [lint/third-rule] "msg""#])],
&[(3, &[r#"unexpected error: 1 [third-rule] "msg""#])],
);
}
@@ -1021,7 +1028,7 @@ mod tests {
0,
&[
"invalid assertion: no rule or message text",
r#"unexpected error: 1 [lint/some-rule] "some message""#,
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
@@ -1046,7 +1053,7 @@ mod tests {
0,
&[
"invalid assertion: no rule or message text",
r#"unexpected error: 1 [lint/some-rule] "some message""#,
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);

View File

@@ -19,6 +19,6 @@ fn check() {
assert_eq!(
result,
vec!["error[unresolved-import] /test.py:1:8 Cannot resolve import `random22`"]
vec!["error[lint:unresolved-import] /test.py:1:8 Cannot resolve import `random22`"]
);
}

View File

@@ -1,5 +1,5 @@
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder};
use red_knot_python_semantic::register_semantic_lints;
use red_knot_python_semantic::register_lints;
pub mod db;
pub mod watch;
@@ -10,6 +10,6 @@ pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock<LintRegistry> =
pub fn default_lints_registry() -> LintRegistry {
let mut builder = LintRegistryBuilder::default();
register_semantic_lints(&mut builder);
register_lints(&mut builder);
builder.build()
}

View File

@@ -1,4 +1,8 @@
use std::{fs, path::Path, process::Command};
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
fn main() {
// The workspace root directory is not available without walking up the tree
@@ -21,27 +25,22 @@ fn commit_info(workspace_root: &Path) {
return;
}
let git_head_path = git_dir.join("HEAD");
println!(
"cargo::rerun-if-changed={}",
git_head_path.as_path().display()
);
if let Some(git_head_path) = git_head(&git_dir) {
println!("cargo:rerun-if-changed={}", git_head_path.display());
let git_head_contents = fs::read_to_string(git_head_path);
if let Ok(git_head_contents) = git_head_contents {
// The contents are either a commit or a reference in the following formats
// - "<commit>" when the head is detached
// - "ref <ref>" when working on a branch
// If a commit, checking if the HEAD file has changed is sufficient
// If a ref, we need to add the head file for that ref to rebuild on commit
let mut git_ref_parts = git_head_contents.split_whitespace();
git_ref_parts.next();
if let Some(git_ref) = git_ref_parts.next() {
let git_ref_path = git_dir.join(git_ref);
println!(
"cargo::rerun-if-changed={}",
git_ref_path.as_path().display()
);
let git_head_contents = fs::read_to_string(git_head_path);
if let Ok(git_head_contents) = git_head_contents {
// The contents are either a commit or a reference in the following formats
// - "<commit>" when the head is detached
// - "ref <ref>" when working on a branch
// If a commit, checking if the HEAD file has changed is sufficient
// If a ref, we need to add the head file for that ref to rebuild on commit
let mut git_ref_parts = git_head_contents.split_whitespace();
git_ref_parts.next();
if let Some(git_ref) = git_ref_parts.next() {
let git_ref_path = git_dir.join(git_ref);
println!("cargo:rerun-if-changed={}", git_ref_path.display());
}
}
}
@@ -78,3 +77,30 @@ fn commit_info(workspace_root: &Path) {
);
}
}
fn git_head(git_dir: &Path) -> Option<PathBuf> {
// The typical case is a standard git repository.
let git_head_path = git_dir.join("HEAD");
if git_head_path.exists() {
return Some(git_head_path);
}
if !git_dir.is_file() {
return None;
}
// If `.git/HEAD` doesn't exist and `.git` is actually a file,
// then let's try to attempt to read it as a worktree. If it's
// a worktree, then its contents will look like this, e.g.:
//
// gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2
//
// And the HEAD file we want to watch will be at:
//
// /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD
let contents = fs::read_to_string(git_dir).ok()?;
let (label, worktree_path) = contents.split_once(':')?;
if label != "gitdir" {
return None;
}
let worktree_path = worktree_path.trim();
Some(PathBuf::from(worktree_path))
}

View File

@@ -25,30 +25,30 @@ const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/
static EXPECTED_DIAGNOSTICS: &[&str] = &[
// We don't support `*` imports yet:
"error[unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
"error[lint:unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
// We don't support terminal statements in control flow yet:
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
"error[conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
"error[conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
"error[conflicting-declarations] /src/tomllib/_parser.py:364:9 Conflicting declared types for `char`: Unknown, str | None",
"error[conflicting-declarations] /src/tomllib/_parser.py:381:13 Conflicting declared types for `char`: Unknown, str | None",
"error[conflicting-declarations] /src/tomllib/_parser.py:395:9 Conflicting declared types for `char`: Unknown, str | None",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
"error[conflicting-declarations] /src/tomllib/_parser.py:590:9 Conflicting declared types for `char`: Unknown, str | None",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:364:9 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:381:13 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:395:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:590:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
];
fn get_test_file(name: &str) -> TestFile {

View File

@@ -1,6 +1,8 @@
use std::borrow::Cow;
use std::fmt::Formatter;
use thiserror::Error;
use ruff_python_parser::ParseError;
use ruff_text_size::TextRange;
@@ -66,12 +68,9 @@ pub enum DiagnosticId {
/// A lint violation.
///
/// Lint's can be suppressed and some lints can be enabled or disabled in the configuration.
/// Lints can be suppressed and some lints can be enabled or disabled in the configuration.
Lint(LintName),
/// Some code is incorrectly formatted.
Format,
/// A revealed type: Created by `reveal_type(expression)`.
RevealedType,
}
@@ -92,28 +91,58 @@ impl DiagnosticId {
matches!(self, DiagnosticId::Lint(self_name) if self_name == name)
}
pub fn matches(&self, name: &str) -> bool {
match self {
DiagnosticId::Lint(self_name) => name
.strip_prefix("lint/")
.is_some_and(|rest| rest == &**self_name),
/// Returns `true` if this `DiagnosticId` matches the given name.
///
/// ## Examples
/// ```
/// use ruff_db::diagnostic::DiagnosticId;
///
/// assert!(DiagnosticId::Io.matches("io"));
/// assert!(DiagnosticId::lint("test").matches("lint:test"));
/// assert!(!DiagnosticId::lint("test").matches("test"));
/// ```
pub fn matches(&self, expected_name: &str) -> bool {
match self.as_str() {
Ok(id) => id == expected_name,
Err(DiagnosticAsStrError::Category { category, name }) => expected_name
.strip_prefix(category)
.and_then(|prefix| prefix.strip_prefix(":"))
.is_some_and(|rest| rest == name),
}
}
DiagnosticId::Io => name == "io",
DiagnosticId::InvalidSyntax => name == "invalid-syntax",
DiagnosticId::Format => name == "format",
DiagnosticId::RevealedType => name == "revealed-type",
pub fn as_str(&self) -> Result<&str, DiagnosticAsStrError> {
match self {
DiagnosticId::Io => Ok("io"),
DiagnosticId::InvalidSyntax => Ok("invalid-syntax"),
DiagnosticId::Lint(name) => Err(DiagnosticAsStrError::Category {
category: "lint",
name: name.as_str(),
}),
DiagnosticId::RevealedType => Ok("revealed-type"),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
pub enum DiagnosticAsStrError {
/// The id can't be converted to a string because it belongs to a sub-category.
#[error("id from a sub-category: {category}:{name}")]
Category {
/// The id's category.
category: &'static str,
/// The diagnostic id in this category.
name: &'static str,
},
}
impl std::fmt::Display for DiagnosticId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
DiagnosticId::InvalidSyntax => f.write_str("invalid-syntax"),
DiagnosticId::Io => f.write_str("io"),
DiagnosticId::Lint(name) => write!(f, "lint/{name}"),
DiagnosticId::Format => f.write_str("format"),
DiagnosticId::RevealedType => f.write_str("revealed-type"),
match self.as_str() {
Ok(name) => f.write_str(name),
Err(DiagnosticAsStrError::Category { category, name }) => {
write!(f, "{category}:{name}")
}
}
}
}
@@ -121,7 +150,7 @@ impl std::fmt::Display for DiagnosticId {
pub trait Diagnostic: Send + Sync + std::fmt::Debug {
fn id(&self) -> DiagnosticId;
fn message(&self) -> std::borrow::Cow<str>;
fn message(&self) -> Cow<str>;
fn file(&self) -> File;

View File

@@ -656,7 +656,7 @@ where
let elements = buffer.elements();
let recorded = if self.start > elements.len() {
// May happen if buffer was rewinded.
// May happen if buffer was rewound.
&[]
} else {
&elements[self.start..]

View File

@@ -5,13 +5,23 @@ Should emit:
B028 - on lines 8 and 9
"""
warnings.warn(DeprecationWarning("test"))
warnings.warn(DeprecationWarning("test"), source=None)
warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2)
warnings.warn(DeprecationWarning("test"), stacklevel=1)
warnings.warn("test", DeprecationWarning)
warnings.warn("test", DeprecationWarning, source=None)
warnings.warn("test", DeprecationWarning, source=None, stacklevel=2)
warnings.warn("test", DeprecationWarning, stacklevel=1)
warnings.warn("test", DeprecationWarning, 1)
warnings.warn("test", category=DeprecationWarning, stacklevel=1)
args = ("test", DeprecationWarning, 1)
warnings.warn(*args)
kwargs = {"message": "test", "category": DeprecationWarning, "stacklevel": 1}
warnings.warn(**kwargs)
args = ("test", DeprecationWarning)
kwargs = {"stacklevel": 1}
warnings.warn(*args, **kwargs)
warnings.warn(
DeprecationWarning("test"),
"test",
DeprecationWarning,
# some comments here
source = None # no trailing comma
)

View File

@@ -0,0 +1,59 @@
from itertools import batched, count, cycle, repeat
# Errors
batched(range(3), 1)
batched("abc", 2)
batched([i for i in range(42)], some_n)
batched((foo for foo in cycle()))
batched(itertools.batched([1, 2, 3], strict=True))
# Errors (limited iterators).
batched(repeat(1, 1))
batched(repeat(1, times=4))
# No fix
batched([], **kwargs)
# No errors
batched()
batched(range(3), 0, strict=True)
batched(["a", "b"], count, strict=False)
batched(("a", "b", "c"), zip(repeat()), strict=True)
# No errors (infinite iterators)
batched(cycle("ABCDEF"), 3)
batched(count(), qux + lorem)
batched(repeat(1), ipsum // 19 @ 0x1)
batched(repeat(1, None))
batched(repeat(1, times=None))
import itertools
# Errors
itertools.batched(range(3), 1)
itertools.batched("abc", 2)
itertools.batched([i for i in range(42)], some_n)
itertools.batched((foo for foo in cycle()))
itertools.batched(itertools.batched([1, 2, 3], strict=True))
# Errors (limited iterators).
itertools.batched(repeat(1, 1))
itertools.batched(repeat(1, times=4))
# No fix
itertools.batched([], **kwargs)
# No errors
itertools.batched()
itertools.batched(range(3), 0, strict=True)
itertools.batched(["a", "b"], count, strict=False)
itertools.batched(("a", "b", "c"), zip(repeat()), strict=True)
# No errors (infinite iterators)
itertools.batched(cycle("ABCDEF"), 3)
itertools.batched(count(), qux + lorem)
itertools.batched(repeat(1), ipsum // 19 @ 0x1)
itertools.batched(repeat(1, None))
itertools.batched(repeat(1, times=None))

View File

@@ -8,7 +8,7 @@ import typing
from abc import ABCMeta, abstractmethod
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
from enum import EnumMeta
from typing import Any, overload
from typing import Any, Generic, ParamSpec, Type, TypeVar, TypeVarTuple, overload
import typing_extensions
from _typeshed import Self
@@ -321,3 +321,41 @@ def __imul__(self, other: Any) -> list[str]:
class UsesStringizedAnnotations:
def __iadd__(self, other: "UsesStringizedAnnotations") -> "typing.Self":
return self
class NonGeneric1(tuple):
def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ...
def __enter__(self: NonGeneric1) -> NonGeneric1: ...
class NonGeneric2(tuple):
def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ...
class Generic1[T](list):
def __new__(cls: type[Generic1]) -> Generic1: ...
def __enter__(self: Generic1) -> Generic1: ...
### Correctness of typevar-likes are not verified.
T = TypeVar('T')
P = ParamSpec()
Ts = TypeVarTuple('foo')
class Generic2(Generic[T]):
def __new__(cls: type[Generic2]) -> Generic2: ...
def __enter__(self: Generic2) -> Generic2: ...
class Generic3(tuple[*Ts]):
def __new__(cls: type[Generic3]) -> Generic3: ...
def __enter__(self: Generic3) -> Generic3: ...
class Generic4(collections.abc.Callable[P, ...]):
def __new__(cls: type[Generic4]) -> Generic4: ...
def __enter__(self: Generic4) -> Generic4: ...
from some_module import PotentialTypeVar
class Generic5(list[PotentialTypeVar]):
def __new__(cls: type[Generic5]) -> Generic5: ...
def __enter__(self: Generic5) -> Generic5: ...

View File

@@ -8,7 +8,7 @@ import typing
from abc import ABCMeta, abstractmethod
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
from enum import EnumMeta
from typing import Any, overload
from typing import Any, Generic, ParamSpec, Type, TypeVar, TypeVarTuple, overload
import typing_extensions
from _typeshed import Self
@@ -215,3 +215,40 @@ def __imul__(self, other: Any) -> list[str]: ...
class UsesStringizedAnnotations:
def __iadd__(self, other: "UsesStringizedAnnotations") -> "typing.Self": ...
class NonGeneric1(tuple):
def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ...
def __enter__(self: NonGeneric1) -> NonGeneric1: ...
class NonGeneric2(tuple):
def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ...
class Generic1[T](list):
def __new__(cls: type[Generic1]) -> Generic1: ...
def __enter__(self: Generic1) -> Generic1: ...
### Correctness of typevar-likes are not verified.
T = TypeVar('T')
P = ParamSpec()
Ts = TypeVarTuple('foo')
class Generic2(Generic[T]):
def __new__(cls: type[Generic2]) -> Generic2: ...
def __enter__(self: Generic2) -> Generic2: ...
class Generic3(tuple[*Ts]):
def __new__(cls: type[Generic3]) -> Generic3: ...
def __enter__(self: Generic3) -> Generic3: ...
class Generic4(collections.abc.Callable[P, ...]):
def __new__(cls: type[Generic4]) -> Generic4: ...
def __enter__(self: Generic4) -> Generic4: ...
from some_module import PotentialTypeVar
class Generic5(list[PotentialTypeVar]):
def __new__(cls: type[Generic5]) -> Generic5: ...
def __enter__(self: Generic5) -> Generic5: ...

View File

@@ -74,7 +74,7 @@ def f():
result.append(i) # Ok
def f():
async def f():
items = [1, 2, 3, 4]
result = []
async for i in items:
@@ -82,17 +82,24 @@ def f():
result.append(i) # PERF401
def f():
async def f():
items = [1, 2, 3, 4]
result = []
async for i in items:
result.append(i) # PERF401
async def f():
items = [1, 2, 3, 4]
result = [1, 2]
async for i in items:
result.append(i) # PERF401
def f():
result, _ = [1,2,3,4], ...
result, _ = [1, 2, 3, 4], ...
for i in range(10):
result.append(i*2) # PERF401
result.append(i * 2) # PERF401
def f():
@@ -100,23 +107,24 @@ def f():
if True:
for i in range(10): # single-line comment 1 should be protected
# single-line comment 2 should be protected
if i % 2: # single-line comment 3 should be protected
result.append(i) # PERF401
if i % 2: # single-line comment 3 should be protected
result.append(i) # PERF401
def f():
result = [] # comment after assignment should be protected
result = [] # comment after assignment should be protected
for i in range(10): # single-line comment 1 should be protected
# single-line comment 2 should be protected
if i % 2: # single-line comment 3 should be protected
result.append(i) # PERF401
if i % 2: # single-line comment 3 should be protected
result.append(i) # PERF401
def f():
result = []
for i in range(10):
"""block comment stops the fix"""
result.append(i*2) # Ok
result.append(i * 2) # Ok
def f(param):
# PERF401
@@ -125,3 +133,107 @@ def f(param):
new_layers = []
for value in param:
new_layers.append(value * 3)
def f():
result = []
var = 1
for _ in range(10):
result.append(var + 1) # PERF401
def f():
# make sure that `tmp` is not deleted
tmp = 1; result = [] # commment should be protected
for i in range(10):
result.append(i + 1) # PERF401
def f():
# make sure that `tmp` is not deleted
result = []; tmp = 1 # commment should be protected
for i in range(10):
result.append(i + 1) # PERF401
def f():
result = [] # comment should be protected
for i in range(10):
result.append(i * 2) # PERF401
def f():
result = []
result.append(1)
for i in range(10):
result.append(i * 2) # PERF401
def f():
result = []
result += [1]
for i in range(10):
result.append(i * 2) # PERF401
def f():
result = []
for val in range(5):
result.append(val * 2) # Ok
print(val)
def f():
result = []
for val in range(5):
result.append(val * 2) # PERF401
val = 1
print(val)
def f():
i = [1, 2, 3]
result = []
for i in i:
result.append(i + 1) # PERF401
def f():
result = []
for i in range( # Comment 1 should not be duplicated
(
2 # Comment 2
+ 1
)
): # Comment 3
if i % 2: # Comment 4
result.append(
(
i + 1,
# Comment 5
2,
)
) # PERF401
def f():
result: list[int] = []
for i in range(10):
result.append(i * 2) # PERF401
def f():
a, b = [1, 2, 3], [4, 5, 6]
result = []
for i in a, b:
result.append(i[0] + i[1]) # PERF401
return result
def f():
values = [1, 2, 3]
result = []
for a in values:
print(a)
for a in values:
result.append(a + 1) # PERF401

View File

@@ -0,0 +1,3 @@
print('No coding coments here')
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,3 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# -*- coding: ascii -*-

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,2 @@
# -*- coding: ascii -*-
# -*- coding: latin -*-

View File

@@ -0,0 +1,2 @@
# -*- coding: ascii -*-
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,3 @@
# -*- coding: utf8 -*-
print("the following is not a coding comment")
# -*- coding: ascii -*-

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# -*- coding: ascii -*-

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# -*- coding: ascii -*-

View File

@@ -1,9 +1,12 @@
import math
inferred_int = 1
inferred_float = 1.
### Safely fixable
# Arguments are not checked
int(id())
int(len([]))
int(ord(foo))
@@ -17,6 +20,15 @@ int(math.lcm())
int(math.isqrt())
int(math.perm())
int(round(1, 0))
int(round(1, 10))
int(round(1))
int(round(1, None))
int(round(1.))
int(round(1., None))
### Unsafe
@@ -24,27 +36,35 @@ int(math.ceil())
int(math.floor())
int(math.trunc())
int(round(inferred_int, 0))
int(round(inferred_int, 10))
### `round()`
int(round(inferred_int))
int(round(inferred_int, None))
## Errors
int(round(0))
int(round(0, 0))
int(round(0, None))
int(round(inferred_float))
int(round(inferred_float, None))
int(round(0.1))
int(round(0.1, None))
int(round(unknown))
int(round(unknown, None))
# Argument type is not checked
foo = type("Foo", (), {"__round__": lambda self: 4.2})()
int(round(foo))
int(round(foo, 0))
int(round(foo, None))
### No errors
int(round(1, unknown))
int(round(1., unknown))
int(round(1., 0))
int(round(inferred_float, 0))
int(round(inferred_int, unknown))
int(round(inferred_float, unknown))
int(round(unknown, 0))
int(round(unknown, unknown))
## No errors
int(round(0, 3.14))
int(round(0, non_literal))
int(round(inferred_int, 3.14))
int(round(0, 0), base)
int(round(0, 0, extra=keyword))
int(round(0.1, 0))

View File

@@ -0,0 +1,130 @@
d = {}
l = []
### Errors
if k in d: # Bare name
del d[k]
if '' in d: # String
del d[""] # Different quotes
if b"" in d: # Bytes
del d[ # Multiline slice
b'''''' # Triple quotes
]
if 0 in d: del d[0] # Single-line statement
if 3j in d: # Complex
del d[3j]
if 0.1234 in d: # Float
del d[.1_2_3_4] # Number separators and shorthand syntax
if True in d: # True
del d[True]
if False in d: # False
del d[False]
if None in d: # None
del d[
# Comment in the middle
None
]
if ... in d: # Ellipsis
del d[
# Comment in the middle, indented
...]
if "a" "bc" in d: # String concatenation
del d['abc']
if r"\foo" in d: # Raw string
del d['\\foo']
if b'yt' b'es' in d: # Bytes concatenation
del d[rb"""ytes"""] # Raw bytes
if k in d:
# comment that gets dropped
del d[k]
### Safely fixable
if k in d:
del d[k]
if '' in d:
del d[""]
if b"" in d:
del d[
b''''''
]
if 0 in d: del d[0]
if 3j in d:
del d[3j]
if 0.1234 in d:
del d[.1_2_3_4]
if True in d:
del d[True]
if False in d:
del d[False]
if None in d:
del d[
None
]
if ... in d:
del d[
...]
if "a" "bc" in d:
del d['abc']
if r"\foo" in d:
del d['\\foo']
if b'yt' b'es' in d:
del d[rb"""ytes"""] # This should not make the fix unsafe
### No errors
if k in l: # Not a dict
del l[k]
if d.__contains__(k): # Explicit dunder call
del d[k]
if a.k in d: # Attribute
del d[a.k]
if (a, b) in d: # Tuple
del d[a, b]
if 2 in d: # Different key value (int)
del d[3]
if 2_4j in d: # Different key value (complex)
del d[3.6] # Different key value (float)
if 0.1 + 0.2 in d: # Complex expression
del d[0.3]
if f"0" in d: # f-string
del d[f"0"]
if k in a.d: # Attribute dict
del a.d[k]

View File

@@ -14,7 +14,6 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
let Stmt::For(stmt_for) = checker.semantic.current_statement() else {
unreachable!("Expected Stmt::For");
};
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, stmt_for);
}
@@ -36,6 +35,9 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
if checker.enabled(Rule::DictIndexMissingItems) {
pylint::rules::dict_index_missing_items(checker, stmt_for);
}
if checker.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(checker, stmt_for);
}
}
}
}

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