Compare commits

...

126 Commits

Author SHA1 Message Date
Micha Reiser
fcde25773d Avoid cloning live declarations 2025-01-11 11:23:25 +01:00
Micha Reiser
c39ca8fe6d Upgrade Rust toolchain to 1.84.0 (#15408) 2025-01-11 09:51:58 +01:00
David Peter
2d82445794 [red-knot] Simplify unions of T and ~T (#15400)
## Summary

Simplify unions of `T` and `~T` to `object`.

## Test Plan

Adapted existing tests.
2025-01-10 23:00:52 +01:00
David Peter
398f2e8b0c [red-knot] Minor fixes in intersection-types tests (#15410)
## Summary

Minor fixes in intersection-types tests
2025-01-10 22:53:03 +01:00
InSync
232fbc1300 [red-knot] Understand type[Unknown] (#15409)
## Summary

Follow-up to #15194.

## Test Plan

Markdown tests.
2025-01-10 13:25:59 -08:00
Alex Waygood
c82932e580 [red-knot] Refactor KnownFunction::takes_expression_arguments() (#15406) 2025-01-10 19:09:03 +00:00
Micha Reiser
12f86f39a4 Ruff 0.9.1 (#15407) 2025-01-10 19:45:06 +01:00
Micha Reiser
2b28d566a4 Associate a trailing end-of-line comment in a parenthesized implicit concatenated string with the last literal (#15378) 2025-01-10 19:21:34 +01:00
Calum Young
adca7bd95c Remove pygments pin (#15404)
## Summary

The recent release of Pygments
([2.19.1](https://github.com/pygments/pygments/releases/tag/2.19.1))
allows the pinned version to be removed as the PYI alias for Python
syntax highlighting has been removed.

## Test Plan

- Follow the steps outlined in
https://github.com/astral-sh/ruff/blob/main/CONTRIBUTING.md#mkdocs to
get the documentation site running locally.
- Spot test rules pages that have PYI code blocks to ensure that syntax
highlighting remains e.g.
[http://127.0.0.1:8000/ruff/rules/complex-if-statement-in-stub/](http://127.0.0.1:8000/ruff/rules/complex-if-statement-in-stub/).

**Note:** I am unable to test the insiders build but would assume that
it functions locally as I do not have access to MkDocs Insiders, but I
would like to assume that it functions in the same way as the
non-insiders build.
2025-01-10 12:15:13 -05:00
InSync
6b98a26452 [red-knot] Support assert_type (#15194)
## Summary

See #15103.

## Test Plan

Markdown tests and unit tests.
2025-01-10 08:45:02 -08:00
David Peter
c87463842a [red-knot] Move tuple-containing-Never tests to Markdown (#15402)
## Summary

See title.

Part of #15397

## Test Plan

Ran new Markdown test.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-10 15:31:30 +00:00
InSync
c364b586f9 [flake8-pie] Correctly remove wrapping parentheses (PIE800) (#15394)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-10 14:52:32 +00:00
Antoine Dechaume
73d424ee5e Fix outdated doc for handling the default file types with the pre-commit hook (#15401)
Co-authored-by: Antoine DECHAUME <>
2025-01-10 15:49:23 +01:00
Dhruv Manilawala
6e9ff445fd Insert the cells from the start position (#15398)
## Summary

The cause of this bug is from
https://github.com/astral-sh/ruff/pull/12575 which was itself a bug fix
but the fix wasn't completely correct.

fixes: #14768 
fixes: https://github.com/astral-sh/ruff-vscode/issues/644

## Test Plan

Consider the following three cells:

1.
```python
class Foo:
    def __init__(self):
        self.x = 1

    def __str__(self):
        return f"Foo({self.x})"
```

2.
```python
def hello():
    print("hello world")
```

3.
```python
y = 1
```

The test case is moving cell 2 to the top i.e., cell 2 goes to position
1 and cell 1 goes to position 2.

Before this fix, it can be seen that the cells were pushed at the end of
the vector:

```
  12.643269917s  INFO ruff:main ruff_server::edit:📓 Before update: [
    NotebookCell {
        document: TextDocument {
            contents: "class Foo:\n    def __init__(self):\n        self.x = 1\n\n    def __str__(self):\n        return f\"Foo({self.x})\"",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "def hello():\n    print(\"hello world\")",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "y = 1",
        },
    },
]
  12.643777667s  INFO ruff:main ruff_server::edit:📓 After update: [
    NotebookCell {
        document: TextDocument {
            contents: "y = 1",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "class Foo:\n    def __init__(self):\n        self.x = 1\n\n    def __str__(self):\n        return f\"Foo({self.x})\"",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "def hello():\n    print(\"hello world\")",
        },
    },
]
```

After the fix in this PR, it can be seen that the cells are being pushed
at the correct `start` index:

```
   6.520570917s  INFO ruff:main ruff_server::edit:📓 Before update: [
    NotebookCell {
        document: TextDocument {
            contents: "class Foo:\n    def __init__(self):\n        self.x = 1\n\n    def __str__(self):\n        return f\"Foo({self.x})\"",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "def hello():\n    print(\"hello world\")",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "y = 1",
        },
    },
]
   6.521084792s  INFO ruff:main ruff_server::edit:📓 After update: [
    NotebookCell {
        document: TextDocument {
            contents: "def hello():\n    print(\"hello world\")",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "class Foo:\n    def __init__(self):\n        self.x = 1\n\n    def __str__(self):\n        return f\"Foo({self.x})\"",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "y = 1",
        },
    },
]
```
2025-01-10 13:11:56 +00:00
David Peter
f2c3ddc5ea [red-knot] Move intersection type tests to Markdown (#15396)
## Summary

[**Rendered version of the new test
suite**](https://github.com/astral-sh/ruff/blob/david/intersection-type-tests/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md)

Moves most of our existing intersection-types tests to a dedicated
Markdown test suite, extends the test coverage, unifies the notation for
these tests, groups tests into a proper structure, and adds some
explanations for various simplification strategies.

This changeset also:
- Adds a new simplification where `~Never` is removed from
intersections.
- Adds a new simplification where adding `~object` simplifies the whole
intersection to `Never`
- Avoids unnecessary assignment-checks between inferred and declared
type. This was added to this changeset to avoid many false positive
errors in this test suite.

Resolves the task described in this old comment
[here](e01da82a5a..e7e432bca2 (r1819924085)).

## Test Plan

Running the new Markdown tests

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-10 14:04:03 +01:00
Dhruv Manilawala
b861551b6a Remove unnecessary backticks (#15393)
Ref: https://github.com/astral-sh/ruff/pull/15367#discussion_r1909448140
2025-01-10 09:22:26 +00:00
Dylan
443bf38565 [ruff] Omit diagnostic for shadowed private function parameters in used-dummy-variable (RUF052) (#15376) 2025-01-10 03:09:25 -06:00
Tom Kuson
23ad319b55 [flake8-bugbear] Improve assert-raises-exception (B017) message (#15389) 2025-01-10 08:48:18 +01:00
InSync
3d9433ca66 [pyupgrade] Handle comments and multiline expressions correctly (UP037) (#15337) 2025-01-10 08:46:01 +01:00
Douglas Creager
baf068361a [red-knot] Consolidate all gradual types into single Type variant (#15386)
Prompted by

> One nit: I think we need to consider `Any` and `Unknown` and `Todo` as
all (gradually) equivalent to each other, and thus `type & Any` and
`type & Unknown` and `type & Todo` as also equivalent. The distinction
between `Any` vs `Unknown` vs `Todo` is entirely about
provenance/debugging, there is no type level distinction. (And I've been
wondering if the `Any` vs `Unknown` distinction is really worth it.)

The thought here is that _most_ places want to treat `Any`, `Unknown`,
and `Todo` identically. So this PR simplifies things by having a single
`Type::Any` variant, and moves the provenance part into a new `AnyType`
type. If you need to treat e.g. `Todo` differently, you still can by
pattern-matching into the `AnyType`. But if you don't, you can just use
`Type::Any(_)`.

(This would also allow us to (more easily) distinguish "unknown via an
unannotated value" from "unknown because of a typing error" should we
want to do that in the future)

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-01-09 21:32:20 -05:00
David Peter
b33cf5baba [red-knot] Move UnionBuilder tests to Markdown (#15374)
## Summary

This moves almost all of our existing `UnionBuilder` tests to a
Markdown-based test suite.

I see how this could be a more controversial change, since these tests
where written specifically for `UnionBuilder`, and by creating the union
types using Python type expressions, we add an additional layer on top
(parsing and inference of these expressions) that moves these tests away
from clean unit tests more in the direction of integration tests. Also,
there are probably a few implementation details of `UnionBuilder` hidden
in the test assertions (e.g. order of union elements after
simplifications).

That said, I think we would like to see all those properties that are
being tested here from *any* implementation of union types. And the
Markdown tests come with the usual advantages:

- More consice
- Better readability
- No re-compiliation when working on tests
- Easier to add additional explanations and structure to the test suite

This changeset adds a few additional tests, but keeps the logic of the
existing tests except for a few minor modifications for consistency.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>
2025-01-09 21:45:06 +01:00
Dylan
b0905c4b04 [pycodestyle] Handle each cell separately for too-many-newlines-at-end-of-file (W391) (#15308)
Jupyter notebooks are converted into source files by joining with
newlines, which confuses the check [too-many-newlines-at-end-of-file
(W391)](https://docs.astral.sh/ruff/rules/too-many-newlines-at-end-of-file/#too-many-newlines-at-end-of-file-w391).
This PR introduces logic to apply the check cell-wise (and, in
particular, correctly handles empty cells.)

Closes #13763
2025-01-09 10:50:39 -06:00
Micha Reiser
d0b2bbd55e Release 0.9.0 (#15371)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-09 14:53:08 +01:00
Josiah Outram Halstead
8628f169e9 [ruff] Stop parsing diagnostics from other sources for code action requests (#15373) 2025-01-09 14:38:13 +01:00
InSync
8bc11c49b2 [flake8-django] Recognize other magic methods (DJ012) (#15365) 2025-01-09 14:36:42 +01:00
David Peter
bf5b0c2688 [red-knot] Minor refactor of red_knot_vendored/build.rs (#15372)
## Summary

See https://github.com/astral-sh/ruff/pull/15370#discussion_r1908611461:

- Rename `zip_dir` to `write_zipped_typeshed_to` to clarify it's not a
generic function (anymore)
- Hard-code `TYPESHED_SOURCE_DIR` instead of using a `directory_path`
argument
2025-01-09 12:23:42 +00:00
David Peter
097aa04c04 [red-knot] Typeshed patching: use build.rs instead of workflow (#15370)
## Summary

The symlink-approach in the typeshed-sync workflow caused some problems
on Windows, even though it seemed to work fine in CI:

https://github.com/astral-sh/ruff/pull/15138#issuecomment-2578642129

Here, we rely on `build.rs` to patch typeshed instead, which allows us
to get rid of the modifications in the workflow (thank you
@MichaReiser for the idea).

## Test Plan

- Made sure that changes to `knot_extensions.pyi` result in a recompile
  of `red_knot_vendored`.
2025-01-09 11:50:32 +01:00
Dhruv Manilawala
f706c3fdf2 Add f-string formatting to the docs (#15367)
Revive https://github.com/astral-sh/ruff/pull/15341 as it got removed
from the latest rebase in https://github.com/astral-sh/ruff/pull/15238.
2025-01-09 10:20:06 +01:00
Micha Reiser
29f6653318 [ruff] Stabilize useless-if-else (RUF034) (#15351) 2025-01-09 10:20:06 +01:00
Micha Reiser
d645525afc [pylint]: Stabilize boolean-chained-comparison (PLR1716) (#15354) 2025-01-09 10:20:06 +01:00
Micha Reiser
6dcf7b35b9 [ruff] Stabilize post-init-default (RUF033) (#15352) 2025-01-09 10:20:06 +01:00
Micha Reiser
943d4fc160 Update formatter preview documentation (#15349) 2025-01-09 10:20:06 +01:00
Micha Reiser
3ea4c63d2c [flake8-pyi] Stabilize: include all python file types for PYI006 (#15340) 2025-01-09 10:20:06 +01:00
Micha Reiser
8e8a07144d [flake8-pyi]: Stabilize: Provide more automated fixes for duplicate-union-members (PYI016) (#15342) 2025-01-09 10:20:06 +01:00
Micha Reiser
225dd0a027 [ruff] Stabilize: Detect attrs dataclasses (RUF008, RUF009) (#15345) 2025-01-09 10:20:06 +01:00
Micha Reiser
52aeb8ae11 [flake8-pyi] Stabilize autofix for redundant-numeric-union (PYI041) (#15343) 2025-01-09 10:20:06 +01:00
Micha Reiser
71b6ac81a6 Remove unnecessary PreviewMode::Enabled in tests (#15344) 2025-01-09 10:20:06 +01:00
Alex Waygood
75fc2c3116 [ruff-0.9] Stabilise two flake8-builtins rules (#15322) 2025-01-09 10:20:06 +01:00
Micha Reiser
9c4d124ba0 [pycodestyle] Stabilize: Exempt pytest.importorskip calls (E402) (#15338) 2025-01-09 10:20:06 +01:00
InSync
8c620b9b4b [flake8-pytest-style] Stabilize "Detect more pytest.mark.parametrize calls" (PT006) (#15327)
Co-authored-by: Micha Reiser <micha@reiser.io>
Resolves #15324. Stabilizes the behavior changes introduced in #14515.
2025-01-09 10:20:06 +01:00
Dylan
1eda27d1a5 [ruff-0.9] Stabilize decimal-from-float-literal (RUF032) (#15333) 2025-01-09 10:20:06 +01:00
Alex Waygood
aaa86cf38d [ruff-0.9] Stabilise slice-to-remove-prefix-or-suffix (FURB188) (#15329)
Stabilise [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`) for the Ruff 0.9 release.

This is a stylistic rule, but I think it's a pretty uncontroversial one. There are no open issues or PRs regarding it and it's been in preview for a while now.
2025-01-09 10:20:06 +01:00
Micha Reiser
7821206b7b Update Black deviations to reflect 2025 style changes (#15127) 2025-01-09 10:20:06 +01:00
Micha Reiser
b76d05e283 Remove formatter incompatibility warning for ISC001 (#15123) 2025-01-09 10:20:06 +01:00
Micha Reiser
424b720c19 Ruff 2025 style guide (#13906)
Closes #13371
2025-01-09 10:20:06 +01:00
Carl Meyer
a95deec00f [red-knot] handle synthetic 'self' argument in call-binding diagnostics (#15362) 2025-01-09 00:36:48 -08:00
InSync
21aa12a073 [red-knot] More precise inference for classes with non-class metaclasses (#15138)
## Summary

Resolves #14208.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-01-09 00:34:04 +00:00
Douglas Creager
5f5eb7c0dd [red-knot] Print non-string panic payloads and (sometimes) backtraces (#15363)
More refinements to the panic messages for failing mdtests to mimic the
output of the default panic hook more closely:

- We now print out `Box<dyn Any>` if the panic payload is not a string
(which is typically the case for salsa panics).
- We now include the panic's backtrace if you set the `RUST_BACKTRACE`
environment variable.
2025-01-08 18:12:16 -05:00
David Peter
b6562ed57e [red-knot] Property test workflow: Fix issue label, link to CI run (#15361)
## Summary

See title. Had to make a minor change, because it failed the zizmor
pre-commit check otherwise:

```
error[template-injection]: code injection via template expansion
  --> /home/shark/ruff/.github/workflows/daily_fuzz.yaml:68:9
   |
68 |          - uses: actions/github-script@v7
   |  __________^
69 | |          with:
70 | |            github-token: ${{ secrets.GITHUB_TOKEN }}
71 | |            script: |
   | | ___________^
72 | ||             await github.rest.issues.create({
...  ||
77 | ||               labels: ["bug", "parser", "fuzzer"],
78 | ||             })
   | ||               ^
   | ||_______________|
   |  |_______________this step
   |                  github.server_url may expand into attacker-controllable code
   |
   = note: audit confidence → High
```
2025-01-08 22:47:16 +01:00
David Peter
4fd82d5f35 [red-knot] Property test improvements (#15358)
## Summary

- Add a workflow to run property tests on a daily basis (based on
`daily_fuzz.yaml`)
- Mark `assignable_to_is_reflexive` as flaky (related to #14899)
- Add new (failing) `intersection_assignable_to_both` test (also related
to #14899)

## Test Plan

Ran:

```bash
export QUICKCHECK_TESTS=100000
while cargo test --release -p red_knot_python_semantic -- \
  --ignored types::property_tests::stable; do :; done
```

Observed successful property_tests CI run
2025-01-08 22:24:57 +01:00
David Peter
beb8e2dfe0 [red-knot] More comprehensive is_assignable_to tests (#15353)
## Summary

This changeset migrates all existing `is_assignable_to` tests to a
Markdown-based test. It also increases our test coverage in a hopefully
meaningful way (not claiming to be complete in any sense). But at least
I found and fixed one bug while doing so.

## Test Plan

Ran property tests to make sure the new test succeeds after fixing it.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-08 20:25:08 +01:00
Alex Waygood
88d07202c1 [red-knot] Reduce Name clones in call signature checking (#15335) 2025-01-08 18:29:35 +00:00
Douglas Creager
2ca31e4b43 Fall back on previous panic hook when not in catch_unwind wrapper (#15319)
This fixes #15317. Our `catch_unwind` wrapper installs a panic hook that
captures (the rendered contents of) the panic info when a panic occurs.
Since the intent is that the caller will render the panic info in some
custom way, the hook silences the default stderr panic output.

However, the panic hook is a global resource, so if any one thread was
in the middle of a `catch_unwind` call, we would silence the default
panic output for _all_ threads.

The solution is to also keep a thread local that indicates whether the
current thread is in the middle of our `catch_unwind`, and to fall back
on the default panic hook if not.

## Test Plan

Artificially added an mdtest parse error, ran tests via `cargo test -p
red_knot_python_semantic` to run a large number of tests in parallel.
Before this patch, the panic message was swallowed as reported in
#15317. After, the panic message was shown.
2025-01-08 11:34:51 -05:00
Auguste Lalande
450d4e0e0c [pylint] Fix unreachable infinite loop (PLW0101) (#15278)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

Fix infinite loop issue reported here #15248.
The issue was caused by the break inside the if block, which caused the
flow to exit in an unforeseen way. This caused other issues, eventually
leading to an infinite loop.

Resolves #15248. Resolves #15336.

## Test Plan

Added failing code to fixture.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-01-08 09:45:04 -06:00
Alex Waygood
7284d68157 fix invalid syntax in workflow file (#15357) 2025-01-08 15:34:33 +00:00
InSync
3820af2f1b [pycodestyle] Avoid false positives related to type aliases (E252) (#15356) 2025-01-08 16:04:08 +01:00
Alex Waygood
ee9a912f47 [flake8-builtins] Disapply A005 to stub files (#15350) 2025-01-08 12:59:27 +00:00
Dhruv Manilawala
1447553bc2 Improve logging system using logLevel, avoid trace value (#15232)
## Summary

Refer to the VS Code PR
(https://github.com/astral-sh/ruff-vscode/pull/659) for details on the
change.

This PR changes the following:

1. Add tracing span for both request (request id and method name) and
notification (method name) handler
2. Remove the `RUFF_TRACE` environment variable. This was being used to
turn on / off logging for the server
3. Similarly, remove reading the `trace` value from the initialization
options
4. Remove handling the `$/setTrace` notification
5. Remove the specialized `TraceLogWriter` used for Zed and VS Code
(https://github.com/astral-sh/ruff/pull/12564)

Regarding the (5) for the Zed editor, the reason that was implemented
was because there was no way of looking at the stderr messages in the
editor which has been changed. Now, it captures the stderr as part of
the "Server Logs".
(82492d74a8/crates/language_tools/src/lsp_log.rs (L548-L552))

### Question

Regarding (1), I think having just a simple trace level message should
be good for now as the spans are not hierarchical. This could be tackled
with #12744. The difference between the two:

<details><summary>Using <code>tracing::trace</code></summary>
<p>

```
   0.019243416s DEBUG ThreadId(08) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.026398750s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.026802125s TRACE ruff:main ruff_server::server::api: Received notification "textDocument/didOpen"
   0.026930666s TRACE ruff:main ruff_server::server::api: Received notification "textDocument/didOpen"
   0.026962333s TRACE ruff:main ruff_server::server::api: Received request "textDocument/diagnostic" (1)
   0.027042875s TRACE ruff:main ruff_server::server::api: Received request "textDocument/diagnostic" (2)
   0.027097500s TRACE ruff:main ruff_server::server::api: Received request "textDocument/codeAction" (3)
   0.027107458s DEBUG ruff:worker:0 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   0.027123541s DEBUG ruff:worker:3 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/organize_imports.py
   0.027514875s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
   0.285689833s TRACE     ruff:main ruff_server::server::api: Received request "textDocument/codeAction" (4)
  45.741101666s TRACE     ruff:main ruff_server::server::api: Received notification "textDocument/didClose"
  47.108745500s TRACE     ruff:main ruff_server::server::api: Received notification "textDocument/didOpen"
  47.109802041s TRACE     ruff:main ruff_server::server::api: Received request "textDocument/diagnostic" (5)
  47.109926958s TRACE     ruff:main ruff_server::server::api: Received request "textDocument/codeAction" (6)
  47.110027791s DEBUG ruff:worker:6 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
  51.863679125s TRACE     ruff:main ruff_server::server::api: Received request "textDocument/hover" (7)
```

</p>
</details> 

<details><summary>Using <code>tracing::trace_span</code></summary>
<p>

Only logging the enter event:

```
   0.018638750s DEBUG ThreadId(11) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.025895791s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.026378791s TRACE ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   0.026531208s TRACE ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   0.026567583s TRACE ruff:main request{id=1 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   0.026652541s TRACE ruff:main request{id=2 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   0.026711041s DEBUG ruff:worker:2 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/organize_imports.py
   0.026729166s DEBUG ruff:worker:1 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   0.027023083s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
   5.197554750s TRACE     ruff:main notification{method="textDocument/didClose"}: ruff_server::server::api: enter
   6.534458000s TRACE     ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   6.535027958s TRACE     ruff:main request{id=3 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   6.535271166s DEBUG ruff:worker:3 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/organize_imports.py
   6.544240583s TRACE     ruff:main request{id=4 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.049692458s TRACE     ruff:main request{id=5 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.508142541s TRACE     ruff:main request{id=6 method="textDocument/hover"}: ruff_server::server::api: enter
   7.872421958s TRACE     ruff:main request{id=7 method="textDocument/hover"}: ruff_server::server::api: enter
   8.024498583s TRACE     ruff:main request{id=8 method="textDocument/codeAction"}: ruff_server::server::api: enter
  13.895063666s TRACE     ruff:main request{id=9 method="textDocument/codeAction"}: ruff_server::server::api: enter
  14.774706083s TRACE     ruff:main request{id=10 method="textDocument/hover"}: ruff_server::server::api: enter
  16.058918958s TRACE     ruff:main notification{method="textDocument/didChange"}: ruff_server::server::api: enter
  16.060562208s TRACE     ruff:main request{id=11 method="textDocument/diagnostic"}: ruff_server::server::api: enter
  16.061109083s DEBUG ruff:worker:8 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
  21.561742875s TRACE     ruff:main notification{method="textDocument/didChange"}: ruff_server::server::api: enter
  21.563573791s TRACE     ruff:main request{id=12 method="textDocument/diagnostic"}: ruff_server::server::api: enter
  21.564206750s DEBUG ruff:worker:4 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
  21.826691375s TRACE     ruff:main request{id=13 method="textDocument/codeAction"}: ruff_server::server::api: enter
  22.091080125s TRACE     ruff:main request{id=14 method="textDocument/codeAction"}: ruff_server::server::api: enter
```

</p>
</details> 


**Todo**

- [x] Update documentation (I'll be adding a troubleshooting section
under "Editors" as a follow-up which is for all editors)
- [x] Check for backwards compatibility. I don't think this should break
backwards compatibility as it's mainly targeted towards improving the
debugging experience.

~**Before I go on to updating the documentation, I'd appreciate initial
review on the chosen approach.**~

resolves: #14959 

## Test Plan

Refer to the test plan in
https://github.com/astral-sh/ruff-vscode/pull/659.

Example logs at `debug` level:

```
   0.010770083s DEBUG ThreadId(15) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.018101916s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.018559916s DEBUG ruff:worker:4 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   0.018992375s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
  23.408802375s DEBUG ruff:worker:11 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
  24.329127416s DEBUG  ruff:worker:6 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
```

Example logs at `trace` level:

```
   0.010296375s DEBUG ThreadId(13) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.017422583s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.018034458s TRACE ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   0.018199708s TRACE ruff:worker:0 request{id=1 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   0.018251167s DEBUG ruff:worker:0 request{id=1 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   0.018528708s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
   1.611798417s TRACE ruff:worker:1 request{id=2 method="textDocument/codeAction"}: ruff_server::server::api: enter
   1.861757542s TRACE ruff:worker:4 request{id=3 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.027361792s TRACE ruff:worker:2 request{id=4 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.851361500s TRACE ruff:worker:5 request{id=5 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.901690875s TRACE     ruff:main notification{method="textDocument/didChange"}: ruff_server::server::api: enter
   7.903063167s TRACE ruff:worker:10 request{id=6 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   7.903183500s DEBUG ruff:worker:10 request{id=6 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   8.702385292s TRACE      ruff:main notification{method="textDocument/didChange"}: ruff_server::server::api: enter
   8.704106625s TRACE  ruff:worker:3 request{id=7 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   8.704304875s DEBUG  ruff:worker:3 request{id=7 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   8.966853458s TRACE  ruff:worker:9 request{id=8 method="textDocument/codeAction"}: ruff_server::server::api: enter
   9.229622792s TRACE  ruff:worker:6 request{id=9 method="textDocument/codeAction"}: ruff_server::server::api: enter
  10.513111583s TRACE  ruff:worker:7 request{id=10 method="textDocument/codeAction"}: ruff_server::server::api: enter
```
2025-01-08 18:18:00 +05:30
Alex Waygood
9a27b37a91 [flake8-builtins] Rename A005 and improve its error message (#15348) 2025-01-08 12:38:34 +00:00
Alex Waygood
487f2f5df0 Spruce up docs for pydoclint rules (#15325) 2025-01-08 12:22:37 +00:00
David Salvisberg
339167d372 [flake8-type-checking] Apply TC008 more eagerly in TYPE_CHECKING blocks and disapply it in stubs (#15180)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-08 12:09:06 +00:00
David Peter
235fdfc57a [red-knot] knot_extensions Python API (#15103)
## Summary

Adds a type-check-time Python API that allows us to create and
manipulate types and to test various of their properties. For example,
this can be used to write a Markdown test to make sure that `A & B` is a
subtype of `A` and `B`, but not of an unrelated class `C` (something
that requires quite a bit more code to do in Rust):
```py
from knot_extensions import Intersection, is_subtype_of, static_assert

class A: ...
class B: ...

type AB = Intersection[A, B]

static_assert(is_subtype_of(AB, A))
static_assert(is_subtype_of(AB, B))

class C: ...
static_assert(not is_subtype_of(AB, C))
```

I think this functionality is also helpful for interactive debugging
sessions, in order to query various properties of Red Knot's type
system. Which is something that otherwise requires a custom Rust unit
test, some boilerplate code and constant re-compilation.

## Test Plan

- New Markdown tests
- Tested the modified typeshed_sync workflow locally
2025-01-08 12:52:07 +01:00
Shaygan Hooshyari
03ff883626 Display Union of Literals as a Literal (#14993)
## Summary

Resolves #14988

Display union of Literals like other type checkers do.

With this change we lose the sorting behavior. And we show the types as
they appeared. So it's deterministic and tests should not be flaky.
This is similar to how Mypy [reveals the
type](https://mypy-play.net/?mypy=latest&python=3.12&gist=51ad03b153bfca3b940d5084345e230f).

In some cases this makes it harder to know what is the order in revealed
type when writing tests but since it's consistent after the test fails
we know the order.

## Test Plan

I adjusted mdtests for this change. Basically merged the int and string
types of the unions.

In cases where we have types other than numbers and strings like this
[one](https://github.com/astral-sh/ruff/pull/14993/files#diff-ac50bce02b9f0ad4dc7d6b8e1046d60dad919ac52d0aeb253e5884f89ea42bfeL51).
We only group the strings and numbers as the issue suggsted.

```
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, "foo", f]` is not callable (due to union elements Literal[1], Literal["foo"])"
    # revealed: Unknown | int
    reveal_type(f())
```

[pyright
example](https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMoAySMApiAIYA2AUNQCYnBQD6AFMJeWgFxQBGYMJQA0UDlwBMvAUICU3alCWYm4nouWamAXigBGDUpKUkqzmimHNYqLoBEwQXavGAziQXXlDVa1lQAWgA%2BTBQYTy9rEBIYAFcQFH0rAGIoMnAQXjsAeT4AKxIAY3wwJngEEigAAyJSCkoAbT1RBydRYABdKsxXKBQwfEKqTj5KStY6WMqYMChYlCQwROMSCBIw3tqyKiaO0S36htawOw7ZZ01U6IA3EioSOl4AVRQAa36Ad0SAH1CYKxud0ozHKJHYflk1CAA)

[mypy
example](https://mypy-play.net/?mypy=latest&python=3.12&gist=31c8bdaa5521860cfeca4b92841cb3b7)

---------

Co-authored-by: Carl Meyer <carl@oddbird.net>
2025-01-08 00:58:38 +00:00
Carl Meyer
fdca2b422e [red-knot] all types are assignable to object (#15332)
## Summary

`Type[Any]` should be assignable to `object`. All types should be
assignable to `object`.

We specifically didn't understand the former; this PR adds a test for
it, and a case to ensure that `Type[Any]` is assignable to anything that
`type` is assignable to (which includes `object`).

This PR also adds a property test that all types are assignable to
object. In order to make it pass, I added a special case to check early
if we are assigning to `object` and just return `true`. In principle,
once we get all the more general cases correct, this special case might
be removable. But having the special case for now allows the property
test to pass.

And we add a property test that all types are subtypes of object. This
failed for the case of an intersection with no positive elements (that
is, a negation type). This really does need to be a special case for
`object`, because there is no other type we can know that a negation
type is a subtype of.

## Test Plan

Added unit test and property test.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-07 15:19:07 -08:00
Dylan
71ad9a2ab1 [ruff] Parenthesize arguments to int when removing int would change semantics in unnecessary-cast-to-int (RUF046) (#15277)
When removing `int` in calls like `int(expr)` we may need to keep
parentheses around `expr` even when it is a function call or subscript,
since there may be newlines in between the function/value name and the
opening parentheses/bracket of the argument.

This PR implements that logic.

Closes #15263

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2025-01-07 21:43:50 +00:00
InSync
3b3c2c5aa4 [eradicate] Correctly handle metadata blocks directly followed by normal blocks (ERA001) (#15330)
## Summary

Resolves #15321.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-01-07 16:22:22 -05:00
Douglas Creager
b2a0d68d70 Narrowing for class patterns in match statements (#15223)
We now support class patterns in a match statement, adding a narrowing
constraint that within the body of that match arm, we can assume that
the subject is an instance of that class.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-07 15:58:12 -05:00
Carl Meyer
f2a86fcfda [red-knot] add call checking (#15200)
## Summary

This implements checking of calls.

I ended up following Micha's original suggestion from back when the
signature representation was first introduced, and flattening it to a
single array of parameters. This turned out to be easier to manage,
because we can represent parameters using indices into that array, and
represent the bound argument types as an array of the same length.

Starred and double-starred arguments are still TODO; these won't be very
useful until we have generics.

The handling of diagnostics is just hacked into `return_ty_result`,
which was already inconsistent about whether it emitted diagnostics or
not; now it's even more inconsistent. This needs to be addressed, but
could be a follow-up.

The new benchmark errors here surface the need for intersection support
in `is_assignable_to`.

Fixes #14161.

## Test Plan

Added mdtests.
2025-01-07 20:39:45 +00:00
Alex Waygood
ac72aca27c Spruce up docs for slice-to-remove-prefix-or-suffix (FURB188) (#15328) 2025-01-07 19:58:35 +00:00
Dylan
a876090715 [internal] Return statements in finally block point to end block for unreachable (PLW0101) (#15276)
Note: `PLW0101` remains in testing rather than preview, so this PR does
not modify any public behavior (hence the title beginning with
`internal` rather than `pylint`, for the sake of the changelog.)

Fixes an error in the processing of `try` statements in the control flow
graph builder.

When processing a try statement, the block following a `return` was
forced to point to the `finally` block. However, if the return was _in_
the `finally` block, this caused the block to point to itself. In the
case where the whole `try-finally` statement was also included inside of
a loop, this caused an infinite loop in the builder for the control flow
graph as it attempted to resolve edges.

Closes #15248

## Test function
### Source
```python
def l():
    while T:
        try:
            while ():
                if 3:
                    break
        finally:
            return
```

### Control Flow Graph
```mermaid
flowchart TD
  start(("Start"))
  return(("End"))
  block0[["`*(empty)*`"]]
  block1[["Loop continue"]]
  block2["return\n"]
  block3[["Loop continue"]]
  block4["break\n"]
  block5["if 3:
                    break\n"]
  block6["while ():
                if 3:
                    break\n"]
  block7[["Exception raised"]]
  block8["try:
            while ():
                if 3:
                    break
        finally:
            return\n"]
  block9["while T:
        try:
            while ():
                if 3:
                    break
        finally:
            return\n"]
  start --> block9
  block9 -- "T" --> block8
  block9 -- "else" --> block0
  block8 -- "Exception raised" --> block7
  block8 -- "else" --> block6
  block7 --> block2
  block6 -- "()" --> block5
  block6 -- "else" --> block2
  block5 -- "3" --> block4
  block5 -- "else" --> block3
  block4 --> block2
  block3 --> block6
  block2 --> return
  block1 --> block9
  block0 --> return
```
2025-01-07 11:26:04 -06:00
InSync
e4139568b8 [ruff] Treat ) as a regex metacharacter (RUF043, RUF055) (#15318)
## Summary

Resolves #15316.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-01-07 12:11:05 -05:00
Charlie Marsh
5567e7c26b Use uv consistently throughout the documentation (#15302)
## Summary

Closes
https://github.com/astral-sh/ruff/issues/15301#issuecomment-2573350821.
2025-01-07 14:43:25 +00:00
Alex Waygood
95294e657c [red-knot] Eagerly normalize type[] types (#15272)
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-01-07 12:53:07 +00:00
InSync
0dc00e63f4 [pyupgrade] Split UP007 to two individual rules for Union and Optional (UP007, UP045) (#15313)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-07 10:22:59 +00:00
David Peter
ce9c4968ae [red-knot] Improve symbol-lookup tracing (#14907)
## Summary

When debugging, I frequently want to know which symbols are being looked
up. `symbol_by_id` adds tracing information, but it only shows the
`ScopedSymbolId`. Since `symbol_by_id` is only called from `symbol`, it
seems reasonable to move the tracing call one level up from
`symbol_by_id` to `symbol`, where we can also show the name of the
symbol.

**Before**:

```
6      └─┐red_knot_python_semantic::types::infer::infer_expression_types{expression=Id(60de), file=/home/shark/tomllib_modified/_parser.py}
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(33)}
6        ┌─┘
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(123)}
6        ┌─┘
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(54)}
6        ┌─┘
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(122)}
6        ┌─┘
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(165)}
6        ┌─┘
6      ┌─┘
6      └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(32)}
6      ┌─┘
6      └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(232)}
6      ┌─┘
6    ┌─┘
6  ┌─┘
6┌─┘
```

**After**:

```
5      └─┐red_knot_python_semantic::types::infer::infer_expression_types{expression=Id(60de), file=/home/shark/tomllib_modified/_parser.py}
5        └─┐red_knot_python_semantic::types::symbol{name="dict"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="dict"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="list"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="list"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="isinstance"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="isinstance"}
5        ┌─┘
5      ┌─┘
5      └─┐red_knot_python_semantic::types::symbol{name="ValueError"}
5      ┌─┘
5      └─┐red_knot_python_semantic::types::symbol{name="ValueError"}
5      ┌─┘
5    ┌─┘
5  ┌─┘
5┌─┘
```

## Test Plan

```
cargo run --bin red_knot -- --current-directory path/to/tomllib -vvv
```
2025-01-07 10:41:27 +01:00
Raphael Gaschignard
066239fe5b [red-knot] improve type shrinking coverage in red-knot property tests (#15297)
## Summary

While looking at #14899, I looked at seeing if I could get shrinking on
the examples. It turned out to be straightforward, with a couple of
caveats.

I'm calling `clone` a lot during shrinking. Since by the shrink step
we're already looking at a test failure this feels fine? Unless I
misunderstood `quickcheck`'s core loop

When shrinking `Intersection`s, in order to just rely on `quickcheck`'s
`Vec` shrinking without thinking about it too much, the shrinking
strategy is:
- try to shrink the negative side (keeping the positive side the same)
- try to shrink the positive side (keeping the negative side the same)

This means that you can't shrink from `(A & B & ~C & ~D)` directly to
`(A & ~C)`! You would first need an intermediate failure at `(A & B &
~C)` or `(A & ~C & ~D)`. This feels good enough. Shrinking the negative
side first also has the benefit of trying to strip down negative
elements in these intersections.

## Test Plan
`cargo test -p red_knot_python_semantic -- --ignored
types::property_tests::stable` still fails as it current does on `main`,
but now the errors seem more minimal.
2025-01-07 10:09:18 +01:00
Victor Westerhuis
1e948f739c [flake8-return] Recognize functions returning Never as non-returning (RET503) (#15298)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-07 07:57:34 +00:00
Steve C
78e26cec02 [flake8-bugbear] Implement class-as-data-structure (B903) (#9601)
## Summary

Adds `class-as-data-structure` rule (`B903`). Also compare pylint's `too-few-public-methods` (`PLR0903`).

Took some creative liberty with this by allowing the class to have any
decorators or base classes. There are years-old issues on pylint that
don't approve of the strictness when it comes to these things.

Especially considering that dataclass is a decorator and namedtuple _can
be_ a base class. I feel ignoring those explicitly is redundant all
things considered, but it's not a hill I'm willing to die on!

See: #970 

## Test Plan

`cargo test`

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-01-06 21:18:28 -06:00
Charlie Marsh
e7248ee43e Avoid treating newline-separated sections as sub-sections (#15311)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15224.
2025-01-06 22:13:35 -05:00
Charlie Marsh
065274d353 Remove call when removing final argument from format (#15309)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15303.
2025-01-07 02:53:42 +00:00
Charlie Marsh
75a24bbc67 Don't enforce object-without-hash-method in stubs (#15310)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15292.
2025-01-07 02:51:06 +00:00
Douglas Creager
5e9259c96c Don't special-case class instances in binary expression inference (#15161)
Just like in #15045 for unary expressions: In binary expressions, we
were only looking for dunder expressions for `Type::Instance` types. We
had some special cases for coercing the various `Literal` types into
their corresponding `Instance` types before doing the lookup. But we can
side-step all of that by using the existing `Type::to_meta_type` and
`Type::to_instance` methods.
2025-01-06 13:50:20 -05:00
Alex Waygood
d45c1ee44f Upgrade zizmor to the latest version in CI (#15300)
## Summary

This PR upgrades zizmor to the latest release in our CI. zizmor is a
static analyzer checking for security issues in GitHub workflows. The
new release finds some new issues in our workflows; this PR fixes some
of the issues, and adds ignores for some other issues.

The issues fixed in this PR are new cases of zizmor's
[`template-injection`](https://woodruffw.github.io/zizmor/audits/#template-injection)
rule being emitted. The issues I'm ignoring for now are all to do with
the
[`cache-poisoning`](https://woodruffw.github.io/zizmor/audits/#cache-poisoning)
rule. The main reason I'm fixing some but ignoring others is that I'm
confident fixing the template-injection diagnostics won't have any
impact on how our workflows operate in CI, but I'm worried that fixing
the cache-poisoning diagnostics could slow down our CI a fair bit. I
don't mind if somebody else is motivated to try to fix these
diagnostics, but for now I think I'd prefer to just ignore them; it
doesn't seem high-priority enough to try to fix them right now :-)

## Test Plan

- `uvx pre-commit run -a --hook-stage=manual` passes locally
- Let's see if CI passes on this PR...
2025-01-06 15:07:46 +00:00
InSync
636288038f [ruff] Dataclass enums (RUF049) (#15299)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-06 14:44:20 +01:00
InSync
832c0fa04b Better error message when --config is given a table key and a non-inline-table value (#15266)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-06 13:20:28 +00:00
renovate[bot]
f29c9e48a4 Update pre-commit dependencies (#15289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-06 11:35:02 +00:00
Enric Calabuig
bafe8714a8 Don't fix in ecosystem check (#15267) 2025-01-06 10:21:34 +01:00
renovate[bot]
e5270e2ac2 Update Rust crate itertools to 0.14.0 (#15287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-06 07:55:32 +00:00
Avasam
643fd7fe07 Remove accidental empty block at the bottom of split-static-string (SIM905) doc (#15290)
## Summary

Removes the following empty code block

![image](https://github.com/user-attachments/assets/4adbacac-0bd9-4dac-af3a-93da2619a1cb)
2025-01-06 04:24:40 +00:00
renovate[bot]
bd02beec85 Update Rust crate clearscreen to v4 (#15288) 2025-01-05 20:06:13 -05:00
renovate[bot]
7f299fceef Update Rust crate insta to v1.42.0 (#15286) 2025-01-05 20:06:02 -05:00
renovate[bot]
6e2800df85 Update NPM Development dependencies (#15285) 2025-01-05 20:05:51 -05:00
renovate[bot]
391332a835 Update dependency uuid to v11.0.4 (#15284) 2025-01-05 20:05:45 -05:00
renovate[bot]
84e13cea14 Update dependency ruff to v0.8.6 (#15283) 2025-01-05 20:05:36 -05:00
renovate[bot]
bcb5f621c5 Update Rust crate syn to v2.0.95 (#15282) 2025-01-05 20:05:32 -05:00
renovate[bot]
47c8f1ad65 Update Rust crate matchit to v0.8.6 (#15281) 2025-01-05 20:04:02 -05:00
renovate[bot]
a4f8b9311e Update Rust crate bstr to v1.11.3 (#15280) 2025-01-05 20:03:54 -05:00
Alex Waygood
6097fd9bbe [red-knot] Future-proof Type::is_disjoint_from() (#15262) 2025-01-05 22:56:16 +00:00
Alex Waygood
0743838438 [red-knot] Improve Type::is_disjoint_from() for KnownInstanceTypes (#15261) 2025-01-05 22:49:42 +00:00
Alex Waygood
980ce941c7 [red-knot] Minor simplifications and improvements to constraint narrowing logic (#15270) 2025-01-05 21:51:22 +00:00
Shaygan Hooshyari
b26448926a Allow assigning ellipsis literal as parameter default value (#14982)
Resolves #14840

## Summary

Usage of ellipsis literal as default argument is allowed in stub files.

## Test Plan

Added mdtest for both python files and stub files.


---------

Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-05 13:11:32 -06:00
Carl Meyer
2ea63620cf [red-knot] fix control flow for assignment expressions in elif tests (#15274)
## Summary

The test expression in an `elif` clause is evaluated whether or not we
take the branch. Our control flow model for if/elif chains failed to
reflect this, causing wrong inference in cases where an assignment
expression occurs inside an `elif` test expression. Our "no branch taken
yet" snapshot (which is the starting state for every new elif branch)
can't simply be the pre-if state, it must be updated after visiting each
test expression.

Once we do this, it also means we no longer need to track a vector of
narrowing constraints to reapply for each new branch, since our "branch
not taken" state (which is the initial state for each branch) is
continuously updated to include the negative narrowing constraints of
all previous branches.

Fixes #15033.

## Test Plan

Added mdtests.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-05 18:35:29 +00:00
InSync
00aa387d9d [refurb] Mark fix as unsafe when the right-hand side is a string (FURB171) (#15273) 2025-01-05 17:54:32 +00:00
Alex Waygood
eb82089551 [red-knot] Type::SubclassOf(SubclassOfType { base: ClassBase::Unknown }).to_instance() should be Unknown, not Any (#15269) 2025-01-05 15:14:01 +00:00
InSync
f144b9684d Add a test for overshadowing redirects (#15259)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-05 09:47:01 +01:00
InSync
df6e5c0293 [ruff] Recode RUF025 to RUF037 (RUF037) (#15258) 2025-01-05 09:35:08 +01:00
Alex Waygood
8f0e01787f [red-knot] Minor cleanup to Type::is_disjoint_from() and Type::is_subtype_of() (#15260) 2025-01-04 17:34:37 +00:00
Micha Reiser
6b907c1305 Ruff 0.8.6 (#15253) 2025-01-04 13:09:26 +01:00
Micha Reiser
f319531632 Make unreachable a test rule for now (#15252) 2025-01-04 12:52:08 +01:00
Micha Reiser
e4d9fe036a Revert "Add all PEP-585 names to UP006 rule" (#15250) 2025-01-04 12:23:53 +01:00
Micha Reiser
baf0d660eb Update salsa (#15243) 2025-01-03 20:04:35 +01:00
Alex Waygood
bde8ecddca [red-knot] Remove unneeded branch in Type::is_equivalent_to() (#15242)
## Summary

We understand `sys.version_info` branches now! As such, I _believe_ this
branch is no longer required; all tests pass without it. I also ran
`QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable`, and no tests failed except for
the known issue with `Type::is_assignable_to()`
(https://github.com/astral-sh/ruff/issues/14899)

## Test Plan

See above
2025-01-03 19:04:01 +00:00
InSync
842f882ef0 [ruff] Avoid reporting when ndigits is possibly negative (RUF057) (#15234) 2025-01-03 19:48:03 +01:00
Douglas Creager
75015b0ed9 Attribute panics to the mdtests that cause them (#15241)
This updates the mdtest harness to catch any panics that occur during
type checking, and to display the panic message as an mdtest failure.
(We don't know which specific line causes the failure, so we attribute
panics to the first line of the test case.)
2025-01-03 13:45:56 -05:00
Dylan
706d87f239 Show errors for attempted fixes only when passed --verbose (#15237)
The default logging level for diagnostics includes logs written using
the `log` crate with level `error`, `warn`, and `info`. An unsuccessful
fix attached to a diagnostic via `try_set_fix` or `try_set_optional_fix`
was logged at level `error`. Note that the user would see these messages
even without passing `--fix`, and possibly also on lines with `noqa`
comments.

This PR changes the logging level here to a `debug`. We also found
ad-hoc instances of error logging in the implementations of several
rules, and have replaced those with either a `debug` or call to
`try_set{_optional}_fix`.

Closes #15229
2025-01-03 08:50:13 -06:00
w0nder1ng
0837cdd931 [RUF] Add rule to detect empty literal in deque call (RUF025) (#15104) 2025-01-03 11:57:13 +01:00
Mike Bernard
0dbfa8d0e0 TD003: remove issue code length restriction (#15175)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-03 10:42:04 +01:00
Micha Reiser
1218bc65ed Preserve multiline implicit concatenated strings in docstring positions (#15126) 2025-01-03 10:27:14 +01:00
InSync
6180f78da4 [pyflakes] Ignore errors in @no_type_check string annotations (F722, F821) (#15215) 2025-01-03 10:05:45 +01:00
Wei Lee
835b453bfd style(AIR302): rename removed_airflow_plugin_extension as check_airflow_plugin_extension (#15233)
## Summary

during the previous refactor, this renaming was missed

## Test Plan

no functionality changed
2025-01-03 10:37:21 +05:30
Auguste Lalande
a3d873ef66 [pylint] Re-implement unreachable (PLW0101) (#10891)
## Summary

This PR re-introduces the control-flow graph implementation which was
first introduced in #5384, and then removed in #9463 due to not being
feature complete. Mainly, it lacked the ability to process
`try`-`except` blocks, along with some more minor bugs.

Closes #8958 and #8959 and #14881.

## Overview of Changes

I will now highlight the major changes implemented in this PR, in order
of implementation.

1. Introduced a post-processing step in loop handling to find any
`continue` or `break` statements within the loop body and redirect them
appropriately.
2. Introduced a loop-continue block which is always placed at the end of
loop blocks, and ensures proper looping regardless of the internal logic
of the block. This resolves #8958.
3. Implemented `try` processing with the following logic (resolves
#8959):
1. In the example below the cfg first encounters a conditional
`ExceptionRaised` forking if an exception was (or will be) raised in the
try block. This is not possible to know (except for trivial cases) so we
assume both paths can be taken unconditionally.
2. Going down the `try` path the cfg goes `try`->`else`->`finally`
unconditionally.
3. Going down the `except` path the cfg will meet several conditional
`ExceptionCaught` which fork depending on the nature of the exception
caught. Again there's no way to know which exceptions may be raised so
both paths are assumed to be taken unconditionally.
4. If none of the exception blocks catch the exception then the cfg
terminates by raising a new exception.
5. A post-processing step is also implemented to redirect any `raises`
or `returns` within the blocks appropriately.
```python
def func():
    try:
        print("try")
    except Exception:
        print("Exception")
    except OtherException as e:
        print("OtherException")
    else:
        print("else")
    finally:
        print("finally")
```
```mermaid
flowchart TD
  start(("Start"))
  return(("End"))
  block0[["`*(empty)*`"]]
  block1["print(#quot;finally#quot;)\n"]
  block2["print(#quot;else#quot;)\n"]
  block3["print(#quot;try#quot;)\n"]
  block4[["Exception raised"]]
  block5["print(#quot;OtherException#quot;)\n"]
  block6["try:
        print(#quot;try#quot;)
    except Exception:
        print(#quot;Exception#quot;)
    except OtherException as e:
        print(#quot;OtherException#quot;)
    else:
        print(#quot;else#quot;)
    finally:
        print(#quot;finally#quot;)\n"]
  block7["print(#quot;Exception#quot;)\n"]
  block8["try:
        print(#quot;try#quot;)
    except Exception:
        print(#quot;Exception#quot;)
    except OtherException as e:
        print(#quot;OtherException#quot;)
    else:
        print(#quot;else#quot;)
    finally:
        print(#quot;finally#quot;)\n"]
  block9["try:
        print(#quot;try#quot;)
    except Exception:
        print(#quot;Exception#quot;)
    except OtherException as e:
        print(#quot;OtherException#quot;)
    else:
        print(#quot;else#quot;)
    finally:
        print(#quot;finally#quot;)\n"]

  start --> block9
  block9 -- "Exception raised" --> block8
  block9 -- "else" --> block3
  block8 -- "Exception" --> block7
  block8 -- "else" --> block6
  block7 --> block1
  block6 -- "OtherException" --> block5
  block6 -- "else" --> block4
  block5 --> block1
  block4 --> return
  block3 --> block2
  block2 --> block1
  block1 --> block0
  block0 --> return
``` 
6. Implemented `with` processing with the following logic:
1. `with` statements have no conditional execution (apart from the
hidden logic handling the enter and exit), so the block is assumed to
execute unconditionally.
2. The one exception is that exceptions raised within the block may
result in control flow resuming at the end of the block. Since it is not
possible know if an exception will be raised, or if it will be handled
by the context manager, we assume that execution always continues after
`with` blocks even if the blocks contain `raise` or `return` statements.
This is handled in a post-processing step.

## Test Plan

Additional test fixtures and control-flow fixtures were added.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-01-02 21:54:59 -06:00
Wei Lee
d464ef67cf refactor(AIR303): move duplicate qualified_name.to_string() to Diagnostic argument (#15220)
## Summary

Refactor airflow rule logic like
86bdc2e7b1

## Test Plan

No functionality change. Existing test cases work as it was
2025-01-03 09:10:37 +05:30
Charlie Marsh
2355472d61 Misc. clean up to rounding rules (#15231) 2025-01-02 17:51:35 -05:00
Charlie Marsh
3c3f35a548 Avoid syntax error when removing int over multiple lines (#15230)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15226.
2025-01-02 17:43:15 -05:00
renovate[bot]
2327082c43 Migrate renovate config (#15228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-01-02 21:53:45 +00:00
David Peter
7671a3bbc7 Remove Type::tuple in favor of TupleType::from_elements (#15218)
## Summary

Remove `Type::tuple` in favor of `TupleType::from_elements`, avoid a few
intermediate `Vec`tors. Resolves an old [review
comment](https://github.com/astral-sh/ruff/pull/14744#discussion_r1867493706).

## Test Plan

New regression test for something I ran into while implementing this.
2025-01-02 17:22:32 +01:00
396 changed files with 23198 additions and 11180 deletions

View File

@@ -45,7 +45,7 @@
groupName: "Artifact GitHub Actions dependencies",
matchManagers: ["github-actions"],
matchDatasources: ["gitea-tags", "github-tags"],
matchPackagePatterns: ["actions/.*-artifact"],
matchPackageNames: ["actions/.*-artifact"],
description: "Weekly update of artifact-related GitHub Actions dependencies",
},
{
@@ -61,7 +61,7 @@
{
// Disable updates of `zip-rs`; intentionally pinned for now due to ownership change
// See: https://github.com/astral-sh/uv/issues/3642
matchPackagePatterns: ["zip"],
matchPackageNames: ["zip"],
matchManagers: ["cargo"],
enabled: false,
},
@@ -70,7 +70,7 @@
// with `mkdocs-material-insider`.
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
matchManagers: ["pip_requirements"],
matchPackagePatterns: ["mkdocs-material"],
matchPackageNames: ["mkdocs-material"],
enabled: false,
},
{
@@ -87,13 +87,13 @@
{
groupName: "Monaco",
matchManagers: ["npm"],
matchPackagePatterns: ["monaco"],
matchPackageNames: ["monaco"],
description: "Weekly update of the Monaco editor",
},
{
groupName: "strum",
matchManagers: ["cargo"],
matchPackagePatterns: ["strum"],
matchPackageNames: ["strum"],
description: "Weekly update of strum dependencies",
},
{

View File

@@ -48,11 +48,13 @@ jobs:
- name: Check tag consistency
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
env:
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ fromJson(inputs.plan).announcement_tag }}" != "${version}" ]; then
if [ "${TAG}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ fromJson(inputs.plan).announcement_tag }}" >&2
echo "${TAG}" >&2
echo "${version}" >&2
exit 1
else
@@ -175,6 +177,8 @@ jobs:
- name: Generate Dynamic Dockerfile Tags
shell: bash
env:
TAG_VALUE: ${{ fromJson(inputs.plan).announcement_tag }}
run: |
set -euo pipefail
@@ -195,8 +199,8 @@ jobs:
# Loop through all base tags and append its docker metadata pattern to the list
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
IFS=','; for TAG in ${BASE_TAGS}; do
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${TAG_VALUE}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${TAG_VALUE}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n"
done

View File

@@ -386,7 +386,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: ./scripts/add_rule.py --name DoTheThing --prefix PL --code C0999 --linter pylint
- run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes
- run: cargo check
- run: cargo fmt --all --check
- run: |

View File

@@ -73,6 +73,6 @@ jobs:
owner: "astral-sh",
repo: "ruff",
title: `Daily parser fuzz failed on ${new Date().toDateString()}`,
body: "Runs listed here: https://github.com/astral-sh/ruff/actions/workflows/daily_fuzz.yml",
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
labels: ["bug", "parser", "fuzzer"],
})

View File

@@ -0,0 +1,71 @@
name: Daily property test run
on:
workflow_dispatch:
schedule:
- cron: "0 12 * * *"
pull_request:
paths:
- ".github/workflows/daily_property_tests.yaml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
FORCE_COLOR: 1
jobs:
property_tests:
name: Property tests
runs-on: ubuntu-latest
timeout-minutes: 20
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@v2
- name: Build Red Knot
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.
run: cargo build --locked --release --package red_knot_python_semantic --tests
- name: Run property tests
shell: bash
run: |
export QUICKCHECK_TESTS=100000
for _ in {1..5}; do
cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable
done
create-issue-on-failure:
name: Create an issue if the daily property test run surfaced any bugs
runs-on: ubuntu-latest
needs: property_tests
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.property_tests.result == 'failure' }}
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.create({
owner: "astral-sh",
repo: "ruff",
title: `Daily property test run failed on ${new Date().toDateString()}`,
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
labels: ["bug", "red-knot", "testing"],
})

View File

@@ -33,8 +33,9 @@ jobs:
python-version: 3.12
- name: "Set docs version"
env:
version: ${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}
run: |
version="${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}"
# if version is missing, use 'latest'
if [ -z "$version" ]; then
echo "Using 'latest' as version"

6
.github/zizmor.yml vendored
View File

@@ -1,6 +1,12 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
#
# TODO: can we remove the ignores here so that our workflows are more secure?
rules:
dangerous-triggers:
ignore:
- pr-comment.yaml
cache-poisoning:
ignore:
- build-docker.yml
- publish-playground.yml

View File

@@ -59,7 +59,7 @@ repos:
- black==24.10.0
- repo: https://github.com/crate-ci/typos
rev: v1.28.4
rev: v1.29.4
hooks:
- id: typos
@@ -73,7 +73,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
rev: v0.8.6
hooks:
- id: ruff-format
- id: ruff
@@ -91,7 +91,7 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v0.10.0
rev: v1.0.0
hooks:
- id: zizmor
@@ -103,7 +103,7 @@ repos:
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
- repo: https://github.com/rhysd/actionlint
rev: v1.7.5
rev: v1.7.6
hooks:
- id: actionlint
stages:

View File

@@ -1,5 +1,9 @@
# Breaking Changes
## 0.9.0
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the [changelog](./CHANGELOG.md#090) for a detailed list of changes.
## 0.8.0
- **Default to Python 3.9**

View File

@@ -1,5 +1,139 @@
# Changelog
## 0.9.1
### Preview features
- \[`pycodestyle`\] Run `too-many-newlines-at-end-of-file` on each cell in notebooks (`W391`) ([#15308](https://github.com/astral-sh/ruff/pull/15308))
- \[`ruff`\] Omit diagnostic for shadowed private function parameters in `used-dummy-variable` (`RUF052`) ([#15376](https://github.com/astral-sh/ruff/pull/15376))
### Rule changes
- \[`flake8-bugbear`\] Improve `assert-raises-exception` message (`B017`) ([#15389](https://github.com/astral-sh/ruff/pull/15389))
### Formatter
- Preserve trailing end-of line comments for the last string literal in implicitly concatenated strings ([#15378](https://github.com/astral-sh/ruff/pull/15378))
### Server
- Fix a bug where the server and client notebooks were out of sync after reordering cells ([#15398](https://github.com/astral-sh/ruff/pull/15398))
### Bug fixes
- \[`flake8-pie`\] Correctly remove wrapping parentheses (`PIE800`) ([#15394](https://github.com/astral-sh/ruff/pull/15394))
- \[`pyupgrade`\] Handle comments and multiline expressions correctly (`UP037`) ([#15337](https://github.com/astral-sh/ruff/pull/15337))
## 0.9.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.9.0) for a migration guide and overview of the changes!
### Breaking changes
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the formatter section for a detailed list of changes.
This release doesnt remove or remap any existing stable rules.
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`).
This rule has also been renamed: previously, it was called `builtin-module-shadowing`.
- [`builtin-lambda-argument-shadowing`](https://docs.astral.sh/ruff/rules/builtin-lambda-argument-shadowing/) (`A006`)
- [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`)
- [`boolean-chained-comparison`](https://docs.astral.sh/ruff/rules/boolean-chained-comparison/) (`PLR1716`)
- [`decimal-from-float-literal`](https://docs.astral.sh/ruff/rules/decimal-from-float-literal/) (`RUF032`)
- [`post-init-default`](https://docs.astral.sh/ruff/rules/post-init-default/) (`RUF033`)
- [`useless-if-else`](https://docs.astral.sh/ruff/rules/useless-if-else/) (`RUF034`)
The following behaviors have been stabilized:
- [`pytest-parametrize-names-wrong-type`](https://docs.astral.sh/ruff/rules/pytest-parametrize-names-wrong-type/) (`PT006`): Detect [`pytest.parametrize`](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#parametrize) calls outside decorators and calls with keyword arguments.
- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`): Ignore [`pytest.importorskip`](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest-importorskip) calls between import statements.
- [`mutable-dataclass-default`](https://docs.astral.sh/ruff/rules/mutable-dataclass-default/) (`RUF008`) and [`function-call-in-dataclass-default-argument`](https://docs.astral.sh/ruff/rules/function-call-in-dataclass-default-argument/) (`RUF009`): Add support for [`attrs`](https://www.attrs.org/en/stable/).
- [`bad-version-info-comparison`](https://docs.astral.sh/ruff/rules/bad-version-info-comparison/) (`PYI006`): Extend the rule to check non-stub files.
The following fixes or improvements to fixes have been stabilized:
- [`redundant-numeric-union`](https://docs.astral.sh/ruff/rules/redundant-numeric-union/) (`PYI041`)
- [`duplicate-union-members`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`)
### Formatter
This release introduces the new 2025 stable style ([#13371](https://github.com/astral-sh/ruff/issues/13371)), stabilizing the following changes:
- Format expressions in f-string elements ([#7594](https://github.com/astral-sh/ruff/issues/7594))
- Alternate quotes for strings inside f-strings ([#13860](https://github.com/astral-sh/ruff/pull/13860))
- Preserve the casing of hex codes in f-string debug expressions ([#14766](https://github.com/astral-sh/ruff/issues/14766))
- Choose the quote style for each string literal in an implicitly concatenated f-string rather than for the entire string ([#13539](https://github.com/astral-sh/ruff/pull/13539))
- Automatically join an implicitly concatenated string into a single string literal if it fits on a single line ([#9457](https://github.com/astral-sh/ruff/issues/9457))
- Remove the [`ISC001`](https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/) incompatibility warning ([#15123](https://github.com/astral-sh/ruff/pull/15123))
- Prefer parenthesizing the `assert` message over breaking the assertion expression ([#9457](https://github.com/astral-sh/ruff/issues/9457))
- Automatically parenthesize over-long `if` guards in `match` `case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513))
- More consistent formatting for `match` `case` patterns ([#6933](https://github.com/astral-sh/ruff/issues/6933))
- Avoid unnecessary parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381))
- Keep the opening parentheses on the same line as the `if` keyword for comprehensions where the condition has a leading comment ([#12282](https://github.com/astral-sh/ruff/pull/12282))
- More consistent formatting for `with` statements with a single context manager for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
- Correctly calculate the line-width for code blocks in docstrings when using `max-doc-code-line-length = "dynamic"` ([#13523](https://github.com/astral-sh/ruff/pull/13523))
### Preview features
- \[`flake8-bugbear`\] Implement `class-as-data-structure` (`B903`) ([#9601](https://github.com/astral-sh/ruff/pull/9601))
- \[`flake8-type-checking`\] Apply `quoted-type-alias` more eagerly in `TYPE_CHECKING` blocks and ignore it in stubs (`TC008`) ([#15180](https://github.com/astral-sh/ruff/pull/15180))
- \[`pylint`\] Ignore `eq-without-hash` in stub files (`PLW1641`) ([#15310](https://github.com/astral-sh/ruff/pull/15310))
- \[`pyupgrade`\] Split `UP007` into two individual rules: `UP007` for `Union` and `UP045` for `Optional` (`UP007`, `UP045`) ([#15313](https://github.com/astral-sh/ruff/pull/15313))
- \[`ruff`\] New rule that detects classes that are both an enum and a `dataclass` (`RUF049`) ([#15299](https://github.com/astral-sh/ruff/pull/15299))
- \[`ruff`\] Recode `RUF025` to `RUF037` (`RUF037`) ([#15258](https://github.com/astral-sh/ruff/pull/15258))
### Rule changes
- \[`flake8-builtins`\] Ignore [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) in stub files(`A005`) ([#15350](https://github.com/astral-sh/ruff/pull/15350))
- \[`flake8-return`\] Add support for functions returning `typing.Never` (`RET503`) ([#15298](https://github.com/astral-sh/ruff/pull/15298))
### Server
- Improve the observability by removing the need for the ["trace" value](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue) to turn on or off logging. The server logging is solely controlled using the [`logLevel` server setting](https://docs.astral.sh/ruff/editors/settings/#loglevel)
which defaults to `info`. This addresses the issue where users were notified about an error and told to consult the log, but it didnt contain any messages. ([#15232](https://github.com/astral-sh/ruff/pull/15232))
- Ignore diagnostics from other sources for code action requests ([#15373](https://github.com/astral-sh/ruff/pull/15373))
### CLI
- Improve the error message for `--config key=value` when the `key` is for a table and its a simple `value`
### Bug fixes
- \[`eradicate`\] Ignore metadata blocks directly followed by normal blocks (`ERA001`) ([#15330](https://github.com/astral-sh/ruff/pull/15330))
- \[`flake8-django`\] Recognize other magic methods (`DJ012`) ([#15365](https://github.com/astral-sh/ruff/pull/15365))
- \[`pycodestyle`\] Avoid false positives related to type aliases (`E252`) ([#15356](https://github.com/astral-sh/ruff/pull/15356))
- \[`pydocstyle`\] Avoid treating newline-separated sections as sub-sections (`D405`) ([#15311](https://github.com/astral-sh/ruff/pull/15311))
- \[`pyflakes`\] Remove call when removing final argument from `format` (`F523`) ([#15309](https://github.com/astral-sh/ruff/pull/15309))
- \[`refurb`\] Mark fix as unsafe when the right-hand side is a string (`FURB171`) ([#15273](https://github.com/astral-sh/ruff/pull/15273))
- \[`ruff`\] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) ([#15318](https://github.com/astral-sh/ruff/pull/15318))
- \[`ruff`\] Parenthesize the `int`-call argument when removing the `int` call would change semantics (`RUF046`) ([#15277](https://github.com/astral-sh/ruff/pull/15277))
## 0.8.6
### Preview features
- \[`format`\]: Preserve multiline implicit concatenated strings in docstring positions ([#15126](https://github.com/astral-sh/ruff/pull/15126))
- \[`ruff`\] Add rule to detect empty literal in deque call (`RUF025`) ([#15104](https://github.com/astral-sh/ruff/pull/15104))
- \[`ruff`\] Avoid reporting when `ndigits` is possibly negative (`RUF057`) ([#15234](https://github.com/astral-sh/ruff/pull/15234))
### Rule changes
- \[`flake8-todos`\] remove issue code length restriction (`TD003`) ([#15175](https://github.com/astral-sh/ruff/pull/15175))
- \[`pyflakes`\] Ignore errors in `@no_type_check` string annotations (`F722`, `F821`) ([#15215](https://github.com/astral-sh/ruff/pull/15215))
### CLI
- Show errors for attempted fixes only when passed `--verbose` ([#15237](https://github.com/astral-sh/ruff/pull/15237))
### Bug fixes
- \[`ruff`\] Avoid syntax error when removing int over multiple lines (`RUF046`) ([#15230](https://github.com/astral-sh/ruff/pull/15230))
- \[`pyupgrade`\] Revert "Add all PEP-585 names to `UP006` rule" ([#15250](https://github.com/astral-sh/ruff/pull/15250))
## 0.8.5
### Preview features

202
Cargo.lock generated
View File

@@ -220,9 +220,9 @@ checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
[[package]]
name = "bstr"
version = "1.11.1"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
dependencies = [
"memchr",
"regex-automata 0.4.8",
@@ -291,12 +291,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@@ -413,7 +407,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -424,15 +418,15 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clearscreen"
version = "3.0.0"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f8c93eb5f77c9050c7750e14f13ef1033a40a0aac70c6371535b6763a01438c"
checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
dependencies = [
"nix 0.28.0",
"nix",
"terminfo",
"thiserror 1.0.67",
"thiserror 2.0.9",
"which",
"winapi",
"windows-sys 0.59.0",
]
[[package]]
@@ -470,7 +464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -668,7 +662,7 @@ version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
dependencies = [
"nix 0.29.0",
"nix",
"windows-sys 0.59.0",
]
@@ -693,7 +687,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -704,7 +698,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -774,16 +768,7 @@ dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 2.0.93",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys 0.3.7",
"syn 2.0.95",
]
[[package]]
@@ -792,18 +777,7 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys 0.4.1",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
"dirs-sys",
]
[[package]]
@@ -826,7 +800,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -875,6 +849,12 @@ dependencies = [
"regex",
]
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "env_logger"
version = "0.11.6"
@@ -1246,7 +1226,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -1365,14 +1345,14 @@ dependencies = [
[[package]]
name = "insta"
version = "1.41.1"
version = "1.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8"
checksum = "6513e4067e16e69ed1db5ab56048ed65db32d10ba5fc1217f5393f8f17d8b5a5"
dependencies = [
"console",
"globset",
"lazy_static",
"linked-hash-map",
"once_cell",
"pest",
"pest_derive",
"regex",
@@ -1420,7 +1400,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -1462,6 +1442,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
@@ -1547,7 +1536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
dependencies = [
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -1647,9 +1636,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0aa4b8ca861b08d68afc8702af3250776898c1508b278e1da9d01e01d4b45c"
checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9"
[[package]]
name = "memchr"
@@ -1719,18 +1708,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nix"
version = "0.29.0"
@@ -1739,7 +1716,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases 0.2.1",
"cfg_aliases",
"libc",
]
@@ -2015,7 +1992,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -2293,7 +2270,7 @@ dependencies = [
"hashbrown 0.15.2",
"indexmap",
"insta",
"itertools 0.13.0",
"itertools 0.14.0",
"memchr",
"ordermap",
"quickcheck",
@@ -2520,7 +2497,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.8.5"
version = "0.9.1"
dependencies = [
"anyhow",
"argfile",
@@ -2540,7 +2517,7 @@ dependencies = [
"insta",
"insta-cmd",
"is-macro",
"itertools 0.13.0",
"itertools 0.14.0",
"log",
"mimalloc",
"notify",
@@ -2606,7 +2583,7 @@ dependencies = [
"filetime",
"glob",
"globset",
"itertools 0.13.0",
"itertools 0.14.0",
"regex",
"ruff_macros",
"seahash",
@@ -2655,7 +2632,7 @@ dependencies = [
"imara-diff",
"indicatif",
"indoc",
"itertools 0.13.0",
"itertools 0.14.0",
"libcst",
"pretty_assertions",
"rayon",
@@ -2739,7 +2716,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.8.5"
version = "0.9.1"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2755,7 +2732,7 @@ dependencies = [
"insta",
"is-macro",
"is-wsl",
"itertools 0.13.0",
"itertools 0.14.0",
"libcst",
"log",
"memchr",
@@ -2768,6 +2745,7 @@ dependencies = [
"regex",
"ruff_cache",
"ruff_diagnostics",
"ruff_index",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
@@ -2802,11 +2780,11 @@ dependencies = [
name = "ruff_macros"
version = "0.0.0"
dependencies = [
"itertools 0.13.0",
"itertools 0.14.0",
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -2814,7 +2792,7 @@ name = "ruff_notebook"
version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.13.0",
"itertools 0.14.0",
"rand",
"ruff_diagnostics",
"ruff_source_file",
@@ -2835,7 +2813,7 @@ dependencies = [
"bitflags 2.6.0",
"compact_str",
"is-macro",
"itertools 0.13.0",
"itertools 0.14.0",
"memchr",
"ruff_cache",
"ruff_macros",
@@ -2877,7 +2855,7 @@ dependencies = [
"clap",
"countme",
"insta",
"itertools 0.13.0",
"itertools 0.14.0",
"memchr",
"regex",
"ruff_cache",
@@ -2915,7 +2893,7 @@ name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.6.0",
"itertools 0.13.0",
"itertools 0.14.0",
"ruff_python_ast",
"unic-ucd-category",
]
@@ -2984,7 +2962,7 @@ dependencies = [
name = "ruff_python_trivia"
version = "0.0.0"
dependencies = [
"itertools 0.13.0",
"itertools 0.14.0",
"ruff_source_file",
"ruff_text_size",
"unicode-ident",
@@ -3055,7 +3033,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.8.5"
version = "0.9.1"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3089,7 +3067,7 @@ dependencies = [
"globset",
"ignore",
"is-macro",
"itertools 0.13.0",
"itertools 0.14.0",
"log",
"matchit",
"path-absolutize",
@@ -3197,7 +3175,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
dependencies = [
"append-only-vec",
"arc-swap",
@@ -3205,7 +3183,6 @@ dependencies = [
"dashmap 6.1.0",
"hashlink",
"indexmap",
"lazy_static",
"parking_lot",
"rayon",
"rustc-hash 2.1.0",
@@ -3218,17 +3195,17 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"synstructure",
]
@@ -3262,7 +3239,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3311,7 +3288,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3322,7 +3299,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3345,7 +3322,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3386,7 +3363,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3415,7 +3392,7 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
dependencies = [
"dirs 5.0.1",
"dirs",
]
[[package]]
@@ -3500,7 +3477,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3522,9 +3499,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.93"
version = "2.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
dependencies = [
"proc-macro2",
"quote",
@@ -3539,7 +3516,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3567,11 +3544,10 @@ dependencies = [
[[package]]
name = "terminfo"
version = "0.8.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f"
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
dependencies = [
"dirs 4.0.0",
"fnv",
"nom",
"phf",
@@ -3602,7 +3578,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3613,7 +3589,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"test-case-core",
]
@@ -3643,7 +3619,7 @@ checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3654,7 +3630,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -3776,7 +3752,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -4046,7 +4022,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -4141,7 +4117,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"wasm-bindgen-shared",
]
@@ -4176,7 +4152,7 @@ checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4210,7 +4186,7 @@ checksum = "222ebde6ea87fbfa6bdd2e9f1fd8a91d60aee5db68792632176c4e16a74fc7d8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -4244,12 +4220,12 @@ dependencies = [
[[package]]
name = "which"
version = "6.0.1"
version = "7.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028"
dependencies = [
"either",
"home",
"env_home",
"rustix",
"winsafe",
]
@@ -4513,7 +4489,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"synstructure",
]
@@ -4534,7 +4510,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@@ -4554,7 +4530,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"synstructure",
]
@@ -4583,7 +4559,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]

View File

@@ -55,7 +55,7 @@ camino = { version = "1.1.7" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "3.0.0" }
clearscreen = { version = "4.0.0" }
codspeed-criterion-compat = { version = "2.6.0", default-features = false }
colored = { version = "2.1.0" }
console_error_panic_hook = { version = "0.1.7" }
@@ -89,7 +89,7 @@ insta = { version = "1.35.1" }
insta-cmd = { version = "0.6.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.13.0" }
itertools = { version = "0.14.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "0.1.2" }
libc = { version = "0.2.153" }
@@ -119,7 +119,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3c7f1694c9efba751dbeeacfbc93b227586e316a" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -211,6 +211,9 @@ redundant_clone = "warn"
debug_assert_with_mut_call = "warn"
unused_peekable = "warn"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times

View File

@@ -116,12 +116,21 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
### Installation
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI.
Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/):
```shell
uvx ruff check # Lint all files in the current directory.
uvx ruff format # Format all files in the current directory.
```
Or install Ruff with `uv` (recommended), `pip`, or `pipx`:
```shell
# With uv.
uv add --dev ruff # to add ruff to your project
uv tool install ruff # to install ruff globally
uv tool install ruff@latest # Install Ruff globally.
uv add --dev ruff # Or add Ruff to your project.
# With pip.
pip install ruff
@@ -140,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.8.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.5/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.9.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.1/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -174,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.8.5
rev: v0.9.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -9,8 +9,6 @@ from typing import Literal
from enum import Enum
mode: Literal["w", "r"]
mode2: Literal["w"] | Literal["r"]
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
a1: Literal[26]
a2: Literal[0x1A]
a3: Literal[-4]
@@ -19,7 +17,6 @@ a5: Literal[b"hello world"]
a6: Literal[True]
a7: Literal[None]
a8: Literal[Literal[1]]
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]
class Color(Enum):
RED = 0
@@ -30,9 +27,6 @@ b1: Literal[Color.RED]
def f():
reveal_type(mode) # revealed: Literal["w", "r"]
reveal_type(mode2) # revealed: Literal["w", "r"]
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
reveal_type(a1) # revealed: Literal[26]
reveal_type(a2) # revealed: Literal[26]
reveal_type(a3) # revealed: Literal[-4]
@@ -41,7 +35,6 @@ def f():
reveal_type(a6) # revealed: Literal[True]
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
# TODO: This should be Color.RED
reveal_type(b1) # revealed: Literal[0]
@@ -61,6 +54,63 @@ invalid4: Literal[
]
```
## Shortening unions of literals
When a Literal is parameterized with more than one value, its treated as exactly to equivalent to
the union of those types.
```py
from typing import Literal
def x(
a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None],
a2: Literal["w"] | Literal["r"],
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
):
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(a2) # revealed: Literal["w", "r"]
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
```
## Display of heterogeneous unions of literals
```py
from typing import Literal, Union
def foo(x: int) -> int:
return x + 1
def bar(s: str) -> str:
return s
class A: ...
class B: ...
def union_example(
x: Union[
# unknown type
# error: [unresolved-reference]
y,
Literal[-1],
Literal["A"],
Literal[b"A"],
Literal[b"\x00"],
Literal[b"\x07"],
Literal[0],
Literal[1],
Literal["B"],
Literal["foo"],
Literal["bar"],
Literal["B"],
Literal[True],
None,
]
):
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
```
## Detecting Literal outside typing and typing_extensions
Only Literal that is defined in typing and typing_extension modules is detected as the special

View File

@@ -107,7 +107,7 @@ def _(flag: bool):
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
baz_3 = "foo" if flag else 1
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
reveal_type(baz_3) # revealed: Literal["foo", 1]
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
```

View File

@@ -105,7 +105,7 @@ def f1(
from typing import Literal
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"]
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
```
## Class variables

View File

@@ -122,3 +122,10 @@ class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
```
## Annotated assignments in stub files are inferred correctly
```pyi path=main.pyi
x: int = 1
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -40,9 +40,9 @@ class C:
return 42
x = C()
# error: [invalid-argument-type]
x -= 1
# TODO: should error, once operand type check is implemented
reveal_type(x) # revealed: int
```

View File

@@ -46,3 +46,50 @@ reveal_type(a | b) # revealed: Literal[True]
reveal_type(b | a) # revealed: Literal[True]
reveal_type(b | b) # revealed: Literal[False]
```
## Arithmetic with a variable
```py
a = True
b = False
def lhs_is_int(x: int):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_int(x: int):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def lhs_is_bool(x: bool):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_bool(x: bool):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def both_are_bool(x: bool, y: bool):
reveal_type(x + y) # revealed: int
reveal_type(x - y) # revealed: int
reveal_type(x * y) # revealed: int
reveal_type(x // y) # revealed: int
reveal_type(x / y) # revealed: float
reveal_type(x % y) # revealed: int
```

View File

@@ -0,0 +1,27 @@
# Binary operations on classes
## Union of two classes
Unioning two classes via the `|` operator is only available in Python 3.10 and later.
```toml
[environment]
python-version = "3.10"
```
```py
class A: ...
class B: ...
reveal_type(A | B) # revealed: UnionType
```
## Union of two classes (prior to 3.10)
```py
class A: ...
class B: ...
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
reveal_type(A | B) # revealed: Unknown
```

View File

@@ -0,0 +1,371 @@
# Custom binary operations
## Class instances
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
def __sub__(self, other) -> Literal["-"]:
return "-"
def __mul__(self, other) -> Literal["*"]:
return "*"
def __matmul__(self, other) -> Literal["@"]:
return "@"
def __truediv__(self, other) -> Literal["/"]:
return "/"
def __mod__(self, other) -> Literal["%"]:
return "%"
def __pow__(self, other) -> Literal["**"]:
return "**"
def __lshift__(self, other) -> Literal["<<"]:
return "<<"
def __rshift__(self, other) -> Literal[">>"]:
return ">>"
def __or__(self, other) -> Literal["|"]:
return "|"
def __xor__(self, other) -> Literal["^"]:
return "^"
def __and__(self, other) -> Literal["&"]:
return "&"
def __floordiv__(self, other) -> Literal["//"]:
return "//"
class Sub(Yes): ...
class No: ...
# Yes implements all of the dunder methods.
reveal_type(Yes() + Yes()) # revealed: Literal["+"]
reveal_type(Yes() - Yes()) # revealed: Literal["-"]
reveal_type(Yes() * Yes()) # revealed: Literal["*"]
reveal_type(Yes() @ Yes()) # revealed: Literal["@"]
reveal_type(Yes() / Yes()) # revealed: Literal["/"]
reveal_type(Yes() % Yes()) # revealed: Literal["%"]
reveal_type(Yes() ** Yes()) # revealed: Literal["**"]
reveal_type(Yes() << Yes()) # revealed: Literal["<<"]
reveal_type(Yes() >> Yes()) # revealed: Literal[">>"]
reveal_type(Yes() | Yes()) # revealed: Literal["|"]
reveal_type(Yes() ^ Yes()) # revealed: Literal["^"]
reveal_type(Yes() & Yes()) # revealed: Literal["&"]
reveal_type(Yes() // Yes()) # revealed: Literal["//"]
# Sub inherits Yes's implementation of the dunder methods.
reveal_type(Sub() + Sub()) # revealed: Literal["+"]
reveal_type(Sub() - Sub()) # revealed: Literal["-"]
reveal_type(Sub() * Sub()) # revealed: Literal["*"]
reveal_type(Sub() @ Sub()) # revealed: Literal["@"]
reveal_type(Sub() / Sub()) # revealed: Literal["/"]
reveal_type(Sub() % Sub()) # revealed: Literal["%"]
reveal_type(Sub() ** Sub()) # revealed: Literal["**"]
reveal_type(Sub() << Sub()) # revealed: Literal["<<"]
reveal_type(Sub() >> Sub()) # revealed: Literal[">>"]
reveal_type(Sub() | Sub()) # revealed: Literal["|"]
reveal_type(Sub() ^ Sub()) # revealed: Literal["^"]
reveal_type(Sub() & Sub()) # revealed: Literal["&"]
reveal_type(Sub() // Sub()) # revealed: Literal["//"]
# No does not implement any of the dunder methods.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`"
reveal_type(No() + No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`"
reveal_type(No() - No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`"
reveal_type(No() * No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`"
reveal_type(No() @ No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`"
reveal_type(No() / No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`"
reveal_type(No() % No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`"
reveal_type(No() ** No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`"
reveal_type(No() << No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`"
reveal_type(No() >> No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`"
reveal_type(No() | No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`"
reveal_type(No() ^ No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`"
reveal_type(No() & No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`"
reveal_type(No() // No()) # revealed: Unknown
# Yes does not implement any of the reflected dunder methods.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() + Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() - Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() * Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() @ Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() / Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() % Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() ** Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() << Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() >> Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() | Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() ^ Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() & Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() // Yes()) # revealed: Unknown
```
## Subclass reflections override superclass dunders
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
def __sub__(self, other) -> Literal["-"]:
return "-"
def __mul__(self, other) -> Literal["*"]:
return "*"
def __matmul__(self, other) -> Literal["@"]:
return "@"
def __truediv__(self, other) -> Literal["/"]:
return "/"
def __mod__(self, other) -> Literal["%"]:
return "%"
def __pow__(self, other) -> Literal["**"]:
return "**"
def __lshift__(self, other) -> Literal["<<"]:
return "<<"
def __rshift__(self, other) -> Literal[">>"]:
return ">>"
def __or__(self, other) -> Literal["|"]:
return "|"
def __xor__(self, other) -> Literal["^"]:
return "^"
def __and__(self, other) -> Literal["&"]:
return "&"
def __floordiv__(self, other) -> Literal["//"]:
return "//"
class Sub(Yes):
def __radd__(self, other) -> Literal["r+"]:
return "r+"
def __rsub__(self, other) -> Literal["r-"]:
return "r-"
def __rmul__(self, other) -> Literal["r*"]:
return "r*"
def __rmatmul__(self, other) -> Literal["r@"]:
return "r@"
def __rtruediv__(self, other) -> Literal["r/"]:
return "r/"
def __rmod__(self, other) -> Literal["r%"]:
return "r%"
def __rpow__(self, other) -> Literal["r**"]:
return "r**"
def __rlshift__(self, other) -> Literal["r<<"]:
return "r<<"
def __rrshift__(self, other) -> Literal["r>>"]:
return "r>>"
def __ror__(self, other) -> Literal["r|"]:
return "r|"
def __rxor__(self, other) -> Literal["r^"]:
return "r^"
def __rand__(self, other) -> Literal["r&"]:
return "r&"
def __rfloordiv__(self, other) -> Literal["r//"]:
return "r//"
class No:
def __radd__(self, other) -> Literal["r+"]:
return "r+"
def __rsub__(self, other) -> Literal["r-"]:
return "r-"
def __rmul__(self, other) -> Literal["r*"]:
return "r*"
def __rmatmul__(self, other) -> Literal["r@"]:
return "r@"
def __rtruediv__(self, other) -> Literal["r/"]:
return "r/"
def __rmod__(self, other) -> Literal["r%"]:
return "r%"
def __rpow__(self, other) -> Literal["r**"]:
return "r**"
def __rlshift__(self, other) -> Literal["r<<"]:
return "r<<"
def __rrshift__(self, other) -> Literal["r>>"]:
return "r>>"
def __ror__(self, other) -> Literal["r|"]:
return "r|"
def __rxor__(self, other) -> Literal["r^"]:
return "r^"
def __rand__(self, other) -> Literal["r&"]:
return "r&"
def __rfloordiv__(self, other) -> Literal["r//"]:
return "r//"
# Subclass reflected dunder methods take precedence over the superclass's regular dunders.
reveal_type(Yes() + Sub()) # revealed: Literal["r+"]
reveal_type(Yes() - Sub()) # revealed: Literal["r-"]
reveal_type(Yes() * Sub()) # revealed: Literal["r*"]
reveal_type(Yes() @ Sub()) # revealed: Literal["r@"]
reveal_type(Yes() / Sub()) # revealed: Literal["r/"]
reveal_type(Yes() % Sub()) # revealed: Literal["r%"]
reveal_type(Yes() ** Sub()) # revealed: Literal["r**"]
reveal_type(Yes() << Sub()) # revealed: Literal["r<<"]
reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"]
reveal_type(Yes() | Sub()) # revealed: Literal["r|"]
reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"]
reveal_type(Yes() & Sub()) # revealed: Literal["r&"]
reveal_type(Yes() // Sub()) # revealed: Literal["r//"]
# But for an unrelated class, the superclass regular dunders are used.
reveal_type(Yes() + No()) # revealed: Literal["+"]
reveal_type(Yes() - No()) # revealed: Literal["-"]
reveal_type(Yes() * No()) # revealed: Literal["*"]
reveal_type(Yes() @ No()) # revealed: Literal["@"]
reveal_type(Yes() / No()) # revealed: Literal["/"]
reveal_type(Yes() % No()) # revealed: Literal["%"]
reveal_type(Yes() ** No()) # revealed: Literal["**"]
reveal_type(Yes() << No()) # revealed: Literal["<<"]
reveal_type(Yes() >> No()) # revealed: Literal[">>"]
reveal_type(Yes() | No()) # revealed: Literal["|"]
reveal_type(Yes() ^ No()) # revealed: Literal["^"]
reveal_type(Yes() & No()) # revealed: Literal["&"]
reveal_type(Yes() // No()) # revealed: Literal["//"]
```
## Classes
Dunder methods defined in a class are available to instances of that class, but not to the class
itself. (For these operators to work on the class itself, they would have to be defined on the
class's type, i.e. `type`.)
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
class Sub(Yes): ...
class No: ...
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`"
reveal_type(Yes + Yes) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`"
reveal_type(Sub + Sub) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`"
reveal_type(No + No) # revealed: Unknown
```
## Subclass
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
class Sub(Yes): ...
class No: ...
def yes() -> type[Yes]:
return Yes
def sub() -> type[Sub]:
return Sub
def no() -> type[No]:
return No
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`"
reveal_type(yes() + yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`"
reveal_type(sub() + sub()) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`"
reveal_type(no() + no()) # revealed: Unknown
```
## Function literals
```py
def f():
pass
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f + f) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f - f) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f * f) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f @ f) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f / f) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f % f) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f**f) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f << f) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f >> f) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f | f) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f ^ f) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f & f) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f // f) # revealed: Unknown
```

View File

@@ -9,6 +9,34 @@ reveal_type(3 * -1) # revealed: Literal[-3]
reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
# formal parameter type.
reveal_type(2 + "f") # revealed: int
def lhs(x: int):
reveal_type(x + 1) # revealed: int
reveal_type(x - 4) # revealed: int
reveal_type(x * -1) # revealed: int
reveal_type(x // 3) # revealed: int
reveal_type(x / 3) # revealed: float
reveal_type(x % 3) # revealed: int
def rhs(x: int):
reveal_type(2 + x) # revealed: int
reveal_type(3 - x) # revealed: int
reveal_type(3 * x) # revealed: int
reveal_type(-3 // x) # revealed: int
reveal_type(-3 / x) # revealed: float
reveal_type(5 % x) # revealed: int
def both(x: int):
reveal_type(x + x) # revealed: int
reveal_type(x - x) # revealed: int
reveal_type(x * x) # revealed: int
reveal_type(x // x) # revealed: int
reveal_type(x / x) # revealed: float
reveal_type(x % x) # revealed: int
```
## Power
@@ -21,6 +49,11 @@ largest_u32 = 4_294_967_295
reveal_type(2**2) # revealed: Literal[4]
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
reveal_type(2**largest_u32) # revealed: int
def variable(x: int):
reveal_type(x**2) # revealed: @Todo(return type)
reveal_type(2**x) # revealed: @Todo(return type)
reveal_type(x**x) # revealed: @Todo(return type)
```
## Division by Zero

View File

@@ -70,3 +70,32 @@ def _(flag: bool):
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
```
## Call binding errors
### Wrong argument type
```py
class C:
def __call__(self, x: int) -> int:
return 1
c = C()
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`"
reveal_type(c("foo")) # revealed: int
```
### Wrong argument type on `self`
```py
class C:
# TODO this definition should also be an error; `C` must be assignable to type of `self`
def __call__(self: int) -> int:
return 1
c = C()
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
reveal_type(c()) # revealed: int
```

View File

@@ -64,3 +64,260 @@ def _(flag: bool):
# error: [possibly-unresolved-reference]
reveal_type(foo()) # revealed: int
```
## Wrong argument type
### Positional argument, positional-or-keyword parameter
```py
def f(x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
reveal_type(f("foo")) # revealed: int
```
### Positional argument, positional-only parameter
```py
def f(x: int, /) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
reveal_type(f("foo")) # revealed: int
```
### Positional argument, variadic parameter
```py
def f(*args: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `*args` of function `f`; expected type `int`"
reveal_type(f("foo")) # revealed: int
```
### Keyword argument, positional-or-keyword parameter
```py
def f(x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
reveal_type(f(x="foo")) # revealed: int
```
### Keyword argument, keyword-only parameter
```py
def f(*, x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
reveal_type(f(x="foo")) # revealed: int
```
### Keyword argument, keywords parameter
```py
def f(**kwargs: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `**kwargs` of function `f`; expected type `int`"
reveal_type(f(x="foo")) # revealed: int
```
### Correctly match keyword out-of-order
```py
def f(x: int = 1, y: str = "foo") -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal[2]` cannot be assigned to parameter `y` of function `f`; expected type `str`"
# error: 20 [invalid-argument-type] "Object of type `Literal["bar"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
reveal_type(f(y=2, x="bar")) # revealed: int
```
## Too many positional arguments
### One too many
```py
def f() -> int:
return 1
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1"
reveal_type(f("foo")) # revealed: int
```
### Two too many
```py
def f() -> int:
return 1
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2"
reveal_type(f("foo", "bar")) # revealed: int
```
### No too-many-positional if variadic is taken
```py
def f(*args: int) -> int:
return 1
reveal_type(f(1, 2, 3)) # revealed: int
```
## Missing arguments
### No defaults or variadic
```py
def f(x: int) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### With default
```py
def f(x: int, y: str = "foo") -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### Defaulted argument is not required
```py
def f(x: int = 1) -> int:
return 1
reveal_type(f()) # revealed: int
```
### With variadic
```py
def f(x: int, *y: str) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### Variadic argument is not required
```py
def f(*args: int) -> int:
return 1
reveal_type(f()) # revealed: int
```
### Keywords argument is not required
```py
def f(**kwargs: int) -> int:
return 1
reveal_type(f()) # revealed: int
```
### Multiple
```py
def f(x: int, y: int) -> int:
return 1
# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`"
reveal_type(f()) # revealed: int
```
## Unknown argument
```py
def f(x: int) -> int:
return 1
# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`"
reveal_type(f(x=1, y=2)) # revealed: int
```
## Parameter already assigned
```py
def f(x: int) -> int:
return 1
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
reveal_type(f(1, x=2)) # revealed: int
```
## Special functions
Some functions require special handling in type inference. Here, we make sure that we still emit
proper diagnostics in case of missing or superfluous arguments.
### `reveal_type`
```py
from typing_extensions import reveal_type
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
reveal_type() # revealed: Unknown
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
reveal_type(1, 2) # revealed: Literal[1]
```
### `static_assert`
```py
from knot_extensions import static_assert
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
# error: [static-assert-error]
static_assert()
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
static_assert(True, 2, 3)
```
### `len`
```py
# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
len()
# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
len([], 1)
```
### Type API predicates
```py
from knot_extensions import is_subtype_of, is_fully_static
# error: [missing-argument]
is_subtype_of()
# error: [missing-argument]
is_subtype_of(int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int, int)
# error: [missing-argument]
is_fully_static()
# error: [too-many-positional-arguments]
is_fully_static(int, int)
```

View File

@@ -0,0 +1,44 @@
# Invalid signatures
## Multiple arguments with the same name
We always map a keyword argument to the first parameter of that name.
```py
# error: [invalid-syntax] "Duplicate parameter "x""
def f(x: int, x: str) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
reveal_type(f(1, x=2)) # revealed: int
```
## Positional after non-positional
When parameter kinds are given in an invalid order, we emit a diagnostic and implicitly reorder them
to the valid order:
```py
# error: [invalid-syntax] "Parameter cannot follow var-keyword parameter"
def f(**kw: int, x: str) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `str`"
reveal_type(f(1)) # revealed: int
```
## Non-defaulted after defaulted
We emit a syntax diagnostic for this, but it doesn't cause any problems for binding.
```py
# error: [invalid-syntax] "Parameter without a default cannot follow a parameter with a default"
def f(x: int = 1, y: str) -> int:
return 1
reveal_type(f(y="foo")) # revealed: int
# error: [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
# error: [missing-argument] "No argument provided for required parameter `y` of function `f`"
reveal_type(f("foo")) # revealed: int
```

View File

@@ -56,7 +56,7 @@ def _(flag: bool, flag2: bool):
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"])"
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
```
@@ -72,6 +72,6 @@ def _(flag: bool):
else:
f = "foo"
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```

View File

@@ -22,7 +22,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
reveal_type(d) # 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"]`"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool

View File

@@ -115,3 +115,35 @@ def _(flag: bool, flag2: bool):
reveal_type(y) # revealed: Literal[2, 3, 4]
```
## if-elif with assignment expressions in tests
```py
def check(x: int) -> bool:
return bool(x)
if check(x := 1):
x = 2
elif check(x := 3):
x = 4
reveal_type(x) # revealed: Literal[2, 3, 4]
```
## constraints apply to later test expressions
```py
def check(x) -> bool:
return bool(x)
def _(flag: bool):
x = 1 if flag else None
y = 0
if x is None:
pass
elif check(y := x):
pass
reveal_type(y) # revealed: Literal[0, 1]
```

View File

@@ -0,0 +1,142 @@
# `assert_type`
## Basic
```py
from typing_extensions import assert_type
def _(x: int):
assert_type(x, int) # fine
assert_type(x, str) # error: [type-assertion-failure]
```
## Narrowing
The asserted type is checked against the inferred type, not the declared type.
```toml
[environment]
python-version = "3.10"
```
```py
from typing_extensions import assert_type
def _(x: int | str):
if isinstance(x, int):
reveal_type(x) # revealed: int
assert_type(x, int) # fine
```
## Equivalence
The actual type must match the asserted type precisely.
```py
from typing import Any, Type, Union
from typing_extensions import assert_type
# Subtype does not count
def _(x: bool):
assert_type(x, int) # error: [type-assertion-failure]
def _(a: type[int], b: type[Any]):
assert_type(a, type[Any]) # error: [type-assertion-failure]
assert_type(b, type[int]) # error: [type-assertion-failure]
# The expression constructing the type is not taken into account
def _(a: type[int]):
assert_type(a, Type[int]) # fine
```
## Gradual types
```py
from typing import Any
from typing_extensions import Literal, assert_type
from knot_extensions import Unknown
# Any and Unknown are considered equivalent
def _(a: Unknown, b: Any):
reveal_type(a) # revealed: Unknown
assert_type(a, Any) # fine
reveal_type(b) # revealed: Any
assert_type(b, Unknown) # fine
def _(a: type[Unknown], b: type[Any]):
reveal_type(a) # revealed: type[Unknown]
assert_type(a, type[Any]) # fine
reveal_type(b) # revealed: type[Any]
assert_type(b, type[Unknown]) # fine
```
## Tuples
Tuple types with the same elements are the same.
```py
from typing_extensions import assert_type
from knot_extensions import Unknown
def _(a: tuple[int, str, bytes]):
assert_type(a, tuple[int, str, bytes]) # fine
assert_type(a, tuple[int, str]) # error: [type-assertion-failure]
assert_type(a, tuple[int, str, bytes, None]) # error: [type-assertion-failure]
assert_type(a, tuple[int, bytes, str]) # error: [type-assertion-failure]
def _(a: tuple[Any, ...], b: tuple[Unknown, ...]):
assert_type(a, tuple[Any, ...]) # fine
assert_type(a, tuple[Unknown, ...]) # fine
assert_type(b, tuple[Unknown, ...]) # fine
assert_type(b, tuple[Any, ...]) # fine
```
## Unions
Unions with the same elements are the same, regardless of order.
```toml
[environment]
python-version = "3.10"
```
```py
from typing_extensions import assert_type
def _(a: str | int):
assert_type(a, str | int) # fine
# TODO: Order-independent union handling in type equivalence
assert_type(a, int | str) # error: [type-assertion-failure]
```
## Intersections
Intersections are the same when their positive and negative parts are respectively the same,
regardless of order.
```py
from typing_extensions import assert_type
from knot_extensions import Intersection, Not
class A: ...
class B: ...
class C: ...
class D: ...
def _(a: A):
if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D):
reveal_type(a) # revealed: A & B & ~C & ~D
assert_type(a, Intersection[A, B, Not[C], Not[D]]) # fine
# TODO: Order-independent intersection handling in type equivalence
assert_type(a, Intersection[B, A, Not[D], Not[C]]) # error: [type-assertion-failure]
```

View File

@@ -17,7 +17,7 @@ def _(flag: bool):
reveal_type(A.always_bound) # revealed: Literal[1]
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
reveal_type(A.union) # revealed: Literal[1, "abc"]
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]

View File

@@ -31,9 +31,9 @@ The test inside an if expression should not affect code outside of the expressio
def _(flag: bool):
x: Literal[42, "hello"] = 42 if flag else "hello"
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
reveal_type(x) # revealed: Literal[42, "hello"]
_ = ... if isinstance(x, str) else ...
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
reveal_type(x) # revealed: Literal[42, "hello"]
```

View File

@@ -119,7 +119,7 @@ class ZeroOrStr:
reveal_type(len(Zero())) # revealed: Literal[0]
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0]
# TODO: Emit a diagnostic
reveal_type(len(OneOrFoo())) # revealed: int

View File

@@ -0,0 +1,748 @@
# Intersection types
## Introduction
This test suite covers certain properties of intersection types and makes sure that we can apply
various simplification strategies. We use `Intersection` (`&`) and `Not` (`~`) to construct
intersection types (note that we display negative contributions at the end; the order does not
matter):
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
def _(
i1: Intersection[P, Q],
i2: Intersection[P, Not[Q]],
i3: Intersection[Not[P], Q],
i4: Intersection[Not[P], Not[Q]],
) -> None:
reveal_type(i1) # revealed: P & Q
reveal_type(i2) # revealed: P & ~Q
reveal_type(i3) # revealed: Q & ~P
reveal_type(i4) # revealed: ~P & ~Q
```
## Notation
Throughout this document, we use the following types as representatives for certain equivalence
classes.
### Non-disjoint types
We use `P`, `Q`, `R`, … to denote types that are non-disjoint:
```py
from knot_extensions import static_assert, is_disjoint_from
class P: ...
class Q: ...
class R: ...
static_assert(not is_disjoint_from(P, Q))
static_assert(not is_disjoint_from(P, R))
static_assert(not is_disjoint_from(Q, R))
```
Although `P` is not a subtype of `Q` and `Q` is not a subtype of `P`, the two types are not disjoint
because it would be possible to create a class `S` that inherits from both `P` and `Q` using
multiple inheritance. An instance of `S` would be a member of the `P` type _and_ the `Q` type.
### Disjoint types
We use `Literal[1]`, `Literal[2]`, … as examples of pairwise-disjoint types, and `int` as a joint
supertype of these:
```py
from knot_extensions import static_assert, is_disjoint_from, is_subtype_of
from typing import Literal
static_assert(is_disjoint_from(Literal[1], Literal[2]))
static_assert(is_disjoint_from(Literal[1], Literal[3]))
static_assert(is_disjoint_from(Literal[2], Literal[3]))
static_assert(is_subtype_of(Literal[1], int))
static_assert(is_subtype_of(Literal[2], int))
static_assert(is_subtype_of(Literal[3], int))
```
### Subtypes
Finally, we use `A <: B <: C` and `A <: B1`, `A <: B2` to denote hierarchies of (proper) subtypes:
```py
from knot_extensions import static_assert, is_subtype_of, is_disjoint_from
class A: ...
class B(A): ...
class C(B): ...
static_assert(is_subtype_of(B, A))
static_assert(is_subtype_of(C, B))
static_assert(is_subtype_of(C, A))
static_assert(not is_subtype_of(A, B))
static_assert(not is_subtype_of(B, C))
static_assert(not is_subtype_of(A, C))
class B1(A): ...
class B2(A): ...
static_assert(is_subtype_of(B1, A))
static_assert(is_subtype_of(B2, A))
static_assert(not is_subtype_of(A, B1))
static_assert(not is_subtype_of(A, B2))
static_assert(not is_subtype_of(B1, B2))
static_assert(not is_subtype_of(B2, B1))
```
## Structural properties
This section covers structural properties of intersection types and documents some decisions on how
to represent mixtures of intersections and unions.
### Single-element intersections
If we have an intersection with a single element, we can simplify to that element. Similarly, we
show an intersection with a single negative contribution as just the negation of that element.
```py
from knot_extensions import Intersection, Not
class P: ...
def _(
i1: Intersection[P],
i2: Intersection[Not[P]],
) -> None:
reveal_type(i1) # revealed: P
reveal_type(i2) # revealed: ~P
```
### Flattening of nested intersections
We eagerly flatten nested intersections types.
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
class S: ...
def positive_contributions(
i1: Intersection[P, Intersection[Q, R]],
i2: Intersection[Intersection[P, Q], R],
) -> None:
reveal_type(i1) # revealed: P & Q & R
reveal_type(i2) # revealed: P & Q & R
def negative_contributions(
i1: Intersection[Not[P], Intersection[Not[Q], Not[R]]],
i2: Intersection[Intersection[Not[P], Not[Q]], Not[R]],
) -> None:
reveal_type(i1) # revealed: ~P & ~Q & ~R
reveal_type(i2) # revealed: ~P & ~Q & ~R
def mixed(
i1: Intersection[P, Intersection[Not[Q], R]],
i2: Intersection[Intersection[P, Not[Q]], R],
i3: Intersection[Not[P], Intersection[Q, Not[R]]],
i4: Intersection[Intersection[Q, Not[R]], Not[P]],
) -> None:
reveal_type(i1) # revealed: P & R & ~Q
reveal_type(i2) # revealed: P & R & ~Q
reveal_type(i3) # revealed: Q & ~P & ~R
reveal_type(i4) # revealed: Q & ~R & ~P
def multiple(
i1: Intersection[Intersection[P, Q], Intersection[R, S]],
):
reveal_type(i1) # revealed: P & Q & R & S
def nested(
i1: Intersection[Intersection[Intersection[P, Q], R], S],
i2: Intersection[P, Intersection[Q, Intersection[R, S]]],
):
reveal_type(i1) # revealed: P & Q & R & S
reveal_type(i2) # revealed: P & Q & R & S
```
### Union of intersections
We always normalize our representation to a _union of intersections_, so when we add a _union to an
intersection_, we distribute the union over the respective elements:
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
class S: ...
def _(
i1: Intersection[P, Q | R | S],
i2: Intersection[P | Q | R, S],
i3: Intersection[P | Q, R | S],
) -> None:
reveal_type(i1) # revealed: P & Q | P & R | P & S
reveal_type(i2) # revealed: P & S | Q & S | R & S
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
def simplifications_for_same_elements(
i1: Intersection[P, Q | P],
i2: Intersection[Q, P | Q],
i3: Intersection[P | Q, Q | R],
i4: Intersection[P | Q, P | Q],
i5: Intersection[P | Q, Q | P],
) -> None:
# P & (Q | P)
# = P & Q | P & P
# = P & Q | P
# = P
# (because P is a supertype of P & Q)
reveal_type(i1) # revealed: P
# similar here:
reveal_type(i2) # revealed: Q
# (P | Q) & (Q | R)
# = P & Q | P & R | Q & Q | Q & R
# = P & Q | P & R | Q | Q & R
# = Q | P & R
# (again, because Q is a supertype of P & Q and of Q & R)
reveal_type(i3) # revealed: Q | P & R
# (P | Q) & (P | Q)
# = P & P | P & Q | Q & P | Q & Q
# = P | P & Q | Q
# = P | Q
reveal_type(i4) # revealed: P | Q
```
### Negation distributes over union
Distribution also applies to a negation operation. This is a manifestation of one of
[De Morgan's laws], namely `~(P | Q) = ~P & ~Q`:
```py
from knot_extensions import Not
from typing import Literal
class P: ...
class Q: ...
class R: ...
def _(i1: Not[P | Q], i2: Not[P | Q | R]) -> None:
reveal_type(i1) # revealed: ~P & ~Q
reveal_type(i2) # revealed: ~P & ~Q & ~R
def example_literals(i: Not[Literal[1, 2]]) -> None:
reveal_type(i) # revealed: ~Literal[1] & ~Literal[2]
```
### Negation of intersections
The other of [De Morgan's laws], `~(P & Q) = ~P | ~Q`, also holds:
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
def _(
i1: Not[Intersection[P, Q]],
i2: Not[Intersection[P, Q, R]],
) -> None:
reveal_type(i1) # revealed: ~P | ~Q
reveal_type(i2) # revealed: ~P | ~Q | ~R
```
### `Never` is dual to `object`
`Never` represents the empty set of values, while `object` represents the set of all values, so
`~Never` is equivalent to `object`, and `~object` is equivalent to `Never`. This is a manifestation
of the [complement laws] of set theory.
```py
from knot_extensions import Intersection, Not
from typing_extensions import Never
def _(
not_never: Not[Never],
not_object: Not[object],
) -> None:
reveal_type(not_never) # revealed: object
reveal_type(not_object) # revealed: Never
```
### Intersection of a type and its negation
Continuing with more [complement laws], if we see both `P` and `~P` in an intersection, we can
simplify to `Never`, even in the presence of other types:
```py
from knot_extensions import Intersection, Not
from typing import Any
class P: ...
class Q: ...
def _(
i1: Intersection[P, Not[P]],
i2: Intersection[Not[P], P],
i3: Intersection[P, Q, Not[P]],
i4: Intersection[Not[P], Q, P],
i5: Intersection[P, Any, Not[P]],
i6: Intersection[Not[P], Any, P],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
reveal_type(i5) # revealed: Never
reveal_type(i6) # revealed: Never
```
### Union of a type and its negation
Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `object`.
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
def _(
i1: P | Not[P],
i2: Not[P] | P,
i3: P | Q | Not[P],
i4: Not[P] | Q | P,
) -> None:
reveal_type(i1) # revealed: object
reveal_type(i2) # revealed: object
reveal_type(i3) # revealed: object
reveal_type(i4) # revealed: object
```
### Negation is an involution
The final of the [complement laws] states that negating twice is equivalent to not negating at all:
```py
from knot_extensions import Not
class P: ...
def _(
i1: Not[P],
i2: Not[Not[P]],
i3: Not[Not[Not[P]]],
i4: Not[Not[Not[Not[P]]]],
) -> None:
reveal_type(i1) # revealed: ~P
reveal_type(i2) # revealed: P
reveal_type(i3) # revealed: ~P
reveal_type(i4) # revealed: P
```
## Simplification strategies
In this section, we present various simplification strategies that go beyond the structure of the
representation.
### `Never` in intersections
If we intersect with `Never`, we can simplify the whole intersection to `Never`, even if there are
dynamic types involved:
```py
from knot_extensions import Intersection, Not
from typing_extensions import Never, Any
class P: ...
class Q: ...
def _(
i1: Intersection[P, Never],
i2: Intersection[Never, P],
i3: Intersection[Any, Never],
i4: Intersection[Never, Not[Any]],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
```
### Simplifications using disjointness
#### Positive contributions
If we intersect disjoint types, we can simplify to `Never`, even in the presence of other types:
```py
from knot_extensions import Intersection, Not
from typing import Literal, Any
class P: ...
def _(
i01: Intersection[Literal[1], Literal[2]],
i02: Intersection[Literal[2], Literal[1]],
i03: Intersection[Literal[1], Literal[2], P],
i04: Intersection[Literal[1], P, Literal[2]],
i05: Intersection[P, Literal[1], Literal[2]],
i06: Intersection[Literal[1], Literal[2], Any],
i07: Intersection[Literal[1], Any, Literal[2]],
i08: Intersection[Any, Literal[1], Literal[2]],
) -> None:
reveal_type(i01) # revealed: Never
reveal_type(i02) # revealed: Never
reveal_type(i03) # revealed: Never
reveal_type(i04) # revealed: Never
reveal_type(i05) # revealed: Never
reveal_type(i06) # revealed: Never
reveal_type(i07) # revealed: Never
reveal_type(i08) # revealed: Never
# `bool` is final and can not be subclassed, so `type[bool]` is equivalent to `Literal[bool]`, which
# is disjoint from `type[str]`:
def example_type_bool_type_str(
i: Intersection[type[bool], type[str]],
) -> None:
reveal_type(i) # revealed: Never
```
#### Positive and negative contributions
If we intersect a type `X` with the negation `~Y` of a disjoint type `Y`, we can remove the negative
contribution `~Y`, as `~Y` must fully contain the positive contribution `X` as a subtype:
```py
from knot_extensions import Intersection, Not
from typing import Literal
def _(
i1: Intersection[Literal[1], Not[Literal[2]]],
i2: Intersection[Not[Literal[2]], Literal[1]],
i3: Intersection[Literal[1], Not[Literal[2]], int],
i4: Intersection[Literal[1], int, Not[Literal[2]]],
i5: Intersection[int, Literal[1], Not[Literal[2]]],
) -> None:
reveal_type(i1) # revealed: Literal[1]
reveal_type(i2) # revealed: Literal[1]
reveal_type(i3) # revealed: Literal[1]
reveal_type(i4) # revealed: Literal[1]
reveal_type(i5) # revealed: Literal[1]
# None is disjoint from int, so this simplification applies here
def example_none(
i1: Intersection[int, Not[None]],
i2: Intersection[Not[None], int],
) -> None:
reveal_type(i1) # revealed: int
reveal_type(i2) # revealed: int
```
### Simplifications using subtype relationships
#### Positive type and positive subtype
Subtypes are contained within their supertypes, so we can simplify intersections by removing
superfluous supertypes:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i01: Intersection[A, B],
i02: Intersection[B, A],
i03: Intersection[A, C],
i04: Intersection[C, A],
i05: Intersection[B, C],
i06: Intersection[C, B],
i07: Intersection[A, B, C],
i08: Intersection[C, B, A],
i09: Intersection[B, C, A],
i10: Intersection[A, B, Unrelated],
i11: Intersection[B, A, Unrelated],
i12: Intersection[B, Unrelated, A],
i13: Intersection[A, Unrelated, B],
i14: Intersection[Unrelated, A, B],
i15: Intersection[Unrelated, B, A],
i16: Intersection[A, B, Any],
i17: Intersection[B, A, Any],
i18: Intersection[B, Any, A],
i19: Intersection[A, Any, B],
i20: Intersection[Any, A, B],
i21: Intersection[Any, B, A],
) -> None:
reveal_type(i01) # revealed: B
reveal_type(i02) # revealed: B
reveal_type(i03) # revealed: C
reveal_type(i04) # revealed: C
reveal_type(i05) # revealed: C
reveal_type(i06) # revealed: C
reveal_type(i07) # revealed: C
reveal_type(i08) # revealed: C
reveal_type(i09) # revealed: C
reveal_type(i10) # revealed: B & Unrelated
reveal_type(i11) # revealed: B & Unrelated
reveal_type(i12) # revealed: B & Unrelated
reveal_type(i13) # revealed: Unrelated & B
reveal_type(i14) # revealed: Unrelated & B
reveal_type(i15) # revealed: Unrelated & B
reveal_type(i16) # revealed: B & Any
reveal_type(i17) # revealed: B & Any
reveal_type(i18) # revealed: B & Any
reveal_type(i19) # revealed: Any & B
reveal_type(i20) # revealed: Any & B
reveal_type(i21) # revealed: Any & B
```
#### Negative type and negative subtype
For negative contributions, this property is reversed. Here we can remove superfluous _subtypes_:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i01: Intersection[Not[B], Not[A]],
i02: Intersection[Not[A], Not[B]],
i03: Intersection[Not[A], Not[C]],
i04: Intersection[Not[C], Not[A]],
i05: Intersection[Not[B], Not[C]],
i06: Intersection[Not[C], Not[B]],
i07: Intersection[Not[A], Not[B], Not[C]],
i08: Intersection[Not[C], Not[B], Not[A]],
i09: Intersection[Not[B], Not[C], Not[A]],
i10: Intersection[Not[B], Not[A], Unrelated],
i11: Intersection[Not[A], Not[B], Unrelated],
i12: Intersection[Not[A], Unrelated, Not[B]],
i13: Intersection[Not[B], Unrelated, Not[A]],
i14: Intersection[Unrelated, Not[A], Not[B]],
i15: Intersection[Unrelated, Not[B], Not[A]],
i16: Intersection[Not[B], Not[A], Any],
i17: Intersection[Not[A], Not[B], Any],
i18: Intersection[Not[A], Any, Not[B]],
i19: Intersection[Not[B], Any, Not[A]],
i20: Intersection[Any, Not[A], Not[B]],
i21: Intersection[Any, Not[B], Not[A]],
) -> None:
reveal_type(i01) # revealed: ~A
reveal_type(i02) # revealed: ~A
reveal_type(i03) # revealed: ~A
reveal_type(i04) # revealed: ~A
reveal_type(i05) # revealed: ~B
reveal_type(i06) # revealed: ~B
reveal_type(i07) # revealed: ~A
reveal_type(i08) # revealed: ~A
reveal_type(i09) # revealed: ~A
reveal_type(i10) # revealed: Unrelated & ~A
reveal_type(i11) # revealed: Unrelated & ~A
reveal_type(i12) # revealed: Unrelated & ~A
reveal_type(i13) # revealed: Unrelated & ~A
reveal_type(i14) # revealed: Unrelated & ~A
reveal_type(i15) # revealed: Unrelated & ~A
reveal_type(i16) # revealed: Any & ~A
reveal_type(i17) # revealed: Any & ~A
reveal_type(i18) # revealed: Any & ~A
reveal_type(i19) # revealed: Any & ~A
reveal_type(i20) # revealed: Any & ~A
reveal_type(i21) # revealed: Any & ~A
```
#### Negative type and multiple negative subtypes
If there are multiple negative subtypes, all of them can be removed:
```py
from knot_extensions import Intersection, Not
class A: ...
class B1(A): ...
class B2(A): ...
def _(
i1: Intersection[Not[A], Not[B1], Not[B2]],
i2: Intersection[Not[A], Not[B2], Not[B1]],
i3: Intersection[Not[B1], Not[A], Not[B2]],
i4: Intersection[Not[B1], Not[B2], Not[A]],
i5: Intersection[Not[B2], Not[A], Not[B1]],
i6: Intersection[Not[B2], Not[B1], Not[A]],
) -> None:
reveal_type(i1) # revealed: ~A
reveal_type(i2) # revealed: ~A
reveal_type(i3) # revealed: ~A
reveal_type(i4) # revealed: ~A
reveal_type(i5) # revealed: ~A
reveal_type(i6) # revealed: ~A
```
#### Negative type and positive subtype
When `A` is a supertype of `B`, its negation `~A` is disjoint from `B`, so we can simplify the
intersection to `Never`:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i1: Intersection[Not[A], B],
i2: Intersection[B, Not[A]],
i3: Intersection[Not[A], C],
i4: Intersection[C, Not[A]],
i5: Intersection[Unrelated, Not[A], B],
i6: Intersection[B, Not[A], Not[Unrelated]],
i7: Intersection[Any, Not[A], B],
i8: Intersection[B, Not[A], Not[Any]],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
reveal_type(i5) # revealed: Never
reveal_type(i6) # revealed: Never
reveal_type(i7) # revealed: Never
reveal_type(i8) # revealed: Never
```
## Non fully-static types
### Negation of dynamic types
`Any` represents the dynamic type, an unknown set of runtime values. The negation of that, `~Any`,
is still an unknown set of runtime values, so `~Any` is equivalent to `Any`. We therefore eagerly
simplify `~Any` to `Any` in intersections. The same applies to `Unknown`.
```py
from knot_extensions import Intersection, Not, Unknown
from typing_extensions import Any, Never
class P: ...
def any(
i1: Not[Any],
i2: Intersection[P, Not[Any]],
i3: Intersection[Never, Not[Any]],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: P & Any
reveal_type(i3) # revealed: Never
def unknown(
i1: Not[Unknown],
i2: Intersection[P, Not[Unknown]],
i3: Intersection[Never, Not[Unknown]],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: P & Unknown
reveal_type(i3) # revealed: Never
```
### Collapsing of multiple `Any`/`Unknown` contributions
The intersection of an unknown set of runtime values with (another) unknown set of runtime values is
still an unknown set of runtime values:
```py
from knot_extensions import Intersection, Not, Unknown
from typing_extensions import Any
class P: ...
def any(
i1: Intersection[Any, Any],
i2: Intersection[P, Any, Any],
i3: Intersection[Any, P, Any],
i4: Intersection[Any, Any, P],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: P & Any
reveal_type(i3) # revealed: Any & P
reveal_type(i4) # revealed: Any & P
def unknown(
i1: Intersection[Unknown, Unknown],
i2: Intersection[P, Unknown, Unknown],
i3: Intersection[Unknown, P, Unknown],
i4: Intersection[Unknown, Unknown, P],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: P & Unknown
reveal_type(i3) # revealed: Unknown & P
reveal_type(i4) # revealed: Unknown & P
```
### No self-cancellation
Dynamic types do not cancel each other out. Intersecting an unknown set of values with the negation
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
```py
from knot_extensions import Intersection, Not, Unknown
def any(
i1: Intersection[Any, Not[Any]],
i2: Intersection[Not[Any], Any],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: Any
def unknown(
i1: Intersection[Unknown, Not[Unknown]],
i2: Intersection[Not[Unknown], Unknown],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: Unknown
```
### Mixed dynamic types
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
```py
from knot_extensions import Intersection, Not, Unknown
def mixed(
i1: Intersection[Any, Unknown],
i2: Intersection[Any, Not[Unknown]],
i3: Intersection[Not[Any], Unknown],
i4: Intersection[Not[Any], Not[Unknown]],
) -> None:
reveal_type(i1) # revealed: Any & Unknown
reveal_type(i2) # revealed: Any & Unknown
reveal_type(i3) # revealed: Any & Unknown
reveal_type(i4) # revealed: Any & Unknown
```
[complement laws]: https://en.wikipedia.org/wiki/Complement_(set_theory)
[de morgan's laws]: https://en.wikipedia.org/wiki/De_Morgan%27s_laws

View File

@@ -98,7 +98,7 @@ reveal_type(x)
for x in (1, "a", b"foo"):
pass
# revealed: Literal[1] | Literal["a"] | Literal[b"foo"]
# revealed: Literal[1, "a", b"foo"]
# error: [possibly-unresolved-reference]
reveal_type(x)
```

View File

@@ -41,7 +41,7 @@ def _(flag: bool, flag2: bool):
x = 3
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[1, 2, 4]
reveal_type(y) # revealed: Literal[4, 1, 2]
```
## Nested `while` loops

View File

@@ -170,8 +170,35 @@ def f(*args, **kwargs) -> int: ...
class A(metaclass=f): ...
# TODO should be `type[int]`
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
# TODO: Should be `int`
reveal_type(A) # revealed: Literal[A]
reveal_type(A.__class__) # revealed: type[int]
def _(n: int):
# error: [invalid-metaclass]
class B(metaclass=n): ...
# TODO: Should be `Unknown`
reveal_type(B) # revealed: Literal[B]
reveal_type(B.__class__) # revealed: type[Unknown]
def _(flag: bool):
m = f if flag else 42
# error: [invalid-metaclass]
class C(metaclass=m): ...
# TODO: Should be `int | Unknown`
reveal_type(C) # revealed: Literal[C]
reveal_type(C.__class__) # revealed: type[Unknown]
class SignatureMismatch: ...
# TODO: Emit a diagnostic
class D(metaclass=SignatureMismatch): ...
# TODO: Should be `Unknown`
reveal_type(D) # revealed: Literal[D]
# TODO: Should be `type[Unknown]`
reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
```
## Cyclic

View File

@@ -56,7 +56,7 @@ def _(x_flag: bool, y_flag: bool):
def _(flag1: bool, flag2: bool):
x = None if flag1 else (1 if flag2 else True)
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
reveal_type(x) # revealed: None | Literal[1, True]
if x is None:
reveal_type(x) # revealed: None
elif x is True:

View File

@@ -17,7 +17,7 @@ def _(flag: bool):
reveal_type(x) # revealed: Never
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
```
## `classinfo` is a tuple of types
@@ -30,7 +30,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
x = 1 if flag else "a"
if isinstance(x, (int, str)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
else:
reveal_type(x) # revealed: Never
@@ -43,19 +43,19 @@ def _(flag: bool, flag1: bool, flag2: bool):
# 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"]
reveal_type(x) # revealed: Literal[1, "a"]
else:
reveal_type(x) # revealed: Never
y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
reveal_type(y) # revealed: Literal[1] | Literal["a"]
reveal_type(y) # revealed: Literal[1, "a"]
if isinstance(y, (int, bytes)):
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
reveal_type(y) # revealed: Literal[1, b"b"]
if isinstance(y, (str, bytes)):
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
reveal_type(y) # revealed: Literal["a", b"b"]
```
## `classinfo` is a nested tuple of types
@@ -107,7 +107,7 @@ def _(flag: bool):
x = 1 if flag else "foo"
if isinstance(x, t):
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
reveal_type(x) # revealed: Literal[1, "foo"]
```
## Do not use custom `isinstance` for narrowing
@@ -119,7 +119,7 @@ def _(flag: bool):
x = 1 if flag else "a"
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
```
## Do support narrowing if `isinstance` is aliased
@@ -155,12 +155,12 @@ def _(flag: bool):
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "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"]
reveal_type(x) # revealed: Literal[1, "a"]
```
## Do not narrow if there are keyword arguments
@@ -169,8 +169,15 @@ def _(flag: bool):
def _(flag: bool):
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic
# (`isinstance` has no `foo` parameter)
# error: [unknown-argument]
if isinstance(x, int, foo="bar"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
```
## `type[]` types are narrowed as well as class-literal types
```py
def _(x: object, y: type[int]):
if isinstance(x, y):
reveal_type(x) # revealed: int
```

View File

@@ -90,7 +90,7 @@ def _(t: type[object]):
if issubclass(t, B):
reveal_type(t) # revealed: type[A] & type[B]
else:
reveal_type(t) # revealed: type[object] & ~type[A]
reveal_type(t) # revealed: type & ~type[A]
```
### Handling of `None`
@@ -146,7 +146,7 @@ class A: ...
t = object()
# TODO: we should emit a diagnostic here
# error: [invalid-argument-type]
if issubclass(t, A):
reveal_type(t) # revealed: type[A]
```
@@ -160,7 +160,7 @@ branch:
```py
t = 1
# TODO: we should emit a diagnostic here
# error: [invalid-argument-type]
if issubclass(t, int):
reveal_type(t) # revealed: Never
```
@@ -234,8 +234,15 @@ def flag() -> bool: ...
t = int if flag() else str
# TODO: this should cause us to emit a diagnostic
# (`issubclass` has no `foo` parameter)
# error: [unknown-argument]
if issubclass(t, int, foo="bar"):
reveal_type(t) # revealed: Literal[int, str]
```
### `type[]` types are narrowed as well as class-literal types
```py
def _(x: type, y: type[int]):
if issubclass(x, y):
reveal_type(x) # revealed: type[int]
```

View File

@@ -16,3 +16,48 @@ def _(flag: bool):
reveal_type(y) # revealed: Literal[0] | None
```
## Class patterns
```py
def get_object() -> object: ...
class A: ...
class B: ...
x = get_object()
reveal_type(x) # revealed: object
match x:
case A():
reveal_type(x) # revealed: A
case B():
# TODO could be `B & ~A`
reveal_type(x) # revealed: B
reveal_type(x) # revealed: object
```
## Class pattern with guard
```py
def get_object() -> object: ...
class A:
def y() -> int: ...
class B: ...
x = get_object()
reveal_type(x) # revealed: object
match x:
case A() if reveal_type(x): # revealed: A
pass
case B() if reveal_type(x): # revealed: B
pass
reveal_type(x) # revealed: object
```

View File

@@ -9,39 +9,39 @@ def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[(
x = foo()
if x:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
else:
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
if not x:
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
if x and not x:
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
if not (x and not x):
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
else:
reveal_type(x) # revealed: Never
if x or not x:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
else:
reveal_type(x) # revealed: Never
if not (x or not x):
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
if (isinstance(x, int) or isinstance(x, str)) and x:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
reveal_type(x) # revealed: Literal[-1, True, "foo"]
else:
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
reveal_type(x) # revealed: Literal[b"", b"bar", 0, False, ""] | None | tuple[()]
```
## Function Literals
@@ -87,10 +87,10 @@ def f(x: A | B):
if x and not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy | A & ~AlwaysFalsy | B & ~AlwaysFalsy
reveal_type(x) # revealed: A | B
if x or not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
```
@@ -166,16 +166,16 @@ y = literals()
if isinstance(x, str) and not isinstance(x, B):
reveal_type(x) # revealed: A & str & ~B
reveal_type(y) # revealed: Literal[0, 42] | Literal["", "hello"]
reveal_type(y) # revealed: Literal[0, 42, "", "hello"]
z = x if flag() else y
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
if z:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
else:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
```
## Narrowing Multiple Variables
@@ -214,10 +214,9 @@ if x and not x:
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
else:
y = x
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
reveal_type(y) # revealed: A
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
reveal_type(y) # revealed: A
```
## Truthiness of classes

View File

@@ -37,7 +37,7 @@ class C:
# error: [possibly-unresolved-reference]
y = x
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
reveal_type(C.y) # revealed: Literal[1, "abc"]
```
## Unbound function local

View File

@@ -167,7 +167,7 @@ class A:
__slots__ = ()
__slots__ += ("a", "b")
reveal_type(A.__slots__) # revealed: @Todo(Support for more binary expressions)
reveal_type(A.__slots__) # revealed: @Todo(return type)
class B:
__slots__ = ("c", "d")

View File

@@ -0,0 +1,78 @@
# Ellipsis
## Function and methods
The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a
stub file only, regardless of the type of the parameter.
```py path=test.pyi
def f(x: int = ...) -> None:
reveal_type(x) # revealed: int
def f2(x: str = ...) -> None:
reveal_type(x) # revealed: str
```
## Class and module symbols
The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type,
in a stub file only.
```py path=test.pyi
y: bytes = ...
reveal_type(y) # revealed: bytes
x = ...
reveal_type(x) # revealed: Unknown
class Foo:
y: int = ...
reveal_type(Foo.y) # revealed: int
```
## Unpacking ellipsis literal in assignment
No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an
assignment statement:
```py path=test.pyi
x, y = ...
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: Unknown
```
## Unpacking ellipsis literal in for loops
Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and
results in a diagnostic:
```py path=test.pyi
# error: [not-iterable] "Object of type `ellipsis` is not iterable"
for a, b in ...:
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
## Ellipsis usage in non stub file
In a non-stub file, there's no special treatment of ellipsis literals. An ellipsis literal can only
be assigned if `EllipsisType` is actually assignable to the annotated type.
```py
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
def f(x: int = ...) -> None: ...
# error: 1 [invalid-assignment] "Object of type `ellipsis` is not assignable to `int`"
a: int = ...
b = ...
reveal_type(b) # revealed: ellipsis
```
## Use of `Ellipsis` symbol
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
```py path=test.pyi
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
def f(x: int = Ellipsis) -> None: ...
```

View File

@@ -0,0 +1,352 @@
# Type API (`knot_extensions`)
This document describes the internal `knot_extensions` API for creating and manipulating types as
well as testing various type system properties.
## Type extensions
The Python language itself allows us to perform a variety of operations on types. For example, we
can build a union of types like `int | None`, or we can use type constructors such as `list[int]`
and `type[int]` to create new types. But some type-level operations that we rely on in Red Knot,
like intersections, cannot yet be expressed in Python. The `knot_extensions` module provides the
`Intersection` and `Not` type constructors (special forms) which allow us to construct these types
directly.
### Negation
```py
from knot_extensions import Not, static_assert
def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
reveal_type(n1) # revealed: ~int
reveal_type(n2) # revealed: int
reveal_type(n3) # revealed: ~int
def static_truthiness(not_one: Not[Literal[1]]) -> None:
static_assert(not_one != 1)
static_assert(not (not_one == 1))
# error: "Special form `knot_extensions.Not` expected exactly one type parameter"
n: Not[int, str]
```
### Intersection
```py
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
from typing_extensions import Never
class S: ...
class T: ...
def x(x1: Intersection[S, T], x2: Intersection[S, Not[T]]) -> None:
reveal_type(x1) # revealed: S & T
reveal_type(x2) # revealed: S & ~T
def y(y1: Intersection[int, object], y2: Intersection[int, bool], y3: Intersection[int, Never]) -> None:
reveal_type(y1) # revealed: int
reveal_type(y2) # revealed: bool
reveal_type(y3) # revealed: Never
def z(z1: Intersection[int, Not[Literal[1]], Not[Literal[2]]]) -> None:
reveal_type(z1) # revealed: int & ~Literal[1] & ~Literal[2]
class A: ...
class B: ...
class C: ...
type ABC = Intersection[A, B, C]
static_assert(is_subtype_of(ABC, A))
static_assert(is_subtype_of(ABC, B))
static_assert(is_subtype_of(ABC, C))
class D: ...
static_assert(not is_subtype_of(ABC, D))
```
### Unknown type
The `Unknown` type is a special type that we use to represent actually unknown types (no
annotation), as opposed to `Any` which represents an explicitly unknown type.
```py
from knot_extensions import Unknown, static_assert, is_assignable_to, is_fully_static
static_assert(is_assignable_to(Unknown, int))
static_assert(is_assignable_to(int, Unknown))
static_assert(not is_fully_static(Unknown))
def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None:
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: tuple[str, Unknown]
reveal_type(z) # revealed: Unknown | Literal[1]
# Unknown can be subclassed, just like Any
class C(Unknown): ...
# revealed: tuple[Literal[C], Unknown, Literal[object]]
reveal_type(C.__mro__)
# error: "Special form `knot_extensions.Unknown` expected no type parameter"
u: Unknown[str]
```
## Static assertions
### Basics
The `knot_extensions` module provides a `static_assert` function that can be used to enforce
properties at type-check time. The function takes an arbitrary expression and raises a type error if
the expression is not of statically known truthiness.
```py
from knot_extensions import static_assert
from typing import TYPE_CHECKING
import sys
static_assert(True)
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
static_assert(False or True)
static_assert(True and True)
static_assert(False or False) # error: "Static assertion error: argument evaluates to `False`"
static_assert(False and True) # error: "Static assertion error: argument evaluates to `False`"
static_assert(1 + 1 == 2)
static_assert(1 + 1 == 3) # error: "Static assertion error: argument evaluates to `False`"
static_assert("a" in "abc")
static_assert("d" in "abc") # error: "Static assertion error: argument evaluates to `False`"
n = None
static_assert(n is None)
static_assert(TYPE_CHECKING)
static_assert(sys.version_info >= (3, 6))
```
### Narrowing constraints
Static assertions can be used to enforce narrowing constraints:
```py
from knot_extensions import static_assert
def f(x: int) -> None:
if x != 0:
static_assert(x != 0)
else:
# `int` can be subclassed, so we cannot assert that `x == 0` here:
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
static_assert(x == 0)
```
### Truthy expressions
See also: <https://docs.python.org/3/library/stdtypes.html#truth-value-testing>
```py
from knot_extensions import static_assert
static_assert(True)
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
static_assert(None) # error: "Static assertion error: argument of type `None` is statically known to be falsy"
static_assert(1)
static_assert(0) # error: "Static assertion error: argument of type `Literal[0]` is statically known to be falsy"
static_assert((0,))
static_assert(()) # error: "Static assertion error: argument of type `tuple[()]` is statically known to be falsy"
static_assert("a")
static_assert("") # error: "Static assertion error: argument of type `Literal[""]` is statically known to be falsy"
static_assert(b"a")
static_assert(b"") # error: "Static assertion error: argument of type `Literal[b""]` is statically known to be falsy"
```
### Error messages
We provide various tailored error messages for wrong argument types to `static_assert`:
```py
from knot_extensions import static_assert
static_assert(2 * 3 == 6)
# error: "Static assertion error: argument evaluates to `False`"
static_assert(2 * 3 == 7)
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
static_assert(int(2.0 * 3.0) == 6)
class InvalidBoolDunder:
def __bool__(self) -> int:
return 1
# error: "Static assertion error: argument of type `InvalidBoolDunder` has an ambiguous static truthiness"
static_assert(InvalidBoolDunder())
```
### Custom error messages
Alternatively, users can provide custom error messages:
```py
from knot_extensions import static_assert
# error: "Static assertion error: I really want this to be true"
static_assert(1 + 1 == 3, "I really want this to be true")
error_message = "A custom message "
error_message += "constructed from multiple string literals"
# error: "Static assertion error: A custom message constructed from multiple string literals"
static_assert(False, error_message)
# There are limitations to what we can still infer as a string literal. In those cases,
# we simply fall back to the default message.
shouted_message = "A custom message".upper()
# error: "Static assertion error: argument evaluates to `False`"
static_assert(False, shouted_message)
```
## Type predicates
The `knot_extensions` module also provides predicates to test various properties of types. These are
implemented as functions that return `Literal[True]` or `Literal[False]` depending on the result of
the test.
### Equivalence
```py
from knot_extensions import is_equivalent_to, static_assert
from typing_extensions import Never, Union
static_assert(is_equivalent_to(type, type[object]))
static_assert(is_equivalent_to(tuple[int, Never], Never))
static_assert(is_equivalent_to(int | str, Union[int, str]))
static_assert(not is_equivalent_to(int, str))
static_assert(not is_equivalent_to(int | str, int | str | bytes))
```
### Subtyping
```py
from knot_extensions import is_subtype_of, static_assert
static_assert(is_subtype_of(bool, int))
static_assert(not is_subtype_of(str, int))
static_assert(is_subtype_of(bool, int | str))
static_assert(is_subtype_of(str, int | str))
static_assert(not is_subtype_of(bytes, int | str))
class Base: ...
class Derived(Base): ...
class Unrelated: ...
static_assert(is_subtype_of(Derived, Base))
static_assert(not is_subtype_of(Base, Derived))
static_assert(is_subtype_of(Base, Base))
static_assert(not is_subtype_of(Unrelated, Base))
static_assert(not is_subtype_of(Base, Unrelated))
```
### Assignability
```py
from knot_extensions import is_assignable_to, static_assert
from typing import Any
static_assert(is_assignable_to(int, Any))
static_assert(is_assignable_to(Any, str))
static_assert(not is_assignable_to(int, str))
```
### Disjointness
```py
from knot_extensions import is_disjoint_from, static_assert
static_assert(is_disjoint_from(None, int))
static_assert(not is_disjoint_from(Literal[2] | str, int))
```
### Fully static types
```py
from knot_extensions import is_fully_static, static_assert
from typing import Any
static_assert(is_fully_static(int | str))
static_assert(is_fully_static(type[int]))
static_assert(not is_fully_static(int | Any))
static_assert(not is_fully_static(type[Any]))
```
### Singleton types
```py
from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(None))
static_assert(is_singleton(Literal[True]))
static_assert(not is_singleton(int))
static_assert(not is_singleton(Literal["a"]))
```
### Single-valued types
```py
from knot_extensions import is_single_valued, static_assert
static_assert(is_single_valued(None))
static_assert(is_single_valued(Literal[True]))
static_assert(is_single_valued(Literal["a"]))
static_assert(not is_single_valued(int))
static_assert(not is_single_valued(Literal["a"] | Literal["b"]))
```
## `TypeOf`
We use `TypeOf` to get the inferred type of an expression. This is useful when we want to refer to
it in a type expression. For example, if we want to make sure that the class literal type `str` is a
subtype of `type[str]`, we can not use `is_subtype_of(str, type[str])`, as that would test if the
type `str` itself is a subtype of `type[str]`. Instead, we can use `TypeOf[str]` to get the type of
the expression `str`:
```py
from knot_extensions import TypeOf, is_subtype_of, static_assert
# This is incorrect and therefore fails with ...
# error: "Static assertion error: argument evaluates to `False`"
static_assert(is_subtype_of(str, type[str]))
# Correct, returns True:
static_assert(is_subtype_of(TypeOf[str], type[str]))
class Base: ...
class Derived(Base): ...
# `TypeOf` can be used in annotations:
def type_of_annotation() -> None:
t1: TypeOf[Base] = Base
t2: TypeOf[Base] = Derived # error: [invalid-assignment]
# Note how this is different from `type[…]` which includes subclasses:
s1: type[Base] = Base
s2: type[Base] = Derived # no error here
# error: "Special form `knot_extensions.TypeOf` expected exactly one type parameter"
t: TypeOf[int, str, bytes]
```

View File

@@ -142,3 +142,25 @@ 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]]
```
## `@final` classes
`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is
used as the type argument. This applies to standard-library classes and user-defined classes:
```toml
[environment]
python-version = "3.10"
```
```py
from types import EllipsisType
from typing import final
@final
class Foo: ...
def _(x: type[Foo], y: type[EllipsisType]):
reveal_type(x) # revealed: Literal[Foo]
reveal_type(y) # revealed: Literal[EllipsisType]
```

View File

@@ -47,9 +47,8 @@ x: type = A() # error: [invalid-assignment]
```py
def f(x: type[object]):
reveal_type(x) # revealed: type[object]
# TODO: bound method types
reveal_type(x.__repr__) # revealed: Literal[__repr__]
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
class A: ...

View File

@@ -0,0 +1,352 @@
# Assignable-to relation
The `is_assignable_to(S, T)` relation below checks if type `S` is assignable to type `T` (target).
This allows us to check if a type `S` can be used in a context where a type `T` is expected
(function arguments, variable assignments). See the [typing documentation] for a precise definition
of this concept.
## Basic types
### Fully static
Fully static types participate in subtyping. If a type `S` is a subtype of `T`, `S` will also be
assignable to `T`. Two equivalent types are subtypes of each other:
```py
from knot_extensions import static_assert, is_assignable_to
class Parent: ...
class Child1(Parent): ...
class Child2(Parent): ...
class Grandchild(Child1, Child2): ...
class Unrelated: ...
static_assert(is_assignable_to(int, int))
static_assert(is_assignable_to(Parent, Parent))
static_assert(is_assignable_to(Child1, Parent))
static_assert(is_assignable_to(Grandchild, Parent))
static_assert(is_assignable_to(Unrelated, Unrelated))
static_assert(not is_assignable_to(str, int))
static_assert(not is_assignable_to(object, int))
static_assert(not is_assignable_to(Parent, Child1))
static_assert(not is_assignable_to(Unrelated, Parent))
static_assert(not is_assignable_to(Child1, Child2))
```
### Gradual types
Gradual types do not participate in subtyping, but can still be assignable to other types (and
static types can be assignable to gradual types):
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Any
static_assert(is_assignable_to(Unknown, Literal[1]))
static_assert(is_assignable_to(Any, Literal[1]))
static_assert(is_assignable_to(Literal[1], Unknown))
static_assert(is_assignable_to(Literal[1], Any))
```
## Literal types
### Boolean literals
`Literal[True]` and `Literal[False]` are both subtypes of (and therefore assignable to) `bool`,
which is in turn a subtype of `int`:
```py
from knot_extensions import static_assert, is_assignable_to
from typing import Literal
static_assert(is_assignable_to(Literal[True], Literal[True]))
static_assert(is_assignable_to(Literal[True], bool))
static_assert(is_assignable_to(Literal[True], int))
static_assert(not is_assignable_to(Literal[True], Literal[False]))
static_assert(not is_assignable_to(bool, Literal[True]))
```
### Integer literals
```py
from knot_extensions import static_assert, is_assignable_to
from typing import Literal
static_assert(is_assignable_to(Literal[1], Literal[1]))
static_assert(is_assignable_to(Literal[1], int))
static_assert(not is_assignable_to(Literal[1], Literal[2]))
static_assert(not is_assignable_to(int, Literal[1]))
static_assert(not is_assignable_to(Literal[1], str))
```
### String literals and `LiteralString`
All string-literal types are subtypes of (and therefore assignable to) `LiteralString`, which is in
turn a subtype of `str`:
```py
from knot_extensions import static_assert, is_assignable_to
from typing_extensions import Literal, LiteralString
static_assert(is_assignable_to(Literal["foo"], Literal["foo"]))
static_assert(is_assignable_to(Literal["foo"], LiteralString))
static_assert(is_assignable_to(Literal["foo"], str))
static_assert(is_assignable_to(LiteralString, str))
static_assert(not is_assignable_to(Literal["foo"], Literal["bar"]))
static_assert(not is_assignable_to(str, Literal["foo"]))
static_assert(not is_assignable_to(str, LiteralString))
```
### Byte literals
```py
from knot_extensions import static_assert, is_assignable_to
from typing_extensions import Literal, LiteralString
static_assert(is_assignable_to(Literal[b"foo"], bytes))
static_assert(is_assignable_to(Literal[b"foo"], Literal[b"foo"]))
static_assert(not is_assignable_to(Literal[b"foo"], str))
static_assert(not is_assignable_to(Literal[b"foo"], LiteralString))
static_assert(not is_assignable_to(Literal[b"foo"], Literal[b"bar"]))
static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"]))
static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"]))
```
## `type[…]` and class literals
In the following tests, `TypeOf[str]` is a singleton type with a single inhabitant, the class `str`.
This contrasts with `type[str]`, which represents "all possible subclasses of `str`".
Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, which both represent
"all possible instances of `type`"; therefore both `type[str]` and `TypeOf[str]` are assignable to
`type`. `type[Any]`, on the other hand, represents a type of unknown size or inhabitants, but which
is known to be no larger than the set of possible objects represented by `type`.
```py
from knot_extensions import static_assert, is_assignable_to, Unknown, TypeOf
from typing import Any
static_assert(is_assignable_to(type, type))
static_assert(is_assignable_to(type[object], type[object]))
static_assert(is_assignable_to(type, type[object]))
static_assert(is_assignable_to(type[object], type))
static_assert(is_assignable_to(type[str], type[object]))
static_assert(is_assignable_to(TypeOf[str], type[object]))
static_assert(is_assignable_to(type[str], type))
static_assert(is_assignable_to(TypeOf[str], type))
static_assert(is_assignable_to(type[str], type[str]))
static_assert(is_assignable_to(TypeOf[str], type[str]))
static_assert(not is_assignable_to(TypeOf[int], type[str]))
static_assert(not is_assignable_to(type, type[str]))
static_assert(not is_assignable_to(type[object], type[str]))
static_assert(is_assignable_to(type[Any], type[Any]))
static_assert(is_assignable_to(type[Any], type[object]))
static_assert(is_assignable_to(type[object], type[Any]))
static_assert(is_assignable_to(type, type[Any]))
static_assert(is_assignable_to(type[Any], type[str]))
static_assert(is_assignable_to(type[str], type[Any]))
static_assert(is_assignable_to(TypeOf[str], type[Any]))
static_assert(is_assignable_to(type[Unknown], type[Unknown]))
static_assert(is_assignable_to(type[Unknown], type[object]))
static_assert(is_assignable_to(type[object], type[Unknown]))
static_assert(is_assignable_to(type, type[Unknown]))
static_assert(is_assignable_to(type[Unknown], type[str]))
static_assert(is_assignable_to(type[str], type[Unknown]))
static_assert(is_assignable_to(TypeOf[str], type[Unknown]))
static_assert(is_assignable_to(type[Unknown], type[Any]))
static_assert(is_assignable_to(type[Any], type[Unknown]))
static_assert(not is_assignable_to(object, type[Any]))
static_assert(not is_assignable_to(str, type[Any]))
class Meta(type): ...
static_assert(is_assignable_to(type[Any], Meta))
static_assert(is_assignable_to(type[Unknown], Meta))
static_assert(is_assignable_to(Meta, type[Any]))
static_assert(is_assignable_to(Meta, type[Unknown]))
```
## Tuple types
```py
from knot_extensions import static_assert, is_assignable_to
from typing import Literal, Any
static_assert(is_assignable_to(tuple[()], tuple[()]))
static_assert(is_assignable_to(tuple[int], tuple[int]))
static_assert(is_assignable_to(tuple[int], tuple[Any]))
static_assert(is_assignable_to(tuple[Any], tuple[int]))
static_assert(is_assignable_to(tuple[int, str], tuple[int, str]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int]))
static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int]))
static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int]))
static_assert(not is_assignable_to(tuple[()], tuple[int]))
static_assert(not is_assignable_to(tuple[int], tuple[str]))
static_assert(not is_assignable_to(tuple[int], tuple[int, str]))
static_assert(not is_assignable_to(tuple[int, str], tuple[int]))
static_assert(not is_assignable_to(tuple[int, int], tuple[Literal[1], int]))
static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str]))
```
## Union types
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Literal, Any
static_assert(is_assignable_to(int, int | str))
static_assert(is_assignable_to(str, int | str))
static_assert(is_assignable_to(int | str, int | str))
static_assert(is_assignable_to(str | int, int | str))
static_assert(is_assignable_to(Literal[1], int | str))
static_assert(is_assignable_to(Literal[1], Unknown | str))
static_assert(is_assignable_to(Literal[1] | Literal[2], Literal[1] | Literal[2]))
static_assert(is_assignable_to(Literal[1] | Literal[2], int))
static_assert(is_assignable_to(Literal[1] | None, int | None))
static_assert(is_assignable_to(Any, int | str))
static_assert(is_assignable_to(Any | int, int))
static_assert(is_assignable_to(str, int | Any))
static_assert(not is_assignable_to(int | None, int))
static_assert(not is_assignable_to(int | None, str | None))
static_assert(not is_assignable_to(Literal[1] | None, int))
static_assert(not is_assignable_to(Literal[1] | None, str | None))
static_assert(not is_assignable_to(Any | int | str, int))
```
## Intersection types
```py
from knot_extensions import static_assert, is_assignable_to, Intersection, Not
from typing_extensions import Any, Literal
class Parent: ...
class Child1(Parent): ...
class Child2(Parent): ...
class Grandchild(Child1, Child2): ...
class Unrelated: ...
static_assert(is_assignable_to(Intersection[Child1, Child2], Child1))
static_assert(is_assignable_to(Intersection[Child1, Child2], Child2))
static_assert(is_assignable_to(Intersection[Child1, Child2], Parent))
static_assert(is_assignable_to(Intersection[Child1, Parent], Parent))
static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent))
static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1))
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1))
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent))
static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent))
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2]))
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1]))
static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2]))
static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated]))
static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]]))
static_assert(not is_assignable_to(int, Not[int]))
static_assert(not is_assignable_to(int, Not[Literal[1]]))
static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated))
# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Any, int], int))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
```
## General properties
See also: our property tests in `property_tests.rs`.
### Everything is assignable to `object`
`object` is Python's top type; the set of all possible objects at runtime:
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Literal, Any
static_assert(is_assignable_to(str, object))
static_assert(is_assignable_to(Literal[1], object))
static_assert(is_assignable_to(object, object))
static_assert(is_assignable_to(type, object))
static_assert(is_assignable_to(Any, object))
static_assert(is_assignable_to(Unknown, object))
static_assert(is_assignable_to(type[object], object))
static_assert(is_assignable_to(type[str], object))
static_assert(is_assignable_to(type[Any], object))
```
### Every type is assignable to `Any` / `Unknown`
`Any` and `Unknown` are gradual types. They could materialize to any given type at runtime, and so
any type is assignable to them:
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Literal, Any
static_assert(is_assignable_to(str, Any))
static_assert(is_assignable_to(Literal[1], Any))
static_assert(is_assignable_to(object, Any))
static_assert(is_assignable_to(type, Any))
static_assert(is_assignable_to(Any, Any))
static_assert(is_assignable_to(Unknown, Any))
static_assert(is_assignable_to(type[object], Any))
static_assert(is_assignable_to(type[str], Any))
static_assert(is_assignable_to(type[Any], Any))
static_assert(is_assignable_to(str, Unknown))
static_assert(is_assignable_to(Literal[1], Unknown))
static_assert(is_assignable_to(object, Unknown))
static_assert(is_assignable_to(type, Unknown))
static_assert(is_assignable_to(Any, Unknown))
static_assert(is_assignable_to(Unknown, Unknown))
static_assert(is_assignable_to(type[object], Unknown))
static_assert(is_assignable_to(type[str], Unknown))
static_assert(is_assignable_to(type[Any], Unknown))
```
### `Never` is assignable to every type
`Never` is Python's bottom type: the empty set, a type with no inhabitants. It is therefore
assignable to any arbitrary type.
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing_extensions import Never, Any
static_assert(is_assignable_to(Never, str))
static_assert(is_assignable_to(Never, Literal[1]))
static_assert(is_assignable_to(Never, object))
static_assert(is_assignable_to(Never, type))
static_assert(is_assignable_to(Never, Any))
static_assert(is_assignable_to(Never, Unknown))
static_assert(is_assignable_to(Never, type[object]))
static_assert(is_assignable_to(Never, type[str]))
static_assert(is_assignable_to(Never, type[Any]))
```
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

View File

@@ -0,0 +1,33 @@
# Tuples containing `Never`
A heterogeneous `tuple[…]` type that contains `Never` as a type argument simplifies to `Never`. One
way to think about this is the following: in order to construct a tuple, you need to have an object
of every element type. But since there is no object of type `Never`, you cannot construct the tuple.
Such a tuple type is therefore uninhabited and equivalent to `Never`.
In the language of algebraic data types, a tuple type is a product type and `Never` acts like the
zero element in multiplication, similar to how a Cartesian product with the empty set is the empty
set.
```py
from knot_extensions import static_assert, is_equivalent_to
from typing_extensions import Never, NoReturn
static_assert(is_equivalent_to(Never, tuple[Never]))
static_assert(is_equivalent_to(Never, tuple[Never, int]))
static_assert(is_equivalent_to(Never, tuple[int, Never]))
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
# The empty tuple is *not* equivalent to Never!
static_assert(not is_equivalent_to(Never, tuple[()]))
# NoReturn is just a different spelling of Never, so the same is true for NoReturn
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn]))
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn, int]))
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn]))
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn, str]))
static_assert(is_equivalent_to(NoReturn, tuple[int, tuple[str, NoReturn]]))
static_assert(is_equivalent_to(NoReturn, tuple[tuple[str, NoReturn], int]))
```

View File

@@ -34,6 +34,10 @@ reveal_type(~No()) # revealed: Unknown
## Classes
Dunder methods defined in a class are available to instances of that class, but not to the class
itself. (For these operators to work on the class itself, they would have to be defined on the
class's type, i.e. `type`.)
```py
class Yes:
def __pos__(self) -> bool:

View File

@@ -0,0 +1,143 @@
# Union types
This test suite covers certain basic properties and simplification strategies for union types.
## Basic unions
```py
from typing import Literal
def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None:
reveal_type(u1) # revealed: int | str
reveal_type(u2) # revealed: Literal[0, 1]
```
## Duplicate elements are collapsed
```py
def _(u1: int | int | str, u2: int | str | int) -> None:
reveal_type(u1) # revealed: int | str
reveal_type(u2) # revealed: int | str
```
## `Never` is removed
`Never` is an empty set, a type with no inhabitants. Its presence in a union is always redundant,
and so we eagerly simplify it away. `NoReturn` is equivalent to `Never`.
```py
from typing_extensions import Never, NoReturn
def never(u1: int | Never, u2: int | Never | str) -> None:
reveal_type(u1) # revealed: int
reveal_type(u2) # revealed: int | str
def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None:
reveal_type(u1) # revealed: int
reveal_type(u2) # revealed: int | str
```
## Flattening of nested unions
```py
from typing import Literal
def _(
u1: (int | str) | bytes,
u2: int | (str | bytes),
u3: int | (str | (bytes | complex)),
) -> None:
reveal_type(u1) # revealed: int | str | bytes
reveal_type(u2) # revealed: int | str | bytes
reveal_type(u3) # revealed: int | str | bytes | complex
```
## Simplification using subtyping
The type `S | T` can be simplified to `T` if `S` is a subtype of `T`:
```py
from typing_extensions import Literal, LiteralString
def _(
u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString
) -> None:
reveal_type(u1) # revealed: str
reveal_type(u2) # revealed: str
reveal_type(u3) # revealed: str
reveal_type(u4) # revealed: str | bytes
```
## Boolean literals
The union `Literal[True] | Literal[False]` is exactly equivalent to `bool`:
```py
from typing import Literal
def _(
u1: Literal[True, False],
u2: bool | Literal[True],
u3: Literal[True] | bool,
u4: Literal[True] | Literal[True, 17],
u5: Literal[True, False, True, 17],
) -> None:
reveal_type(u1) # revealed: bool
reveal_type(u2) # revealed: bool
reveal_type(u3) # revealed: bool
reveal_type(u4) # revealed: Literal[True, 17]
reveal_type(u5) # revealed: bool | Literal[17]
```
## Do not erase `Unknown`
```py
from knot_extensions import Unknown
def _(u1: Unknown | str, u2: str | Unknown) -> None:
reveal_type(u1) # revealed: Unknown | str
reveal_type(u2) # revealed: str | Unknown
```
## Collapse multiple `Unknown`s
Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple `Unknown`s in a
union are still redundant:
```py
from knot_extensions import Unknown
def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None:
reveal_type(u1) # revealed: Unknown | str
reveal_type(u2) # revealed: Unknown | str
reveal_type(u3) # revealed: str | Unknown
```
## Subsume multiple elements
Simplifications still apply when `Unknown` is present.
```py
from knot_extensions import Unknown
def _(u1: str | Unknown | int | object):
reveal_type(u1) # revealed: Unknown | object
```
## Union of intersections
We can simplify unions of intersections:
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
def _(
i1: Intersection[P, Q] | Intersection[P, Q],
i2: Intersection[P, Q] | Intersection[Q, P],
) -> None:
reveal_type(i1) # revealed: P & Q
reveal_type(i2) # revealed: P & Q
```

View File

@@ -426,8 +426,8 @@ def _(flag: bool):
value = ("a", "b")
a, b = value
reveal_type(a) # revealed: Literal[1] | Literal["a"]
reveal_type(b) # revealed: Literal[2] | Literal["b"]
reveal_type(a) # revealed: Literal[1, "a"]
reveal_type(b) # revealed: Literal[2, "b"]
```
### Typing literal
@@ -528,8 +528,8 @@ for a, b in ((1, 2), (3, 4)):
```py
for a, b in ((1, 2), ("a", "b")):
reveal_type(a) # revealed: Literal[1] | Literal["a"]
reveal_type(b) # revealed: Literal[2] | Literal["b"]
reveal_type(a) # revealed: Literal[1, "a"]
reveal_type(b) # revealed: Literal[2, "b"]
```
### Mixed literals values (2)

View File

@@ -109,6 +109,7 @@ pub enum KnownModule {
#[allow(dead_code)]
Abc, // currently only used in tests
Collections,
KnotExtensions,
}
impl KnownModule {
@@ -122,6 +123,7 @@ impl KnownModule {
Self::Sys => "sys",
Self::Abc => "abc",
Self::Collections => "collections",
Self::KnotExtensions => "knot_extensions",
}
}
@@ -147,6 +149,7 @@ impl KnownModule {
"sys" => Some(Self::Sys),
"abc" => Some(Self::Abc),
"collections" => Some(Self::Collections),
"knot_extensions" => Some(Self::KnotExtensions),
_ => None,
}
}
@@ -154,4 +157,8 @@ impl KnownModule {
pub const fn is_typing(self) -> bool {
matches!(self, Self::Typing)
}
pub const fn is_knot_extensions(self) -> bool {
matches!(self, Self::KnotExtensions)
}
}

View File

@@ -404,6 +404,17 @@ impl<'db> SemanticIndexBuilder<'db> {
pattern: &ast::Pattern,
guard: Option<&ast::Expr>,
) -> Constraint<'db> {
// This is called for the top-level pattern of each match arm. We need to create a
// standalone expression for each arm of a match statement, since they can introduce
// constraints on the match subject. (Or more accurately, for the match arm's pattern,
// since its the pattern that introduces any constraints, not the body.) Ideally, that
// standalone expression would wrap the match arm's pattern as a whole. But a standalone
// expression can currently only wrap an ast::Expr, which patterns are not. So, we need to
// choose an Expr that can “stand in” for the pattern, which we can wrap in a standalone
// expression.
//
// See the comment in TypeInferenceBuilder::infer_match_pattern for more details.
let guard = guard.map(|guard| self.add_standalone_expression(guard));
let kind = match pattern {
@@ -414,6 +425,10 @@ impl<'db> SemanticIndexBuilder<'db> {
ast::Pattern::MatchSingleton(singleton) => {
PatternConstraintKind::Singleton(singleton.value, guard)
}
ast::Pattern::MatchClass(pattern) => {
let cls = self.add_standalone_expression(&pattern.cls);
PatternConstraintKind::Class(cls, guard)
}
_ => PatternConstraintKind::Unsupported,
};
@@ -878,12 +893,11 @@ where
}
ast::Stmt::If(node) => {
self.visit_expr(&node.test);
let pre_if = self.flow_snapshot();
let constraint = self.record_expression_constraint(&node.test);
let mut constraints = vec![constraint];
let mut no_branch_taken = self.flow_snapshot();
let mut last_constraint = self.record_expression_constraint(&node.test);
self.visit_body(&node.body);
let visibility_constraint_id = self.record_visibility_constraint(constraint);
let visibility_constraint_id = self.record_visibility_constraint(last_constraint);
let mut vis_constraints = vec![visibility_constraint_id];
let mut post_clauses: Vec<FlowSnapshot> = vec![];
@@ -907,26 +921,27 @@ where
// the state that we merge the other snapshots into
post_clauses.push(self.flow_snapshot());
// we can only take an elif/else branch if none of the previous ones were
// taken, so the block entry state is always `pre_if`
self.flow_restore(pre_if.clone());
for constraint in &constraints {
self.record_negated_constraint(*constraint);
}
// taken
self.flow_restore(no_branch_taken.clone());
self.record_negated_constraint(last_constraint);
let elif_constraint = if let Some(elif_test) = clause_test {
self.visit_expr(elif_test);
// A test expression is evaluated whether the branch is taken or not
no_branch_taken = self.flow_snapshot();
let constraint = self.record_expression_constraint(elif_test);
constraints.push(constraint);
Some(constraint)
} else {
None
};
self.visit_body(clause_body);
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
}
if let Some(elif_constraint) = elif_constraint {
last_constraint = elif_constraint;
let id = self.record_visibility_constraint(elif_constraint);
vis_constraints.push(id);
}
@@ -936,7 +951,7 @@ where
self.flow_merge(post_clause_state);
}
self.simplify_visibility_constraints(pre_if);
self.simplify_visibility_constraints(no_branch_taken);
}
ast::Stmt::While(ast::StmtWhile {
test,
@@ -1089,37 +1104,35 @@ where
cases,
range: _,
}) => {
debug_assert_eq!(self.current_match_case, None);
let subject_expr = self.add_standalone_expression(subject);
self.visit_expr(subject);
let after_subject = self.flow_snapshot();
let Some((first, remaining)) = cases.split_first() else {
if cases.is_empty() {
return;
};
let first_constraint_id = self.add_pattern_constraint(
subject_expr,
&first.pattern,
first.guard.as_deref(),
);
self.visit_match_case(first);
let first_vis_constraint_id =
self.record_visibility_constraint(first_constraint_id);
let mut vis_constraints = vec![first_vis_constraint_id];
let after_subject = self.flow_snapshot();
let mut vis_constraints = vec![];
let mut post_case_snapshots = vec![];
for case in remaining {
post_case_snapshots.push(self.flow_snapshot());
self.flow_restore(after_subject.clone());
for (i, case) in cases.iter().enumerate() {
if i != 0 {
post_case_snapshots.push(self.flow_snapshot());
self.flow_restore(after_subject.clone());
}
self.current_match_case = Some(CurrentMatchCase::new(&case.pattern));
self.visit_pattern(&case.pattern);
self.current_match_case = None;
let constraint_id = self.add_pattern_constraint(
subject_expr,
&case.pattern,
case.guard.as_deref(),
);
self.visit_match_case(case);
if let Some(expr) = &case.guard {
self.visit_expr(expr);
}
self.visit_body(&case.body);
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
}
@@ -1538,18 +1551,6 @@ where
}
}
fn visit_match_case(&mut self, match_case: &'ast ast::MatchCase) {
debug_assert!(self.current_match_case.is_none());
self.current_match_case = Some(CurrentMatchCase::new(&match_case.pattern));
self.visit_pattern(&match_case.pattern);
self.current_match_case = None;
if let Some(expr) = &match_case.guard {
self.visit_expr(expr);
}
self.visit_body(&match_case.body);
}
fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) {
if let ast::Pattern::MatchStar(ast::PatternMatchStar {
name: Some(name),
@@ -1636,6 +1637,7 @@ impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> {
}
}
#[derive(Debug, PartialEq)]
struct CurrentMatchCase<'a> {
/// The pattern that's part of the current match case.
pattern: &'a ast::Pattern,

View File

@@ -22,6 +22,7 @@ pub(crate) enum ConstraintNode<'db> {
pub(crate) enum PatternConstraintKind<'db> {
Singleton(Singleton, Option<Expression<'db>>),
Value(Expression<'db>, Option<Expression<'db>>),
Class(Expression<'db>, Option<Expression<'db>>),
Unsupported,
}

View File

@@ -74,6 +74,11 @@ impl<'db> Definition<'db> {
Some(KnownModule::Typing | KnownModule::TypingExtensions)
)
}
pub(crate) fn is_knot_extensions_definition(self, db: &'db dyn Db) -> bool {
file_to_module(db, self.file(db))
.is_some_and(|module| module.is_known(KnownModule::KnotExtensions))
}
}
#[derive(Copy, Clone, Debug)]

View File

@@ -310,7 +310,7 @@ impl SymbolState {
visibility_constraints: VisibilityConstraintPerBinding::default(),
},
declarations: SymbolDeclarations {
live_declarations: self.declarations.live_declarations.clone(),
live_declarations: Declarations::default(),
visibility_constraints: VisibilityConstraintPerDeclaration::default(),
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,8 @@ impl<'db> UnionBuilder<'db> {
let mut to_add = ty;
let mut to_remove = SmallVec::<[usize; 2]>::new();
let ty_negated = ty.negate(self.db);
for (index, element) in self.elements.iter().enumerate() {
if Some(*element) == bool_pair {
to_add = KnownClass::Bool.to_instance(self.db);
@@ -80,6 +82,17 @@ impl<'db> UnionBuilder<'db> {
return self;
} else if element.is_subtype_of(self.db, ty) {
to_remove.push(index);
} else if ty_negated.is_subtype_of(self.db, *element) {
// We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`.
// This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the
// first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is
// just `object`. Since `object` is a subtype of `element | ty`, we can only conclude that
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
// the whole union to just `object`, since all other potential elements would also be subtypes of
// `object`.
self.elements.clear();
self.elements.push(KnownClass::Object.to_instance(self.db));
return self;
}
}
match to_remove[..] {
@@ -321,7 +334,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.add_positive(db, *neg);
}
}
ty @ (Type::Any | Type::Unknown | Type::Todo(_)) => {
Type::Never => {
// Adding ~Never to an intersection is a no-op.
}
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => {
// Adding ~object to an intersection results in Never.
*self = Self::default();
self.positive.insert(Type::Never);
}
ty @ Type::Dynamic(_) => {
// Adding any of these types to the negative side of an intersection
// is equivalent to adding it to the positive side. We do this to
// simplify the representation.
@@ -386,18 +407,34 @@ impl<'db> InnerIntersectionBuilder<'db> {
#[cfg(test)]
mod tests {
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
use crate::db::tests::setup_db;
use crate::types::{KnownClass, Truthiness};
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem;
use test_case::test_case;
#[test]
fn build_union() {
fn build_union_no_elements() {
let db = setup_db();
let empty_union = UnionBuilder::new(&db).build();
assert_eq!(empty_union, Type::Never);
}
#[test]
fn build_union_single_element() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let union = UnionType::from_elements(&db, [t0]);
assert_eq!(union, t0);
}
#[test]
fn build_union_two_elements() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
@@ -405,605 +442,12 @@ mod tests {
assert_eq!(union.elements(&db), &[t0, t1]);
}
#[test]
fn build_union_single() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let ty = UnionType::from_elements(&db, [t0]);
assert_eq!(ty, t0);
}
#[test]
fn build_union_empty() {
let db = setup_db();
let ty = UnionBuilder::new(&db).build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_union_never() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let ty = UnionType::from_elements(&db, [t0, Type::Never]);
assert_eq!(ty, t0);
}
#[test]
fn build_union_bool() {
let db = setup_db();
let bool_instance_ty = KnownClass::Bool.to_instance(&db);
let t0 = Type::BooleanLiteral(true);
let t1 = Type::BooleanLiteral(true);
let t2 = Type::BooleanLiteral(false);
let t3 = Type::IntLiteral(17);
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
assert_eq!(union.elements(&db), &[t0, t3]);
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
assert_eq!(union.elements(&db), &[bool_instance_ty, t3]);
let result_ty = UnionType::from_elements(&db, [bool_instance_ty, t0]);
assert_eq!(result_ty, bool_instance_ty);
let result_ty = UnionType::from_elements(&db, [t0, bool_instance_ty]);
assert_eq!(result_ty, bool_instance_ty);
}
#[test]
fn build_union_flatten() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let t2 = Type::IntLiteral(2);
let u1 = UnionType::from_elements(&db, [t0, t1]);
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
assert_eq!(union.elements(&db), &[t0, t1, t2]);
}
#[test]
fn build_union_simplify_subtype() {
let db = setup_db();
let t0 = KnownClass::Str.to_instance(&db);
let t1 = Type::LiteralString;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let u1 = UnionType::from_elements(&db, [t1, t0]);
assert_eq!(u0, t0);
assert_eq!(u1, t0);
}
#[test]
fn build_union_no_simplify_unknown() {
let db = setup_db();
let t0 = KnownClass::Str.to_instance(&db);
let t1 = Type::Unknown;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let u1 = UnionType::from_elements(&db, [t1, t0]);
assert_eq!(u0.expect_union().elements(&db), &[t0, t1]);
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
}
#[test]
fn build_union_simplify_multiple_unknown() {
let db = setup_db();
let t0 = KnownClass::Str.to_instance(&db);
let t1 = Type::Unknown;
let u = UnionType::from_elements(&db, [t0, t1, t1]);
assert_eq!(u.expect_union().elements(&db), &[t0, t1]);
}
#[test]
fn build_union_subsume_multiple() {
let db = setup_db();
let str_ty = KnownClass::Str.to_instance(&db);
let int_ty = KnownClass::Int.to_instance(&db);
let object_ty = KnownClass::Object.to_instance(&db);
let unknown_ty = Type::Unknown;
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
assert_eq!(u0.expect_union().elements(&db), &[unknown_ty, object_ty]);
}
impl<'db> IntersectionType<'db> {
fn pos_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
self.positive(db).into_iter().copied().collect()
}
fn neg_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
self.negative(db).into_iter().copied().collect()
}
}
#[test]
fn build_intersection() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let ta = Type::Any;
let intersection = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_negative(t0)
.build()
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[ta]);
assert_eq!(intersection.neg_vec(&db), &[t0]);
}
#[test]
fn build_intersection_empty_intersection_equals_object() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db).build();
assert_eq!(ty, KnownClass::Object.to_instance(&db));
}
#[test]
fn build_intersection_flatten_positive() {
let db = setup_db();
let ta = Type::Any;
let t1 = Type::IntLiteral(1);
let t2 = Type::IntLiteral(2);
let i0 = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_negative(t1)
.build();
let intersection = IntersectionBuilder::new(&db)
.add_positive(t2)
.add_positive(i0)
.build()
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
assert_eq!(intersection.neg_vec(&db), &[]);
}
#[test]
fn build_intersection_flatten_negative() {
let db = setup_db();
let ta = Type::Any;
let t1 = Type::IntLiteral(1);
let t2 = KnownClass::Int.to_instance(&db);
// i0 = Any & ~Literal[1]
let i0 = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_negative(t1)
.build();
// ta_not_i0 = int & ~(Any & ~Literal[1])
// -> int & (~Any | Literal[1])
// (~Any is equivalent to Any)
// -> (int & Any) | (int & Literal[1])
// -> (int & Any) | Literal[1]
let ta_not_i0 = IntersectionBuilder::new(&db)
.add_positive(t2)
.add_negative(i0)
.build();
assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]");
}
#[test]
fn build_intersection_simplify_negative_any() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_negative(Type::Any)
.build();
assert_eq!(ty, Type::Any);
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::Never)
.add_negative(Type::Any)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_multiple_unknown() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::Unknown)
.add_positive(Type::Unknown)
.build();
assert_eq!(ty, Type::Unknown);
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::Unknown)
.add_negative(Type::Unknown)
.build();
assert_eq!(ty, Type::Unknown);
let ty = IntersectionBuilder::new(&db)
.add_negative(Type::Unknown)
.add_negative(Type::Unknown)
.build();
assert_eq!(ty, Type::Unknown);
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::Unknown)
.add_positive(Type::IntLiteral(0))
.add_negative(Type::Unknown)
.build();
assert_eq!(
ty,
IntersectionBuilder::new(&db)
.add_positive(Type::Unknown)
.add_positive(Type::IntLiteral(0))
.build()
);
}
#[test]
fn intersection_distributes_over_union() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let ta = Type::Any;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let union = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_positive(u0)
.build()
.expect_union();
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements(&db)[..] else {
panic!("expected a union of two intersections");
};
assert_eq!(i0.pos_vec(&db), &[ta, t0]);
assert_eq!(i1.pos_vec(&db), &[ta, t1]);
}
#[test]
fn intersection_negation_distributes_over_union() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
r#"
class A: ...
class B: ...
"#,
)
.unwrap();
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
let a = global_symbol(&db, module, "A")
.expect_type()
.to_instance(&db);
let b = global_symbol(&db, module, "B")
.expect_type()
.to_instance(&db);
// intersection: A & B
let intersection = IntersectionBuilder::new(&db)
.add_positive(a)
.add_positive(b)
.build()
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[a, b]);
assert_eq!(intersection.neg_vec(&db), &[]);
// ~intersection => ~A | ~B
let negated_intersection = IntersectionBuilder::new(&db)
.add_negative(Type::Intersection(intersection))
.build()
.expect_union();
// should have as elements ~A and ~B
let not_a = a.negate(&db);
let not_b = b.negate(&db);
assert_eq!(negated_intersection.elements(&db), &[not_a, not_b]);
}
#[test]
fn mixed_intersection_negation_distributes_over_union() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
r#"
class A: ...
class B: ...
"#,
)
.unwrap();
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
let a = global_symbol(&db, module, "A")
.expect_type()
.to_instance(&db);
let b = global_symbol(&db, module, "B")
.expect_type()
.to_instance(&db);
let int = KnownClass::Int.to_instance(&db);
// a_not_b: A & ~B
let a_not_b = IntersectionBuilder::new(&db)
.add_positive(a)
.add_negative(b)
.build()
.expect_intersection();
assert_eq!(a_not_b.pos_vec(&db), &[a]);
assert_eq!(a_not_b.neg_vec(&db), &[b]);
// let's build
// int & ~(A & ~B)
// = int & ~(A & ~B)
// = int & (~A | B)
// = (int & ~A) | (int & B)
let t = IntersectionBuilder::new(&db)
.add_positive(int)
.add_negative(Type::Intersection(a_not_b))
.build();
assert_eq!(t.display(&db).to_string(), "int & ~A | int & B");
}
#[test]
fn build_intersection_self_negation() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::none(&db))
.add_negative(Type::none(&db))
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_negative_never() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::none(&db))
.add_negative(Type::Never)
.build();
assert_eq!(ty, Type::none(&db));
}
#[test]
fn build_intersection_simplify_positive_never() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::none(&db))
.add_positive(Type::Never)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_negative_none() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_negative(Type::none(&db))
.add_positive(Type::IntLiteral(1))
.build();
assert_eq!(ty, Type::IntLiteral(1));
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::IntLiteral(1))
.add_negative(Type::none(&db))
.build();
assert_eq!(ty, Type::IntLiteral(1));
}
#[test]
fn build_negative_union_de_morgan() {
let db = setup_db();
let union = UnionBuilder::new(&db)
.add(Type::IntLiteral(1))
.add(Type::IntLiteral(2))
.build();
assert_eq!(union.display(&db).to_string(), "Literal[1, 2]");
let ty = IntersectionBuilder::new(&db).add_negative(union).build();
let expected = IntersectionBuilder::new(&db)
.add_negative(Type::IntLiteral(1))
.add_negative(Type::IntLiteral(2))
.build();
assert_eq!(ty.display(&db).to_string(), "~Literal[1] & ~Literal[2]");
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_positive_type_and_positive_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let ty = IntersectionBuilder::new(&db)
.add_positive(t)
.add_positive(s)
.build();
assert_eq!(ty, s);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_positive(t)
.build();
assert_eq!(ty, s);
let literal = Type::string_literal(&db, "a");
let expected = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(literal)
.build();
let ty = IntersectionBuilder::new(&db)
.add_positive(t)
.add_negative(literal)
.add_positive(s)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(literal)
.add_positive(t)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_negative_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_negative(s)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_negative(s)
.add_negative(t)
.build();
assert_eq!(ty, expected);
let object = KnownClass::Object.to_instance(&db);
let expected = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(object)
.build();
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(object)
.add_negative(s)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_multiple_negative_subtypes() {
let db = setup_db();
let s1 = Type::IntLiteral(1);
let s2 = Type::IntLiteral(2);
let t = KnownClass::Int.to_instance(&db);
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
let ty = IntersectionBuilder::new(&db)
.add_negative(s1)
.add_negative(s2)
.add_negative(t)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_positive_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(s)
.build();
assert_eq!(ty, Type::Never);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(t)
.build();
assert_eq!(ty, Type::Never);
// This should also work in the presence of additional contributions:
let ty = IntersectionBuilder::new(&db)
.add_positive(KnownClass::Object.to_instance(&db))
.add_negative(t)
.add_positive(s)
.build();
assert_eq!(ty, Type::Never);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(Type::string_literal(&db, "a"))
.add_negative(t)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_disjoint_positive_types() {
let db = setup_db();
let t1 = Type::IntLiteral(1);
let t2 = Type::none(&db);
let ty = IntersectionBuilder::new(&db)
.add_positive(t1)
.add_positive(t2)
.build();
assert_eq!(ty, Type::Never);
// If there are any negative contributions, they should
// be removed too.
let ty = IntersectionBuilder::new(&db)
.add_positive(KnownClass::Str.to_instance(&db))
.add_negative(Type::LiteralString)
.add_positive(t2)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_disjoint_positive_and_negative_types() {
let db = setup_db();
let t_p = KnownClass::Int.to_instance(&db);
let t_n = Type::string_literal(&db, "t_n");
let ty = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(t_n)
.build();
assert_eq!(ty, t_p);
let ty = IntersectionBuilder::new(&db)
.add_negative(t_n)
.add_positive(t_p)
.build();
assert_eq!(ty, t_p);
let int_literal = Type::IntLiteral(1);
let expected = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(int_literal)
.build();
let ty = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(int_literal)
.add_negative(t_n)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_negative(t_n)
.add_negative(int_literal)
.add_positive(t_p)
.build();
assert_eq!(ty, expected);
let intersection = IntersectionBuilder::new(&db).build();
assert_eq!(intersection, KnownClass::Object.to_instance(&db));
}
#[test_case(Type::BooleanLiteral(true))]
@@ -1048,85 +492,4 @@ mod tests {
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
}
#[test_case(Type::Any)]
#[test_case(Type::Unknown)]
#[test_case(todo_type!())]
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
let db = setup_db();
let result = IntersectionBuilder::new(&db)
.add_positive(ty)
.add_negative(ty)
.build();
assert_eq!(result, ty);
let result = IntersectionBuilder::new(&db)
.add_negative(ty)
.add_positive(ty)
.build();
assert_eq!(result, ty);
}
#[test]
fn build_intersection_of_two_unions_simplify() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
"
class A: ...
class B: ...
a = A()
b = B()
",
)
.unwrap();
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
let a = global_symbol(&db, file, "a").expect_type();
let b = global_symbol(&db, file, "b").expect_type();
let union = UnionBuilder::new(&db).add(a).add(b).build();
assert_eq!(union.display(&db).to_string(), "A | B");
let reversed_union = UnionBuilder::new(&db).add(b).add(a).build();
assert_eq!(reversed_union.display(&db).to_string(), "B | A");
let intersection = IntersectionBuilder::new(&db)
.add_positive(union)
.add_positive(reversed_union)
.build();
assert_eq!(intersection.display(&db).to_string(), "B | A");
}
#[test]
fn build_union_of_two_intersections_simplify() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
"
class A: ...
class B: ...
a = A()
b = B()
",
)
.unwrap();
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
let a = global_symbol(&db, file, "a").expect_type();
let b = global_symbol(&db, file, "b").expect_type();
let intersection = IntersectionBuilder::new(&db)
.add_positive(a)
.add_positive(b)
.build();
let reversed_intersection = IntersectionBuilder::new(&db)
.add_positive(b)
.add_positive(a)
.build();
let union = UnionBuilder::new(&db)
.add(intersection)
.add(reversed_intersection)
.build();
assert_eq!(union.display(&db).to_string(), "A & B");
}
}

View File

@@ -1,17 +1,32 @@
use super::context::InferContext;
use super::diagnostic::CALL_NON_CALLABLE;
use super::{Severity, Type, TypeArrayDisplay, UnionBuilder};
use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE};
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
use crate::Db;
use ruff_db::diagnostic::DiagnosticId;
use ruff_python_ast as ast;
mod arguments;
mod bind;
pub(super) use arguments::{Argument, CallArguments};
pub(super) use bind::{bind_call, CallBinding};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum StaticAssertionErrorKind<'db> {
ArgumentIsFalse,
ArgumentIsFalsy(Type<'db>),
ArgumentTruthinessIsAmbiguous(Type<'db>),
CustomError(&'db str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallOutcome<'db> {
Callable {
return_ty: Type<'db>,
binding: CallBinding<'db>,
},
RevealType {
return_ty: Type<'db>,
binding: CallBinding<'db>,
revealed_ty: Type<'db>,
},
NotCallable {
@@ -25,12 +40,20 @@ pub(super) enum CallOutcome<'db> {
called_ty: Type<'db>,
call_outcome: Box<CallOutcome<'db>>,
},
StaticAssertionError {
binding: CallBinding<'db>,
error_kind: StaticAssertionErrorKind<'db>,
},
AssertType {
binding: CallBinding<'db>,
asserted_ty: Type<'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 }
pub(super) fn callable(binding: CallBinding<'db>) -> CallOutcome<'db> {
CallOutcome::Callable { binding }
}
/// Create a new `CallOutcome::NotCallable` with given not-callable type.
@@ -39,9 +62,9 @@ impl<'db> CallOutcome<'db> {
}
/// 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> {
pub(super) fn revealed(binding: CallBinding<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::RevealType {
return_ty,
binding,
revealed_ty,
}
}
@@ -57,14 +80,22 @@ impl<'db> CallOutcome<'db> {
}
}
/// Create a new `CallOutcome::AssertType` with given revealed and return types.
pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::AssertType {
binding,
asserted_ty,
}
}
/// 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::Callable { binding } => Some(binding.return_ty()),
Self::RevealType {
return_ty,
binding,
revealed_ty: _,
} => Some(*return_ty),
} => Some(binding.return_ty()),
Self::NotCallable { not_callable_ty: _ } => None,
Self::Union {
outcomes,
@@ -78,11 +109,16 @@ impl<'db> CallOutcome<'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))),
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::unknown()))),
}
})
.map(UnionBuilder::build),
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
Self::StaticAssertionError { .. } => Some(Type::none(db)),
Self::AssertType {
binding,
asserted_ty: _,
} => Some(binding.return_ty()),
}
}
@@ -163,23 +199,30 @@ impl<'db> CallOutcome<'db> {
context: &InferContext<'db>,
node: ast::AnyNodeRef,
) -> Result<Type<'db>, NotCallableError<'db>> {
// TODO should this method emit diagnostics directly, or just return results that allow the
// caller to decide about emitting diagnostics? Currently it emits binding diagnostics, but
// only non-callable diagnostics in the union case, which is inconsistent.
match self {
Self::Callable { return_ty } => Ok(*return_ty),
Self::Callable { binding } => {
binding.report_diagnostics(context, node);
Ok(binding.return_ty())
}
Self::RevealType {
return_ty,
binding,
revealed_ty,
} => {
binding.report_diagnostics(context, node);
context.report_diagnostic(
node,
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
);
Ok(*return_ty)
Ok(binding.return_ty())
}
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
not_callable_ty: *not_callable_ty,
return_ty: Type::Unknown,
return_ty: Type::unknown(),
}),
Self::PossiblyUnboundDunderCall {
called_ty,
@@ -188,7 +231,7 @@ impl<'db> CallOutcome<'db> {
callable_ty: *called_ty,
return_ty: call_outcome
.return_ty(context.db())
.unwrap_or(Type::Unknown),
.unwrap_or(Type::unknown()),
}),
Self::Union {
outcomes,
@@ -201,14 +244,14 @@ impl<'db> CallOutcome<'db> {
let return_ty = match outcome {
Self::NotCallable { not_callable_ty } => {
not_callable.push(*not_callable_ty);
Type::Unknown
Type::unknown()
}
Self::RevealType {
return_ty,
binding,
revealed_ty: _,
} => {
if revealed {
*return_ty
binding.return_ty()
} else {
revealed = true;
outcome.unwrap_with_diagnostic(context, node)
@@ -237,6 +280,73 @@ impl<'db> CallOutcome<'db> {
}),
}
}
CallOutcome::StaticAssertionError {
binding,
error_kind,
} => {
binding.report_diagnostics(context, node);
match error_kind {
StaticAssertionErrorKind::ArgumentIsFalse => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!("Static assertion error: argument evaluates to `False`"),
);
}
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
parameter_ty=parameter_ty.display(context.db())
),
);
}
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(parameter_ty) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
parameter_ty=parameter_ty.display(context.db())
),
);
}
StaticAssertionErrorKind::CustomError(message) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!("Static assertion error: {message}"),
);
}
}
Ok(Type::unknown())
}
CallOutcome::AssertType {
binding,
asserted_ty,
} => {
let [actual_ty, _asserted] = binding.parameter_tys() else {
return Ok(binding.return_ty());
};
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
context.report_lint(
&TYPE_ASSERTION_FAILURE,
node,
format_args!(
"Actual type `{}` is not the same as asserted type `{}`",
actual_ty.display(context.db()),
asserted_ty.display(context.db()),
),
);
}
Ok(binding.return_ty())
}
}
}
}

View File

@@ -0,0 +1,73 @@
use super::Type;
/// Typed arguments for a single call, in source order.
#[derive(Clone, Debug, Default)]
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
impl<'a, 'db> CallArguments<'a, 'db> {
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
positional_tys
.into_iter()
.map(Argument::Positional)
.collect()
}
/// Prepend an extra positional argument.
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
let mut arguments = Vec::with_capacity(self.0.len() + 1);
arguments.push(Argument::Synthetic(self_ty));
arguments.extend_from_slice(&self.0);
Self(arguments)
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
self.0.iter()
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
self.0.first().map(Argument::ty)
}
}
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
type Item = &'b Argument<'a, 'db>;
type IntoIter = std::slice::Iter<'b, Argument<'a, 'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'a, 'db> FromIterator<Argument<'a, 'db>> for CallArguments<'a, 'db> {
fn from_iter<T: IntoIterator<Item = Argument<'a, 'db>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Clone, Debug)]
pub(crate) enum Argument<'a, 'db> {
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
Synthetic(Type<'db>),
/// A positional argument.
Positional(Type<'db>),
/// A starred positional argument (e.g. `*args`).
Variadic(Type<'db>),
/// A keyword argument (e.g. `a=1`).
Keyword { name: &'a str, ty: Type<'db> },
/// The double-starred keywords argument (e.g. `**kwargs`).
Keywords(Type<'db>),
}
impl<'db> Argument<'_, 'db> {
fn ty(&self) -> Type<'db> {
match self {
Self::Synthetic(ty) => *ty,
Self::Positional(ty) => *ty,
Self::Variadic(ty) => *ty,
Self::Keyword { name: _, ty } => *ty,
Self::Keywords(ty) => *ty,
}
}
}

View File

@@ -0,0 +1,411 @@
use super::{Argument, CallArguments, InferContext, Signature, Type};
use crate::db::Db;
use crate::types::diagnostic::{
INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED,
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
};
use crate::types::signatures::Parameter;
use crate::types::UnionType;
use ruff_python_ast as ast;
/// Bind a [`CallArguments`] against a callable [`Signature`].
///
/// The returned [`CallBinding`] provides the return type of the call, the bound types for all
/// parameters, and any errors resulting from binding the call.
pub(crate) fn bind_call<'db>(
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
signature: &Signature<'db>,
callable_ty: Option<Type<'db>>,
) -> CallBinding<'db> {
let parameters = signature.parameters();
// The type assigned to each parameter at this call site.
let mut parameter_tys = vec![None; parameters.len()];
let mut errors = vec![];
let mut next_positional = 0;
let mut first_excess_positional = None;
let mut num_synthetic_args = 0;
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
if argument_index >= num_synthetic_args {
// Adjust the argument index to skip synthetic args, which don't appear at the call
// site and thus won't be in the Call node arguments list.
Some(argument_index - num_synthetic_args)
} else {
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
// entire Call node, since there's no argument node for this argument at the call site
None
}
};
for (argument_index, argument) in arguments.iter().enumerate() {
let (index, parameter, argument_ty, positional) = match argument {
Argument::Positional(ty) | Argument::Synthetic(ty) => {
if matches!(argument, Argument::Synthetic(_)) {
num_synthetic_args += 1;
}
let Some((index, parameter)) = parameters
.get_positional(next_positional)
.map(|param| (next_positional, param))
.or_else(|| parameters.variadic())
else {
first_excess_positional.get_or_insert(argument_index);
next_positional += 1;
continue;
};
next_positional += 1;
(index, parameter, ty, !parameter.is_variadic())
}
Argument::Keyword { name, ty } => {
let Some((index, parameter)) = parameters
.keyword_by_name(name)
.or_else(|| parameters.keyword_variadic())
else {
errors.push(CallBindingError::UnknownArgument {
argument_name: ast::name::Name::new(name),
argument_index: get_argument_index(argument_index, num_synthetic_args),
});
continue;
};
(index, parameter, ty, false)
}
Argument::Variadic(_) | Argument::Keywords(_) => {
// TODO
continue;
}
};
if let Some(expected_ty) = parameter.annotated_ty() {
if !argument_ty.is_assignable_to(db, expected_ty) {
errors.push(CallBindingError::InvalidArgumentType {
parameter: ParameterContext::new(parameter, index, positional),
argument_index: get_argument_index(argument_index, num_synthetic_args),
expected_ty,
provided_ty: *argument_ty,
});
}
}
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
if parameter.is_variadic() {
let union = UnionType::from_elements(db, [existing, *argument_ty]);
parameter_tys[index].replace(union);
} else {
errors.push(CallBindingError::ParameterAlreadyAssigned {
argument_index: get_argument_index(argument_index, num_synthetic_args),
parameter: ParameterContext::new(parameter, index, positional),
});
}
}
}
if let Some(first_excess_argument_index) = first_excess_positional {
errors.push(CallBindingError::TooManyPositionalArguments {
first_excess_argument_index: get_argument_index(
first_excess_argument_index,
num_synthetic_args,
),
expected_positional_count: parameters.positional().count(),
provided_positional_count: next_positional,
});
}
let mut missing = vec![];
for (index, bound_ty) in parameter_tys.iter().enumerate() {
if bound_ty.is_none() {
let param = &parameters[index];
if param.is_variadic() || param.is_keyword_variadic() || param.default_ty().is_some() {
// variadic/keywords and defaulted arguments are not required
continue;
}
missing.push(ParameterContext::new(param, index, false));
}
}
if !missing.is_empty() {
errors.push(CallBindingError::MissingArguments {
parameters: ParameterContexts(missing),
});
}
CallBinding {
callable_ty,
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
parameter_tys: parameter_tys
.into_iter()
.map(|opt_ty| opt_ty.unwrap_or(Type::unknown()))
.collect(),
errors,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CallBinding<'db> {
/// Type of the callable object (function, class...)
callable_ty: Option<Type<'db>>,
/// Return type of the call.
return_ty: Type<'db>,
/// Bound types for parameters, in parameter source order.
parameter_tys: Box<[Type<'db>]>,
/// Call binding errors, if any.
errors: Vec<CallBindingError<'db>>,
}
impl<'db> CallBinding<'db> {
// TODO remove this constructor and construct always from `bind_call`
pub(crate) fn from_return_ty(return_ty: Type<'db>) -> Self {
Self {
callable_ty: None,
return_ty,
parameter_tys: Box::default(),
errors: vec![],
}
}
pub(crate) fn set_return_ty(&mut self, return_ty: Type<'db>) {
self.return_ty = return_ty;
}
pub(crate) fn return_ty(&self) -> Type<'db> {
self.return_ty
}
pub(crate) fn parameter_tys(&self) -> &[Type<'db>] {
&self.parameter_tys
}
pub(crate) fn one_parameter_ty(&self) -> Option<Type<'db>> {
match self.parameter_tys() {
[ty] => Some(*ty),
_ => None,
}
}
pub(crate) fn two_parameter_tys(&self) -> Option<(Type<'db>, Type<'db>)> {
match self.parameter_tys() {
[first, second] => Some((*first, *second)),
_ => None,
}
}
fn callable_name(&self, db: &'db dyn Db) -> Option<&str> {
match self.callable_ty {
Some(Type::FunctionLiteral(function)) => Some(function.name(db)),
Some(Type::ClassLiteral(class_type)) => Some(class_type.class.name(db)),
_ => None,
}
}
pub(super) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
let callable_name = self.callable_name(context.db());
for error in &self.errors {
error.report_diagnostic(context, node, callable_name);
}
}
}
/// Information needed to emit a diagnostic regarding a parameter.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ParameterContext {
name: Option<ast::name::Name>,
index: usize,
/// Was the argument for this parameter passed positionally, and matched to a non-variadic
/// positional parameter? (If so, we will provide the index in the diagnostic, not just the
/// name.)
positional: bool,
}
impl ParameterContext {
fn new(parameter: &Parameter, index: usize, positional: bool) -> Self {
Self {
name: parameter.display_name(),
index,
positional,
}
}
}
impl std::fmt::Display for ParameterContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(name) = &self.name {
if self.positional {
write!(f, "{} (`{name}`)", self.index + 1)
} else {
write!(f, "`{name}`")
}
} else {
write!(f, "{}", self.index + 1)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ParameterContexts(Vec<ParameterContext>);
impl std::fmt::Display for ParameterContexts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut iter = self.0.iter();
if let Some(first) = iter.next() {
write!(f, "{first}")?;
for param in iter {
f.write_str(", ")?;
write!(f, "{param}")?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum CallBindingError<'db> {
/// The type of an argument is not assignable to the annotated type of its corresponding
/// parameter.
InvalidArgumentType {
parameter: ParameterContext,
argument_index: Option<usize>,
expected_ty: Type<'db>,
provided_ty: Type<'db>,
},
/// One or more required parameters (that is, with no default) is not supplied by any argument.
MissingArguments { parameters: ParameterContexts },
/// A call argument can't be matched to any parameter.
UnknownArgument {
argument_name: ast::name::Name,
argument_index: Option<usize>,
},
/// More positional arguments are provided in the call than can be handled by the signature.
TooManyPositionalArguments {
first_excess_argument_index: Option<usize>,
expected_positional_count: usize,
provided_positional_count: usize,
},
/// Multiple arguments were provided for a single parameter.
ParameterAlreadyAssigned {
argument_index: Option<usize>,
parameter: ParameterContext,
},
}
impl<'db> CallBindingError<'db> {
pub(super) fn report_diagnostic(
&self,
context: &InferContext<'db>,
node: ast::AnyNodeRef,
callable_name: Option<&str>,
) {
match self {
Self::InvalidArgumentType {
parameter,
argument_index,
expected_ty,
provided_ty,
} => {
let provided_ty_display = provided_ty.display(context.db());
let expected_ty_display = expected_ty.display(context.db());
context.report_lint(
&INVALID_ARGUMENT_TYPE,
Self::get_node(node, *argument_index),
format_args!(
"Object of type `{provided_ty_display}` cannot be assigned to \
parameter {parameter}{}; expected type `{expected_ty_display}`",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
} else {
String::new()
}
),
);
}
Self::TooManyPositionalArguments {
first_excess_argument_index,
expected_positional_count,
provided_positional_count,
} => {
context.report_lint(
&TOO_MANY_POSITIONAL_ARGUMENTS,
Self::get_node(node, *first_excess_argument_index),
format_args!(
"Too many positional arguments{}: expected \
{expected_positional_count}, got {provided_positional_count}",
if let Some(callable_name) = callable_name {
format!(" to function `{callable_name}`")
} else {
String::new()
}
),
);
}
Self::MissingArguments { parameters } => {
let s = if parameters.0.len() == 1 { "" } else { "s" };
context.report_lint(
&MISSING_ARGUMENT,
node,
format_args!(
"No argument{s} provided for required parameter{s} {parameters}{}",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
} else {
String::new()
}
),
);
}
Self::UnknownArgument {
argument_name,
argument_index,
} => {
context.report_lint(
&UNKNOWN_ARGUMENT,
Self::get_node(node, *argument_index),
format_args!(
"Argument `{argument_name}` does not match any known parameter{}",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
} else {
String::new()
}
),
);
}
Self::ParameterAlreadyAssigned {
argument_index,
parameter,
} => {
context.report_lint(
&PARAMETER_ALREADY_ASSIGNED,
Self::get_node(node, *argument_index),
format_args!(
"Multiple values provided for parameter {parameter}{}",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
} else {
String::new()
}
),
);
}
}
}
fn get_node(node: ast::AnyNodeRef, argument_index: Option<usize>) -> ast::AnyNodeRef {
// If we have a Call node and an argument index, report the diagnostic on the correct
// argument node; otherwise, report it on the entire provided node.
match (node, argument_index) {
(ast::AnyNodeRef::ExprCall(call_node), Some(argument_index)) => {
match call_node
.arguments
.arguments_source_order()
.nth(argument_index)
.expect("argument index should not be out of range")
{
ast::ArgOrKeyword::Arg(expr) => expr.into(),
ast::ArgOrKeyword::Keyword(keyword) => keyword.into(),
}
}
_ => node,
}
}
}

View File

@@ -1,5 +1,5 @@
use crate::types::{
todo_type, Class, ClassLiteralType, KnownClass, KnownInstanceType, TodoType, Type,
todo_type, Class, ClassLiteralType, DynamicType, KnownClass, KnownInstanceType, Type,
};
use crate::Db;
use itertools::Either;
@@ -8,16 +8,29 @@ use itertools::Either;
///
/// 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`]
/// transformed into [`ClassBase::unknown`]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
pub enum ClassBase<'db> {
Any,
Unknown,
Todo(TodoType),
Dynamic(DynamicType),
Class(Class<'db>),
}
impl<'db> ClassBase<'db> {
pub const fn any() -> Self {
Self::Dynamic(DynamicType::Any)
}
pub const fn unknown() -> Self {
Self::Dynamic(DynamicType::Unknown)
}
pub const fn is_dynamic(self) -> bool {
match self {
ClassBase::Dynamic(_) => true,
ClassBase::Class(_) => false,
}
}
pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
struct Display<'db> {
base: ClassBase<'db>,
@@ -27,9 +40,7 @@ impl<'db> ClassBase<'db> {
impl std::fmt::Display for Display<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.base {
ClassBase::Any => f.write_str("Any"),
ClassBase::Todo(todo) => todo.fmt(f),
ClassBase::Unknown => f.write_str("Unknown"),
ClassBase::Dynamic(dynamic) => dynamic.fmt(f),
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
}
}
@@ -43,7 +54,7 @@ impl<'db> ClassBase<'db> {
KnownClass::Object
.to_class_literal(db)
.into_class_literal()
.map_or(Self::Unknown, |ClassLiteralType { class }| {
.map_or(Self::unknown(), |ClassLiteralType { class }| {
Self::Class(class)
})
}
@@ -53,9 +64,7 @@ impl<'db> ClassBase<'db> {
/// Return `None` if `ty` is not an acceptable type for a class base.
pub(super) 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(todo) => Some(Self::Todo(todo)),
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
@@ -93,8 +102,12 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::Required
| KnownInstanceType::TypeAlias
| KnownInstanceType::ReadOnly
| KnownInstanceType::Optional => None,
KnownInstanceType::Any => Some(Self::Any),
| KnownInstanceType::Optional
| KnownInstanceType::Not
| KnownInstanceType::Intersection
| KnownInstanceType::TypeOf => None,
KnownInstanceType::Unknown => Some(Self::unknown()),
KnownInstanceType::Any => Some(Self::any()),
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
KnownInstanceType::Dict => {
Self::try_from_ty(db, KnownClass::Dict.to_class_literal(db))
@@ -139,7 +152,7 @@ impl<'db> ClassBase<'db> {
pub(super) fn into_class(self) -> Option<Class<'db>> {
match self {
Self::Class(class) => Some(class),
_ => None,
Self::Dynamic(_) => None,
}
}
@@ -149,13 +162,7 @@ impl<'db> ClassBase<'db> {
db: &'db dyn Db,
) -> Either<impl Iterator<Item = ClassBase<'db>>, impl Iterator<Item = ClassBase<'db>>> {
match self {
ClassBase::Any => Either::Left([ClassBase::Any, ClassBase::object(db)].into_iter()),
ClassBase::Unknown => {
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
}
ClassBase::Todo(todo) => {
Either::Left([ClassBase::Todo(todo), ClassBase::object(db)].into_iter())
}
ClassBase::Dynamic(_) => Either::Left([self, ClassBase::object(db)].into_iter()),
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
}
}
@@ -170,9 +177,7 @@ impl<'db> From<Class<'db>> for ClassBase<'db> {
impl<'db> From<ClassBase<'db>> for Type<'db> {
fn from(value: ClassBase<'db>) -> Self {
match value {
ClassBase::Any => Type::Any,
ClassBase::Todo(todo) => Type::Todo(todo),
ClassBase::Unknown => Type::Unknown,
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
ClassBase::Class(class) => Type::class_literal(class),
}
}

View File

@@ -162,6 +162,11 @@ impl<'db> InferContext<'db> {
}
}
/// Are we currently inferring types in a stub file?
pub(crate) fn in_stub(&self) -> bool {
self.file.is_stub(self.db().upcast())
}
#[must_use]
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
self.bomb.defuse();

View File

@@ -30,27 +30,35 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INCOMPATIBLE_SLOTS);
registry.register_lint(&INCONSISTENT_MRO);
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
registry.register_lint(&INVALID_ARGUMENT_TYPE);
registry.register_lint(&INVALID_ASSIGNMENT);
registry.register_lint(&INVALID_BASE);
registry.register_lint(&INVALID_CONTEXT_MANAGER);
registry.register_lint(&INVALID_DECLARATION);
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_RAISE);
registry.register_lint(&INVALID_TYPE_FORM);
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&MISSING_ARGUMENT);
registry.register_lint(&NON_SUBSCRIPTABLE);
registry.register_lint(&NOT_ITERABLE);
registry.register_lint(&PARAMETER_ALREADY_ASSIGNED);
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
registry.register_lint(&SUBCLASS_OF_FINAL_CLASS);
registry.register_lint(&TYPE_ASSERTION_FAILURE);
registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS);
registry.register_lint(&UNDEFINED_REVEAL);
registry.register_lint(&UNKNOWN_ARGUMENT);
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
registry.register_lint(&UNRESOLVED_IMPORT);
registry.register_lint(&UNRESOLVED_REFERENCE);
registry.register_lint(&UNSUPPORTED_OPERATOR);
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
registry.register_lint(&STATIC_ASSERT_ERROR);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -226,6 +234,27 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Detects call arguments whose type is not assignable to the corresponding typed parameter.
///
/// ## Why is this bad?
/// Passing an argument of a type the function (or callable object) does not accept violates
/// the expectations of the function author and may cause unexpected runtime errors within the
/// body of the function.
///
/// ## Examples
/// ```python
/// def func(x: int): ...
/// func("foo") # error: [invalid-argument-type]
/// ```
pub(crate) static INVALID_ARGUMENT_TYPE = {
summary: "detects call arguments whose type is not assignable to the corresponding typed parameter",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
pub(crate) static INVALID_ASSIGNMENT = {
@@ -263,6 +292,7 @@ declare_lint! {
}
declare_lint! {
/// ## What it does
/// Checks for exception handlers that catch non-exception classes.
///
/// ## Why is this bad?
@@ -297,6 +327,33 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for arguments to `metaclass=` that are invalid.
///
/// ## Why is this bad?
/// Python allows arbitrary expressions to be used as the argument to `metaclass=`.
/// These expressions, however, need to be callable and accept the same arguments
/// as `type.__new__`.
///
/// ## Example
///
/// ```python
/// def f(): ...
///
/// # TypeError: f() takes 0 positional arguments but 3 were given
/// class B(metaclass=f): ...
/// ```
///
/// ## References
/// - [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
pub(crate) static INVALID_METACLASS = {
summary: "detects invalid `metaclass=` arguments",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for default values that can't be assigned to the parameter's annotated type.
@@ -375,6 +432,25 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for missing required arguments in a call.
///
/// ## Why is this bad?
/// Failing to provide a required argument will raise a `TypeError` at runtime.
///
/// ## Examples
/// ```python
/// def func(x: int): ...
/// func() # TypeError: func() missing 1 required positional argument: 'x'
/// ```
pub(crate) static MISSING_ARGUMENT = {
summary: "detects missing required arguments in a call",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for subscripting objects that do not support subscripting.
@@ -413,6 +489,27 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for calls which provide more than one argument for a single parameter.
///
/// ## Why is this bad?
/// Providing multiple values for a single parameter will raise a `TypeError` at runtime.
///
/// ## Examples
///
/// ```python
/// def f(x: int) -> int: ...
///
/// f(1, x=2) # Error raised here
/// ```
pub(crate) static PARAMETER_ALREADY_ASSIGNED = {
summary: "detects multiple arguments for the same parameter",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for possibly unbound attributes.
@@ -479,6 +576,49 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for `assert_type()` calls where the actual type
/// is not the same as the asserted type.
///
/// ## Why is this bad?
/// `assert_type()` allows confirming the inferred type of a certain value.
///
/// ## Example
///
/// ```python
/// def _(x: int):
/// assert_type(x, int) # fine
/// assert_type(x, str) # error: Actual type does not match asserted type
/// ```
pub(crate) static TYPE_ASSERTION_FAILURE = {
summary: "detects failed type assertions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls that pass more positional arguments than the callable can accept.
///
/// ## Why is this bad?
/// Passing too many positional arguments will raise `TypeError` at runtime.
///
/// ## Example
///
/// ```python
/// def f(): ...
///
/// f("foo") # Error raised here
/// ```
pub(crate) static TOO_MANY_POSITIONAL_ARGUMENTS = {
summary: "detects calls passing too many positional arguments",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to `reveal_type` without importing it.
@@ -495,6 +635,27 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for keyword arguments in calls that don't match any parameter of the callable.
///
/// ## Why is this bad?
/// Providing an unknown keyword argument will raise `TypeError` at runtime.
///
/// ## Example
///
/// ```python
/// def f(x: int) -> int: ...
///
/// f(x=1, y=2) # Error raised here
/// ```
pub(crate) static UNKNOWN_ARGUMENT = {
summary: "detects unknown keyword arguments in calls",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for unresolved attributes.
@@ -570,6 +731,25 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Makes sure that the argument of `static_assert` is statically known to be true.
///
/// ## Examples
/// ```python
/// from knot_extensions import static_assert
///
/// static_assert(1 + 1 == 3) # error: evaluates to `False`
///
/// static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness
/// ```
pub(crate) static STATIC_ASSERT_ERROR = {
summary: "Failed static assertion",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic {
pub(crate) id: DiagnosticId,

View File

@@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape;
use crate::types::class_base::ClassBase;
use crate::types::{
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
SubclassOfType, Type, UnionType,
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type,
UnionType,
};
use crate::Db;
use rustc_hash::FxHashMap;
@@ -65,9 +65,8 @@ struct DisplayRepresentation<'db> {
impl Display for DisplayRepresentation<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.ty {
Type::Any => f.write_str("Any"),
Type::Dynamic(dynamic) => dynamic.fmt(f),
Type::Never => f.write_str("Never"),
Type::Unknown => f.write_str("Unknown"),
Type::Instance(InstanceType { class }) => {
let representation = match class.known(self.db) {
Some(KnownClass::NoneType) => "None",
@@ -76,24 +75,17 @@ impl Display for DisplayRepresentation<'_> {
};
f.write_str(representation)
}
// `[Type::Todo]`'s display should be explicit that is not a valid display of
// any other type
Type::Todo(todo) => write!(f, "@Todo{todo}"),
Type::ModuleLiteral(module) => {
write!(f, "<module '{}'>", module.module(self.db).name())
}
// 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 {
base: ClassBase::Class(class),
}) => {
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
// 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(self.db))
}
Type::SubclassOf(SubclassOfType { base }) => {
write!(f, "type[{}]", base.display(self.db))
}
ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)),
ClassBase::Dynamic(dynamic) => write!(f, "type[{dynamic}]"),
},
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),
@@ -177,12 +169,9 @@ impl Display for DisplayUnionType<'_> {
for element in elements {
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
let Some(mut condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
let Some(condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
continue;
};
if kind == CondensedDisplayTypeKind::Int {
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
}
join.entry(&DisplayLiteralGroup {
literals: condensed_kind,
db: self.db,
@@ -223,17 +212,12 @@ impl Display for DisplayLiteralGroup<'_> {
/// Enumeration of literal types that are displayed in a "condensed way" inside `Literal` slices.
///
/// For example, `Literal[1] | Literal[2]` is displayed as `"Literal[1, 2]"`.
/// Not all `Literal` types are displayed using `Literal` slices
/// (e.g. it would be inappropriate to display `LiteralString`
/// as `Literal[LiteralString]`).
/// For example, `Literal[1] | Literal[2] | Literal["s"]` is displayed as `"Literal[1, 2, "s"]"`.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum CondensedDisplayTypeKind {
Class,
Function,
Int,
String,
Bytes,
LiteralExpression,
}
impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
@@ -243,9 +227,10 @@ impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
match value {
Type::ClassLiteral(_) => Ok(Self::Class),
Type::FunctionLiteral(_) => Ok(Self::Function),
Type::IntLiteral(_) => Ok(Self::Int),
Type::StringLiteral(_) => Ok(Self::String),
Type::BytesLiteral(_) => Ok(Self::Bytes),
Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::BooleanLiteral(_) => Ok(Self::LiteralExpression),
_ => Err(()),
}
}
@@ -372,64 +357,8 @@ impl Display for DisplayStringLiteralType<'_> {
#[cfg(test)]
mod tests {
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem;
use crate::db::tests::setup_db;
use crate::types::{global_symbol, SliceLiteralType, StringLiteralType, Type, UnionType};
#[test]
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/main.py",
"
def foo(x: int) -> int:
return x + 1
def bar(s: str) -> str:
return s
class A: ...
class B: ...
",
)?;
let mod_file = system_path_to_file(&db, "src/main.py").expect("file to exist");
let union_elements = &[
Type::Unknown,
Type::IntLiteral(-1),
global_symbol(&db, mod_file, "A").expect_type(),
Type::string_literal(&db, "A"),
Type::bytes_literal(&db, &[0u8]),
Type::bytes_literal(&db, &[7u8]),
Type::IntLiteral(0),
Type::IntLiteral(1),
Type::string_literal(&db, "B"),
global_symbol(&db, mod_file, "foo").expect_type(),
global_symbol(&db, mod_file, "bar").expect_type(),
global_symbol(&db, mod_file, "B").expect_type(),
Type::BooleanLiteral(true),
Type::none(&db),
];
let union = UnionType::from_elements(&db, union_elements).expect_union();
let display = format!("{}", union.display(&db));
assert_eq!(
display,
concat!(
"Unknown | ",
"Literal[-1, 0, 1] | ",
"Literal[A, B] | ",
"Literal[\"A\", \"B\"] | ",
"Literal[b\"\\x00\", b\"\\x07\"] | ",
"Literal[foo, bar] | ",
"Literal[True] | ",
"None"
)
);
Ok(())
}
use crate::types::{SliceLiteralType, StringLiteralType, Type};
#[test]
fn test_slice_literal_display() {

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ impl<'db> Mro<'db> {
pub(super) fn from_error(db: &'db dyn Db, class: Class<'db>) -> Self {
Self::from([
ClassBase::Class(class),
ClassBase::Unknown,
ClassBase::unknown(),
ClassBase::object(db),
])
}

View File

@@ -7,8 +7,8 @@ use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
use crate::semantic_index::symbol_table;
use crate::types::{
infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass,
KnownConstraintFunction, KnownFunction, Truthiness, Type, UnionBuilder,
infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, KnownFunction,
SubclassOfType, Truthiness, Type, UnionBuilder,
};
use crate::Db;
use itertools::Itertools;
@@ -83,28 +83,39 @@ fn all_negative_narrowing_constraints_for_expression<'db>(
NarrowingConstraintsBuilder::new(db, ConstraintNode::Expression(expression), false).finish()
}
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
///
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
fn generate_classinfo_constraint<'db, F>(
db: &'db dyn Db,
classinfo: &Type<'db>,
to_constraint: F,
) -> Option<Type<'db>>
where
F: Fn(ClassLiteralType<'db>) -> Type<'db> + Copy,
{
match classinfo {
Type::Tuple(tuple) => {
let mut builder = UnionBuilder::new(db);
for element in tuple.elements(db) {
builder = builder.add(generate_classinfo_constraint(db, element, to_constraint)?);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KnownConstraintFunction {
/// `builtins.isinstance`
IsInstance,
/// `builtins.issubclass`
IsSubclass,
}
impl KnownConstraintFunction {
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
///
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
let constraint_fn = |class| match self {
KnownConstraintFunction::IsInstance => Type::instance(class),
KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class),
};
match classinfo {
Type::Tuple(tuple) => {
let mut builder = UnionBuilder::new(db);
for element in tuple.elements(db) {
builder = builder.add(self.generate_constraint(db, *element)?);
}
Some(builder.build())
}
Some(builder.build())
Type::ClassLiteral(ClassLiteralType { class }) => Some(constraint_fn(class)),
Type::SubclassOf(subclass_of_ty) => {
subclass_of_ty.subclass_of().into_class().map(constraint_fn)
}
_ => None,
}
Type::ClassLiteral(class_literal_type) => Some(to_constraint(*class_literal_type)),
_ => None,
}
}
@@ -222,6 +233,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
PatternConstraintKind::Singleton(singleton, _guard) => {
self.evaluate_match_pattern_singleton(*subject, *singleton)
}
PatternConstraintKind::Class(cls, _guard) => {
self.evaluate_match_pattern_class(*subject, *cls)
}
// TODO: support more pattern kinds
PatternConstraintKind::Value(..) | PatternConstraintKind::Unsupported => None,
}
@@ -429,24 +443,13 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
let class_info_ty =
inference.expression_ty(class_info.scoped_expression_id(self.db, scope));
let to_constraint = match function {
KnownConstraintFunction::IsInstance => {
|class_literal: ClassLiteralType<'db>| Type::instance(class_literal.class)
}
KnownConstraintFunction::IsSubclass => {
|class_literal: ClassLiteralType<'db>| {
Type::subclass_of(class_literal.class)
}
}
};
generate_classinfo_constraint(self.db, &class_info_ty, to_constraint).map(
|constraint| {
function
.generate_constraint(self.db, class_info_ty)
.map(|constraint| {
let mut constraints = NarrowingConstraints::default();
constraints.insert(symbol, constraint.negate_if(self.db, !is_positive));
constraints
},
)
})
}
// for the expression `bool(E)`, we further narrow the type based on `E`
Type::ClassLiteral(class_type)
@@ -486,6 +489,27 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
}
fn evaluate_match_pattern_class(
&mut self,
subject: Expression<'db>,
cls: Expression<'db>,
) -> Option<NarrowingConstraints<'db>> {
if let Some(ast::ExprName { id, .. }) = subject.node_ref(self.db).as_name_expr() {
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let scope = self.scope();
let inference = infer_expression_types(self.db, cls);
let ty = inference
.expression_ty(cls.node_ref(self.db).scoped_expression_id(self.db, scope))
.to_instance(self.db);
let mut constraints = NarrowingConstraints::default();
constraints.insert(symbol, ty);
Some(constraints)
} else {
None
}
}
fn evaluate_bool_op(
&mut self,
expr_bool_op: &ExprBoolOp,

View File

@@ -123,14 +123,61 @@ impl Arbitrary for Ty {
}
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
// This is incredibly naive. We can do much better here by
// trying various subsets of the elements in unions, tuples,
// and intersections. For now, we only try to shrink by
// reducing unions/tuples/intersections to a single element.
match self.clone() {
Ty::Union(types) => Box::new(types.into_iter()),
Ty::Tuple(types) => Box::new(types.into_iter()),
Ty::Intersection { pos, neg } => Box::new(pos.into_iter().chain(neg)),
Ty::Union(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() {
0 => None,
1 => Some(elts.into_iter().next().unwrap()),
_ => Some(Ty::Union(elts)),
})),
Ty::Tuple(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() {
0 => None,
1 => Some(elts.into_iter().next().unwrap()),
_ => Some(Ty::Tuple(elts)),
})),
Ty::Intersection { pos, neg } => {
// Shrinking on intersections is not exhaustive!
//
// We try to shrink the positive side or the negative side,
// but we aren't shrinking both at the same time.
//
// This should remove positive or negative constraints but
// won't shrink (A & B & ~C & ~D) to (A & ~C) in one shrink
// iteration.
//
// Instead, it hopes that (A & B & ~C) or (A & ~C & ~D) fails
// so that shrinking can happen there.
let pos_orig = pos.clone();
let neg_orig = neg.clone();
Box::new(
// we shrink negative constraints first, as
// intersections with only negative constraints are
// more confusing
neg.shrink()
.map(move |shrunk_neg| Ty::Intersection {
pos: pos_orig.clone(),
neg: shrunk_neg,
})
.chain(pos.shrink().map(move |shrunk_pos| Ty::Intersection {
pos: shrunk_pos,
neg: neg_orig.clone(),
}))
.filter_map(|ty| {
if let Ty::Intersection { pos, neg } = &ty {
match (pos.len(), neg.len()) {
// an empty intersection does not mean
// anything
(0, 0) => None,
// a single positive element should be
// unwrapped
(1, 0) => Some(pos[0].clone()),
_ => Some(ty),
}
} else {
unreachable!()
}
}),
)
}
_ => Box::new(std::iter::empty()),
}
}
@@ -173,6 +220,8 @@ macro_rules! type_property_test {
}
mod stable {
use crate::types::{KnownClass, Type};
// `T` is equivalent to itself.
type_property_test!(
equivalent_to_is_reflexive, db,
@@ -209,12 +258,6 @@ mod stable {
forall types s, t. s.is_subtype_of(db, t) => !s.is_disjoint_from(db, t) || s.is_never()
);
// `T` can be assigned to itself.
type_property_test!(
assignable_to_is_reflexive, db,
forall types t. t.is_assignable_to(db, t)
);
// `S <: T` implies that `S` can be assigned to `T`.
type_property_test!(
subtype_of_implies_assignable_to, db,
@@ -238,6 +281,30 @@ mod stable {
non_fully_static_types_do_not_participate_in_subtyping, db,
forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s)
);
// All types should be assignable to `object`
type_property_test!(
all_types_assignable_to_object, db,
forall types t. t.is_assignable_to(db, KnownClass::Object.to_instance(db))
);
// And for fully static types, they should also be subtypes of `object`
type_property_test!(
all_fully_static_types_subtype_of_object, db,
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, KnownClass::Object.to_instance(db))
);
// Never should be assignable to every type
type_property_test!(
never_assignable_to_every_type, db,
forall types t. Type::Never.is_assignable_to(db, t)
);
// And it should be a subtype of all fully static types
type_property_test!(
never_subtype_of_every_fully_static_type, db,
forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t)
);
}
/// This module contains property tests that currently lead to many false positives.
@@ -248,6 +315,32 @@ mod stable {
/// tests to the `stable` section. In the meantime, it can still be useful to run these
/// tests (using [`types::property_tests::flaky`]), to see if there are any new obvious bugs.
mod flaky {
use crate::{
db::tests::TestDb,
types::{IntersectionBuilder, Type},
};
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
// `T` can be assigned to itself.
type_property_test!(
assignable_to_is_reflexive, db,
forall types t. t.is_assignable_to(db, t)
);
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
// An intersection of two types should be assignable to both of them
fn intersection<'db>(db: &'db TestDb, s: Type<'db>, t: Type<'db>) -> Type<'db> {
IntersectionBuilder::new(db)
.add_positive(s)
.add_positive(t)
.build()
}
type_property_test!(
intersection_assignable_to_both, db,
forall types s, t. intersection(db, s, t).is_assignable_to(db, s) && intersection(db, s, t).is_assignable_to(db, t)
);
// `S <: T` and `T <: S` implies that `S` is equivalent to `T`.
type_property_test!(
subtype_of_is_antisymmetric, db,

View File

@@ -1,4 +1,3 @@
#![allow(dead_code)]
use super::{definition_expression_ty, Type};
use crate::Db;
use crate::{semantic_index::definition::Definition, types::todo_type};
@@ -7,10 +6,18 @@ use ruff_python_ast::{self as ast, name::Name};
/// A typed callable signature.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Signature<'db> {
/// Parameters, in source order.
///
/// The ordering of parameters in a valid signature must be: first positional-only parameters,
/// then positional-or-keyword, then optionally the variadic parameter, then keyword-only
/// parameters, and last, optionally the variadic keywords parameter. Parameters with defaults
/// must come after parameters without defaults.
///
/// We may get invalid signatures, though, and need to handle them without panicking.
parameters: Parameters<'db>,
/// Annotated return type (Unknown if no annotation.)
pub(crate) return_ty: Type<'db>,
/// Annotated return type, if any.
pub(crate) return_ty: Option<Type<'db>>,
}
impl<'db> Signature<'db> {
@@ -18,7 +25,7 @@ impl<'db> Signature<'db> {
pub(crate) fn todo() -> Self {
Self {
parameters: Parameters::todo(),
return_ty: todo_type!("return type"),
return_ty: Some(todo_type!("return type")),
}
}
@@ -28,17 +35,13 @@ impl<'db> Signature<'db> {
definition: Definition<'db>,
function_node: &'db ast::StmtFunctionDef,
) -> Self {
let return_ty = function_node
.returns
.as_ref()
.map(|returns| {
if function_node.is_async {
todo_type!("generic types.CoroutineType")
} else {
definition_expression_ty(db, definition, returns.as_ref())
}
})
.unwrap_or(Type::Unknown);
let return_ty = function_node.returns.as_ref().map(|returns| {
if function_node.is_async {
todo_type!("generic types.CoroutineType")
} else {
definition_expression_ty(db, definition, returns.as_ref())
}
});
Self {
parameters: Parameters::from_parameters(
@@ -49,45 +52,32 @@ impl<'db> Signature<'db> {
return_ty,
}
}
/// Return the parameters in this signature.
pub(crate) fn parameters(&self) -> &Parameters<'db> {
&self.parameters
}
}
/// The parameters portion of a typed signature.
///
/// The ordering of parameters is always as given in this struct: first positional-only parameters,
/// then positional-or-keyword, then optionally the variadic parameter, then keyword-only
/// parameters, and last, optionally the variadic keywords parameter.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct Parameters<'db> {
/// Parameters which may only be filled by positional arguments.
positional_only: Box<[ParameterWithDefault<'db>]>,
/// Parameters which may be filled by positional or keyword arguments.
positional_or_keyword: Box<[ParameterWithDefault<'db>]>,
/// The `*args` variadic parameter, if any.
variadic: Option<Parameter<'db>>,
/// Parameters which may only be filled by keyword arguments.
keyword_only: Box<[ParameterWithDefault<'db>]>,
/// The `**kwargs` variadic keywords parameter, if any.
keywords: Option<Parameter<'db>>,
}
// TODO: use SmallVec here once invariance bug is fixed
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
impl<'db> Parameters<'db> {
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
fn todo() -> Self {
Self {
variadic: Some(Parameter {
Self(vec![
Parameter {
name: Some(Name::new_static("args")),
annotated_ty: todo_type!(),
}),
keywords: Some(Parameter {
annotated_ty: Some(todo_type!("todo signature *args")),
kind: ParameterKind::Variadic,
},
Parameter {
name: Some(Name::new_static("kwargs")),
annotated_ty: todo_type!(),
}),
..Default::default()
}
annotated_ty: Some(todo_type!("todo signature **kwargs")),
kind: ParameterKind::KeywordVariadic,
},
])
}
fn from_parameters(
@@ -103,94 +93,238 @@ impl<'db> Parameters<'db> {
kwarg,
range: _,
} = parameters;
let positional_only = posonlyargs
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let positional_or_keyword = args
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let variadic = vararg
.as_ref()
.map(|arg| Parameter::from_node(db, definition, arg));
let keyword_only = kwonlyargs
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let keywords = kwarg
.as_ref()
.map(|arg| Parameter::from_node(db, definition, arg));
Self {
positional_only,
positional_or_keyword,
variadic,
keyword_only,
keywords,
}
}
}
/// A single parameter of a typed signature, with optional default value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ParameterWithDefault<'db> {
parameter: Parameter<'db>,
/// Type of the default value, if any.
default_ty: Option<Type<'db>>,
}
impl<'db> ParameterWithDefault<'db> {
fn from_node(
db: &'db dyn Db,
definition: Definition<'db>,
parameter_with_default: &'db ast::ParameterWithDefault,
) -> Self {
Self {
default_ty: parameter_with_default
let default_ty = |parameter_with_default: &ast::ParameterWithDefault| {
parameter_with_default
.default
.as_deref()
.map(|default| definition_expression_ty(db, definition, default)),
parameter: Parameter::from_node(db, definition, &parameter_with_default.parameter),
}
.map(|default| definition_expression_ty(db, definition, default))
};
let positional_only = posonlyargs.iter().map(|arg| {
Parameter::from_node_and_kind(
db,
definition,
&arg.parameter,
ParameterKind::PositionalOnly {
default_ty: default_ty(arg),
},
)
});
let positional_or_keyword = args.iter().map(|arg| {
Parameter::from_node_and_kind(
db,
definition,
&arg.parameter,
ParameterKind::PositionalOrKeyword {
default_ty: default_ty(arg),
},
)
});
let variadic = vararg
.as_ref()
.map(|arg| Parameter::from_node_and_kind(db, definition, arg, ParameterKind::Variadic));
let keyword_only = kwonlyargs.iter().map(|arg| {
Parameter::from_node_and_kind(
db,
definition,
&arg.parameter,
ParameterKind::KeywordOnly {
default_ty: default_ty(arg),
},
)
});
let keywords = kwarg.as_ref().map(|arg| {
Parameter::from_node_and_kind(db, definition, arg, ParameterKind::KeywordVariadic)
});
Self(
positional_only
.chain(positional_or_keyword)
.chain(variadic)
.chain(keyword_only)
.chain(keywords)
.collect(),
)
}
pub(crate) fn len(&self) -> usize {
self.0.len()
}
pub(crate) fn iter(&self) -> std::slice::Iter<Parameter<'db>> {
self.0.iter()
}
/// Iterate initial positional parameters, not including variadic parameter, if any.
///
/// For a valid signature, this will be all positional parameters. In an invalid signature,
/// there could be non-initial positional parameters; effectively, we just won't consider those
/// to be positional, which is fine.
pub(crate) fn positional(&self) -> impl Iterator<Item = &Parameter<'db>> {
self.iter().take_while(|param| param.is_positional())
}
/// Return parameter at given index, or `None` if index is out-of-range.
pub(crate) fn get(&self, index: usize) -> Option<&Parameter<'db>> {
self.0.get(index)
}
/// Return positional parameter at given index, or `None` if `index` is out of range.
///
/// Does not return variadic parameter.
pub(crate) fn get_positional(&self, index: usize) -> Option<&Parameter<'db>> {
self.get(index)
.and_then(|parameter| parameter.is_positional().then_some(parameter))
}
/// Return the variadic parameter (`*args`), if any, and its index, or `None`.
pub(crate) fn variadic(&self) -> Option<(usize, &Parameter<'db>)> {
self.iter()
.enumerate()
.find(|(_, parameter)| parameter.is_variadic())
}
/// Return parameter (with index) for given name, or `None` if no such parameter.
///
/// Does not return keywords (`**kwargs`) parameter.
///
/// In an invalid signature, there could be multiple parameters with the same name; we will
/// just return the first that matches.
pub(crate) fn keyword_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> {
self.iter()
.enumerate()
.find(|(_, parameter)| parameter.callable_by_name(name))
}
/// Return the keywords parameter (`**kwargs`), if any, and its index, or `None`.
pub(crate) fn keyword_variadic(&self) -> Option<(usize, &Parameter<'db>)> {
self.iter()
.enumerate()
.rfind(|(_, parameter)| parameter.is_keyword_variadic())
}
}
impl<'db, 'a> IntoIterator for &'a Parameters<'db> {
type Item = &'a Parameter<'db>;
type IntoIter = std::slice::Iter<'a, Parameter<'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'db> std::ops::Index<usize> for Parameters<'db> {
type Output = Parameter<'db>;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
/// A single parameter of a typed signature.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Parameter<'db> {
pub(crate) struct Parameter<'db> {
/// Parameter name.
///
/// It is possible for signatures to be defined in ways that leave positional-only parameters
/// nameless (e.g. via `Callable` annotations).
name: Option<Name>,
/// Annotated type of the parameter (Unknown if no annotation.)
annotated_ty: Type<'db>,
/// Annotated type of the parameter.
annotated_ty: Option<Type<'db>>,
kind: ParameterKind<'db>,
}
impl<'db> Parameter<'db> {
fn from_node(
fn from_node_and_kind(
db: &'db dyn Db,
definition: Definition<'db>,
parameter: &'db ast::Parameter,
kind: ParameterKind<'db>,
) -> Self {
Parameter {
Self {
name: Some(parameter.name.id.clone()),
annotated_ty: parameter
.annotation
.as_deref()
.map(|annotation| definition_expression_ty(db, definition, annotation))
.unwrap_or(Type::Unknown),
.map(|annotation| definition_expression_ty(db, definition, annotation)),
kind,
}
}
pub(crate) fn is_variadic(&self) -> bool {
matches!(self.kind, ParameterKind::Variadic)
}
pub(crate) fn is_keyword_variadic(&self) -> bool {
matches!(self.kind, ParameterKind::KeywordVariadic)
}
pub(crate) fn is_positional(&self) -> bool {
matches!(
self.kind,
ParameterKind::PositionalOnly { .. } | ParameterKind::PositionalOrKeyword { .. }
)
}
pub(crate) fn callable_by_name(&self, name: &str) -> bool {
match self.kind {
ParameterKind::PositionalOrKeyword { .. } | ParameterKind::KeywordOnly { .. } => self
.name
.as_ref()
.is_some_and(|param_name| param_name == name),
_ => false,
}
}
/// Annotated type of the parameter, if annotated.
pub(crate) fn annotated_ty(&self) -> Option<Type<'db>> {
self.annotated_ty
}
/// Name of the parameter (if it has one).
pub(crate) fn name(&self) -> Option<&ast::name::Name> {
self.name.as_ref()
}
/// Display name of the parameter, if it has one.
pub(crate) fn display_name(&self) -> Option<ast::name::Name> {
self.name().map(|name| match self.kind {
ParameterKind::Variadic => ast::name::Name::new(format!("*{name}")),
ParameterKind::KeywordVariadic => ast::name::Name::new(format!("**{name}")),
_ => name.clone(),
})
}
/// Default-value type of the parameter, if any.
pub(crate) fn default_ty(&self) -> Option<Type<'db>> {
match self.kind {
ParameterKind::PositionalOnly { default_ty } => default_ty,
ParameterKind::PositionalOrKeyword { default_ty } => default_ty,
ParameterKind::Variadic => None,
ParameterKind::KeywordOnly { default_ty } => default_ty,
ParameterKind::KeywordVariadic => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ParameterKind<'db> {
/// Positional-only parameter, e.g. `def f(x, /): ...`
PositionalOnly { default_ty: Option<Type<'db>> },
/// Positional-or-keyword parameter, e.g. `def f(x): ...`
PositionalOrKeyword { default_ty: Option<Type<'db>> },
/// Variadic parameter, e.g. `def f(*args): ...`
Variadic,
/// Keyword-only parameter, e.g. `def f(*, x): ...`
KeywordOnly { default_ty: Option<Type<'db>> },
/// Variadic keywords parameter, e.g. `def f(**kwargs): ...`
KeywordVariadic,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, FunctionType};
use crate::types::{global_symbol, FunctionType, KnownClass};
use ruff_db::system::DbWithTestSystem;
#[track_caller]
@@ -202,39 +336,8 @@ mod tests {
}
#[track_caller]
fn assert_param_with_default<'db>(
db: &'db TestDb,
param_with_default: &ParameterWithDefault<'db>,
expected_name: &'static str,
expected_annotation_ty_display: &'static str,
expected_default_ty_display: Option<&'static str>,
) {
assert_eq!(
param_with_default
.default_ty
.map(|ty| ty.display(db).to_string()),
expected_default_ty_display.map(ToString::to_string)
);
assert_param(
db,
&param_with_default.parameter,
expected_name,
expected_annotation_ty_display,
);
}
#[track_caller]
fn assert_param<'db>(
db: &'db TestDb,
param: &Parameter<'db>,
expected_name: &'static str,
expected_annotation_ty_display: &'static str,
) {
assert_eq!(param.name.as_ref().unwrap(), expected_name);
assert_eq!(
param.annotated_ty.display(db).to_string(),
expected_annotation_ty_display
);
fn assert_params<'db>(signature: &Signature<'db>, expected: &[Parameter<'db>]) {
assert_eq!(signature.parameters.0.as_slice(), expected);
}
#[test]
@@ -245,13 +348,8 @@ mod tests {
let sig = func.internal_signature(&db);
assert_eq!(sig.return_ty.display(&db).to_string(), "Unknown");
let params = sig.parameters;
assert!(params.positional_only.is_empty());
assert!(params.positional_or_keyword.is_empty());
assert!(params.variadic.is_none());
assert!(params.keyword_only.is_empty());
assert!(params.keywords.is_none());
assert!(sig.return_ty.is_none());
assert_params(&sig, &[]);
}
#[test]
@@ -271,34 +369,74 @@ mod tests {
let sig = func.internal_signature(&db);
assert_eq!(sig.return_ty.display(&db).to_string(), "bytes");
let params = sig.parameters;
let [a, b, c, d] = &params.positional_only[..] else {
panic!("expected four positional-only parameters");
};
let [e, f] = &params.positional_or_keyword[..] else {
panic!("expected two positional-or-keyword parameters");
};
let Some(args) = params.variadic else {
panic!("expected a variadic parameter");
};
let [g, h] = &params.keyword_only[..] else {
panic!("expected two keyword-only parameters");
};
let Some(kwargs) = params.keywords else {
panic!("expected a kwargs parameter");
};
assert_param_with_default(&db, a, "a", "Unknown", None);
assert_param_with_default(&db, b, "b", "int", None);
assert_param_with_default(&db, c, "c", "Unknown", Some("Literal[1]"));
assert_param_with_default(&db, d, "d", "int", Some("Literal[2]"));
assert_param_with_default(&db, e, "e", "Unknown", Some("Literal[3]"));
assert_param_with_default(&db, f, "f", "Literal[4]", Some("Literal[4]"));
assert_param_with_default(&db, g, "g", "Unknown", Some("Literal[5]"));
assert_param_with_default(&db, h, "h", "Literal[6]", Some("Literal[6]"));
assert_param(&db, &args, "args", "object");
assert_param(&db, &kwargs, "kwargs", "str");
assert_eq!(sig.return_ty.unwrap().display(&db).to_string(), "bytes");
assert_params(
&sig,
&[
Parameter {
name: Some(Name::new_static("a")),
annotated_ty: None,
kind: ParameterKind::PositionalOnly { default_ty: None },
},
Parameter {
name: Some(Name::new_static("b")),
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
kind: ParameterKind::PositionalOnly { default_ty: None },
},
Parameter {
name: Some(Name::new_static("c")),
annotated_ty: None,
kind: ParameterKind::PositionalOnly {
default_ty: Some(Type::IntLiteral(1)),
},
},
Parameter {
name: Some(Name::new_static("d")),
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
kind: ParameterKind::PositionalOnly {
default_ty: Some(Type::IntLiteral(2)),
},
},
Parameter {
name: Some(Name::new_static("e")),
annotated_ty: None,
kind: ParameterKind::PositionalOrKeyword {
default_ty: Some(Type::IntLiteral(3)),
},
},
Parameter {
name: Some(Name::new_static("f")),
annotated_ty: Some(Type::IntLiteral(4)),
kind: ParameterKind::PositionalOrKeyword {
default_ty: Some(Type::IntLiteral(4)),
},
},
Parameter {
name: Some(Name::new_static("args")),
annotated_ty: Some(KnownClass::Object.to_instance(&db)),
kind: ParameterKind::Variadic,
},
Parameter {
name: Some(Name::new_static("g")),
annotated_ty: None,
kind: ParameterKind::KeywordOnly {
default_ty: Some(Type::IntLiteral(5)),
},
},
Parameter {
name: Some(Name::new_static("h")),
annotated_ty: Some(Type::IntLiteral(6)),
kind: ParameterKind::KeywordOnly {
default_ty: Some(Type::IntLiteral(6)),
},
},
Parameter {
name: Some(Name::new_static("kwargs")),
annotated_ty: Some(KnownClass::Str.to_instance(&db)),
kind: ParameterKind::KeywordVariadic,
},
],
);
}
#[test]
@@ -322,11 +460,17 @@ mod tests {
let sig = func.internal_signature(&db);
let [a] = &sig.parameters.positional_or_keyword[..] else {
let [Parameter {
name: Some(name),
annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
else {
panic!("expected one positional-or-keyword parameter");
};
assert_eq!(name, "a");
// Parameter resolution not deferred; we should see A not B
assert_param_with_default(&db, a, "a", "A", None);
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "A");
}
#[test]
@@ -350,11 +494,17 @@ mod tests {
let sig = func.internal_signature(&db);
let [a] = &sig.parameters.positional_or_keyword[..] else {
let [Parameter {
name: Some(name),
annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
else {
panic!("expected one positional-or-keyword parameter");
};
assert_eq!(name, "a");
// Parameter resolution deferred; we should see B
assert_param_with_default(&db, a, "a", "B", None);
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "B");
}
#[test]
@@ -378,12 +528,23 @@ mod tests {
let sig = func.internal_signature(&db);
let [a, b] = &sig.parameters.positional_or_keyword[..] else {
let [Parameter {
name: Some(a_name),
annotated_ty: a_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}, Parameter {
name: Some(b_name),
annotated_ty: b_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
else {
panic!("expected two positional-or-keyword parameters");
};
assert_eq!(a_name, "a");
assert_eq!(b_name, "b");
// TODO resolution should not be deferred; we should see A not B
assert_param_with_default(&db, a, "a", "B", None);
assert_param_with_default(&db, b, "b", "T", None);
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
}
#[test]
@@ -407,12 +568,23 @@ mod tests {
let sig = func.internal_signature(&db);
let [a, b] = &sig.parameters.positional_or_keyword[..] else {
let [Parameter {
name: Some(a_name),
annotated_ty: a_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}, Parameter {
name: Some(b_name),
annotated_ty: b_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
else {
panic!("expected two positional-or-keyword parameters");
};
assert_eq!(a_name, "a");
assert_eq!(b_name, "b");
// Parameter resolution deferred; we should see B
assert_param_with_default(&db, a, "a", "B", None);
assert_param_with_default(&db, b, "b", "T", None);
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
}
#[test]

View File

@@ -0,0 +1,89 @@
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Symbol, Type};
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub struct SubclassOfType<'db> {
// Keep this field private, so that the only way of constructing the struct is through the `from` method.
subclass_of: ClassBase<'db>,
}
impl<'db> SubclassOfType<'db> {
/// Construct a new [`Type`] instance representing a given class object (or a given dynamic type)
/// and all possible subclasses of that class object/dynamic type.
///
/// This method does not always return a [`Type::SubclassOf`] variant.
/// If the class object is known to be a final class,
/// this method will return a [`Type::ClassLiteral`] variant; this is a more precise type.
/// If the class object is `builtins.object`, `Type::Instance(<builtins.type>)` will be returned;
/// this is no more precise, but it is exactly equivalent to `type[object]`.
///
/// The eager normalization here means that we do not need to worry elsewhere about distinguishing
/// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants.
pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into<ClassBase<'db>>) -> Type<'db> {
let subclass_of = subclass_of.into();
match subclass_of {
ClassBase::Dynamic(_) => Type::SubclassOf(Self { subclass_of }),
ClassBase::Class(class) => {
if class.is_final(db) {
Type::ClassLiteral(ClassLiteralType { class })
} else if class.is_known(db, KnownClass::Object) {
KnownClass::Type.to_instance(db)
} else {
Type::SubclassOf(Self { subclass_of })
}
}
}
}
/// Return a [`Type`] instance representing the type `type[Unknown]`.
pub(crate) const fn subclass_of_unknown() -> Type<'db> {
Type::SubclassOf(SubclassOfType {
subclass_of: ClassBase::unknown(),
})
}
/// Return a [`Type`] instance representing the type `type[Any]`.
pub(crate) const fn subclass_of_any() -> Type<'db> {
Type::SubclassOf(SubclassOfType {
subclass_of: ClassBase::any(),
})
}
/// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`.
pub(crate) const fn subclass_of(self) -> ClassBase<'db> {
self.subclass_of
}
pub const fn is_dynamic(self) -> bool {
// Unpack `self` so that we're forced to update this method if any more fields are added in the future.
let Self { subclass_of } = self;
subclass_of.is_dynamic()
}
pub const fn is_fully_static(self) -> bool {
!self.is_dynamic()
}
pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
Type::from(self.subclass_of).member(db, name)
}
/// Return `true` if `self` is a subtype of `other`.
///
/// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant;
/// only fully static types participate in subtyping.
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool {
match (self.subclass_of, other.subclass_of) {
// Non-fully-static types do not participate in subtyping
(ClassBase::Dynamic(_), _) | (_, ClassBase::Dynamic(_)) => false,
// For example, `type[bool]` describes all possible runtime subclasses of the class `bool`,
// and `type[int]` describes all possible runtime subclasses of the class `int`.
// The first set is a subset of the second set, because `bool` is itself a subclass of `int`.
(ClassBase::Class(self_class), ClassBase::Class(other_class)) => {
// N.B. The subclass relation is fully static
self_class.is_subclass_of(db, other_class)
}
}
}
}

View File

@@ -45,6 +45,15 @@ impl<'db> Unpacker<'db> {
let mut value_ty = infer_expression_types(self.db(), value.expression())
.expression_ty(value.scoped_expression_id(self.db(), self.scope));
if value.is_assign()
&& self.context.in_stub()
&& value
.expression()
.node_ref(self.db())
.is_ellipsis_literal_expr()
{
value_ty = Type::unknown();
}
if value.is_iterable() {
// If the value is an iterable, then the type that needs to be unpacked is the iterator
// type.
@@ -96,7 +105,7 @@ impl<'db> Unpacker<'db> {
// with each individual character, instead of just an array of
// `LiteralString`, but there would be a cost and it's not clear that
// it's worth it.
Type::tuple(
TupleType::from_elements(
self.db(),
std::iter::repeat(Type::LiteralString)
.take(string_literal_ty.python_len(self.db())),
@@ -155,7 +164,7 @@ impl<'db> Unpacker<'db> {
for (index, element) in elts.iter().enumerate() {
// SAFETY: `target_types` is initialized with the same length as `elts`.
let element_ty = match target_types[index].as_slice() {
[] => Type::Unknown,
[] => Type::unknown(),
types => UnionType::from_elements(self.db(), types),
};
self.unpack_inner(element, element_ty);
@@ -232,7 +241,7 @@ impl<'db> Unpacker<'db> {
// Subtract 1 to insert the starred expression type at the correct
// index.
element_types.resize(targets.len() - 1, Type::Unknown);
element_types.resize(targets.len() - 1, Type::unknown());
// TODO: This should be `list[Unknown]`
element_types.insert(starred_index, todo_type!("starred unpacking"));

View File

@@ -76,6 +76,11 @@ impl<'db> UnpackValue<'db> {
matches!(self, UnpackValue::Iterable(_))
}
/// Returns `true` if the value is being assigned to a target.
pub(crate) const fn is_assign(self) -> bool {
matches!(self, UnpackValue::Assign(_))
}
/// Returns the underlying [`Expression`] that is being unpacked.
pub(crate) const fn expression(self) -> Expression<'db> {
match self {

View File

@@ -329,9 +329,9 @@ impl<'db> VisibilityConstraints<'db> {
Truthiness::Ambiguous
}
}
PatternConstraintKind::Singleton(..) | PatternConstraintKind::Unsupported => {
Truthiness::Ambiguous
}
PatternConstraintKind::Singleton(..)
| PatternConstraintKind::Class(..)
| PatternConstraintKind::Unsupported => Truthiness::Ambiguous,
},
}
}

View File

@@ -11,10 +11,10 @@ use crate::server::Server;
mod message;
mod edit;
mod logging;
mod server;
mod session;
mod system;
mod trace;
pub(crate) const SERVER_NAME: &str = "red-knot";
pub(crate) const DIAGNOSTIC_NAME: &str = "Red Knot";

View File

@@ -0,0 +1,114 @@
//! The logging system for `red_knot server`.
//!
//! Log messages are controlled by the `logLevel` setting which defaults to `"info"`. Log messages
//! are written to `stderr` by default, which should appear in the logs for most LSP clients. A
//! `logFile` path can also be specified in the settings, and output will be directed there
//! instead.
use core::str;
use serde::Deserialize;
use std::{path::PathBuf, str::FromStr, sync::Arc};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{
fmt::{time::Uptime, writer::BoxMakeWriter},
layer::SubscriberExt,
Layer,
};
pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Path>) {
let log_file = log_file
.map(|path| {
// this expands `logFile` so that tildes and environment variables
// are replaced with their values, if possible.
if let Some(expanded) = shellexpand::full(&path.to_string_lossy())
.ok()
.and_then(|path| PathBuf::from_str(&path).ok())
{
expanded
} else {
path.to_path_buf()
}
})
.and_then(|path| {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|err| {
#[allow(clippy::print_stderr)]
{
eprintln!(
"Failed to open file at {} for logging: {err}",
path.display()
);
}
})
.ok()
});
let logger = match log_file {
Some(file) => BoxMakeWriter::new(Arc::new(file)),
None => BoxMakeWriter::new(std::io::stderr),
};
let subscriber = tracing_subscriber::Registry::default().with(
tracing_subscriber::fmt::layer()
.with_timer(Uptime::default())
.with_thread_names(true)
.with_ansi(false)
.with_writer(logger)
.with_filter(LogLevelFilter { filter: log_level }),
);
tracing::subscriber::set_global_default(subscriber)
.expect("should be able to set global default subscriber");
}
/// The log level for the server as provided by the client during initialization.
///
/// The default log level is `info`.
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LogLevel {
Error,
Warn,
#[default]
Info,
Debug,
Trace,
}
impl LogLevel {
fn trace_level(self) -> tracing::Level {
match self {
Self::Error => tracing::Level::ERROR,
Self::Warn => tracing::Level::WARN,
Self::Info => tracing::Level::INFO,
Self::Debug => tracing::Level::DEBUG,
Self::Trace => tracing::Level::TRACE,
}
}
}
/// Filters out traces which have a log level lower than the `logLevel` set by the client.
struct LogLevelFilter {
filter: LogLevel,
}
impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
fn enabled(
&self,
meta: &tracing::Metadata<'_>,
_: &tracing_subscriber::layer::Context<'_, S>,
) -> bool {
let filter = if meta.target().starts_with("red_knot") {
self.filter.trace_level()
} else {
tracing::Level::INFO
};
meta.level() <= &filter
}
fn max_level_hint(&self) -> Option<tracing::level_filters::LevelFilter> {
Some(LevelFilter::from_level(self.filter.trace_level()))
}
}

View File

@@ -51,10 +51,6 @@ impl Server {
crate::version(),
)?;
if let Some(trace) = init_params.trace {
crate::trace::set_trace_value(trace);
}
crate::message::init_messenger(connection.make_sender());
let AllSettings {
@@ -66,14 +62,9 @@ impl Server {
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
);
crate::trace::init_tracing(
connection.make_sender(),
global_settings
.tracing
.log_level
.unwrap_or(crate::trace::LogLevel::Info),
crate::logging::init_logging(
global_settings.tracing.log_level.unwrap_or_default(),
global_settings.tracing.log_file.as_deref(),
init_params.client_info.as_ref(),
);
let mut workspace_for_url = |url: Url| {

View File

@@ -52,9 +52,6 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
notification::DidCloseNotebookHandler::METHOD => {
local_notification_task::<notification::DidCloseNotebookHandler>(notif)
}
notification::SetTraceHandler::METHOD => {
local_notification_task::<notification::SetTraceHandler>(notif)
}
method => {
tracing::warn!("Received notification {method} which does not have a handler.");
return Task::nothing();

View File

@@ -3,11 +3,9 @@ mod did_close;
mod did_close_notebook;
mod did_open;
mod did_open_notebook;
mod set_trace;
pub(super) use did_change::DidChangeTextDocumentHandler;
pub(super) use did_close::DidCloseTextDocumentHandler;
pub(super) use did_close_notebook::DidCloseNotebookHandler;
pub(super) use did_open::DidOpenTextDocumentHandler;
pub(super) use did_open_notebook::DidOpenNotebookHandler;
pub(super) use set_trace::SetTraceHandler;

View File

@@ -1,25 +0,0 @@
use lsp_types::notification::SetTrace;
use lsp_types::SetTraceParams;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::server::client::{Notifier, Requester};
use crate::server::Result;
use crate::session::Session;
pub(crate) struct SetTraceHandler;
impl NotificationHandler for SetTraceHandler {
type NotificationType = SetTrace;
}
impl SyncNotificationHandler for SetTraceHandler {
fn run(
_session: &mut Session,
_notifier: Notifier,
_requester: &mut Requester,
params: SetTraceParams,
) -> Result<()> {
crate::trace::set_trace_value(params.value);
Ok(())
}
}

View File

@@ -74,7 +74,7 @@ impl Index {
DocumentKey::NotebookCell(url)
} else if Path::new(url.path())
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("ipynb"))
.is_some_and(|ext| ext.eq_ignore_ascii_case("ipynb"))
{
DocumentKey::Notebook(url)
} else {

View File

@@ -24,7 +24,7 @@ pub struct ClientSettings {
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct TracingSettings {
pub(crate) log_level: Option<crate::trace::LogLevel>,
pub(crate) log_level: Option<crate::logging::LogLevel>,
/// Path to the log file - tildes and environment variables are supported.
pub(crate) log_file: Option<PathBuf>,
}

View File

@@ -1,221 +0,0 @@
//! The tracing system for `ruff server`.
//!
//! Traces are controlled by the `logLevel` setting, along with the
//! trace level set through the LSP. On VS Code, the trace level can
//! also be set with `ruff.trace.server`. A trace level of `messages` or
//! `verbose` will enable tracing - otherwise, no traces will be shown.
//!
//! `logLevel` can be used to configure the level of tracing that is shown.
//! By default, `logLevel` is set to `"info"`.
//!
//! The server also supports the `RUFF_TRACE` environment variable, which will
//! override the trace value provided by the LSP client. Use this if there's no good way
//! to set the trace value through your editor's configuration.
//!
//! Tracing will write to `stderr` by default, which should appear in the logs for most LSP clients.
//! A `logFile` path can also be specified in the settings, and output will be directed there instead.
use core::str;
use lsp_server::{Message, Notification};
use lsp_types::{
notification::{LogTrace, Notification as _},
ClientInfo, TraceValue,
};
use serde::Deserialize;
use std::{
io::{Error as IoError, ErrorKind, Write},
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex, OnceLock},
};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{
fmt::{time::Uptime, writer::BoxMakeWriter, MakeWriter},
layer::SubscriberExt,
Layer,
};
use crate::server::ClientSender;
const TRACE_ENV_KEY: &str = "RUFF_TRACE";
static LOGGING_SENDER: OnceLock<ClientSender> = OnceLock::new();
static TRACE_VALUE: Mutex<lsp_types::TraceValue> = Mutex::new(lsp_types::TraceValue::Off);
pub(crate) fn set_trace_value(trace_value: TraceValue) {
let mut global_trace_value = TRACE_VALUE
.lock()
.expect("trace value mutex should be available");
*global_trace_value = trace_value;
}
// A tracing writer that uses LSPs logTrace method.
struct TraceLogWriter;
impl Write for TraceLogWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let message = str::from_utf8(buf).map_err(|e| IoError::new(ErrorKind::InvalidData, e))?;
LOGGING_SENDER
.get()
.expect("logging sender should be initialized at this point")
.send(Message::Notification(Notification {
method: LogTrace::METHOD.to_owned(),
params: serde_json::json!({
"message": message
}),
}))
.map_err(|e| IoError::new(ErrorKind::Other, e))?;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for TraceLogWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
Self
}
}
pub(crate) fn init_tracing(
sender: ClientSender,
log_level: LogLevel,
log_file: Option<&std::path::Path>,
client: Option<&ClientInfo>,
) {
LOGGING_SENDER
.set(sender)
.expect("logging sender should only be initialized once");
let log_file = log_file
.map(|path| {
// this expands `logFile` so that tildes and environment variables
// are replaced with their values, if possible.
if let Some(expanded) = shellexpand::full(&path.to_string_lossy())
.ok()
.and_then(|path| PathBuf::from_str(&path).ok())
{
expanded
} else {
path.to_path_buf()
}
})
.and_then(|path| {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|err| {
#[allow(clippy::print_stderr)]
{
eprintln!(
"Failed to open file at {} for logging: {err}",
path.display()
);
}
})
.ok()
});
let logger = match log_file {
Some(file) => BoxMakeWriter::new(Arc::new(file)),
None => {
if client.is_some_and(|client| {
client.name.starts_with("Zed") || client.name.starts_with("Visual Studio Code")
}) {
BoxMakeWriter::new(TraceLogWriter)
} else {
BoxMakeWriter::new(std::io::stderr)
}
}
};
let subscriber = tracing_subscriber::Registry::default().with(
tracing_subscriber::fmt::layer()
.with_timer(Uptime::default())
.with_thread_names(true)
.with_ansi(false)
.with_writer(logger)
.with_filter(TraceLevelFilter)
.with_filter(LogLevelFilter { filter: log_level }),
);
tracing::subscriber::set_global_default(subscriber)
.expect("should be able to set global default subscriber");
}
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LogLevel {
#[default]
Error,
Warn,
Info,
Debug,
Trace,
}
impl LogLevel {
fn trace_level(self) -> tracing::Level {
match self {
Self::Error => tracing::Level::ERROR,
Self::Warn => tracing::Level::WARN,
Self::Info => tracing::Level::INFO,
Self::Debug => tracing::Level::DEBUG,
Self::Trace => tracing::Level::TRACE,
}
}
}
/// Filters out traces which have a log level lower than the `logLevel` set by the client.
struct LogLevelFilter {
filter: LogLevel,
}
/// Filters out traces if the trace value set by the client is `off`.
struct TraceLevelFilter;
impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
fn enabled(
&self,
meta: &tracing::Metadata<'_>,
_: &tracing_subscriber::layer::Context<'_, S>,
) -> bool {
let filter = if meta.target().starts_with("ruff") {
self.filter.trace_level()
} else {
tracing::Level::INFO
};
meta.level() <= &filter
}
fn max_level_hint(&self) -> Option<tracing::level_filters::LevelFilter> {
Some(LevelFilter::from_level(self.filter.trace_level()))
}
}
impl<S> tracing_subscriber::layer::Filter<S> for TraceLevelFilter {
fn enabled(
&self,
_: &tracing::Metadata<'_>,
_: &tracing_subscriber::layer::Context<'_, S>,
) -> bool {
trace_value() != lsp_types::TraceValue::Off
}
}
#[inline]
fn trace_value() -> lsp_types::TraceValue {
std::env::var(TRACE_ENV_KEY)
.ok()
.and_then(|trace| serde_json::from_value(serde_json::Value::String(trace)).ok())
.unwrap_or_else(|| {
*TRACE_VALUE
.lock()
.expect("trace value mutex should be available")
})
}

View File

@@ -6,10 +6,11 @@ use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::Program;
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
use ruff_db::files::{system_path_to_file, File, Files};
use ruff_db::panic::catch_unwind;
use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
use ruff_source_file::LineIndex;
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextSize;
use salsa::Setter;
@@ -136,7 +137,33 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
})
.collect();
let type_diagnostics = check_types(db, test_file.file);
let type_diagnostics = match catch_unwind(|| check_types(db, test_file.file)) {
Ok(type_diagnostics) => type_diagnostics,
Err(info) => {
let mut by_line = matcher::FailuresByLine::default();
let mut messages = vec![];
match info.location {
Some(location) => messages.push(format!("panicked at {location}")),
None => messages.push("panicked at unknown location".to_string()),
};
match info.payload {
Some(payload) => messages.push(payload),
// Mimic the default panic hook's rendering of the panic payload if it's
// not a string.
None => messages.push("Box<dyn Any>".to_string()),
};
if let Some(backtrace) = info.backtrace {
if std::env::var("RUST_BACKTRACE").is_ok() {
messages.extend(backtrace.to_string().split('\n').map(String::from));
}
}
by_line.push(OneIndexed::from_zero_indexed(0), messages);
return Some(FileFailures {
backtick_offset: test_file.backtick_offset,
by_line,
});
}
};
diagnostics.extend(type_diagnostics.into_iter().map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new((*diagnostic).clone());
diagnostic

View File

@@ -27,7 +27,7 @@ impl FailuresByLine {
})
}
fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
pub(super) fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
let start = self.failures.len();
self.failures.extend(messages);
self.lines.push(LineFailures {

View File

@@ -6,6 +6,7 @@
//! in `crates/red_knot_vendored/vendor/typeshed` change.
use std::fs::File;
use std::io::Write;
use std::path::Path;
use path_slash::PathExt;
@@ -14,13 +15,15 @@ use zip::write::{FileOptions, ZipWriter};
use zip::CompressionMethod;
const TYPESHED_SOURCE_DIR: &str = "vendor/typeshed";
const KNOT_EXTENSIONS_STUBS: &str = "knot_extensions/knot_extensions.pyi";
const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip";
/// Recursively zip the contents of an entire directory.
/// Recursively zip the contents of the entire typeshed directory and patch typeshed
/// on the fly to include the `knot_extensions` module.
///
/// This routine is adapted from a recipe at
/// <https://github.com/zip-rs/zip-old/blob/5d0f198124946b7be4e5969719a7f29f363118cd/examples/write_dir.rs>
fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
fn write_zipped_typeshed_to(writer: File) -> ZipResult<File> {
let mut zip = ZipWriter::new(writer);
// Use deflated compression for WASM builds because compiling `zstd-sys` requires clang
@@ -42,11 +45,11 @@ fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
.compression_method(method)
.unix_permissions(0o644);
for entry in walkdir::WalkDir::new(directory_path) {
for entry in walkdir::WalkDir::new(TYPESHED_SOURCE_DIR) {
let dir_entry = entry.unwrap();
let absolute_path = dir_entry.path();
let normalized_relative_path = absolute_path
.strip_prefix(Path::new(directory_path))
.strip_prefix(Path::new(TYPESHED_SOURCE_DIR))
.unwrap()
.to_slash()
.expect("Unexpected non-utf8 typeshed path!");
@@ -55,9 +58,14 @@ fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
// Some unzip tools unzip files with directory paths correctly, some do not!
if absolute_path.is_file() {
println!("adding file {absolute_path:?} as {normalized_relative_path:?} ...");
zip.start_file(normalized_relative_path, options)?;
zip.start_file(&*normalized_relative_path, options)?;
let mut f = File::open(absolute_path)?;
std::io::copy(&mut f, &mut zip).unwrap();
// Patch the VERSIONS file to make `knot_extensions` available
if normalized_relative_path == "stdlib/VERSIONS" {
writeln!(&mut zip, "knot_extensions: 3.0-")?;
}
} else if !normalized_relative_path.is_empty() {
// Only if not root! Avoids path spec / warning
// and mapname conversion failed error on unzip
@@ -65,11 +73,17 @@ fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
zip.add_directory(normalized_relative_path, options)?;
}
}
// Patch typeshed and add the stubs for the `knot_extensions` module
println!("adding file {KNOT_EXTENSIONS_STUBS} as stdlib/knot_extensions.pyi ...");
zip.start_file("stdlib/knot_extensions.pyi", options)?;
let mut f = File::open(KNOT_EXTENSIONS_STUBS)?;
std::io::copy(&mut f, &mut zip).unwrap();
zip.finish()
}
fn main() {
println!("cargo::rerun-if-changed={TYPESHED_SOURCE_DIR}");
assert!(
Path::new(TYPESHED_SOURCE_DIR).is_dir(),
"Where is typeshed?"
@@ -84,6 +98,6 @@ fn main() {
// which can't be done at compile time.)
let zipped_typeshed_location = format!("{out_dir}{TYPESHED_ZIP_LOCATION}");
let zipped_typeshed = File::create(zipped_typeshed_location).unwrap();
zip_dir(TYPESHED_SOURCE_DIR, zipped_typeshed).unwrap();
let zipped_typeshed_file = File::create(zipped_typeshed_location).unwrap();
write_zipped_typeshed_to(zipped_typeshed_file).unwrap();
}

View File

@@ -0,0 +1,3 @@
The `knot_extensions.pyi` file in this directory will be symlinked into
the `vendor/typeshed/stdlib` directory every time we sync our `typeshed`
stubs (see `.github/workflows/sync_typeshed.yaml`).

View File

@@ -0,0 +1,24 @@
from typing import _SpecialForm, Any, LiteralString
# Special operations
def static_assert(condition: object, msg: LiteralString | None = None) -> None: ...
# Types
Unknown = object()
# Special forms
Not: _SpecialForm
Intersection: _SpecialForm
TypeOf: _SpecialForm
# Predicates on types
#
# Ideally, these would be annotated using `TypeForm`, but that has not been
# standardized yet (https://peps.python.org/pep-0747).
def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
def is_subtype_of(type_derived: Any, typ_ebase: Any) -> bool: ...
def is_assignable_to(type_target: Any, type_source: Any) -> bool: ...
def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ...
def is_fully_static(type: Any) -> bool: ...
def is_singleton(type: Any) -> bool: ...
def is_single_valued(type: Any) -> bool: ...

View File

@@ -0,0 +1,17 @@
"""
Regression test that makes sure we do not short-circuit here after
determining that the overall type will be `Never` and still infer
a type for the second tuple element `2`.
Relevant discussion:
https://github.com/astral-sh/ruff/pull/15218#discussion_r1900811073
"""
from typing_extensions import Never
def never() -> Never:
return never()
(never(), 2)

View File

@@ -285,7 +285,7 @@ impl Workspace {
open_files.contains(&file)
} else if let Some(system_path) = file.path(db).as_system_path() {
self.package(db, system_path)
.map_or(false, |package| package.contains_file(db, file))
.is_some_and(|package| package.contains_file(db, file))
} else {
file.path(db).is_system_virtual_path()
}

View File

@@ -272,7 +272,8 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
#[rustfmt::skip]
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
// related to circular references in class definitions
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false),
("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false),
// related to circular references in type aliases (salsa cycle panic):

View File

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

View File

@@ -9,6 +9,7 @@ use anyhow::bail;
use clap::builder::{TypedValueParser, ValueParserFactory};
use clap::{command, Parser, Subcommand};
use colored::Colorize;
use itertools::Itertools;
use path_absolutize::path_dedot;
use regex::Regex;
use ruff_graph::Direction;
@@ -24,6 +25,7 @@ use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextRange;
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::{Options, PycodestyleOptions};
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
use ruff_workspace::resolver::ConfigurationTransformer;
use rustc_hash::FxHashMap;
use toml;
@@ -957,7 +959,7 @@ A `--config` flag must either be a path to a `.toml` configuration file
// We want to display the most helpful error to the user as possible.
if Path::new(value)
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("toml"))
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
{
if !value.contains('=') {
tip.push_str(&format!(
@@ -967,11 +969,36 @@ It looks like you were trying to pass a path to a configuration file.
The path `{value}` does not point to a configuration file"
));
}
} else if value.contains('=') {
tip.push_str(&format!(
"\n\n{}:\n\n{underlying_error}",
config_parse_error.description()
));
} else if let Some((key, value)) = value.split_once('=') {
let key = key.trim_ascii();
let value = value.trim_ascii_start();
match Options::metadata().find(key) {
Some(OptionEntry::Set(set)) if !value.starts_with('{') => {
let prefixed_subfields = set
.collect_fields()
.iter()
.map(|(name, _)| format!("- `{key}.{name}`"))
.join("\n");
tip.push_str(&format!(
"
`{key}` is a table of configuration options.
Did you want to override one of the table's subkeys?
Possible choices:
{prefixed_subfields}"
));
}
_ => {
tip.push_str(&format!(
"\n\n{}:\n\n{underlying_error}",
config_parse_error.description()
));
}
}
}
let tip = tip.trim_end().to_owned().into();

View File

@@ -11,6 +11,7 @@ use log::{debug, error, warn};
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use ruff_db::panic::catch_unwind;
use ruff_diagnostics::Diagnostic;
use ruff_linter::message::Message;
use ruff_linter::package::PackageRoot;
@@ -27,7 +28,6 @@ use ruff_workspace::resolver::{
use crate::args::ConfigArguments;
use crate::cache::{Cache, PackageCacheMap, PackageCaches};
use crate::diagnostics::Diagnostics;
use crate::panic::catch_unwind;
/// Run the linter over a collection of files.
#[allow(clippy::too_many_arguments)]

View File

@@ -15,6 +15,7 @@ use rustc_hash::FxHashSet;
use thiserror::Error;
use tracing::debug;
use ruff_db::panic::{catch_unwind, PanicError};
use ruff_diagnostics::SourceMap;
use ruff_linter::fs;
use ruff_linter::logging::{DisplayParseError, LogLevel};
@@ -32,7 +33,6 @@ use ruff_workspace::FormatterSettings;
use crate::args::{ConfigArguments, FormatArguments, FormatRange};
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
use crate::panic::{catch_unwind, PanicError};
use crate::resolve::resolve;
use crate::{resolve_default_files, ExitStatus};
@@ -233,7 +233,7 @@ pub(crate) fn format(
}
/// Format the file at the given [`Path`].
#[tracing::instrument(level="debug", skip_all, fields(path = %path.display()))]
#[tracing::instrument(level = "debug", skip_all, fields(path = %path.display()))]
pub(crate) fn format_path(
path: &Path,
settings: &FormatterSettings,
@@ -788,8 +788,6 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
let mut incompatible_rules = FxHashSet::default();
for setting in resolver.settings() {
for rule in [
// The formatter might collapse implicit string concatenation on a single line.
Rule::SingleLineImplicitStringConcatenation,
// Flags missing trailing commas when all arguments are on its own line:
// ```python
// def args(
@@ -829,6 +827,19 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
warn_user_once!("The `format.indent-style=\"tab\"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`.");
}
if !setting
.linter
.rules
.enabled(Rule::SingleLineImplicitStringConcatenation)
&& setting
.linter
.rules
.enabled(Rule::MultiLineImplicitStringConcatenation)
&& !setting.linter.flake8_implicit_str_concat.allow_multiline
{
warn_user_once!("The `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`.");
}
// Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::DocstringTabIndentation)
&& setting.formatter.indent_style.is_tab()

View File

@@ -29,7 +29,6 @@ pub mod args;
mod cache;
mod commands;
mod diagnostics;
mod panic;
mod printer;
pub mod resolve;
mod stdin;

View File

@@ -1,46 +0,0 @@
#[derive(Default, Debug)]
pub(crate) struct PanicError {
pub(crate) info: String,
pub(crate) backtrace: Option<std::backtrace::Backtrace>,
}
impl std::fmt::Display for PanicError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.info)?;
if let Some(backtrace) = &self.backtrace {
writeln!(f, "Backtrace: {backtrace}")
} else {
Ok(())
}
}
}
thread_local! {
static LAST_PANIC: std::cell::Cell<Option<PanicError>> = const { std::cell::Cell::new(None) };
}
/// [`catch_unwind`](std::panic::catch_unwind) wrapper that sets a custom [`set_hook`](std::panic::set_hook)
/// to extract the backtrace. The original panic-hook gets restored before returning.
pub(crate) fn catch_unwind<F, R>(f: F) -> Result<R, PanicError>
where
F: FnOnce() -> R + std::panic::UnwindSafe,
{
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|info| {
let info = info.to_string();
let backtrace = std::backtrace::Backtrace::force_capture();
LAST_PANIC.with(|cell| {
cell.set(Some(PanicError {
info,
backtrace: Some(backtrace),
}));
});
}));
let result = std::panic::catch_unwind(f)
.map_err(|_| LAST_PANIC.with(std::cell::Cell::take).unwrap_or_default());
std::panic::set_hook(prev);
result
}

View File

@@ -877,7 +877,7 @@ indent-width = 2
[lint]
select = ["ALL"]
ignore = ["D203", "D212"]
ignore = ["D203", "D212", "ISC001"]
[lint.isort]
lines-after-imports = 3
@@ -891,6 +891,9 @@ inline-quotes = "single"
docstring-quotes = "single"
multiline-quotes = "single"
[lint.flake8-implicit-str-concat]
allow-multiline = false
[format]
skip-magic-trailing-comma = true
indent-style = "tab"
@@ -915,8 +918,9 @@ def say_hy(name: str):
1 file reformatted
----- stderr -----
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`.
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
@@ -974,7 +978,7 @@ def say_hy(name: str):
print(f"Hy {name}")
----- stderr -----
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
@@ -1114,7 +1118,7 @@ def say_hy(name: str):
----- stderr -----
warning: `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `incorrect-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
");
Ok(())
}

View File

@@ -780,6 +780,90 @@ fn each_toml_option_requires_a_new_flag_2() {
");
}
#[test]
fn value_given_to_table_key_is_not_inline_table_1() {
// https://github.com/astral-sh/ruff/issues/13995
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args([".", "--config", r#"lint.flake8-pytest-style="csv""#]),
@r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'lint.flake8-pytest-style="csv"' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
`lint.flake8-pytest-style` is a table of configuration options.
Did you want to override one of the table's subkeys?
Possible choices:
- `lint.flake8-pytest-style.fixture-parentheses`
- `lint.flake8-pytest-style.parametrize-names-type`
- `lint.flake8-pytest-style.parametrize-values-type`
- `lint.flake8-pytest-style.parametrize-values-row-type`
- `lint.flake8-pytest-style.raises-require-match-for`
- `lint.flake8-pytest-style.raises-extend-require-match-for`
- `lint.flake8-pytest-style.mark-parentheses`
For more information, try '--help'.
"#);
}
#[test]
fn value_given_to_table_key_is_not_inline_table_2() {
// https://github.com/astral-sh/ruff/issues/13995
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args([".", "--config", r#"lint=123"#]),
@r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'lint=123' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
`lint` is a table of configuration options.
Did you want to override one of the table's subkeys?
Possible choices:
- `lint.allowed-confusables`
- `lint.dummy-variable-rgx`
- `lint.extend-ignore`
- `lint.extend-select`
- `lint.extend-fixable`
- `lint.external`
- `lint.fixable`
- `lint.ignore`
- `lint.extend-safe-fixes`
- `lint.extend-unsafe-fixes`
- `lint.ignore-init-module-imports`
- `lint.logger-objects`
- `lint.select`
- `lint.explicit-preview-rules`
- `lint.task-tags`
- `lint.typing-modules`
- `lint.unfixable`
- `lint.per-file-ignores`
- `lint.extend-per-file-ignores`
- `lint.exclude`
- `lint.preview`
For more information, try '--help'.
");
}
#[test]
fn config_doubly_overridden_via_cli() -> Result<()> {
let tempdir = TempDir::new()?;

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