Compare commits

...

45 Commits

Author SHA1 Message Date
Micha Reiser
9ee8dc3fff Enable symlink tests for windows 2025-02-26 14:11:29 +01:00
Dhruv Manilawala
bf2c9a41cd Avoid unnecessary info at non-trace server log level (#16389)
## Summary

Currently, the log messages emitted by the server includes multiple
information which isn't really required most of the time.

Here's the current format:
```
   0.000755625s DEBUG main ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/playground/ruff
   0.016334666s DEBUG ThreadId(10) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.019954541s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.020160416s TRACE ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   0.020209625s TRACE ruff:worker:0 request{id=1 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   0.020228166s DEBUG ruff:worker:0 request{id=1 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/test.py
   0.020359833s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
```

This PR updates the following:
* Uses current timestamp (same as red-knot) for all log levels instead
of the uptime value
* Includes the target and thread names only at the trace level

What this means is that the message is reduced to only important
information at DEBUG level:

```
2025-02-26 11:35:02.198375000 DEBUG Indexing settings for workspace: /Users/dhruv/playground/ruff
2025-02-26 11:35:02.209933000 DEBUG Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
2025-02-26 11:35:02.217165000  INFO Registering workspace: /Users/dhruv/playground/ruff
2025-02-26 11:35:02.217631000 DEBUG Included path via `include`: /Users/dhruv/playground/ruff/lsp/test.py
2025-02-26 11:35:02.217684000  INFO Configuration file watcher successfully registered
```

while still showing the other information (thread names and target) at
trace level:
```
2025-02-26 11:35:27.819617000 DEBUG main ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/playground/ruff
2025-02-26 11:35:27.830500000 DEBUG ThreadId(11) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
2025-02-26 11:35:27.837212000  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
2025-02-26 11:35:27.837714000 TRACE ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
2025-02-26 11:35:27.838019000  INFO ruff:main ruff_server::server: Configuration file watcher successfully registered
2025-02-26 11:35:27.838084000 TRACE ruff:worker:1 request{id=1 method="textDocument/diagnostic"}: ruff_server::server::api: enter
2025-02-26 11:35:27.838205000 DEBUG ruff:worker:1 request{id=1 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/test.py
```
2025-02-26 13:31:17 +05:30
Dhruv Manilawala
be03cb04c1 Expand ruff.configuration to allow inline config (#16296)
## Summary

[Internal design
document](https://www.notion.so/astral-sh/In-editor-settings-19e48797e1ca807fa8c2c91b689d9070?pvs=4)

This PR expands `ruff.configuration` to allow inline configuration
directly in the editor. For example:

```json
{
	"ruff.configuration": {
		"line-length": 100,
		"lint": {
			"unfixable": ["F401"],
			"flake8-tidy-imports": {
				"banned-api": {
					"typing.TypedDict": {
						"msg": "Use `typing_extensions.TypedDict` instead"
					}
				}
			}
		},
		"format": {
			"quote-style": "single"
		}
	}
}
```

This means that now `ruff.configuration` accepts either a path to
configuration file or the raw config itself. It's _mostly_ similar to
`--config` with one difference that's highlighted in the following
section. So, it can be said that the format of `ruff.configuration` when
provided the config map is same as the one on the [playground] [^1].

## Limitations

<details><summary><b>Casing (<code>kebab-case</code> v/s/
<code>camelCase</code>)</b></summary>
<p>


The config keys needs to be in `kebab-case` instead of `camelCase` which
is being used for other settings in the editor.

This could be a bit confusing. For example, the `line-length` option can
be set directly via an editor setting or can be configured via
`ruff.configuration`:

```json
{
	"ruff.configuration": {
        "line-length": 100
    },
    "ruff.lineLength": 120
}
```

#### Possible solution

We could use feature flag with [conditional
compilation](https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute)
to indicate that when used in `ruff_server`, we need the `Options`
fields to be renamed as `camelCase` while for other crates it needs to
be renamed as `kebab-case`. But, this might not work very easily because
it will require wrapping the `Options` struct and create two structs in
which we'll have to add `#[cfg_attr(...)]` because otherwise `serde`
will complain:

```
error: duplicate serde attribute `rename_all`
  --> crates/ruff_workspace/src/options.rs:43:38
   |
43 | #[cfg_attr(feature = "editor", serde(rename_all = "camelCase"))]
   |                                      ^^^^^^^^^^
```

</p>
</details> 

<details><summary><b>Nesting (flat v/s nested keys)</b></summary>
<p>

This is the major difference between `--config` flag on the command-line
v/s `ruff.configuration` and it makes it such that `ruff.configuration`
has same value format as [playground] [^1].

The config keys needs to be split up into keys which can result in
nested structure instead of flat structure:

So, the following **won't work**:

```json
{
	"ruff.configuration": {
		"format.quote-style": "single",
		"lint.flake8-tidy-imports.banned-api.\"typing.TypedDict\".msg": "Use `typing_extensions.TypedDict` instead"
	}
}
```

But, instead it would need to be split up like the following:
```json
{
	"ruff.configuration": {
		"format": {
			"quote-style": "single"
		},
		"lint": {
			"flake8-tidy-imports": {
				"banned-api": {
					"typing.TypedDict": {
						"msg": "Use `typing_extensions.TypedDict` instead"
					}
				}
			}
		}
	}
}
```

#### Possible solution (1)

The way we could solve this and make it same as `--config` would be to
add a manual logic of converting the JSON map into an equivalent TOML
string which would be then parsed into `Options`.

So, the following JSON map:
```json
{ "lint.flake8-tidy-imports": { "banned-api": {"\"typing.TypedDict\".msg": "Use typing_extensions.TypedDict instead"}}}
```

would need to be converted into the following TOML string:
```toml
lint.flake8-tidy-imports = { banned-api = { "typing.TypedDict".msg = "Use typing_extensions.TypedDict instead" } }
```

by recursively convering `"key": value` into `key = value` which is to
remove the quotes from key and replacing `:` with `=`.

#### Possible solution (2)

Another would be to just accept `Map<String, String>` strictly and
convert it into `key = value` and then parse it as a TOML string. This
would also match `--config` but quotes might become a nuisance because
JSON only allows double quotes and so it'll require escaping any inner
quotes or use single quotes.

</p>
</details> 

## Test Plan

### VS Code

**Requires https://github.com/astral-sh/ruff-vscode/pull/702**

**`settings.json`**:
```json
{
  "ruff.lint.extendSelect": ["TID"],
  "ruff.configuration": {
    "line-length": 50,
    "format": {
      "quote-style": "single"
    },
    "lint": {
      "unfixable": ["F401"],
      "flake8-tidy-imports": {
        "banned-api": {
          "typing.TypedDict": {
            "msg": "Use `typing_extensions.TypedDict` instead"
          }
        }
      }
    }
  }
}
```

Following video showcases me doing the following:
1. Check diagnostics that it includes `TID`
2. Run `Ruff: Fix all auto-fixable problems` to test `unfixable`
3. Run `Format: Document` to test `line-length` and `quote-style`


https://github.com/user-attachments/assets/0a38176f-3fb0-4960-a213-73b2ea5b1180

### Neovim

**`init.lua`**:
```lua
require('lspconfig').ruff.setup {
  init_options = {
    settings = {
      lint = {
        extendSelect = { 'TID' },
      },
      configuration = {
        ['line-length'] = 50,
        format = {
          ['quote-style'] = 'single',
        },
        lint = {
          unfixable = { 'F401' },
          ['flake8-tidy-imports'] = {
            ['banned-api'] = {
              ['typing.TypedDict'] = {
                msg = 'Use typing_extensions.TypedDict instead',
              },
            },
          },
        },
      },
    },
  },
}
```

Same steps as in the VS Code test:



https://github.com/user-attachments/assets/cfe49a9b-9a89-43d7-94f2-7f565d6e3c9d

## Documentation Preview



https://github.com/user-attachments/assets/e0062f58-6ec8-4e01-889d-fac76fd8b3c7



[playground]: https://play.ruff.rs

[^1]: This has one advantage that the value can be copy-pasted directly
into the playground
2025-02-26 10:17:11 +05:30
Brent Westbrook
78806361fd Start detecting version-related syntax errors in the parser (#16090)
## Summary

This PR builds on the changes in #16220 to pass a target Python version
to the parser. It also adds the `Parser::unsupported_syntax_errors` field, which
collects version-related syntax errors while parsing. These syntax
errors are then turned into `Message`s in ruff (in preview mode).

This PR only detects one syntax error (`match` statement before Python
3.10), but it has been pretty quick to extend to several other simple
errors (see #16308 for example).

## Test Plan

The current tests are CLI tests in the linter crate, but these could be
supplemented with inline parser tests after #16357.

I also tested the display of these syntax errors in VS Code:


![image](https://github.com/user-attachments/assets/062b4441-740e-46c3-887c-a954049ef26e)

![image](https://github.com/user-attachments/assets/101f55b8-146c-4d59-b6b0-922f19bcd0fa)

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-02-25 23:03:48 -05:00
Douglas Creager
b39a4ad01d [red-knot] Rename constraint to predicate (#16382)
In https://github.com/astral-sh/ruff/pull/16306#discussion_r1966290700,
@carljm pointed out that #16306 introduced a terminology problem, with
too many things called a "constraint". This is a follow-up PR that
renames `Constraint` to `Predicate` to hopefully clear things up a bit.
So now we have that:

- a _predicate_ is a Python expression that might influence type
inference
- a _narrowing constraint_ is a list of predicates that constraint the
type of a binding that is visible at a use
- a _visibility constraint_ is a ternary formula of predicates that
define whether a binding is visible or a statement is reachable

This is a pure renaming, with no behavioral changes.
2025-02-25 14:52:40 -05:00
David Peter
86b01d2d3c [red-knot] Correct modeling of dunder calls (#16368)
## Summary

Model dunder-calls correctly (and in one single place), by implementing
this behavior (using `__getitem__` as an example).

```py
def getitem_desugared(obj: object, key: object) -> object:
    getitem_callable = find_in_mro(type(obj), "__getitem__")
    if hasattr(getitem_callable, "__get__"):
        getitem_callable = getitem_callable.__get__(obj, type(obj))

    return getitem_callable(key)
```

See the new `calls/dunder.md` test suite for more information. The new
behavior also needs much fewer lines of code (the diff is positive due
to new tests).

## Test Plan

New tests; fix TODOs in existing tests.
2025-02-25 20:38:15 +01:00
David Peter
f88328eedd [red-knot] Handle possibly-unbound instance members (#16363)
## Summary

Adds support for possibly-unbound/undeclared instance members.

## Test Plan

New MD tests.
2025-02-25 20:00:38 +01:00
Douglas Creager
fa76f6cbb2 [red-knot] Use arena-allocated association lists for narrowing constraints (#16306)
This PR adds an implementation of [association
lists](https://en.wikipedia.org/wiki/Association_list), and uses them to
replace the previous `BitSet`/`SmallVec` representation for narrowing
constraints.

An association list is a linked list of key/value pairs. We additionally
guarantee that the elements of an association list are sorted (by their
keys), and that they do not contain any entries with duplicate keys.

Association lists have fallen out of favor in recent decades, since you
often need operations that are inefficient on them. In particular,
looking up a random element by index is O(n), just like a linked list;
and looking up an element by key is also O(n), since you must do a
linear scan of the list to find the matching element. Luckily we don't
need either of those operations for narrowing constraints!

The typical implementation also suffers from poor cache locality and
high memory allocation overhead, since individual list cells are
typically allocated separately from the heap. We solve that last problem
by storing the cells of an association list in an `IndexVec` arena.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-02-25 10:58:56 -05:00
Alex Waygood
5c007db7e2 [red-knot] Rewrite Type::try_iterate() to improve type inference and diagnostic messages (#16321) 2025-02-25 14:02:03 +00:00
Zanie Blue
1be0dc6885 Add issue templates (#16213)
Follows https://github.com/astral-sh/ruff/pull/15651

Preview: https://github.com/dhruvmanila/ruff-issue-templates/issues

GitHub made the interface for single-template repositories worse. While
they might fix it, it encouragement to just do this work. They still
haven't fixed the teeny tiny emojis which makes me think this won't be
fixed quickly.

Before:

<img width="1267" alt="Screenshot 2025-02-17 at 8 26 08 AM"
src="https://github.com/user-attachments/assets/e69ef630-4296-470e-ab4d-a22d55785444"
/>

After:

<img width="1688" alt="Screenshot 2025-02-24 at 3 05 35 PM"
src="https://github.com/user-attachments/assets/61033666-1fe5-421b-a69c-1aa79bcc85b5"
/>

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-02-25 16:29:16 +05:30
Muspi Merol
a1a536b2c5 Normalize inconsistent markdown headings in docstrings (#16364)
I am working on a project that uses ruff linters' docs to generate a
fine-tuning dataset for LLMs.

To achieve this, I first ran the command `ruff rule --all
--output-format json` to retrieve all the rules. Then, I parsed the
explanation field to get these 3 consistent sections:

- `Why is this bad?`
- `What it does`
- `Example`

However, during the initial processing, I noticed that the markdown
headings are not that consistent. For instance:

- In most cases, `Use instead` appears as a normal paragraph within the
`Example` section, but in the file
`crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs` it is
a level-2 heading
- The heading "What it does**?**" is used in some places, while others
consistently use "What it does"
- There are 831 `Example` headings and 65 `Examples`. But all of them
only have one example case

This PR normalized these across all rules.

## Test Plan

CI are passed.
2025-02-25 15:42:55 +05:30
David Peter
aac79e453a [red-knot] Better diagnostics for method calls (#16362)
## Summary

Add better error messages and additional spans for method calls. Can be
reviewed commit-by-commit.

before:

```
error: lint:invalid-argument-type
 --> /home/shark/playground/test.py:6:10
  |
5 | c = C()
6 | c.square("hello")  # error: [invalid-argument-type]
  |          ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`x`); expected type `int`
7 |
8 | # import inspect
  |
```

after:

```
error: lint:invalid-argument-type
 --> /home/shark/playground/test.py:6:10
  |
5 | c = C()
6 | c.square("hello")  # error: [invalid-argument-type]
  |          ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`x`) of bound method `square`; expected type `int`
7 |
8 | # import inspect
  |
 ::: /home/shark/playground/test.py:2:22
  |
1 | class C:
2 |     def square(self, x: int) -> int:
  |                      ------ info: parameter declared in function definition here
3 |         return x * x
  |
```

## Test Plan

New snapshot test
2025-02-25 09:58:08 +01:00
Micha Reiser
fd7b3c83ad [red-knot] Add argfile and windows glob path support (#16353) 2025-02-25 08:43:13 +01:00
Micha Reiser
d895ee0014 [red-knot] Handle pipe-errors gracefully (#16354) 2025-02-25 08:42:52 +01:00
Micha Reiser
4732c58829 Rename venv-path to python (#16347) 2025-02-24 19:41:06 +01:00
Alex Waygood
45bae29a4b [red-knot] Fixup some formatting in infer.rs (#16348) 2025-02-24 14:44:49 +00:00
Alex Waygood
7059f4249b [red-knot] Restrict visibility of more things in class.rs (#16346) 2025-02-24 14:30:56 +00:00
Mike Perlov
68991d09a8 [red-knot] Add diagnostic for class-object access to pure instance variables (#16036)
## Summary

Add a diagnostic if a pure instance variable is accessed on a class object. For example

```py
class C:
    instance_only: str

    def __init__(self):
        self.instance_only = "a"

# error: Attribute `instance_only` can only be accessed on instances, not on the class object `Literal[C]` itself.
C.instance_only
```


---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-02-24 15:17:16 +01:00
Brent Westbrook
e7a6c19e3a Add per-file-target-version option (#16257)
## Summary

This PR is another step in preparing to detect syntax errors in the
parser. It introduces the new `per-file-target-version` top-level
configuration option, which holds a mapping of compiled glob patterns to
Python versions. I intend to use the
`LinterSettings::resolve_target_version` method here to pass to the
parser:


f50849aeef/crates/ruff_linter/src/linter.rs (L491-L493)

## Test Plan

I added two new CLI tests to show that the `per-file-target-version` is
respected in both the formatter and the linter.
2025-02-24 08:47:13 -05:00
Vasco Schiavo
42a5f5ef6a [PLW1507] Mark fix unsafe (#16343) 2025-02-24 13:42:44 +01:00
Alex Waygood
5bac4f6bd4 [red-knot] Add a test to ensure that KnownClass::try_from_file_and_name() is kept up to date (#16326) 2025-02-24 12:14:20 +00:00
Micha Reiser
320a3c68ae Extract class and instance types (#16337) 2025-02-24 11:36:20 +00:00
Dhruv Manilawala
24e08d17c4 Re-order changelog entries for 0.9.7 (#16344)
## Summary

This is mainly on me for not noticing this during the last release but I
noticed in the last changelog that there's only 1 bug fix which didn't
seem correct as I saw multiple of them so I looked at a couple of PRs
that are in "Rule changes" section and the PRs that were marked with the
`bug` label was categorized there because

1. It _also_ had other labels like `rule` and `fixes`
(https://github.com/astral-sh/ruff/pull/16080,
https://github.com/astral-sh/ruff/pull/16110,
https://github.com/astral-sh/ruff/pull/16219, etc.)
2. Some PRs didn't have the `bug` label (but the issue as marked as
`bug`) but _only_ labels like "fixes"
(https://github.com/astral-sh/ruff/pull/16011,
https://github.com/astral-sh/ruff/pull/16132, etc.)
2025-02-24 09:10:14 +00:00
David Peter
141ba253da [red-knot] Add support for @classmethods (#16305)
## Summary

Add support for `@classmethod`s.

```py
class C:
    @classmethod
    def f(cls, x: int) -> str:
        return "a"

reveal_type(C.f(1))  # revealed: str
```

## Test Plan

New Markdown tests
2025-02-24 09:55:34 +01:00
Micha Reiser
81a57656d8 Update Salsa (#16338) 2025-02-24 09:44:19 +01:00
Micha Reiser
5eaf225fc3 Update Salsa part 1 (#16340) 2025-02-24 09:35:21 +01:00
Micha Reiser
bc018bf2e5 Upgrade Rust toolchain to 1.85.0 (#16339) 2025-02-24 09:20:22 +01:00
renovate[bot]
0fad53d203 Update NPM Development dependencies (#16327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 08:26:14 +01:00
renovate[bot]
e6b1c89fb7 Update Rust crate clap to v4.5.30 (#16329)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.29` -> `4.5.30` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.5.30`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4530---2025-02-17)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.29...v4.5.30)

##### Fixes

-   *(assert)* Allow `num_args(0..=1)` to be used with `SetTrue`
-   *(assert)* Clean up rendering of `takes_values` assertions

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 12:18:48 +05:30
renovate[bot]
222588645b Update dependency ruff to v0.9.7 (#16336)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [ruff](https://docs.astral.sh/ruff)
([source](https://redirect.github.com/astral-sh/ruff),
[changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md))
| `==0.9.6` -> `==0.9.7` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.9.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/ruff/0.9.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/ruff/0.9.6/0.9.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.9.6/0.9.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>astral-sh/ruff (ruff)</summary>

###
[`v0.9.7`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#097)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.9.6...0.9.7)

##### Preview features

- Consider `__new__` methods as special function type for enforcing
class method or static method rules
([#&#8203;13305](https://redirect.github.com/astral-sh/ruff/pull/13305))
- \[`airflow`] Improve the internal logic to differentiate deprecated
symbols (`AIR303`)
([#&#8203;16013](https://redirect.github.com/astral-sh/ruff/pull/16013))
- \[`refurb`] Manual timezone monkeypatching (`FURB162`)
([#&#8203;16113](https://redirect.github.com/astral-sh/ruff/pull/16113))
- \[`ruff`] Implicit class variable in dataclass (`RUF045`)
([#&#8203;14349](https://redirect.github.com/astral-sh/ruff/pull/14349))
- \[`ruff`] Skip singleton starred expressions for
`incorrectly-parenthesized-tuple-in-subscript` (`RUF031`)
([#&#8203;16083](https://redirect.github.com/astral-sh/ruff/pull/16083))
- \[`refurb`] Check for subclasses includes subscript expressions
(`FURB189`)
([#&#8203;16155](https://redirect.github.com/astral-sh/ruff/pull/16155))

##### Rule changes

- \[`flake8-comprehensions`]: Handle trailing comma in `C403` fix
([#&#8203;16110](https://redirect.github.com/astral-sh/ruff/pull/16110))
- \[`flake8-debugger`] Also flag `sys.breakpointhook` and
`sys.__breakpointhook__` (`T100`)
([#&#8203;16191](https://redirect.github.com/astral-sh/ruff/pull/16191))
- \[`pydocstyle`] Handle arguments with the same names as sections
(`D417`)
([#&#8203;16011](https://redirect.github.com/astral-sh/ruff/pull/16011))
- \[`pylint`] Correct ordering of arguments in fix for `if-stmt-min-max`
(`PLR1730`)
([#&#8203;16080](https://redirect.github.com/astral-sh/ruff/pull/16080))
- \[`pylint`] Do not offer fix for raw strings (`PLE251`)
([#&#8203;16132](https://redirect.github.com/astral-sh/ruff/pull/16132))
- \[`pyupgrade`] Do not upgrade functional `TypedDicts` with private
field names to the class-based syntax (`UP013`)
([#&#8203;16219](https://redirect.github.com/astral-sh/ruff/pull/16219))
- \[`pyupgrade`] Handle micro version numbers correctly (`UP036`)
([#&#8203;16091](https://redirect.github.com/astral-sh/ruff/pull/16091))
- \[`pyupgrade`] Unwrap unary expressions correctly (`UP018`)
([#&#8203;15919](https://redirect.github.com/astral-sh/ruff/pull/15919))
- \[`ruff`] Skip `RUF001` diagnostics when visiting string type
definitions
([#&#8203;16122](https://redirect.github.com/astral-sh/ruff/pull/16122))
- \[`flake8-pyi`] Avoid flagging `custom-typevar-for-self` on metaclass
methods (`PYI019`)
([#&#8203;16141](https://redirect.github.com/astral-sh/ruff/pull/16141))
- \[`pycodestyle`] Exempt `site.addsitedir(...)` calls (`E402`)
([#&#8203;16251](https://redirect.github.com/astral-sh/ruff/pull/16251))

##### Formatter

- Fix unstable formatting of trailing end-of-line comments of
parenthesized attribute values
([#&#8203;16187](https://redirect.github.com/astral-sh/ruff/pull/16187))

##### Server

- Fix handling of requests received after shutdown message
([#&#8203;16262](https://redirect.github.com/astral-sh/ruff/pull/16262))
- Ignore `source.organizeImports.ruff` and `source.fixAll.ruff` code
actions for a notebook cell
([#&#8203;16154](https://redirect.github.com/astral-sh/ruff/pull/16154))
- Include document specific debug info for `ruff.printDebugInformation`
([#&#8203;16215](https://redirect.github.com/astral-sh/ruff/pull/16215))
- Update server to return the debug info as string with
`ruff.printDebugInformation`
([#&#8203;16214](https://redirect.github.com/astral-sh/ruff/pull/16214))

##### CLI

- Warn on invalid `noqa` even when there are no diagnostics
([#&#8203;16178](https://redirect.github.com/astral-sh/ruff/pull/16178))
- Better error messages while loading configuration `extend`s
([#&#8203;15658](https://redirect.github.com/astral-sh/ruff/pull/15658))

##### Bug fixes

- \[`refurb`] Correctly handle lengths of literal strings in
`slice-to-remove-prefix-or-suffix` (`FURB188`)
([#&#8203;16237](https://redirect.github.com/astral-sh/ruff/pull/16237))

##### Documentation

- Add FAQ entry for `source.*` code actions in Notebook
([#&#8203;16212](https://redirect.github.com/astral-sh/ruff/pull/16212))
- Add `SECURITY.md`
([#&#8203;16224](https://redirect.github.com/astral-sh/ruff/pull/16224))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 12:01:55 +05:30
renovate[bot]
b7dab13c79 Update Rust crate anyhow to v1.0.96 (#16328)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.95` -> `1.0.96` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>dtolnay/anyhow (anyhow)</summary>

###
[`v1.0.96`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.96)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.95...1.0.96)

-   Documentation improvements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 12:01:08 +05:30
renovate[bot]
81f6561af4 Update Rust crate libc to v0.2.170 (#16330)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [libc](https://redirect.github.com/rust-lang/libc) |
workspace.dependencies | patch | `0.2.169` -> `0.2.170` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>rust-lang/libc (libc)</summary>

###
[`v0.2.170`](https://redirect.github.com/rust-lang/libc/releases/tag/0.2.170)

[Compare
Source](https://redirect.github.com/rust-lang/libc/compare/0.2.169...0.2.170)

##### Added

- Android: Declare `setdomainname` and `getdomainname`
[#&#8203;4212](https://redirect.github.com/rust-lang/libc/pull/4212)
- FreeBSD: Add `evdev` structures
[#&#8203;3756](https://redirect.github.com/rust-lang/libc/pull/3756)
- FreeBSD: Add the new `st_filerev` field to `stat32`
([#&#8203;4254](https://redirect.github.com/rust-lang/libc/pull/4254))
- Linux: Add ` SI_*`` and `TRAP_\*\`\` signal codes
[#&#8203;4225](https://redirect.github.com/rust-lang/libc/pull/4225)
- Linux: Add experimental configuration to enable 64-bit time in kernel
APIs, set by `RUST_LIBC_UNSTABLE_LINUX_TIME_BITS64`.
[#&#8203;4148](https://redirect.github.com/rust-lang/libc/pull/4148)
- Linux: Add recent socket timestamping flags
[#&#8203;4273](https://redirect.github.com/rust-lang/libc/pull/4273)
- Linux: Added new CANFD_FDF flag for the flags field of canfd_frame
[#&#8203;4223](https://redirect.github.com/rust-lang/libc/pull/4223)
- Musl: add CLONE_NEWTIME
[#&#8203;4226](https://redirect.github.com/rust-lang/libc/pull/4226)
- Solarish: add the posix_spawn family of functions
[#&#8203;4259](https://redirect.github.com/rust-lang/libc/pull/4259)

##### Deprecated

- Linux: deprecate kernel modules syscalls
[#&#8203;4228](https://redirect.github.com/rust-lang/libc/pull/4228)

##### Changed

- Emscripten: Assume version is at least 3.1.42
[#&#8203;4243](https://redirect.github.com/rust-lang/libc/pull/4243)

##### Fixed

- BSD: Correct the definition of `WEXITSTATUS`
[#&#8203;4213](https://redirect.github.com/rust-lang/libc/pull/4213)
- Hurd: Fix CMSG_DATA on 64bit systems
([#&#8203;4240](https://redirect.github.com/rust-lang/libc/pull/424))
- NetBSD: fix `getmntinfo`
([#&#8203;4265](https://redirect.github.com/rust-lang/libc/pull/4265)
- VxWorks: Fix the size of `time_t`
[#&#8203;426](https://redirect.github.com/rust-lang/libc/pull/426)

##### Other

- Add labels to FIXMEs
[#&#8203;4230](https://redirect.github.com/rust-lang/libc/pull/4230),
[#&#8203;4229](https://redirect.github.com/rust-lang/libc/pull/4229),
[#&#8203;4237](https://redirect.github.com/rust-lang/libc/pull/4237)
- CI: Bump FreeBSD CI to 13.4 and 14.2
[#&#8203;4260](https://redirect.github.com/rust-lang/libc/pull/4260)
- Copy definitions from core::ffi and centralize them
[#&#8203;4256](https://redirect.github.com/rust-lang/libc/pull/4256)
- Define c_char at top-level and remove per-target c_char definitions
[#&#8203;4202](https://redirect.github.com/rust-lang/libc/pull/4202)
- Port style.rs to syn and add tests for the style checker
[#&#8203;4220](https://redirect.github.com/rust-lang/libc/pull/4220)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 11:58:01 +05:30
renovate[bot]
c37c078142 Update Rust crate serde_json to v1.0.139 (#16333)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.138` -> `1.0.139` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.139`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.139)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.138...v1.0.139)

-   Documentation improvements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 11:53:12 +05:30
renovate[bot]
dd5f9d1df9 Update Rust crate log to v0.4.26 (#16331)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [log](https://redirect.github.com/rust-lang/log) |
workspace.dependencies | patch | `0.4.25` -> `0.4.26` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>rust-lang/log (log)</summary>

###
[`v0.4.26`](https://redirect.github.com/rust-lang/log/blob/HEAD/CHANGELOG.md#0426---2025-02-18)

[Compare
Source](https://redirect.github.com/rust-lang/log/compare/0.4.25...0.4.26)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 11:52:58 +05:30
renovate[bot]
f05cfe134e Update Rust crate serde to v1.0.218 (#16332)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.217` -> `1.0.218` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>serde-rs/serde (serde)</summary>

###
[`v1.0.218`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.218)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.217...v1.0.218)

-   Documentation improvements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 11:52:14 +05:30
renovate[bot]
a3d8b31cdd Update Rust crate tempfile to v3.17.1 (#16334)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tempfile](https://stebalien.com/projects/tempfile-rs/)
([source](https://redirect.github.com/Stebalien/tempfile)) |
workspace.dependencies | patch | `3.17.0` -> `3.17.1` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>Stebalien/tempfile (tempfile)</summary>

###
[`v3.17.1`](https://redirect.github.com/Stebalien/tempfile/blob/HEAD/CHANGELOG.md#3171)

[Compare
Source](https://redirect.github.com/Stebalien/tempfile/compare/v3.17.0...v3.17.1)

- Fix build with `windows-sys` 0.52. Unfortunately, we have no CI for
older `windows-sys` versions at the moment...

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 11:51:38 +05:30
renovate[bot]
558282649e Update Rust crate unicode-ident to v1.0.17 (#16335)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [unicode-ident](https://redirect.github.com/dtolnay/unicode-ident) |
workspace.dependencies | patch | `1.0.16` -> `1.0.17` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>dtolnay/unicode-ident (unicode-ident)</summary>

###
[`v1.0.17`](https://redirect.github.com/dtolnay/unicode-ident/releases/tag/1.0.17)

[Compare
Source](https://redirect.github.com/dtolnay/unicode-ident/compare/1.0.16...1.0.17)

-   Documentation improvements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzYuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3Ni4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 11:50:48 +05:30
Vasco Schiavo
b312b53c2e [flake8-pyi] Mark PYI030 fix unsafe when comments are deleted (#16322) 2025-02-23 21:22:14 +00:00
InSync
c814745643 [flake8-self] Ignore attribute accesses on instance-like variables (SLF001) (#16149) 2025-02-23 10:00:49 +00:00
Ari Pollak
aa88f2dbe5 Fix example for S611 (#16316)
## Summary

* Existing example did not include RawSQL() call like it should
* Also clarify the example a bit to make it clearer that the code is not
secure
## Test Plan

N/A, only documentation updated
2025-02-22 14:15:29 -05:00
Alex Waygood
64effa4aea [red-knot] Add a regression test for recent improvement to TypeInferenceBuilder::infer_name_load() (#16310) 2025-02-21 22:28:42 +00:00
Alex Waygood
224a36f5f3 Teach red-knot that type(x) is the same as x.__class__ (#16301) 2025-02-21 21:05:48 +00:00
Alex Waygood
5347abc766 [red-knot] Generalise special-casing for KnownClasses in Type::bool (#16300) 2025-02-21 20:46:36 +00:00
Micha Reiser
5fab97f1ef [red-knot] Diagnostics for incorrect bool usages (#16238) 2025-02-21 19:26:05 +01:00
David Peter
3aa7ba31b1 [red-knot] Fix descriptor __get__ call on class objects (#16304)
## Summary

I spotted a minor mistake in my descriptor protocol implementation where
`C.descriptor` would pass the meta type (`type`) of the type of `C`
(`Literal[C]`) as the owner argument to `__get__`, instead of passing
`Literal[C]` directly.

## Test Plan

New test.
2025-02-21 15:35:41 +01:00
274 changed files with 9791 additions and 3545 deletions

View File

@@ -0,0 +1,31 @@
name: Bug report
description: Report an error or unexpected behavior
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to report an issue! We're glad to have you involved with Ruff.
**Before reporting, please make sure to search through [existing issues](https://github.com/astral-sh/ruff/issues?q=is:issue+is:open+label:bug) (including [closed](https://github.com/astral-sh/ruff/issues?q=is:issue%20state:closed%20label:bug)).**
- type: textarea
attributes:
label: Summary
description: |
A clear and concise description of the bug, including a minimal reproducible example.
Be sure to include the command you invoked (e.g., `ruff check /path/to/file.py --fix`), ideally including the `--isolated` flag and
the current Ruff settings (e.g., relevant sections from your `pyproject.toml`).
If possible, try to include the [playground](https://play.ruff.rs) link that reproduces this issue.
validations:
required: true
- type: input
attributes:
label: Version
description: What version of ruff are you using? (see `ruff version`)
placeholder: e.g., ruff 0.9.3 (90589372d 2025-01-23)
validations:
required: false

View File

@@ -0,0 +1,10 @@
name: Rule request
description: Anything related to lint rules (proposing new rules, changes to existing rules, auto-fixes, etc.)
body:
- type: textarea
attributes:
label: Summary
description: |
A clear and concise description of the relevant request. If applicable, please describe the current behavior as well.
validations:
required: true

18
.github/ISSUE_TEMPLATE/3_question.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Question
description: Ask a question about Ruff
labels: ["question"]
body:
- type: textarea
attributes:
label: Question
description: Describe your question in detail.
validations:
required: true
- type: input
attributes:
label: Version
description: What version of ruff are you using? (see `ruff version`)
placeholder: e.g., ruff 0.9.3 (90589372d 2025-01-23)
validations:
required: false

View File

@@ -1,2 +1,8 @@
# This file cannot use the extension `.yaml`.
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Documentation
url: https://docs.astral.sh/ruff
about: Please consult the documentation before creating an issue.
- name: Community
url: https://discord.com/invite/astral-sh
about: Join our Discord community to ask questions and collaborate.

View File

@@ -1,22 +0,0 @@
name: New issue
description: A generic issue
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to report an issue! We're glad to have you involved with Ruff.
If you're filing a bug report, please consider including the following information:
* List of keywords you searched for before creating this issue. Write them down here so that others can find this issue more easily and help provide feedback.
e.g. "RUF001", "unused variable", "Jupyter notebook"
* A minimal code snippet that reproduces the bug.
* The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
* The current Ruff settings (any relevant sections from your `pyproject.toml`).
* The current Ruff version (`ruff --version`).
- type: textarea
attributes:
label: Description
description: A description of the issue

View File

@@ -13,16 +13,7 @@
### Rule changes
- \[`flake8-comprehensions`\]: Handle trailing comma in `C403` fix ([#16110](https://github.com/astral-sh/ruff/pull/16110))
- \[`flake8-debugger`\] Also flag `sys.breakpointhook` and `sys.__breakpointhook__` (`T100`) ([#16191](https://github.com/astral-sh/ruff/pull/16191))
- \[`pydocstyle`\] Handle arguments with the same names as sections (`D417`) ([#16011](https://github.com/astral-sh/ruff/pull/16011))
- \[`pylint`\] Correct ordering of arguments in fix for `if-stmt-min-max` (`PLR1730`) ([#16080](https://github.com/astral-sh/ruff/pull/16080))
- \[`pylint`\] Do not offer fix for raw strings (`PLE251`) ([#16132](https://github.com/astral-sh/ruff/pull/16132))
- \[`pyupgrade`\] Do not upgrade functional `TypedDicts` with private field names to the class-based syntax (`UP013`) ([#16219](https://github.com/astral-sh/ruff/pull/16219))
- \[`pyupgrade`\] Handle micro version numbers correctly (`UP036`) ([#16091](https://github.com/astral-sh/ruff/pull/16091))
- \[`pyupgrade`\] Unwrap unary expressions correctly (`UP018`) ([#15919](https://github.com/astral-sh/ruff/pull/15919))
- \[`ruff`\] Skip `RUF001` diagnostics when visiting string type definitions ([#16122](https://github.com/astral-sh/ruff/pull/16122))
- \[`flake8-pyi`\] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#16141](https://github.com/astral-sh/ruff/pull/16141))
- \[`pycodestyle`\] Exempt `site.addsitedir(...)` calls (`E402`) ([#16251](https://github.com/astral-sh/ruff/pull/16251))
### Formatter
@@ -43,7 +34,16 @@
### Bug fixes
- \[`flake8-comprehensions`\] Handle trailing comma in `C403` fix ([#16110](https://github.com/astral-sh/ruff/pull/16110))
- \[`flake8-pyi`\] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#16141](https://github.com/astral-sh/ruff/pull/16141))
- \[`pydocstyle`\] Handle arguments with the same names as sections (`D417`) ([#16011](https://github.com/astral-sh/ruff/pull/16011))
- \[`pylint`\] Correct ordering of arguments in fix for `if-stmt-min-max` (`PLR1730`) ([#16080](https://github.com/astral-sh/ruff/pull/16080))
- \[`pylint`\] Do not offer fix for raw strings (`PLE251`) ([#16132](https://github.com/astral-sh/ruff/pull/16132))
- \[`pyupgrade`\] Do not upgrade functional `TypedDicts` with private field names to the class-based syntax (`UP013`) ([#16219](https://github.com/astral-sh/ruff/pull/16219))
- \[`pyupgrade`\] Handle micro version numbers correctly (`UP036`) ([#16091](https://github.com/astral-sh/ruff/pull/16091))
- \[`pyupgrade`\] Unwrap unary expressions correctly (`UP018`) ([#15919](https://github.com/astral-sh/ruff/pull/15919))
- \[`refurb`\] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#16237](https://github.com/astral-sh/ruff/pull/16237))
- \[`ruff`\] Skip `RUF001` diagnostics when visiting string type definitions ([#16122](https://github.com/astral-sh/ruff/pull/16122))
### Documentation

52
Cargo.lock generated
View File

@@ -124,9 +124,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.95"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
[[package]]
name = "arc-swap"
@@ -345,9 +345,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.29"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
dependencies = [
"clap_builder",
"clap_derive",
@@ -355,9 +355,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.29"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
dependencies = [
"anstream",
"anstyle",
@@ -1582,9 +1582,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.170"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
[[package]]
name = "libcst"
@@ -1674,9 +1674,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.25"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "loom"
@@ -2409,6 +2409,7 @@ name = "red_knot"
version = "0.0.0"
dependencies = [
"anyhow",
"argfile",
"chrono",
"clap",
"colored 3.0.0",
@@ -2433,6 +2434,7 @@ dependencies = [
"tracing-flame",
"tracing-subscriber",
"tracing-tree",
"wild",
]
[[package]]
@@ -2499,6 +2501,8 @@ dependencies = [
"serde",
"smallvec",
"static_assertions",
"strum",
"strum_macros",
"tempfile",
"test-case",
"thiserror 2.0.11",
@@ -3186,6 +3190,7 @@ dependencies = [
"serde_json",
"shellexpand",
"thiserror 2.0.11",
"toml",
"tracing",
"tracing-subscriber",
]
@@ -3323,7 +3328,7 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
source = "git+https://github.com/salsa-rs/salsa.git?rev=687251fb50b4893dc373a7e2609ceaefb8accbe7#687251fb50b4893dc373a7e2609ceaefb8accbe7"
dependencies = [
"arc-swap",
"boxcar",
@@ -3345,12 +3350,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
source = "git+https://github.com/salsa-rs/salsa.git?rev=687251fb50b4893dc373a7e2609ceaefb8accbe7#687251fb50b4893dc373a7e2609ceaefb8accbe7"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
source = "git+https://github.com/salsa-rs/salsa.git?rev=687251fb50b4893dc373a7e2609ceaefb8accbe7#687251fb50b4893dc373a7e2609ceaefb8accbe7"
dependencies = [
"heck",
"proc-macro2",
@@ -3412,9 +3417,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [
"serde_derive",
]
@@ -3432,9 +3437,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [
"proc-macro2",
"quote",
@@ -3454,9 +3459,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.138"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [
"itoa",
"memchr",
@@ -3682,9 +3687,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.17.0"
version = "3.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a40f762a77d2afa88c2d919489e390a12bdd261ed568e60cfa7e48d4e20f0d33"
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
dependencies = [
"cfg-if",
"fastrand",
@@ -3985,6 +3990,7 @@ version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"chrono",
"matchers",
"nu-ansi-term 0.46.0",
"once_cell",
@@ -4082,9 +4088,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.16"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "unicode-normalization"

View File

@@ -123,7 +123,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 = "c8826fa4d1d9e3cba4c6e578763878b71fa9a10d" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "687251fb50b4893dc373a7e2609ceaefb8accbe7" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -19,6 +19,7 @@ ruff_db = { workspace = true, features = ["os", "cache"] }
ruff_python_ast = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }
colored = { workspace = true }
@@ -31,6 +32,7 @@ tracing = { workspace = true, features = ["release_max_level_debug"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
tracing-flame = { workspace = true }
tracing-tree = { workspace = true }
wild = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing"] }

View File

@@ -41,12 +41,14 @@ pub(crate) struct CheckCommand {
#[arg(long, value_name = "PROJECT")]
pub(crate) project: Option<SystemPathBuf>,
/// Path to the virtual environment the project uses.
/// Path to the Python installation from which Red Knot resolves type information and third-party dependencies.
///
/// If provided, red-knot will use the `site-packages` directory of this virtual environment
/// to resolve type information for the project's third-party dependencies.
/// Red Knot will search in the path's `site-packages` directories for type information and
/// third-party imports.
///
/// This option is commonly used to specify the path to a virtual environment.
#[arg(long, value_name = "PATH")]
pub(crate) venv_path: Option<SystemPathBuf>,
pub(crate) python: Option<SystemPathBuf>,
/// Custom directory to use for stdlib typeshed stubs.
#[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")]
@@ -97,7 +99,7 @@ impl CheckCommand {
python_version: self
.python_version
.map(|version| RangedValue::cli(version.into())),
venv_path: self.venv_path.map(RelativePathBuf::cli),
python: self.python.map(RelativePathBuf::cli),
typeshed: self.typeshed.map(RelativePathBuf::cli),
extra_paths: self.extra_search_path.map(|extra_search_paths| {
extra_search_paths

View File

@@ -1,4 +1,4 @@
use std::io::{self, BufWriter, Write};
use std::io::{self, stdout, BufWriter, Write};
use std::process::{ExitCode, Termination};
use anyhow::Result;
@@ -16,7 +16,7 @@ use red_knot_project::{watch, Db};
use red_knot_project::{ProjectDatabase, ProjectMetadata};
use red_knot_server::run_server;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
mod args;
@@ -39,6 +39,15 @@ pub fn main() -> ExitStatus {
// the configuration it is help to chain errors ("resolving configuration failed" ->
// "failed to read file: subdir/pyproject.toml")
for cause in error.chain() {
// Exit "gracefully" on broken pipe errors.
//
// See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14
if let Some(ioerr) = cause.downcast_ref::<io::Error>() {
if ioerr.kind() == io::ErrorKind::BrokenPipe {
return ExitStatus::Success;
}
}
writeln!(stderr, " {} {cause}", "Cause:".bold()).ok();
}
@@ -47,7 +56,10 @@ pub fn main() -> ExitStatus {
}
fn run() -> anyhow::Result<ExitStatus> {
let args = Args::parse_from(std::env::args());
let args = wild::args_os();
let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX)
.context("Failed to read CLI arguments from file")?;
let args = Args::parse_from(args);
match args.command {
Command::Server => run_server().map(|()| ExitStatus::Success),
@@ -69,7 +81,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let _guard = setup_tracing(verbosity)?;
// The base path to which all CLI arguments are relative to.
let cli_base_path = {
let cwd = {
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
SystemPathBuf::from_path_buf(cwd)
.map_err(|path| {
@@ -80,25 +92,27 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
})?
};
let cwd = args
let project_path = args
.project
.as_ref()
.map(|cwd| {
if cwd.as_std_path().is_dir() {
Ok(SystemPath::absolute(cwd, &cli_base_path))
.map(|project| {
if project.as_std_path().is_dir() {
Ok(SystemPath::absolute(project, &cwd))
} else {
Err(anyhow!("Provided project path `{cwd}` is not a directory"))
Err(anyhow!(
"Provided project path `{project}` is not a directory"
))
}
})
.transpose()?
.unwrap_or_else(|| cli_base_path.clone());
.unwrap_or_else(|| cwd.clone());
let system = OsSystem::new(cwd);
let watch = args.watch;
let exit_zero = args.exit_zero;
let cli_options = args.into_options();
let mut project_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?;
project_metadata.apply_cli_options(cli_options.clone());
project_metadata.apply_configuration_files(&system)?;
@@ -119,7 +133,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let exit_status = if watch {
main_loop.watch(&mut db)?
} else {
main_loop.run(&mut db)
main_loop.run(&mut db)?
};
tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all());
@@ -179,7 +193,7 @@ impl MainLoop {
)
}
fn watch(mut self, db: &mut ProjectDatabase) -> anyhow::Result<ExitStatus> {
fn watch(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
tracing::debug!("Starting watch mode");
let sender = self.sender.clone();
let watcher = watch::directory_watcher(move |event| {
@@ -188,12 +202,12 @@ impl MainLoop {
self.watcher = Some(ProjectWatcher::new(watcher, db));
self.run(db);
self.run(db)?;
Ok(ExitStatus::Success)
}
fn run(mut self, db: &mut ProjectDatabase) -> ExitStatus {
fn run(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
let result = self.main_loop(db);
@@ -203,7 +217,7 @@ impl MainLoop {
result
}
fn main_loop(&mut self, db: &mut ProjectDatabase) -> ExitStatus {
fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
// Schedule the first check.
tracing::debug!("Starting main loop");
@@ -246,9 +260,9 @@ impl MainLoop {
.any(|diagnostic| diagnostic.severity() >= min_error_severity);
if check_revision == revision {
#[allow(clippy::print_stdout)]
let mut stdout = stdout().lock();
for diagnostic in result {
println!("{}", diagnostic.display(db, &display_config));
writeln!(stdout, "{}", diagnostic.display(db, &display_config))?;
}
} else {
tracing::debug!(
@@ -257,11 +271,11 @@ impl MainLoop {
}
if self.watcher.is_none() {
return if failed {
return Ok(if failed {
ExitStatus::Failure
} else {
ExitStatus::Success
};
});
}
tracing::trace!("Counts after last check:\n{}", countme::get_all());
@@ -281,14 +295,14 @@ impl MainLoop {
// TODO: Don't use Salsa internal APIs
// [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries)
let _ = db.zalsa_mut();
return ExitStatus::Success;
return Ok(ExitStatus::Success);
}
}
tracing::debug!("Waiting for next main loop message.");
}
ExitStatus::Success
Ok(ExitStatus::Success)
}
}

View File

@@ -1298,6 +1298,31 @@ mod unix {
Ok(())
}
}
#[cfg(any(windows, unix))]
mod symlink {
use crate::{event_for_file, setup, setup_with_options, update_file, SetupContext};
use anyhow::Context;
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
use red_knot_python_semantic::{resolve_module, ModuleName};
use ruff_db::source::source_text;
use ruff_db::system::SystemPath;
use ruff_db::Upcast;
use ruff_python_ast::PythonVersion;
#[cfg(unix)]
fn symlink_file(original: &SystemPath, target: &SystemPath) -> anyhow::Result<()> {
std::os::unix::fs::symlink(original.as_std_path(), target.as_std_path())
.context("Failed to create symlink")
}
#[cfg(windows)]
fn symlink_file(original: &SystemPath, target: &SystemPath) -> anyhow::Result<()> {
std::os::windows::fs::symlink_file(original.as_std_path(), target.as_std_path())
.context("Failed to create symlink")
}
/// Project contains a symlink to another directory inside the project.
/// Changes to files in the symlinked directory should be reflected
@@ -1327,7 +1352,7 @@ mod unix {
// Create a symlink inside site-packages
let bar_in_project = context.join_project_path("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_project.as_std_path())
symlink_file(&link_target, &bar_in_project)
.context("Failed to create symlink to bar package")?;
Ok(())
@@ -1422,11 +1447,8 @@ mod unix {
context.join_project_path(".venv/lib/python3.12/site-packages");
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
.context("Failed to create .venv directory")?;
std::os::unix::fs::symlink(
site_packages.as_std_path(),
venv_site_packages.as_std_path(),
)
.context("Failed to create symlink to site-packages")?;
symlink_file(&site_packages, &venv_site_packages)
.context("Failed to create symlink to site-packages")?;
Ok(())
},

View File

@@ -1,6 +1,6 @@
use std::{collections::HashMap, hash::BuildHasher};
use red_knot_python_semantic::{PythonPlatform, SitePackages};
use red_knot_python_semantic::{PythonPath, PythonPlatform};
use ruff_db::system::SystemPathBuf;
use ruff_python_ast::PythonVersion;
@@ -128,7 +128,7 @@ macro_rules! impl_noop_combine {
impl_noop_combine!(SystemPathBuf);
impl_noop_combine!(PythonPlatform);
impl_noop_combine!(SitePackages);
impl_noop_combine!(PythonPath);
impl_noop_combine!(PythonVersion);
// std types

View File

@@ -1,7 +1,7 @@
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
use crate::Db;
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
use red_knot_python_semantic::{ProgramSettings, PythonPlatform, SearchPathSettings, SitePackages};
use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::system_path_to_file;
use ruff_db::system::{System, SystemPath};
@@ -90,7 +90,7 @@ impl Options {
.map(|env| {
(
env.extra_paths.clone(),
env.venv_path.clone(),
env.python.clone(),
env.typeshed.clone(),
)
})
@@ -104,11 +104,11 @@ impl Options {
.collect(),
src_roots,
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
site_packages: python
.map(|venv_path| SitePackages::Derived {
venv_path: venv_path.absolute(project_root, system),
python_path: python
.map(|python_path| {
PythonPath::SysPrefix(python_path.absolute(project_root, system))
})
.unwrap_or(SitePackages::Known(vec![])),
.unwrap_or(PythonPath::KnownSitePackages(vec![])),
}
}
@@ -236,10 +236,14 @@ pub struct EnvironmentOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub typeshed: Option<RelativePathBuf>,
// TODO: Rename to python, see https://github.com/astral-sh/ruff/issues/15530
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
/// Path to the Python installation from which Red Knot resolves type information and third-party dependencies.
///
/// Red Knot will search in the path's `site-packages` directories for type information and
/// third-party imports.
///
/// This option is commonly used to specify the path to a virtual environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub venv_path: Option<RelativePathBuf>,
pub python: Option<RelativePathBuf>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]

View File

@@ -42,6 +42,8 @@ smallvec = { workspace = true }
static_assertions = { workspace = true }
test-case = { workspace = true }
memchr = { workspace = true }
strum = { workspace = true}
strum_macros = { workspace = true}
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }

View File

@@ -73,12 +73,12 @@ qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(decorated method)
reveal_type(foo.join(qux)) # revealed: @Todo(overloaded method)
template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
# TODO: Infer `LiteralString`
reveal_type(template.format(foo, bar)) # revealed: @Todo(decorated method)
reveal_type(template.format(foo, bar)) # revealed: @Todo(overloaded method)
```
### Assignability

View File

@@ -116,8 +116,8 @@ MyType = int
class Aliases:
MyType = str
forward: "MyType"
not_forward: MyType
forward: "MyType" = "value"
not_forward: MyType = "value"
reveal_type(Aliases.forward) # revealed: str
reveal_type(Aliases.not_forward) # revealed: str

View File

@@ -54,13 +54,12 @@ c_instance.declared_and_bound = False
# error: [invalid-assignment] "Object of type `Literal["incompatible"]` is not assignable to attribute `declared_and_bound` of type `bool`"
c_instance.declared_and_bound = "incompatible"
# TODO: we already show an error here but the message might be improved?
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
# error: [unresolved-attribute] "Type `Literal[C]` has no attribute `inferred_from_value`"
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `Literal[C]` itself."
reveal_type(C.inferred_from_value) # revealed: Unknown
# TODO: this should be an error (pure instance variables cannot be accessed on the class)
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `Literal[C]`"
C.inferred_from_value = "overwritten on class"
# This assignment is fine:
@@ -90,13 +89,13 @@ c_instance = C()
reveal_type(c_instance.declared_and_bound) # revealed: str | None
# TODO: we currently plan to emit a diagnostic here. Note that both mypy
# and pyright show no error in this case! So we may reconsider this in
# the future, if it turns out to produce too many false positives.
reveal_type(C.declared_and_bound) # revealed: str | None
# Note that both mypy and pyright show no error in this case! So we may reconsider this in
# the future, if it turns out to produce too many false positives. We currently emit:
# error: [unresolved-attribute] "Attribute `declared_and_bound` can only be accessed on instances, not on the class object `Literal[C]` itself."
reveal_type(C.declared_and_bound) # revealed: Unknown
# TODO: same as above. We plan to emit a diagnostic here, even if both mypy
# and pyright allow this.
# Same as above. Mypy and pyright do not show an error here.
# error: [invalid-attribute-access] "Cannot assign to instance attribute `declared_and_bound` from the class object `Literal[C]`"
C.declared_and_bound = "overwritten on class"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `declared_and_bound` of type `str | None`"
@@ -116,11 +115,11 @@ c_instance = C()
reveal_type(c_instance.only_declared) # revealed: str
# TODO: mypy and pyright do not show an error here, but we plan to emit a diagnostic.
# The type could be changed to 'Unknown' if we decide to emit an error?
reveal_type(C.only_declared) # revealed: str
# Mypy and pyright do not show an error here. We treat this as a pure instance variable.
# error: [unresolved-attribute] "Attribute `only_declared` can only be accessed on instances, not on the class object `Literal[C]` itself."
reveal_type(C.only_declared) # revealed: Unknown
# TODO: mypy and pyright do not show an error here, but we plan to emit one.
# error: [invalid-attribute-access] "Cannot assign to instance attribute `only_declared` from the class object `Literal[C]`"
C.only_declared = "overwritten on class"
```
@@ -191,11 +190,10 @@ reveal_type(c_instance.declared_only) # revealed: bytes
reveal_type(c_instance.declared_and_bound) # revealed: bool
# TODO: We already show an error here, but the message might be improved?
# error: [unresolved-attribute]
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `Literal[C]` itself."
reveal_type(C.inferred_from_value) # revealed: Unknown
# TODO: this should be an error
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `Literal[C]`"
C.inferred_from_value = "overwritten on class"
```
@@ -598,6 +596,9 @@ C.class_method()
# error: [unresolved-attribute]
reveal_type(C.pure_class_variable) # revealed: Unknown
# TODO: should be no error when descriptor protocol is supported
# and the assignment is properly attributed to the class method.
# error: [invalid-attribute-access] "Cannot assign to instance attribute `pure_class_variable` from the class object `Literal[C]`"
C.pure_class_variable = "overwritten on class"
# TODO: should be `Unknown | Literal["value set in class method"]` or
@@ -782,6 +783,9 @@ def _(flag1: bool, flag2: bool):
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
```
### Possibly-unbound within a class
@@ -805,6 +809,28 @@ def _(flag: bool, flag1: bool, flag2: bool):
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
# Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually,
# see the "Possibly unbound/undeclared instance attribute" section below.
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
```
### Possibly-unbound within gradual types
```py
from typing import Any
def _(flag: bool):
class Base:
x: Any
class Derived(Base):
if flag:
# Redeclaring `x` with a more static type is okay in terms of LSP.
x: int
reveal_type(Derived().x) # revealed: int | Any
```
### Attribute possibly unbound on a subclass but not on a superclass
@@ -819,6 +845,8 @@ def _(flag: bool):
x = 2
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
```
### Attribute possibly unbound on a subclass and on a superclass
@@ -835,6 +863,41 @@ def _(flag: bool):
# error: [possibly-unbound-attribute]
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
# error: [possibly-unbound-attribute]
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
```
### Possibly unbound/undeclared instance attribute
#### Possibly unbound and undeclared
```py
def _(flag: bool):
class Foo:
if flag:
x: int
def __init(self):
if flag:
self.x = 1
# error: [possibly-unbound-attribute]
reveal_type(Foo().x) # revealed: int
```
#### Possibly unbound
```py
def _(flag: bool):
class Foo:
def __init(self):
if flag:
self.x = 1
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
# if we ever will (or want to)
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
```
### Attribute access on `Any`
@@ -884,13 +947,18 @@ def _(flag: bool):
## Objects of all types have a `__class__` method
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
`type(x)`.
```py
import typing_extensions
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
reveal_type(type(typing_extensions)) # revealed: Literal[ModuleType]
a = 42
reveal_type(a.__class__) # revealed: Literal[int]
reveal_type(type(a)) # revealed: Literal[int]
b = "42"
reveal_type(b.__class__) # revealed: Literal[str]
@@ -906,8 +974,13 @@ reveal_type(e.__class__) # revealed: Literal[tuple]
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
reveal_type(a.__class__) # revealed: type[int]
reveal_type(type(a)) # revealed: type[int]
reveal_type(b.__class__) # revealed: Literal[str]
reveal_type(type(b)) # revealed: Literal[str]
reveal_type(c.__class__) # revealed: type[int] | type[str]
reveal_type(type(c)) # revealed: type[int] | type[str]
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
# It would be incorrect to infer `Literal[type]` here,
@@ -1032,8 +1105,8 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
bools are instances of that class:
```py
reveal_type(True.__and__) # revealed: @Todo(decorated method)
reveal_type(False.__or__) # revealed: @Todo(decorated method)
reveal_type(True.__and__) # revealed: @Todo(overloaded method)
reveal_type(False.__or__) # revealed: @Todo(overloaded method)
```
Some attributes are special-cased, however:

View File

@@ -259,11 +259,17 @@ class A:
class B:
__add__ = A()
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
# TODO: Should not be an error: `A` instance is not a method descriptor, don't prepend `self` arg.
# Revealed type should be `Unknown | int`.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `B` and `B`"
reveal_type(B() + B()) # revealed: Unknown
reveal_type(B() + B()) # revealed: Unknown | int
```
Note that we union with `Unknown` here because `__add__` is not declared. We do infer just `int` if
the callable is declared:
```py
class B2:
__add__: A = A()
reveal_type(B2() + B2()) # revealed: int
```
## Integration test: numbers from typeshed
@@ -306,7 +312,7 @@ reveal_type(1 + A()) # revealed: A
reveal_type(A() + "foo") # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type("foo" + A()) # revealed: @Todo(return type)
reveal_type("foo" + A()) # revealed: @Todo(return type of decorated function)
reveal_type(A() + b"foo") # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
@@ -314,7 +320,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(A() + ()) # revealed: A
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
reveal_type(() + A()) # revealed: @Todo(return type)
reveal_type(() + A()) # revealed: @Todo(return type of decorated function)
literal_string_instance = "foo" * 1_000_000_000
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
@@ -323,7 +329,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString
reveal_type(A() + literal_string_instance) # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type)
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of decorated function)
```
## Operations involving instances of classes inheriting from `Any`
@@ -351,6 +357,20 @@ class Y(Foo): ...
reveal_type(X() + Y()) # revealed: int
```
## Operations involving types with invalid `__bool__` methods
<!-- snapshot-diagnostics -->
```py
class NotBoolable:
__bool__ = 3
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Unsupported
### Dunder as instance attribute

View File

@@ -51,9 +51,9 @@ 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)
reveal_type(x**2) # revealed: @Todo(return type of decorated function)
reveal_type(2**x) # revealed: @Todo(return type of decorated function)
reveal_type(x**x) # revealed: @Todo(return type of decorated function)
```
## Division by Zero

View File

@@ -0,0 +1,37 @@
# Calling builtins
## `bool` with incorrect arguments
```py
class NotBool:
__bool__ = None
# TODO: We should emit an `invalid-argument` error here for `2` because `bool` only takes one argument.
bool(1, 2)
# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly.
bool(NotBool())
```
## Calls to `type()`
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
tested more extensively in `crates/red_knot_python_semantic/resources/mdtest/attributes.md`,
alongside the tests for the `__class__` attribute.)
```py
reveal_type(type(1)) # revealed: Literal[int]
```
But a three-argument call to type creates a dynamic instance of the `type` class:
```py
reveal_type(type("Foo", (), {})) # revealed: type
```
Other numbers of arguments are invalid (TODO -- these should emit a diagnostic)
```py
type("Foo", ())
type("Foo", (), {}, weird_other_arg=42)
```

View File

@@ -82,7 +82,7 @@ class C:
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`"
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`"
reveal_type(c("foo")) # revealed: int
```
@@ -96,7 +96,7 @@ class C:
c = C()
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of bound method `__call__`; expected type `int`"
reveal_type(c()) # revealed: int
```

View File

@@ -0,0 +1,128 @@
# Dunder calls
## Introduction
This test suite explains and documents how dunder methods are looked up and called. Throughout the
document, we use `__getitem__` as an example, but the same principles apply to other dunder methods.
Dunder methods are implicitly called when using certain syntax. For example, the index operator
`obj[key]` calls the `__getitem__` method under the hood. Exactly *how* a dunder method is looked up
and called works slightly different from regular methods. Dunder methods are not looked up on `obj`
directly, but rather on `type(obj)`. But in many ways, they still *act* as if they were called on
`obj` directly. If the `__getitem__` member of `type(obj)` is a descriptor, it is called with `obj`
as the `instance` argument to `__get__`. A desugared version of `obj[key]` is roughly equivalent to
`getitem_desugared(obj, key)` as defined below:
```py
from typing import Any
def find_name_in_mro(typ: type, name: str) -> Any:
# See implementation in https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance
pass
def getitem_desugared(obj: object, key: object) -> object:
getitem_callable = find_name_in_mro(type(obj), "__getitem__")
if hasattr(getitem_callable, "__get__"):
getitem_callable = getitem_callable.__get__(obj, type(obj))
return getitem_callable(key)
```
In the following tests, we demonstrate that we implement this behavior correctly.
## Operating on class objects
If we invoke a dunder method on a class, it is looked up on the *meta* class, since any class is an
instance of its metaclass:
```py
class Meta(type):
def __getitem__(cls, key: int) -> str:
return str(key)
class DunderOnMetaClass(metaclass=Meta):
pass
reveal_type(DunderOnMetaClass[0]) # revealed: str
```
## Operating on instances
When invoking a dunder method on an instance of a class, it is looked up on the class:
```py
class ClassWithNormalDunder:
def __getitem__(self, key: int) -> str:
return str(key)
class_with_normal_dunder = ClassWithNormalDunder()
reveal_type(class_with_normal_dunder[0]) # revealed: str
```
Which can be demonstrated by trying to attach a dunder method to an instance, which will not work:
```py
def external_getitem(instance, key: int) -> str:
return str(key)
class ThisFails:
def __init__(self):
self.__getitem__ = external_getitem
this_fails = ThisFails()
# error: [non-subscriptable] "Cannot subscript object of type `ThisFails` with no `__getitem__` method"
reveal_type(this_fails[0]) # revealed: Unknown
```
However, the attached dunder method *can* be called if accessed directly:
```py
# TODO: `this_fails.__getitem__` is incorrectly treated as a bound method. This
# should be fixed with https://github.com/astral-sh/ruff/issues/16367
# error: [too-many-positional-arguments]
# error: [invalid-argument-type]
reveal_type(this_fails.__getitem__(this_fails, 0)) # revealed: Unknown | str
```
## When the dunder is not a method
A dunder can also be a non-method callable:
```py
class SomeCallable:
def __call__(self, key: int) -> str:
return str(key)
class ClassWithNonMethodDunder:
__getitem__: SomeCallable = SomeCallable()
class_with_callable_dunder = ClassWithNonMethodDunder()
reveal_type(class_with_callable_dunder[0]) # revealed: str
```
## Dunders are looked up using the descriptor protocol
Here, we demonstrate that the descriptor protocol is invoked when looking up a dunder method. Note
that the `instance` argument is on object of type `ClassWithDescriptorDunder`:
```py
from __future__ import annotations
class SomeCallable:
def __call__(self, key: int) -> str:
return str(key)
class Descriptor:
def __get__(self, instance: ClassWithDescriptorDunder, owner: type[ClassWithDescriptorDunder]) -> SomeCallable:
return SomeCallable()
class ClassWithDescriptorDunder:
__getitem__: Descriptor = Descriptor()
class_with_descriptor_dunder = ClassWithDescriptorDunder()
reveal_type(class_with_descriptor_dunder[0]) # revealed: str
```

View File

@@ -44,7 +44,7 @@ def bar() -> str:
return "bar"
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
reveal_type(bar()) # revealed: @Todo(return type)
reveal_type(bar()) # revealed: @Todo(return type of decorated function)
```
## Invalid callable

View File

@@ -239,11 +239,11 @@ method_wrapper(None, C)
method_wrapper(None)
# Passing something that is not assignable to `type` as the `owner` argument is an
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`); expected type `type`"
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`"
method_wrapper(None, 1)
# Passing `None` as the `owner` argument when `instance` is `None` is an
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`); expected type `type`"
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`"
method_wrapper(None, None)
# Calling `__get__` without any arguments is an
@@ -251,8 +251,130 @@ method_wrapper(None, None)
method_wrapper()
# Calling `__get__` with too many positional arguments is an
# error: [too-many-positional-arguments] "Too many positional arguments: expected 2, got 3"
# error: [too-many-positional-arguments] "Too many positional arguments to method wrapper `__get__` of function `f`: expected 2, got 3"
method_wrapper(C(), C, "one too many")
```
## `@classmethod`
### Basic
When a `@classmethod` attribute is accessed, it returns a bound method object, even when accessed on
the class object itself:
```py
from __future__ import annotations
class C:
@classmethod
def f(cls: type[C], x: int) -> str:
return "a"
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(C().f) # revealed: <bound method `f` of `type[C]`>
```
The `cls` method argument is then implicitly passed as the first argument when calling the method:
```py
reveal_type(C.f(1)) # revealed: str
reveal_type(C().f(1)) # revealed: str
```
When the class method is called incorrectly, we detect it:
```py
C.f("incorrect") # error: [invalid-argument-type]
C.f() # error: [missing-argument]
C.f(1, 2) # error: [too-many-positional-arguments]
```
If the `cls` parameter is wrongly annotated, we emit an error at the call site:
```py
class D:
@classmethod
def f(cls: D):
# This function is wrongly annotated, it should be `type[D]` instead of `D`
pass
# error: [invalid-argument-type] "Object of type `Literal[D]` cannot be assigned to parameter 1 (`cls`) of bound method `f`; expected type `D`"
D.f()
```
When a class method is accessed on a derived class, it is bound to that derived class:
```py
class Derived(C):
pass
reveal_type(Derived.f) # revealed: <bound method `f` of `Literal[Derived]`>
reveal_type(Derived().f) # revealed: <bound method `f` of `type[Derived]`>
reveal_type(Derived.f(1)) # revealed: str
reveal_type(Derived().f(1)) # revealed: str
```
### Accessing the classmethod as a static member
Accessing a `@classmethod`-decorated function at runtime returns a `classmethod` object. We
currently don't model this explicitly:
```py
from inspect import getattr_static
class C:
@classmethod
def f(cls): ...
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
```
But we correctly model how the `classmethod` descriptor works:
```py
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: <bound method `f` of `type[C]`>
```
The `owner` argument takes precedence over the `instance` argument:
```py
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: <bound method `f` of `Literal[C]`>
```
### Classmethods mixed with other decorators
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
class method:
```py
from __future__ import annotations
def does_nothing[T](f: T) -> T:
return f
class C:
@classmethod
@does_nothing
def f1(cls: type[C], x: int) -> str:
return "a"
@does_nothing
@classmethod
def f2(cls: type[C], x: int) -> str:
return "a"
# TODO: We do not support decorators yet (only limited special cases). Eventually,
# these should all return `str`:
reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function)
reveal_type(C().f1(1)) # revealed: @Todo(decorated method)
reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function)
reveal_type(C().f2(1)) # revealed: @Todo(decorated method)
```
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods

View File

@@ -160,3 +160,45 @@ reveal_type(42 in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `str` and `A`, in comparing `Literal["hello"]` with `A`"
reveal_type("hello" in A()) # revealed: bool
```
## Return type that doesn't implement `__bool__` correctly
`in` and `not in` operations will fail at runtime if the object on the right-hand side of the
operation has a `__contains__` method that returns a type which is not convertible to `bool`. This
is because of the way these operations are handled by the Python interpreter at runtime. If we
assume that `y` is an object that has a `__contains__` method, the Python expression `x in y`
desugars to a `contains(y, x)` call, where `contains` looks something like this:
```ignore
def contains(y, x):
return bool(type(y).__contains__(y, x))
```
where the `bool()` conversion itself implicitly calls `__bool__` under the hood.
TODO: Ideally the message would explain to the user what's wrong. E.g,
```ignore
error: [operator] cannot use `in` operator on object of type `WithContains`
note: This is because the `in` operator implicitly calls `WithContains.__contains__`, but `WithContains.__contains__` is invalidly defined
note: `WithContains.__contains__` is invalidly defined because it returns an instance of `NotBoolable`, which cannot be evaluated in a boolean context
note: `NotBoolable` cannot be evaluated in a boolean context because its `__bool__` attribute is not callable
```
It may also be more appropriate to use `unsupported-operator` as the error code.
<!-- snapshot-diagnostics -->
```py
class NotBoolable:
__bool__ = 3
class WithContains:
def __contains__(self, item) -> NotBoolable:
return NotBoolable()
# error: [unsupported-bool-conversion]
10 in WithContains()
# error: [unsupported-bool-conversion]
10 not in WithContains()
```

View File

@@ -345,3 +345,47 @@ def f(x: bool, y: int):
reveal_type(4.2 < x) # revealed: bool
reveal_type(x < 4.2) # revealed: bool
```
## Chained comparisons with objects that don't implement `__bool__` correctly
<!-- snapshot-diagnostics -->
Python implicitly calls `bool` on the comparison result of preceding elements (but not for the last
element) of a chained comparison.
```py
class NotBoolable:
__bool__ = 3
class Comparable:
def __lt__(self, item) -> NotBoolable:
return NotBoolable()
def __gt__(self, item) -> NotBoolable:
return NotBoolable()
# error: [unsupported-bool-conversion]
10 < Comparable() < 20
# error: [unsupported-bool-conversion]
10 < Comparable() < Comparable()
Comparable() < Comparable() # fine
```
## Callables as comparison dunders
```py
from typing import Literal
class AlwaysTrue:
def __call__(self, other: object) -> Literal[True]:
return True
class A:
__eq__: AlwaysTrue = AlwaysTrue()
__lt__: AlwaysTrue = AlwaysTrue()
reveal_type(A() == A()) # revealed: Literal[True]
reveal_type(A() < A()) # revealed: Literal[True]
reveal_type(A() > A()) # revealed: Literal[True]
```

View File

@@ -334,3 +334,61 @@ reveal_type(a is not c) # revealed: Literal[True]
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
// TODO
## Chained comparisons with elements that incorrectly implement `__bool__`
<!-- snapshot-diagnostics -->
For an operation `A() < A()` to succeed at runtime, the `A.__lt__` method does not necessarily need
to return an object that is convertible to a `bool`. However, the return type _does_ need to be
convertible to a `bool` for the operation `A() < A() < A()` (a _chained_ comparison) to succeed.
This is because `A() < A() < A()` desugars to something like this, which involves several implicit
conversions to `bool`:
```ignore
def compute_chained_comparison():
a1 = A()
a2 = A()
first_comparison = a1 < a2
return first_comparison and (a2 < A())
```
```py
class NotBoolable:
__bool__ = 5
class Comparable:
def __lt__(self, other) -> NotBoolable:
return NotBoolable()
def __gt__(self, other) -> NotBoolable:
return NotBoolable()
a = (1, Comparable())
b = (1, Comparable())
# error: [unsupported-bool-conversion]
a < b < b
a < b # fine
```
## Equality with elements that incorrectly implement `__bool__`
<!-- snapshot-diagnostics -->
Python does not generally attempt to coerce the result of `==` and `!=` operations between two
arbitrary objects to a `bool`, but a comparison of tuples will fail if the result of comparing any
pair of elements at equivalent positions cannot be converted to a `bool`:
```py
class A:
def __eq__(self, other) -> NotBoolable:
return NotBoolable()
class NotBoolable:
__bool__ = None
# error: [unsupported-bool-conversion]
(A(),) == (A(),)
```

View File

@@ -35,3 +35,13 @@ def _(flag: bool):
x = 1 if flag else None
reveal_type(x) # revealed: Literal[1] | None
```
## Condition with object that implements `__bool__` incorrectly
```py
class NotBoolable:
__bool__ = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
3 if NotBoolable() else 4
```

View File

@@ -147,3 +147,17 @@ def _(flag: bool):
reveal_type(y) # revealed: Literal[0, 1]
```
## Condition with object that implements `__bool__` incorrectly
```py
class NotBoolable:
__bool__ = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
if NotBoolable():
...
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
elif NotBoolable():
...
```

View File

@@ -43,3 +43,21 @@ def _(target: int):
reveal_type(y) # revealed: Literal[2, 3, 4]
```
## Guard with object that implements `__bool__` incorrectly
```py
class NotBoolable:
__bool__ = 3
def _(target: int, flag: NotBoolable):
y = 1
match target:
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
case 1 if flag:
y = 2
case 2:
y = 3
reveal_type(y) # revealed: Literal[1, 2, 3]
```

View File

@@ -201,14 +201,11 @@ class C:
c1 = C.factory("test") # okay
# TODO: should be `C`
reveal_type(c1) # revealed: @Todo(return type)
reveal_type(c1) # revealed: C
# TODO: should be `str`
reveal_type(C.get_name()) # revealed: @Todo(return type)
reveal_type(C.get_name()) # revealed: str
# TODO: should be `str`
reveal_type(C("42").get_name()) # revealed: @Todo(decorated method)
reveal_type(C("42").get_name()) # revealed: str
```
## Descriptors only work when used as class variables
@@ -285,6 +282,31 @@ C.descriptor = "something else"
reveal_type(C.descriptor) # revealed: Unknown | int
```
## `__get__` is called with correct arguments
```py
from __future__ import annotations
class TailoredForClassObjectAccess:
def __get__(self, instance: None, owner: type[C]) -> int:
return 1
class TailoredForInstanceAccess:
def __get__(self, instance: C, owner: type[C] | None = None) -> str:
return "a"
class C:
class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess()
instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess()
reveal_type(C.class_object_access) # revealed: int
reveal_type(C().instance_access) # revealed: str
# TODO: These should emit a diagnostic
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
reveal_type(C.instance_access) # revealed: TailoredForInstanceAccess
```
## Descriptors with incorrect `__get__` signature
```py
@@ -403,15 +425,15 @@ wrapper_descriptor(f, None)
wrapper_descriptor(f, C())
# Calling it with something that is not a `FunctionType` as the first argument is an
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`); expected type `FunctionType`"
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`"
wrapper_descriptor(1, None, type(f))
# Calling it with something that is not a `type` as the `owner` argument is an
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`); expected type `type`"
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of wrapper descriptor `FunctionType.__get__`; expected type `type`"
wrapper_descriptor(f, None, f)
# Calling it with too many positional arguments is an
# error: [too-many-positional-arguments] "Too many positional arguments: expected 3, got 4"
# error: [too-many-positional-arguments] "Too many positional arguments to wrapper descriptor `FunctionType.__get__`: expected 3, got 4"
wrapper_descriptor(f, None, type(f), "one too many")
```

View File

@@ -182,3 +182,16 @@ class C:
c = C()
c("wrong") # error: [invalid-argument-type]
```
## Calls to methods
Tests that we also see a reference to a function if the callable is a bound method.
```py
class C:
def square(self, x: int) -> int:
return x * x
c = C()
c.square("hello") # error: [invalid-argument-type]
```

View File

@@ -0,0 +1,9 @@
## Condition with object that implements `__bool__` incorrectly
```py
class NotBoolable:
__bool__ = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
assert NotBoolable()
```

View File

@@ -101,3 +101,55 @@ reveal_type(bool([])) # revealed: bool
reveal_type(bool({})) # revealed: bool
reveal_type(bool(set())) # revealed: bool
```
## `__bool__` returning `NoReturn`
```py
from typing import NoReturn
class NotBoolable:
def __bool__(self) -> NoReturn:
raise NotImplementedError("This object can't be converted to a boolean")
# TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't
# because `Never` is assignable to `bool`. This probably requires dead code analysis to fix.
if NotBoolable():
...
```
## Not callable `__bool__`
```py
class NotBoolable:
__bool__ = None
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
if NotBoolable():
...
```
## Not-boolable union
```py
def test(cond: bool):
class NotBoolable:
__bool__ = None if cond else 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__`"
if NotBoolable():
...
```
## Union with some variants implementing `__bool__` incorrectly
```py
def test(cond: bool):
class NotBoolable:
__bool__ = None
a = 10 if cond else NotBoolable()
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable"
if a:
...
```

View File

@@ -105,7 +105,11 @@ reveal_type(x)
## With non-callable iterator
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class NotIterable:
if flag:
@@ -113,7 +117,8 @@ def _(flag: bool):
else:
__iter__: None = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
# error: [not-iterable]
for x in NotIterable():
pass
# revealed: Unknown
@@ -123,21 +128,25 @@ def _(flag: bool):
## Invalid iterable
<!-- snapshot-diagnostics -->
```py
nonsense = 123
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
for x in nonsense: # error: [not-iterable]
pass
```
## New over old style iteration protocol
<!-- snapshot-diagnostics -->
```py
class NotIterable:
def __getitem__(self, key: int) -> int:
return 42
__iter__: None = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
for x in NotIterable(): # error: [not-iterable]
pass
```
@@ -221,7 +230,11 @@ def _(flag: bool):
## Union type as iterable where one union element has no `__iter__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class TestIter:
def __next__(self) -> int:
return 42
@@ -231,14 +244,18 @@ class Test:
return TestIter()
def _(flag: bool):
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
# error: [not-iterable]
for x in Test() if flag else 42:
reveal_type(x) # revealed: int
```
## Union type as iterable where one union element has invalid `__iter__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class TestIter:
def __next__(self) -> int:
return 42
@@ -253,7 +270,7 @@ class Test2:
def _(flag: bool):
# TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
# error: "Object of type `Test | Test2` is not iterable"
# error: [not-iterable]
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int
```
@@ -269,7 +286,454 @@ class Test:
def __iter__(self) -> TestIter | int:
return TestIter()
# error: [not-iterable] "Object of type `Test` is not iterable"
# error: [not-iterable] "Object of type `Test` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method"
for x in Test():
reveal_type(x) # revealed: int
```
## Possibly-not-callable `__iter__` method
```py
def _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class CustomCallable:
if flag:
def __call__(self, *args, **kwargs) -> Iterator:
return Iterator()
else:
__call__: None = None
class Iterable1:
__iter__: CustomCallable = CustomCallable()
class Iterable2:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
__iter__: None = None
# error: [not-iterable] "Object of type `Iterable1` may not be iterable because its `__iter__` attribute (with type `CustomCallable`) may not be callable"
for x in Iterable1():
# TODO... `int` might be ideal here?
reveal_type(x) # revealed: int | Unknown
# error: [not-iterable] "Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `<bound method `__iter__` of `Iterable2`> | None`) may not be callable"
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown
```
## `__iter__` method with a bad signature
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self, extra_arg) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int
```
## `__iter__` does not return an iterator
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Bad:
def __iter__(self) -> int:
return 42
# error: [not-iterable]
for x in Bad():
reveal_type(x) # revealed: Unknown
```
## `__iter__` returns an object with a possibly unbound `__next__` method
```py
def _(flag: bool):
class Iterator:
if flag:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable] "Object of type `Iterable` may not be iterable because its `__iter__` method returns an object of type `Iterator`, which may not have a `__next__` method"
for x in Iterable():
reveal_type(x) # revealed: int
```
## `__iter__` returns an iterator with an invalid `__next__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator1:
def __next__(self, extra_arg) -> int:
return 42
class Iterator2:
__next__: None = None
class Iterable1:
def __iter__(self) -> Iterator1:
return Iterator1()
class Iterable2:
def __iter__(self) -> Iterator2:
return Iterator2()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: Unknown
```
## Possibly unbound `__iter__` and bad `__getitem__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
# invalid signature because it only accepts a `str`,
# but the old-style iteration protocol will pass it an `int`
def __getitem__(self, key: str) -> bytes:
return 42
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int | bytes
```
## Possibly unbound `__iter__` and not-callable `__getitem__`
This snippet tests that we infer the element type correctly in the following edge case:
- `__iter__` is a method with the correct parameter spec that returns a valid iterator; BUT
- `__iter__` is possibly unbound; AND
- `__getitem__` is set to a non-callable type
It's important that we emit a diagnostic here, but it's also important that we still use the return
type of the iterator's `__next__` method as the inferred type of `x` in the `for` loop:
```py
def _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
__getitem__: None = None
# error: [not-iterable] "Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable"
for x in Iterable():
reveal_type(x) # revealed: int
```
## Possibly unbound `__iter__` and possibly unbound `__getitem__`
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator:
def __next__(self) -> int:
return 42
def _(flag1: bool, flag2: bool):
class Iterable:
if flag1:
def __iter__(self) -> Iterator:
return Iterator()
if flag2:
def __getitem__(self, key: int) -> bytes:
return 42
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int | bytes
```
## No `__iter__` method and `__getitem__` is not callable
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Bad:
__getitem__: None = None
# error: [not-iterable]
for x in Bad():
reveal_type(x) # revealed: Unknown
```
## Possibly-not-callable `__getitem__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class CustomCallable:
if flag:
def __call__(self, *args, **kwargs) -> int:
return 42
else:
__call__: None = None
class Iterable1:
__getitem__: CustomCallable = CustomCallable()
class Iterable2:
if flag:
def __getitem__(self, key: int) -> int:
return 42
else:
__getitem__: None = None
# error: [not-iterable]
for x in Iterable1():
# TODO... `int` might be ideal here?
reveal_type(x) # revealed: int | Unknown
# error: [not-iterable]
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown
```
## Bad `__getitem__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterable:
# invalid because it will implicitly be passed an `int`
# by the interpreter
def __getitem__(self, key: str) -> int:
return 42
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int
```
## Possibly unbound `__iter__` but definitely bound `__getitem__`
Here, we should not emit a diagnostic: if `__iter__` is unbound, we should fallback to
`__getitem__`:
```py
class Iterator:
def __next__(self) -> str:
return "foo"
def _(flag: bool):
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
def __getitem__(self, key: int) -> bytes:
return b"foo"
for x in Iterable():
reveal_type(x) # revealed: str | bytes
```
## Possibly invalid `__iter__` methods
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator:
def __next__(self) -> int:
return 42
def _(flag: bool):
class Iterable1:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
def __iter__(self, invalid_extra_arg) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int
class Iterable2:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
__iter__: None = None
# error: [not-iterable]
for x in Iterable2():
# TODO: `int` would probably be better here:
reveal_type(x) # revealed: int | Unknown
```
## Possibly invalid `__next__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class Iterator1:
if flag:
def __next__(self) -> int:
return 42
else:
def __next__(self, invalid_extra_arg) -> str:
return "foo"
class Iterator2:
if flag:
def __next__(self) -> int:
return 42
else:
__next__: None = None
class Iterable1:
def __iter__(self) -> Iterator1:
return Iterator1()
class Iterable2:
def __iter__(self) -> Iterator2:
return Iterator2()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int | str
# error: [not-iterable]
for y in Iterable2():
# TODO: `int` would probably be better here:
reveal_type(y) # revealed: int | Unknown
```
## Possibly invalid `__getitem__` methods
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class Iterable1:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
__getitem__: None = None
class Iterable2:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
def __getitem__(self, item: str) -> int:
return "foo"
# error: [not-iterable]
for x in Iterable1():
# TODO: `str` might be better
reveal_type(x) # revealed: str | Unknown
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: str | int
```
## Possibly unbound `__iter__` and possibly invalid `__getitem__`
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator:
def __next__(self) -> bytes:
return b"foo"
def _(flag: bool, flag2: bool):
class Iterable1:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
__getitem__: None = None
if flag2:
def __iter__(self) -> Iterator:
return Iterator()
class Iterable2:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
def __getitem__(self, item: str) -> int:
return "foo"
if flag2:
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable1():
# TODO: `bytes | str` might be better
reveal_type(x) # revealed: bytes | str | Unknown
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: bytes | str | int
```

View File

@@ -116,3 +116,14 @@ def _(flag: bool, flag2: bool):
# error: [possibly-unresolved-reference]
y
```
## Condition with object that implements `__bool__` incorrectly
```py
class NotBoolable:
__bool__ = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
while NotBoolable():
...
```

View File

@@ -97,12 +97,7 @@ else:
## No narrowing for instances of `builtins.type`
```py
def _(flag: bool):
t = type("t", (), {})
# This isn't testing what we want it to test if we infer anything more precise here:
reveal_type(t) # revealed: type
def _(flag: bool, t: type):
x = 1 if flag else "foo"
if isinstance(x, t):

View File

@@ -112,8 +112,7 @@ def _(flag: bool):
reveal_type(t) # revealed: Literal[NoneType]
if issubclass(t, type(None)):
# TODO: this should be just `Literal[NoneType]`
reveal_type(t) # revealed: Literal[int, NoneType]
reveal_type(t) # revealed: Literal[NoneType]
```
## `classinfo` contains multiple types

View File

@@ -266,7 +266,7 @@ def _(
if af:
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
# TODO: Emit a diagnostic (`d` is not valid in boolean context)
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool"
if d:
# TODO: Should be `Unknown`
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy

View File

@@ -341,10 +341,12 @@ annotation are looked up lazily, even if they occur in an eager scope.
### Eager annotations in a Python file
```py
from typing import ClassVar
x = int
class C:
var: x
var: ClassVar[x]
reveal_type(C.var) # revealed: int
@@ -356,10 +358,12 @@ x = str
```py
from __future__ import annotations
from typing import ClassVar
x = int
class C:
var: x
var: ClassVar[x]
reveal_type(C.var) # revealed: Unknown | str
@@ -369,10 +373,12 @@ x = str
### Deferred annotations in a stub file
```pyi
from typing import ClassVar
x = int
class C:
var: x
var: ClassVar[x]
reveal_type(C.var) # revealed: Unknown | str

View File

@@ -136,3 +136,42 @@ if returns_bool():
reveal_type(__file__) # revealed: Literal[42]
reveal_type(__name__) # revealed: Literal[1] | str
```
## Implicit global attributes in the current module override implicit globals from builtins
Here, we take the type of the implicit global symbol `__name__` from the `types.ModuleType` stub
(which in this custom typeshed specifies the type as `bytes`). This is because the `main` module has
an implicit `__name__` global that shadows the builtin `__name__` symbol.
```toml
[environment]
typeshed = "/typeshed"
```
`/typeshed/stdlib/builtins.pyi`:
```pyi
class int: ...
class bytes: ...
__name__: int = 42
```
`/typeshed/stdlib/types.pyi`:
```pyi
class ModuleType:
__name__: bytes
```
`/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ...
```
`main.py`:
```py
reveal_type(__name__) # revealed: bytes
```

View File

@@ -167,7 +167,7 @@ class A:
__slots__ = ()
__slots__ += ("a", "b")
reveal_type(A.__slots__) # revealed: @Todo(return type)
reveal_type(A.__slots__) # revealed: @Todo(return type of decorated function)
class B:
__slots__ = ("c", "d")

View File

@@ -0,0 +1,52 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Bad `__getitem__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterable:
4 | # invalid because it will implicitly be passed an `int`
5 | # by the interpreter
6 | def __getitem__(self, key: str) -> int:
7 | return 42
8 |
9 | # error: [not-iterable]
10 | for x in Iterable():
11 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:10:10
|
9 | # error: [not-iterable]
10 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
11 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:11:5
|
9 | # error: [not-iterable]
10 | for x in Iterable():
11 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
|
```

View File

@@ -0,0 +1,32 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Invalid iterable
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | nonsense = 123
2 | for x in nonsense: # error: [not-iterable]
3 | pass
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:2:10
|
1 | nonsense = 123
2 | for x in nonsense: # error: [not-iterable]
| ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
3 | pass
|
```

View File

@@ -0,0 +1,37 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - New over old style iteration protocol
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | class NotIterable:
2 | def __getitem__(self, key: int) -> int:
3 | return 42
4 | __iter__: None = None
5 |
6 | for x in NotIterable(): # error: [not-iterable]
7 | pass
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:6:10
|
4 | __iter__: None = None
5 |
6 | for x in NotIterable(): # error: [not-iterable]
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
7 | pass
|
```

View File

@@ -0,0 +1,49 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - No `__iter__` method and `__getitem__` is not callable
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Bad:
4 | __getitem__: None = None
5 |
6 | # error: [not-iterable]
7 | for x in Bad():
8 | reveal_type(x) # revealed: Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:7:10
|
6 | # error: [not-iterable]
7 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
8 | reveal_type(x) # revealed: Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:8:5
|
6 | # error: [not-iterable]
7 | for x in Bad():
8 | reveal_type(x) # revealed: Unknown
| -------------- info: Revealed type is `Unknown`
|
```

View File

@@ -0,0 +1,98 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly-not-callable `__getitem__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class CustomCallable:
5 | if flag:
6 | def __call__(self, *args, **kwargs) -> int:
7 | return 42
8 | else:
9 | __call__: None = None
10 |
11 | class Iterable1:
12 | __getitem__: CustomCallable = CustomCallable()
13 |
14 | class Iterable2:
15 | if flag:
16 | def __getitem__(self, key: int) -> int:
17 | return 42
18 | else:
19 | __getitem__: None = None
20 |
21 | # error: [not-iterable]
22 | for x in Iterable1():
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
25 |
26 | # error: [not-iterable]
27 | for y in Iterable2():
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:22:14
|
21 | # error: [not-iterable]
22 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:24:9
|
22 | for x in Iterable1():
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
| -------------- info: Revealed type is `int | Unknown`
25 |
26 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:27:14
|
26 | # error: [not-iterable]
27 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable2`> | None`) may not be callable
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:29:9
|
27 | for y in Iterable2():
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
| -------------- info: Revealed type is `int | Unknown`
|
```

View File

@@ -0,0 +1,94 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly invalid `__getitem__` methods
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class Iterable1:
5 | if flag:
6 | def __getitem__(self, item: int) -> str:
7 | return "foo"
8 | else:
9 | __getitem__: None = None
10 |
11 | class Iterable2:
12 | if flag:
13 | def __getitem__(self, item: int) -> str:
14 | return "foo"
15 | else:
16 | def __getitem__(self, item: str) -> int:
17 | return "foo"
18 |
19 | # error: [not-iterable]
20 | for x in Iterable1():
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
23 |
24 | # error: [not-iterable]
25 | for y in Iterable2():
26 | reveal_type(y) # revealed: str | int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:20:14
|
19 | # error: [not-iterable]
20 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable1`> | None`) may not be callable
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:22:9
|
20 | for x in Iterable1():
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
| -------------- info: Revealed type is `str | Unknown`
23 |
24 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:25:14
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `<bound method `__getitem__` of `Iterable2`> | <bound method `__getitem__` of `Iterable2`>`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
26 | reveal_type(y) # revealed: str | int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:26:9
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
26 | reveal_type(y) # revealed: str | int
| -------------- info: Revealed type is `str | int`
|
```

View File

@@ -0,0 +1,98 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly invalid `__iter__` methods
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | def _(flag: bool):
8 | class Iterable1:
9 | if flag:
10 | def __iter__(self) -> Iterator:
11 | return Iterator()
12 | else:
13 | def __iter__(self, invalid_extra_arg) -> Iterator:
14 | return Iterator()
15 |
16 | # error: [not-iterable]
17 | for x in Iterable1():
18 | reveal_type(x) # revealed: int
19 |
20 | class Iterable2:
21 | if flag:
22 | def __iter__(self) -> Iterator:
23 | return Iterator()
24 | else:
25 | __iter__: None = None
26 |
27 | # error: [not-iterable]
28 | for x in Iterable2():
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `<bound method `__iter__` of `Iterable1`> | <bound method `__iter__` of `Iterable1`>`) may have an invalid signature (expected `def __iter__(self): ...`)
18 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:18:9
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
18 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
19 |
20 | class Iterable2:
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `<bound method `__iter__` of `Iterable2`> | None`) may not be callable
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:30:9
|
28 | for x in Iterable2():
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
| -------------- info: Revealed type is `int | Unknown`
|
```

View File

@@ -0,0 +1,102 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly invalid `__next__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class Iterator1:
5 | if flag:
6 | def __next__(self) -> int:
7 | return 42
8 | else:
9 | def __next__(self, invalid_extra_arg) -> str:
10 | return "foo"
11 |
12 | class Iterator2:
13 | if flag:
14 | def __next__(self) -> int:
15 | return 42
16 | else:
17 | __next__: None = None
18 |
19 | class Iterable1:
20 | def __iter__(self) -> Iterator1:
21 | return Iterator1()
22 |
23 | class Iterable2:
24 | def __iter__(self) -> Iterator2:
25 | return Iterator2()
26 |
27 | # error: [not-iterable]
28 | for x in Iterable1():
29 | reveal_type(x) # revealed: int | str
30 |
31 | # error: [not-iterable]
32 | for y in Iterable2():
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
29 | reveal_type(x) # revealed: int | str
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:29:9
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
29 | reveal_type(x) # revealed: int | str
| -------------- info: Revealed type is `int | str`
30 |
31 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:32:14
|
31 | # error: [not-iterable]
32 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:34:9
|
32 | for y in Iterable2():
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
| -------------- info: Revealed type is `int | Unknown`
|
```

View File

@@ -0,0 +1,60 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly unbound `__iter__` and bad `__getitem__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class Iterator:
5 | def __next__(self) -> int:
6 | return 42
7 |
8 | class Iterable:
9 | if flag:
10 | def __iter__(self) -> Iterator:
11 | return Iterator()
12 | # invalid signature because it only accepts a `str`,
13 | # but the old-style iteration protocol will pass it an `int`
14 | def __getitem__(self, key: str) -> bytes:
15 | return 42
16 |
17 | # error: [not-iterable]
18 | for x in Iterable():
19 | reveal_type(x) # revealed: int | bytes
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:18:14
|
17 | # error: [not-iterable]
18 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
19 | reveal_type(x) # revealed: int | bytes
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:19:9
|
17 | # error: [not-iterable]
18 | for x in Iterable():
19 | reveal_type(x) # revealed: int | bytes
| -------------- info: Revealed type is `int | bytes`
|
```

View File

@@ -0,0 +1,106 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly invalid `__getitem__`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator:
4 | def __next__(self) -> bytes:
5 | return b"foo"
6 |
7 | def _(flag: bool, flag2: bool):
8 | class Iterable1:
9 | if flag:
10 | def __getitem__(self, item: int) -> str:
11 | return "foo"
12 | else:
13 | __getitem__: None = None
14 |
15 | if flag2:
16 | def __iter__(self) -> Iterator:
17 | return Iterator()
18 |
19 | class Iterable2:
20 | if flag:
21 | def __getitem__(self, item: int) -> str:
22 | return "foo"
23 | else:
24 | def __getitem__(self, item: str) -> int:
25 | return "foo"
26 | if flag2:
27 | def __iter__(self) -> Iterator:
28 | return Iterator()
29 |
30 | # error: [not-iterable]
31 | for x in Iterable1():
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
34 |
35 | # error: [not-iterable]
36 | for y in Iterable2():
37 | reveal_type(y) # revealed: bytes | str | int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:31:14
|
30 | # error: [not-iterable]
31 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable1`> | None`) may not be callable
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:33:9
|
31 | for x in Iterable1():
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
| -------------- info: Revealed type is `bytes | str | Unknown`
34 |
35 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:36:14
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `<bound method `__getitem__` of `Iterable2`> | <bound method `__getitem__` of `Iterable2`>`)
may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
37 | reveal_type(y) # revealed: bytes | str | int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:37:9
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
37 | reveal_type(y) # revealed: bytes | str | int
| -------------- info: Revealed type is `bytes | str | int`
|
```

View File

@@ -0,0 +1,59 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly unbound `__getitem__`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | def _(flag1: bool, flag2: bool):
8 | class Iterable:
9 | if flag1:
10 | def __iter__(self) -> Iterator:
11 | return Iterator()
12 | if flag2:
13 | def __getitem__(self, key: int) -> bytes:
14 | return 42
15 |
16 | # error: [not-iterable]
17 | for x in Iterable():
18 | reveal_type(x) # revealed: int | bytes
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
18 | reveal_type(x) # revealed: int | bytes
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:18:9
|
16 | # error: [not-iterable]
17 | for x in Iterable():
18 | reveal_type(x) # revealed: int | bytes
| -------------- info: Revealed type is `int | bytes`
|
```

View File

@@ -0,0 +1,61 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Union type as iterable where one union element has invalid `__iter__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class TestIter:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | class Test:
8 | def __iter__(self) -> TestIter:
9 | return TestIter()
10 |
11 | class Test2:
12 | def __iter__(self) -> int:
13 | return 42
14 |
15 | def _(flag: bool):
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
19 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:18:14
|
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
19 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:19:9
|
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
19 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
|
```

View File

@@ -0,0 +1,56 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Union type as iterable where one union element has no `__iter__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class TestIter:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | class Test:
8 | def __iter__(self) -> TestIter:
9 | return TestIter()
10 |
11 | def _(flag: bool):
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
14 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:13:14
|
11 | def _(flag: bool):
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
| ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
14 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:14:9
|
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
14 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
|
```

View File

@@ -0,0 +1,69 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - With non-callable iterator
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class NotIterable:
5 | if flag:
6 | __iter__: int = 1
7 | else:
8 | __iter__: None = None
9 |
10 | # error: [not-iterable]
11 | for x in NotIterable():
12 | pass
13 |
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:11:14
|
10 | # error: [not-iterable]
11 | for x in NotIterable():
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
12 | pass
|
```
```
warning: lint:possibly-unresolved-reference
--> /src/mdtest_snippet.py:16:17
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
| - Name `x` used when possibly not defined
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:16:5
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
| -------------- info: Revealed type is `Unknown`
|
```

View File

@@ -0,0 +1,50 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - `__iter__` does not return an iterator
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Bad:
4 | def __iter__(self) -> int:
5 | return 42
6 |
7 | # error: [not-iterable]
8 | for x in Bad():
9 | reveal_type(x) # revealed: Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:8:10
|
7 | # error: [not-iterable]
8 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
9 | reveal_type(x) # revealed: Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:9:5
|
7 | # error: [not-iterable]
8 | for x in Bad():
9 | reveal_type(x) # revealed: Unknown
| -------------- info: Revealed type is `Unknown`
|
```

View File

@@ -0,0 +1,54 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - `__iter__` method with a bad signature
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | class Iterable:
8 | def __iter__(self, extra_arg) -> Iterator:
9 | return Iterator()
10 |
11 | # error: [not-iterable]
12 | for x in Iterable():
13 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:12:10
|
11 | # error: [not-iterable]
12 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
13 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:13:5
|
11 | # error: [not-iterable]
12 | for x in Iterable():
13 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
|
```

View File

@@ -0,0 +1,91 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - `__iter__` returns an iterator with an invalid `__next__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator1:
4 | def __next__(self, extra_arg) -> int:
5 | return 42
6 |
7 | class Iterator2:
8 | __next__: None = None
9 |
10 | class Iterable1:
11 | def __iter__(self) -> Iterator1:
12 | return Iterator1()
13 |
14 | class Iterable2:
15 | def __iter__(self) -> Iterator2:
16 | return Iterator2()
17 |
18 | # error: [not-iterable]
19 | for x in Iterable1():
20 | reveal_type(x) # revealed: int
21 |
22 | # error: [not-iterable]
23 | for y in Iterable2():
24 | reveal_type(y) # revealed: Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:19:10
|
18 | # error: [not-iterable]
19 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
20 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:20:5
|
18 | # error: [not-iterable]
19 | for x in Iterable1():
20 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
21 |
22 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:23:10
|
22 | # error: [not-iterable]
23 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
24 | reveal_type(y) # revealed: Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:24:5
|
22 | # error: [not-iterable]
23 | for y in Iterable2():
24 | reveal_type(y) # revealed: Unknown
| -------------- info: Revealed type is `Unknown`
|
```

View File

@@ -0,0 +1,35 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: instances.md - Binary operations on instances - Operations involving types with invalid `__bool__` methods
mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.md
---
# Python source files
## mdtest_snippet.py
```
1 | class NotBoolable:
2 | __bool__ = 3
3 |
4 | a = NotBoolable()
5 |
6 | # error: [unsupported-bool-conversion]
7 | 10 and a and True
```
# Diagnostics
```
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:7:8
|
6 | # error: [unsupported-bool-conversion]
7 | 10 and a and True
| ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
```

View File

@@ -0,0 +1,41 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Calls to methods
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C:
2 | def square(self, x: int) -> int:
3 | return x * x
4 |
5 | c = C()
6 | c.square("hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:6:10
|
5 | c = C()
6 | c.square("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`x`) of bound method `square`; expected type `int`
|
::: /src/mdtest_snippet.py:2:22
|
1 | class C:
2 | def square(self, x: int) -> int:
| ------ info: parameter declared in function definition here
3 | return x * x
|
```

View File

@@ -28,7 +28,7 @@ error: lint:invalid-argument-type
|
5 | c = C()
6 | c("wrong") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["wrong"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`
| ^^^^^^^ Object of type `Literal["wrong"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`
|
::: /src/mdtest_snippet.py:2:24
|

View File

@@ -0,0 +1,53 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: membership_test.md - Comparison: Membership Test - Return type that doesn't implement `__bool__` correctly
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
---
# Python source files
## mdtest_snippet.py
```
1 | class NotBoolable:
2 | __bool__ = 3
3 |
4 | class WithContains:
5 | def __contains__(self, item) -> NotBoolable:
6 | return NotBoolable()
7 |
8 | # error: [unsupported-bool-conversion]
9 | 10 in WithContains()
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
```
# Diagnostics
```
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | 10 in WithContains()
| ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
|
```
```
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:11:1
|
9 | 10 in WithContains()
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
| ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
```

View File

@@ -0,0 +1,33 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: not.md - Unary not - Object that implements `__bool__` incorrectly
mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
---
# Python source files
## mdtest_snippet.py
```
1 | class NotBoolable:
2 | __bool__ = 3
3 |
4 | # error: [unsupported-bool-conversion]
5 | not NotBoolable()
```
# Diagnostics
```
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:5:1
|
4 | # error: [unsupported-bool-conversion]
5 | not NotBoolable()
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
```

View File

@@ -0,0 +1,60 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: rich_comparison.md - Comparison: Rich Comparison - Chained comparisons with objects that don't implement `__bool__` correctly
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
---
# Python source files
## mdtest_snippet.py
```
1 | class NotBoolable:
2 | __bool__ = 3
3 |
4 | class Comparable:
5 | def __lt__(self, item) -> NotBoolable:
6 | return NotBoolable()
7 |
8 | def __gt__(self, item) -> NotBoolable:
9 | return NotBoolable()
10 |
11 | # error: [unsupported-bool-conversion]
12 | 10 < Comparable() < 20
13 | # error: [unsupported-bool-conversion]
14 | 10 < Comparable() < Comparable()
15 |
16 | Comparable() < Comparable() # fine
```
# Diagnostics
```
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:12:1
|
11 | # error: [unsupported-bool-conversion]
12 | 10 < Comparable() < 20
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
13 | # error: [unsupported-bool-conversion]
14 | 10 < Comparable() < Comparable()
|
```
```
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:14:1
|
12 | 10 < Comparable() < 20
13 | # error: [unsupported-bool-conversion]
14 | 10 < Comparable() < Comparable()
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
15 |
16 | Comparable() < Comparable() # fine
|
```

View File

@@ -0,0 +1,47 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: tuples.md - Comparison: Tuples - Chained comparisons with elements that incorrectly implement `__bool__`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
---
# Python source files
## mdtest_snippet.py
```
1 | class NotBoolable:
2 | __bool__ = 5
3 |
4 | class Comparable:
5 | def __lt__(self, other) -> NotBoolable:
6 | return NotBoolable()
7 |
8 | def __gt__(self, other) -> NotBoolable:
9 | return NotBoolable()
10 |
11 | a = (1, Comparable())
12 | b = (1, Comparable())
13 |
14 | # error: [unsupported-bool-conversion]
15 | a < b < b
16 |
17 | a < b # fine
```
# Diagnostics
```
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:15:1
|
14 | # error: [unsupported-bool-conversion]
15 | a < b < b
| ^^^^^ Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable
16 |
17 | a < b # fine
|
```

View File

@@ -0,0 +1,37 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: tuples.md - Comparison: Tuples - Equality with elements that incorrectly implement `__bool__`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
---
# Python source files
## mdtest_snippet.py
```
1 | class A:
2 | def __eq__(self, other) -> NotBoolable:
3 | return NotBoolable()
4 |
5 | class NotBoolable:
6 | __bool__ = None
7 |
8 | # error: [unsupported-bool-conversion]
9 | (A(),) == (A(),)
```
# Diagnostics
```
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | (A(),) == (A(),)
| ^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
```

View File

@@ -22,7 +22,7 @@ error: lint:not-iterable
--> /src/mdtest_snippet.py:1:8
|
1 | a, b = 1 # error: [not-iterable]
| ^ Object of type `Literal[1]` is not iterable
| ^ Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
|
```

View File

@@ -25,7 +25,7 @@ reveal_type(y) # revealed: Unknown
def _(n: int):
a = b"abcde"[n]
# TODO: Support overloads... Should be `bytes`
reveal_type(a) # revealed: @Todo(return type)
reveal_type(a) # revealed: @Todo(return type of decorated function)
```
## Slices
@@ -44,10 +44,10 @@ b[::0] # error: [zero-stepsize-in-slice]
def _(m: int, n: int):
byte_slice1 = b[m:n]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice1) # revealed: @Todo(return type)
reveal_type(byte_slice1) # revealed: @Todo(return type of decorated function)
def _(s: bytes) -> bytes:
byte_slice2 = s[0:5]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice2) # revealed: @Todo(return type)
reveal_type(byte_slice2) # revealed: @Todo(return type of decorated function)
```

View File

@@ -12,13 +12,13 @@ x = [1, 2, 3]
reveal_type(x) # revealed: list
# TODO reveal int
reveal_type(x[0]) # revealed: @Todo(return type)
reveal_type(x[0]) # revealed: @Todo(return type of decorated function)
# TODO reveal list
reveal_type(x[0:1]) # revealed: @Todo(return type)
reveal_type(x[0:1]) # revealed: @Todo(return type of decorated function)
# TODO error
reveal_type(x["a"]) # revealed: @Todo(return type)
reveal_type(x["a"]) # revealed: @Todo(return type of decorated function)
```
## Assignments within list assignment

View File

@@ -22,7 +22,7 @@ reveal_type(b) # revealed: Unknown
def _(n: int):
a = "abcde"[n]
# TODO: Support overloads... Should be `str`
reveal_type(a) # revealed: @Todo(return type)
reveal_type(a) # revealed: @Todo(return type of decorated function)
```
## Slices
@@ -76,11 +76,11 @@ def _(m: int, n: int, s2: str):
substring1 = s[m:n]
# TODO: Support overloads... Should be `LiteralString`
reveal_type(substring1) # revealed: @Todo(return type)
reveal_type(substring1) # revealed: @Todo(return type of decorated function)
substring2 = s2[0:5]
# TODO: Support overloads... Should be `str`
reveal_type(substring2) # revealed: @Todo(return type)
reveal_type(substring2) # revealed: @Todo(return type of decorated function)
```
## Unsupported slice types

View File

@@ -70,7 +70,7 @@ def _(m: int, n: int):
tuple_slice = t[m:n]
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: @Todo(return type)
reveal_type(tuple_slice) # revealed: @Todo(return type of decorated function)
```
## Inheritance

View File

@@ -223,7 +223,7 @@ class InvalidBoolDunder:
def __bool__(self) -> int:
return 1
# error: "Static assertion error: argument of type `InvalidBoolDunder` has an ambiguous static truthiness"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`; the return type of its bool method (`int`) isn't assignable to `bool"
static_assert(InvalidBoolDunder())
```

View File

@@ -91,3 +91,32 @@ class PossiblyUnboundTrue:
reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool
```
### Special-cased classes
Some special-cased `@final` classes are known by red-knot to have instances that are either always
truthy or always falsy.
```toml
[environment]
python-version = "3.12"
```
```py
import types
import typing
import sys
from knot_extensions import AlwaysTruthy, static_assert, is_subtype_of
from typing_extensions import _NoDefaultType
static_assert(is_subtype_of(sys.version_info.__class__, AlwaysTruthy))
static_assert(is_subtype_of(types.EllipsisType, AlwaysTruthy))
static_assert(is_subtype_of(_NoDefaultType, AlwaysTruthy))
static_assert(is_subtype_of(slice, AlwaysTruthy))
static_assert(is_subtype_of(types.FunctionType, AlwaysTruthy))
static_assert(is_subtype_of(types.MethodType, AlwaysTruthy))
static_assert(is_subtype_of(typing.TypeVar, AlwaysTruthy))
static_assert(is_subtype_of(typing.TypeAliasType, AlwaysTruthy))
static_assert(is_subtype_of(types.MethodWrapperType, AlwaysTruthy))
static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy))
```

View File

@@ -64,6 +64,24 @@ c = C()
c.a = 2
```
and similarly here:
```py
class Base:
a: ClassVar[int] = 1
class Derived(Base):
if flag():
a: int
reveal_type(Derived.a) # revealed: int
d = Derived()
# error: [invalid-attribute-access]
d.a = 2
```
## Too many arguments
```py

View File

@@ -183,12 +183,11 @@ class WithBothLenAndBool2:
# revealed: Literal[False]
reveal_type(not WithBothLenAndBool2())
# TODO: raise diagnostic when __bool__ method is not valid: [unsupported-operator] "Method __bool__ for type `MethodBoolInvalid` should return `bool`, returned type `int`"
# https://docs.python.org/3/reference/datamodel.html#object.__bool__
class MethodBoolInvalid:
def __bool__(self) -> int:
return 0
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`; the return type of its bool method (`int`) isn't assignable to `bool"
# revealed: bool
reveal_type(not MethodBoolInvalid())
@@ -204,3 +203,15 @@ class PossiblyUnboundBool:
# revealed: bool
reveal_type(not PossiblyUnboundBool())
```
## Object that implements `__bool__` incorrectly
<!-- snapshot-diagnostics -->
```py
class NotBoolable:
__bool__ = 3
# error: [unsupported-bool-conversion]
not NotBoolable()
```

View File

@@ -7,7 +7,7 @@ use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COM
pub use db::Db;
pub use module_name::ModuleName;
pub use module_resolver::{resolve_module, system_module_search_paths, KnownModule, Module};
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
pub use program::{Program, ProgramSettings, PythonPath, SearchPathSettings};
pub use python_platform::PythonPlatform;
pub use semantic_model::{HasType, SemanticModel};
@@ -27,7 +27,6 @@ pub(crate) mod symbol;
pub mod types;
mod unpack;
mod util;
mod visibility_constraints;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;

View File

@@ -1,4 +1,5 @@
use std::fmt::Formatter;
use std::str::FromStr;
use std::sync::Arc;
use ruff_db::files::File;
@@ -98,10 +99,13 @@ impl ModuleKind {
}
/// Enumeration of various core stdlib modules in which important types are located
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum_macros::EnumString)]
#[cfg_attr(test, derive(strum_macros::EnumIter))]
#[strum(serialize_all = "snake_case")]
pub enum KnownModule {
Builtins,
Types,
#[strum(serialize = "_typeshed")]
Typeshed,
TypingExtensions,
Typing,
@@ -139,21 +143,10 @@ impl KnownModule {
search_path: &SearchPath,
name: &ModuleName,
) -> Option<Self> {
if !search_path.is_standard_library() {
return None;
}
match name.as_str() {
"builtins" => Some(Self::Builtins),
"types" => Some(Self::Types),
"typing" => Some(Self::Typing),
"_typeshed" => Some(Self::Typeshed),
"typing_extensions" => Some(Self::TypingExtensions),
"sys" => Some(Self::Sys),
"abc" => Some(Self::Abc),
"collections" => Some(Self::Collections),
"inspect" => Some(Self::Inspect),
"knot_extensions" => Some(Self::KnotExtensions),
_ => None,
if search_path.is_standard_library() {
Self::from_str(name.as_str()).ok()
} else {
None
}
}
@@ -168,4 +161,29 @@ impl KnownModule {
pub const fn is_knot_extensions(self) -> bool {
matches!(self, Self::KnotExtensions)
}
pub const fn is_inspect(self) -> bool {
matches!(self, Self::Inspect)
}
}
#[cfg(test)]
mod tests {
use super::*;
use strum::IntoEnumIterator;
#[test]
fn known_module_roundtrip_from_str() {
let stdlib_search_path = SearchPath::vendored_stdlib();
for module in KnownModule::iter() {
let module_name = module.name();
assert_eq!(
KnownModule::try_from_search_path_and_name(&stdlib_search_path, &module_name),
Some(module),
"The strum `EnumString` implementation appears to be incorrect for `{module_name}`"
);
}
}
}

View File

@@ -12,7 +12,7 @@ use crate::db::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
use crate::site_packages::VirtualEnvironment;
use crate::{Program, SearchPathSettings, SitePackages};
use crate::{Program, PythonPath, SearchPathSettings};
use super::module::{Module, ModuleKind};
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
@@ -171,7 +171,7 @@ impl SearchPaths {
extra_paths,
src_roots,
custom_typeshed: typeshed,
site_packages: site_packages_paths,
python_path,
} = settings;
let system = db.system();
@@ -222,16 +222,16 @@ impl SearchPaths {
static_paths.push(stdlib_path);
let site_packages_paths = match site_packages_paths {
SitePackages::Derived { venv_path } => {
let site_packages_paths = match python_path {
PythonPath::SysPrefix(sys_prefix) => {
// TODO: We may want to warn here if the venv's python version is older
// than the one resolved in the program settings because it indicates
// that the `target-version` is incorrectly configured or that the
// venv is out of date.
VirtualEnvironment::new(venv_path, system)
VirtualEnvironment::new(sys_prefix, system)
.and_then(|venv| venv.site_packages_directories(system))?
}
SitePackages::Known(paths) => paths
PythonPath::KnownSitePackages(paths) => paths
.iter()
.map(|path| canonicalize(path, system))
.collect(),
@@ -1310,7 +1310,7 @@ mod tests {
extra_paths: vec![],
src_roots: vec![src.clone()],
custom_typeshed: Some(custom_typeshed),
site_packages: SitePackages::Known(vec![site_packages]),
python_path: PythonPath::KnownSitePackages(vec![site_packages]),
},
},
)
@@ -1816,7 +1816,7 @@ not_a_directory
extra_paths: vec![],
src_roots: vec![SystemPathBuf::from("/src")],
custom_typeshed: None,
site_packages: SitePackages::Known(vec![
python_path: PythonPath::KnownSitePackages(vec![
venv_site_packages,
system_site_packages,
]),

View File

@@ -4,7 +4,7 @@ use ruff_python_ast::PythonVersion;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::{ProgramSettings, PythonPlatform, SitePackages};
use crate::{ProgramSettings, PythonPath, PythonPlatform};
/// A test case for the module resolver.
///
@@ -239,7 +239,7 @@ impl TestCaseBuilder<MockedTypeshed> {
extra_paths: vec![],
src_roots: vec![src.clone()],
custom_typeshed: Some(typeshed.clone()),
site_packages: SitePackages::Known(vec![site_packages.clone()]),
python_path: PythonPath::KnownSitePackages(vec![site_packages.clone()]),
},
},
)
@@ -294,7 +294,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
python_version,
python_platform,
search_paths: SearchPathSettings {
site_packages: SitePackages::Known(vec![site_packages.clone()]),
python_path: PythonPath::KnownSitePackages(vec![site_packages.clone()]),
..SearchPathSettings::new(vec![src.clone()])
},
},

View File

@@ -110,8 +110,9 @@ pub struct SearchPathSettings {
/// bundled as a zip file in the binary
pub custom_typeshed: Option<SystemPathBuf>,
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
pub site_packages: SitePackages,
/// Path to the Python installation from which Red Knot resolves third party dependencies
/// and their type information.
pub python_path: PythonPath,
}
impl SearchPathSettings {
@@ -120,17 +121,32 @@ impl SearchPathSettings {
src_roots,
extra_paths: vec![],
custom_typeshed: None,
site_packages: SitePackages::Known(vec![]),
python_path: PythonPath::KnownSitePackages(vec![]),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SitePackages {
Derived {
venv_path: SystemPathBuf,
},
/// Resolved site packages paths
Known(Vec<SystemPathBuf>),
pub enum PythonPath {
/// A path that represents the value of [`sys.prefix`] at runtime in Python
/// for a given Python executable.
///
/// For the case of a virtual environment, where a
/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to
/// the virtual environment the Python binary lies inside, i.e. `/.venv`,
/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`.
/// System Python installations generally work the same way: if a system
/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix`
/// will be `/opt/homebrew`, and `site-packages` will be at
/// `/opt/homebrew/lib/python3.X/site-packages`.
///
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
SysPrefix(SystemPathBuf),
/// Resolved site packages paths.
///
/// This variant is mainly intended for testing where we want to skip resolving `site-packages`
/// because it would unnecessarily complicate the test setup.
KnownSitePackages(Vec<SystemPathBuf>),
}

View File

@@ -25,11 +25,13 @@ use crate::Db;
pub mod ast_ids;
pub mod attribute_assignment;
mod builder;
pub(crate) mod constraint;
pub mod definition;
pub mod expression;
mod narrowing_constraints;
pub(crate) mod predicate;
pub mod symbol;
mod use_def;
mod visibility_constraints;
pub(crate) use self::use_def::{
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,

View File

@@ -15,12 +15,16 @@ use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
use crate::semantic_index::constraint::{PatternConstraintKind, ScopedConstraintId};
use crate::semantic_index::definition::{
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionCategory,
DefinitionNodeKey, DefinitionNodeRef, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef,
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef,
WithItemDefinitionNodeRef,
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::predicate::{
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, ScopedPredicateId,
};
use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId,
SymbolTableBuilder,
@@ -28,17 +32,13 @@ use crate::semantic_index::symbol::{
use crate::semantic_index::use_def::{
EagerBindingsKey, FlowSnapshot, ScopedEagerBindingsId, UseDefMapBuilder,
};
use crate::semantic_index::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraintsBuilder,
};
use crate::semantic_index::SemanticIndex;
use crate::unpack::{Unpack, UnpackValue};
use crate::visibility_constraints::{ScopedVisibilityConstraintId, VisibilityConstraintsBuilder};
use crate::Db;
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
use super::definition::{
DefinitionCategory, ExceptHandlerDefinitionNodeRef, ImportDefinitionNodeRef,
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
};
mod except_handlers;
/// Are we in a state where a `break` statement is allowed?
@@ -385,50 +385,60 @@ impl<'db> SemanticIndexBuilder<'db> {
definition
}
fn record_expression_constraint(&mut self, constraint_node: &ast::Expr) -> Constraint<'db> {
let constraint = self.build_constraint(constraint_node);
self.record_constraint(constraint);
constraint
fn record_expression_narrowing_constraint(
&mut self,
precide_node: &ast::Expr,
) -> Predicate<'db> {
let predicate = self.build_predicate(precide_node);
self.record_narrowing_constraint(predicate);
predicate
}
fn build_constraint(&mut self, constraint_node: &ast::Expr) -> Constraint<'db> {
let expression = self.add_standalone_expression(constraint_node);
Constraint {
node: ConstraintNode::Expression(expression),
fn build_predicate(&mut self, predicate_node: &ast::Expr) -> Predicate<'db> {
let expression = self.add_standalone_expression(predicate_node);
Predicate {
node: PredicateNode::Expression(expression),
is_positive: true,
}
}
/// Adds a new constraint to the list of all constraints, but does not record it. Returns the
/// constraint ID for later recording using [`SemanticIndexBuilder::record_constraint_id`].
fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
self.current_use_def_map_mut().add_constraint(constraint)
/// Adds a new predicate to the list of all predicates, but does not record it. Returns the
/// predicate ID for later recording using
/// [`SemanticIndexBuilder::record_narrowing_constraint_id`].
fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
self.current_use_def_map_mut().add_predicate(predicate)
}
/// Negates a constraint and adds it to the list of all constraints, does not record it.
fn add_negated_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
let negated = Constraint {
node: constraint.node,
/// Negates a predicate and adds it to the list of all predicates, does not record it.
fn add_negated_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
let negated = Predicate {
node: predicate.node,
is_positive: false,
};
self.current_use_def_map_mut().add_constraint(negated)
self.current_use_def_map_mut().add_predicate(negated)
}
/// Records a previously added constraint by adding it to all live bindings.
fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
/// Records a previously added narrowing constraint by adding it to all live bindings.
fn record_narrowing_constraint_id(&mut self, predicate: ScopedPredicateId) {
self.current_use_def_map_mut()
.record_constraint_id(constraint);
.record_narrowing_constraint(predicate);
}
/// Adds and records a constraint, i.e. adds it to all live bindings.
fn record_constraint(&mut self, constraint: Constraint<'db>) {
self.current_use_def_map_mut().record_constraint(constraint);
/// Adds and records a narrowing constraint, i.e. adds it to all live bindings.
fn record_narrowing_constraint(&mut self, predicate: Predicate<'db>) {
let use_def = self.current_use_def_map_mut();
let predicate_id = use_def.add_predicate(predicate);
use_def.record_narrowing_constraint(predicate_id);
}
/// Negates the given constraint and then adds it to all live bindings.
fn record_negated_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
let id = self.add_negated_constraint(constraint);
self.record_constraint_id(id);
/// Negates the given predicate and then adds it as a narrowing constraint to all live
/// bindings.
fn record_negated_narrowing_constraint(
&mut self,
predicate: Predicate<'db>,
) -> ScopedPredicateId {
let id = self.add_negated_predicate(predicate);
self.record_narrowing_constraint_id(id);
id
}
@@ -454,12 +464,12 @@ impl<'db> SemanticIndexBuilder<'db> {
/// Records a visibility constraint by applying it to all live bindings and declarations.
fn record_visibility_constraint(
&mut self,
constraint: Constraint<'db>,
predicate: Predicate<'db>,
) -> ScopedVisibilityConstraintId {
let constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
let predicate_id = self.current_use_def_map_mut().add_predicate(predicate);
let id = self
.current_visibility_constraints_mut()
.add_atom(constraint_id);
.add_atom(predicate_id);
self.record_visibility_constraint_id(id);
id
}
@@ -526,12 +536,12 @@ impl<'db> SemanticIndexBuilder<'db> {
}
}
fn add_pattern_constraint(
fn add_pattern_narrowing_constraint(
&mut self,
subject: Expression<'db>,
pattern: &ast::Pattern,
guard: Option<&ast::Expr>,
) -> Constraint<'db> {
) -> Predicate<'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,
@@ -548,19 +558,19 @@ impl<'db> SemanticIndexBuilder<'db> {
let kind = match pattern {
ast::Pattern::MatchValue(pattern) => {
let value = self.add_standalone_expression(&pattern.value);
PatternConstraintKind::Value(value, guard)
PatternPredicateKind::Value(value, guard)
}
ast::Pattern::MatchSingleton(singleton) => {
PatternConstraintKind::Singleton(singleton.value, guard)
PatternPredicateKind::Singleton(singleton.value, guard)
}
ast::Pattern::MatchClass(pattern) => {
let cls = self.add_standalone_expression(&pattern.cls);
PatternConstraintKind::Class(cls, guard)
PatternPredicateKind::Class(cls, guard)
}
_ => PatternConstraintKind::Unsupported,
_ => PatternPredicateKind::Unsupported,
};
let pattern_constraint = PatternConstraint::new(
let pattern_predicate = PatternPredicate::new(
self.db,
self.file,
self.current_scope(),
@@ -568,12 +578,12 @@ impl<'db> SemanticIndexBuilder<'db> {
kind,
countme::Count::default(),
);
let constraint = Constraint {
node: ConstraintNode::Pattern(pattern_constraint),
let predicate = Predicate {
node: PredicateNode::Pattern(pattern_predicate),
is_positive: true,
};
self.current_use_def_map_mut().record_constraint(constraint);
constraint
self.record_narrowing_constraint(predicate);
predicate
}
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
@@ -1116,10 +1126,10 @@ where
ast::Stmt::If(node) => {
self.visit_expr(&node.test);
let mut no_branch_taken = self.flow_snapshot();
let mut last_constraint = self.record_expression_constraint(&node.test);
let mut last_predicate = self.record_expression_narrowing_constraint(&node.test);
self.visit_body(&node.body);
let visibility_constraint_id = self.record_visibility_constraint(last_constraint);
let visibility_constraint_id = self.record_visibility_constraint(last_predicate);
let mut vis_constraints = vec![visibility_constraint_id];
let mut post_clauses: Vec<FlowSnapshot> = vec![];
@@ -1145,14 +1155,14 @@ where
// we can only take an elif/else branch if none of the previous ones were
// taken
self.flow_restore(no_branch_taken.clone());
self.record_negated_constraint(last_constraint);
self.record_negated_narrowing_constraint(last_predicate);
let elif_constraint = if let Some(elif_test) = clause_test {
let elif_predicate = 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);
Some(constraint)
let predicate = self.record_expression_narrowing_constraint(elif_test);
Some(predicate)
} else {
None
};
@@ -1162,9 +1172,9 @@ where
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);
if let Some(elif_predicate) = elif_predicate {
last_predicate = elif_predicate;
let id = self.record_visibility_constraint(elif_predicate);
vis_constraints.push(id);
}
}
@@ -1184,19 +1194,19 @@ where
self.visit_expr(test);
let pre_loop = self.flow_snapshot();
let constraint = self.record_expression_constraint(test);
let predicate = self.record_expression_narrowing_constraint(test);
// We need multiple copies of the visibility constraint for the while condition,
// since we need to model situations where the first evaluation of the condition
// returns True, but a later evaluation returns False.
let first_constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
let later_constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
let first_predicate_id = self.current_use_def_map_mut().add_predicate(predicate);
let later_predicate_id = self.current_use_def_map_mut().add_predicate(predicate);
let first_vis_constraint_id = self
.current_visibility_constraints_mut()
.add_atom(first_constraint_id);
.add_atom(first_predicate_id);
let later_vis_constraint_id = self
.current_visibility_constraints_mut()
.add_atom(later_constraint_id);
.add_atom(later_predicate_id);
// Save aside any break states from an outer loop
let saved_break_states = std::mem::take(&mut self.loop_break_states);
@@ -1234,7 +1244,7 @@ where
self.flow_restore(pre_loop.clone());
self.record_negated_visibility_constraint(first_vis_constraint_id);
self.flow_merge(post_body);
self.record_negated_constraint(constraint);
self.record_negated_narrowing_constraint(predicate);
self.visit_body(orelse);
self.record_negated_visibility_constraint(later_vis_constraint_id);
@@ -1383,7 +1393,7 @@ where
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(
let predicate = self.add_pattern_narrowing_constraint(
subject_expr,
&case.pattern,
case.guard.as_deref(),
@@ -1395,7 +1405,7 @@ where
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
}
let vis_constraint_id = self.record_visibility_constraint(constraint_id);
let vis_constraint_id = self.record_visibility_constraint(predicate);
vis_constraints.push(vis_constraint_id);
}
@@ -1694,13 +1704,13 @@ where
}) => {
self.visit_expr(test);
let pre_if = self.flow_snapshot();
let constraint = self.record_expression_constraint(test);
let predicate = self.record_expression_narrowing_constraint(test);
self.visit_expr(body);
let visibility_constraint = self.record_visibility_constraint(constraint);
let visibility_constraint = self.record_visibility_constraint(predicate);
let post_body = self.flow_snapshot();
self.flow_restore(pre_if.clone());
self.record_negated_constraint(constraint);
self.record_negated_narrowing_constraint(predicate);
self.visit_expr(orelse);
self.record_negated_visibility_constraint(visibility_constraint);
self.flow_merge(post_body);
@@ -1776,14 +1786,14 @@ where
// For the last value, we don't need to model control flow. There is short-circuiting
// anymore.
if index < values.len() - 1 {
let constraint = self.build_constraint(value);
let constraint_id = match op {
ast::BoolOp::And => self.add_constraint(constraint),
ast::BoolOp::Or => self.add_negated_constraint(constraint),
let predicate = self.build_predicate(value);
let predicate_id = match op {
ast::BoolOp::And => self.add_predicate(predicate),
ast::BoolOp::Or => self.add_negated_predicate(predicate),
};
let visibility_constraint = self
.current_visibility_constraints_mut()
.add_atom(constraint_id);
.add_atom(predicate_id);
let after_expr = self.flow_snapshot();
@@ -1801,7 +1811,7 @@ where
// the application of the visibility constraint until after the expression
// has been evaluated, so we only push it onto the stack here.
self.flow_restore(after_expr);
self.record_constraint_id(constraint_id);
self.record_narrowing_constraint_id(predicate_id);
visibility_constraints.push(visibility_constraint);
}
}

View File

@@ -1,77 +0,0 @@
use ruff_db::files::File;
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast::Singleton;
use crate::db::Db;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
// A scoped identifier for each `Constraint` in a scope.
#[newtype_index]
#[derive(Ord, PartialOrd)]
pub(crate) struct ScopedConstraintId;
// A collection of constraints. This is currently stored in `UseDefMap`, which means we maintain a
// separate set of constraints for each scope in a file.
pub(crate) type Constraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
#[derive(Debug, Default)]
pub(crate) struct ConstraintsBuilder<'db> {
constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
}
impl<'db> ConstraintsBuilder<'db> {
/// Adds a constraint. Note that we do not deduplicate constraints. If you add a `Constraint`
/// more than once, you will get distinct `ScopedConstraintId`s for each one. (This lets you
/// model constraint expressions that might evaluate to different values at different points of
/// execution.)
pub(crate) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
self.constraints.push(constraint)
}
pub(crate) fn build(mut self) -> Constraints<'db> {
self.constraints.shrink_to_fit();
self.constraints
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) struct Constraint<'db> {
pub(crate) node: ConstraintNode<'db>,
pub(crate) is_positive: bool,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) enum ConstraintNode<'db> {
Expression(Expression<'db>),
Pattern(PatternConstraint<'db>),
}
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
pub(crate) enum PatternConstraintKind<'db> {
Singleton(Singleton, Option<Expression<'db>>),
Value(Expression<'db>, Option<Expression<'db>>),
Class(Expression<'db>, Option<Expression<'db>>),
Unsupported,
}
#[salsa::tracked]
pub(crate) struct PatternConstraint<'db> {
pub(crate) file: File,
pub(crate) file_scope: FileScopeId,
pub(crate) subject: Expression<'db>,
#[return_ref]
pub(crate) kind: PatternConstraintKind<'db>,
count: countme::Count<PatternConstraint<'static>>,
}
impl<'db> PatternConstraint<'db> {
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.file_scope(db).to_scope_id(db, self.file(db))
}
}

View File

@@ -0,0 +1,151 @@
//! # Narrowing constraints
//!
//! When building a semantic index for a file, we associate each binding with a _narrowing
//! constraint_, which constrains the type of the binding's symbol. Note that a binding can be
//! associated with a different narrowing constraint at different points in a file. See the
//! [`use_def`][crate::semantic_index::use_def] module for more details.
//!
//! This module defines how narrowing constraints are stored internally.
//!
//! A _narrowing constraint_ consists of a list of _predicates_, each of which corresponds with an
//! expression in the source file (represented by a [`Predicate`]). We need to support the
//! following operations on narrowing constraints:
//!
//! - Adding a new predicate to an existing constraint
//! - Merging two constraints together, which produces the _intersection_ of their predicates
//! - Iterating through the predicates in a constraint
//!
//! In particular, note that we do not need random access to the predicates in a constraint. That
//! means that we can use a simple [_sorted association list_][ruff_index::list] as our data
//! structure. That lets us use a single 32-bit integer to store each narrowing constraint, no
//! matter how many predicates it contains. It also makes merging two narrowing constraints fast,
//! since alists support fast intersection.
//!
//! Because we visit the contents of each scope in source-file order, and assign scoped IDs in
//! source-file order, that means that we will tend to visit narrowing constraints in order by
//! their predicate IDs. This is exactly how to get the best performance from our alist
//! implementation.
//!
//! [`Predicate`]: crate::semantic_index::predicate::Predicate
use ruff_index::list::{ListBuilder, ListSetReverseIterator, ListStorage};
use ruff_index::newtype_index;
use crate::semantic_index::predicate::ScopedPredicateId;
/// A narrowing constraint associated with a live binding.
///
/// A constraint is a list of [`Predicate`]s that each constrain the type of the binding's symbol.
///
/// An instance of this type represents a _non-empty_ narrowing constraint. You will often wrap
/// this in `Option` and use `None` to represent an empty narrowing constraint.
///
/// [`Predicate`]: crate::semantic_index::predicate::Predicate
#[newtype_index]
pub(crate) struct ScopedNarrowingConstraintId;
/// One of the [`Predicate`]s in a narrowing constraint, which constraints the type of the
/// binding's symbol.
///
/// Note that those [`Predicate`]s are stored in [their own per-scope
/// arena][crate::semantic_index::predicate::Predicates], so internally we use a
/// [`ScopedPredicateId`] to refer to the underlying predicate.
///
/// [`Predicate`]: crate::semantic_index::predicate::Predicate
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct ScopedNarrowingConstraintPredicate(ScopedPredicateId);
impl ScopedNarrowingConstraintPredicate {
/// Returns (the ID of) the `Predicate`
pub(crate) fn predicate(self) -> ScopedPredicateId {
self.0
}
}
impl From<ScopedPredicateId> for ScopedNarrowingConstraintPredicate {
fn from(predicate: ScopedPredicateId) -> ScopedNarrowingConstraintPredicate {
ScopedNarrowingConstraintPredicate(predicate)
}
}
/// A collection of narrowing constraints for a given scope.
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct NarrowingConstraints {
lists: ListStorage<ScopedNarrowingConstraintId, ScopedNarrowingConstraintPredicate>,
}
// Building constraints
// --------------------
/// A builder for creating narrowing constraints.
#[derive(Debug, Default, Eq, PartialEq)]
pub(crate) struct NarrowingConstraintsBuilder {
lists: ListBuilder<ScopedNarrowingConstraintId, ScopedNarrowingConstraintPredicate>,
}
impl NarrowingConstraintsBuilder {
pub(crate) fn build(self) -> NarrowingConstraints {
NarrowingConstraints {
lists: self.lists.build(),
}
}
/// Adds a predicate to an existing narrowing constraint.
pub(crate) fn add_predicate_to_constraint(
&mut self,
constraint: Option<ScopedNarrowingConstraintId>,
predicate: ScopedNarrowingConstraintPredicate,
) -> Option<ScopedNarrowingConstraintId> {
self.lists.insert(constraint, predicate)
}
/// Returns the intersection of two narrowing constraints. The result contains the predicates
/// that appear in both inputs.
pub(crate) fn intersect_constraints(
&mut self,
a: Option<ScopedNarrowingConstraintId>,
b: Option<ScopedNarrowingConstraintId>,
) -> Option<ScopedNarrowingConstraintId> {
self.lists.intersect(a, b)
}
}
// Iteration
// ---------
pub(crate) type NarrowingConstraintsIterator<'a> = std::iter::Copied<
ListSetReverseIterator<'a, ScopedNarrowingConstraintId, ScopedNarrowingConstraintPredicate>,
>;
impl NarrowingConstraints {
/// Iterates over the predicates in a narrowing constraint.
pub(crate) fn iter_predicates(
&self,
set: Option<ScopedNarrowingConstraintId>,
) -> NarrowingConstraintsIterator<'_> {
self.lists.iter_set_reverse(set).copied()
}
}
// Test support
// ------------
#[cfg(test)]
mod tests {
use super::*;
impl ScopedNarrowingConstraintPredicate {
pub(crate) fn as_u32(self) -> u32 {
self.0.as_u32()
}
}
impl NarrowingConstraintsBuilder {
pub(crate) fn iter_predicates(
&self,
set: Option<ScopedNarrowingConstraintId>,
) -> NarrowingConstraintsIterator<'_> {
self.lists.iter_set_reverse(set).copied()
}
}
}

View File

@@ -0,0 +1,84 @@
//! _Predicates_ are Python expressions whose runtime values can affect type inference.
//!
//! We currently use predicates in two places:
//!
//! - [_Narrowing constraints_][crate::semantic_index::narrowing_constraints] constrain the type of
//! a binding that is visible at a particular use.
//! - [_Visibility constraints_][crate::semantic_index::visibility_constraints] determine the
//! static visibility of a binding, and the reachability of a statement.
use ruff_db::files::File;
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast::Singleton;
use crate::db::Db;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
// A scoped identifier for each `Predicate` in a scope.
#[newtype_index]
#[derive(Ord, PartialOrd)]
pub(crate) struct ScopedPredicateId;
// A collection of predicates for a given scope.
pub(crate) type Predicates<'db> = IndexVec<ScopedPredicateId, Predicate<'db>>;
#[derive(Debug, Default)]
pub(crate) struct PredicatesBuilder<'db> {
predicates: IndexVec<ScopedPredicateId, Predicate<'db>>,
}
impl<'db> PredicatesBuilder<'db> {
/// Adds a predicate. Note that we do not deduplicate predicates. If you add a `Predicate`
/// more than once, you will get distinct `ScopedPredicateId`s for each one. (This lets you
/// model predicates that might evaluate to different values at different points of execution.)
pub(crate) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
self.predicates.push(predicate)
}
pub(crate) fn build(mut self) -> Predicates<'db> {
self.predicates.shrink_to_fit();
self.predicates
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) struct Predicate<'db> {
pub(crate) node: PredicateNode<'db>,
pub(crate) is_positive: bool,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) enum PredicateNode<'db> {
Expression(Expression<'db>),
Pattern(PatternPredicate<'db>),
}
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
pub(crate) enum PatternPredicateKind<'db> {
Singleton(Singleton, Option<Expression<'db>>),
Value(Expression<'db>, Option<Expression<'db>>),
Class(Expression<'db>, Option<Expression<'db>>),
Unsupported,
}
#[salsa::tracked]
pub(crate) struct PatternPredicate<'db> {
pub(crate) file: File,
pub(crate) file_scope: FileScopeId,
pub(crate) subject: Expression<'db>,
#[return_ref]
pub(crate) kind: PatternPredicateKind<'db>,
count: countme::Count<PatternPredicate<'static>>,
}
impl<'db> PatternPredicate<'db> {
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.file_scope(db).to_scope_id(db, self.file(db))
}
}

View File

@@ -165,7 +165,7 @@
//! don't actually store these "list of visible definitions" as a vector of [`Definition`].
//! Instead, [`SymbolBindings`] and [`SymbolDeclarations`] are structs which use bit-sets to track
//! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `constraints`
//! [`ScopedPredicateId`], which are indices into the `all_definitions` and `predicates`
//! indexvecs in the [`UseDefMap`].
//!
//! There is another special kind of possible "definition" for a symbol: there might be a path from
@@ -260,20 +260,22 @@ use ruff_index::{newtype_index, IndexVec};
use rustc_hash::FxHashMap;
use self::symbol_state::{
ConstraintIndexIterator, LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator,
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, ScopedDefinitionId,
SymbolBindings, SymbolDeclarations, SymbolState,
};
use crate::semantic_index::ast_ids::ScopedUseId;
use crate::semantic_index::constraint::{
Constraint, Constraints, ConstraintsBuilder, ScopedConstraintId,
};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::narrowing_constraints::{
NarrowingConstraints, NarrowingConstraintsBuilder, NarrowingConstraintsIterator,
};
use crate::semantic_index::predicate::{
Predicate, Predicates, PredicatesBuilder, ScopedPredicateId,
};
use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId};
use crate::visibility_constraints::{
use crate::semantic_index::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
};
mod bitset;
mod symbol_state;
/// Applicable definitions and constraints for every use of a name.
@@ -283,8 +285,11 @@ pub(crate) struct UseDefMap<'db> {
/// this represents the implicit "unbound"/"undeclared" definition of every symbol.
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
/// Array of [`Constraint`] in this scope.
constraints: Constraints<'db>,
/// Array of predicates in this scope.
predicates: Predicates<'db>,
/// Array of narrowing constraints in this scope.
narrowing_constraints: NarrowingConstraints,
/// Array of visibility constraints in this scope.
visibility_constraints: VisibilityConstraints,
@@ -369,7 +374,8 @@ impl<'db> UseDefMap<'db> {
) -> BindingWithConstraintsIterator<'map, 'db> {
BindingWithConstraintsIterator {
all_definitions: &self.all_definitions,
constraints: &self.constraints,
predicates: &self.predicates,
narrowing_constraints: &self.narrowing_constraints,
visibility_constraints: &self.visibility_constraints,
inner: bindings.iter(),
}
@@ -381,7 +387,7 @@ impl<'db> UseDefMap<'db> {
) -> DeclarationsIterator<'map, 'db> {
DeclarationsIterator {
all_definitions: &self.all_definitions,
constraints: &self.constraints,
predicates: &self.predicates,
visibility_constraints: &self.visibility_constraints,
inner: declarations.iter(),
}
@@ -415,7 +421,8 @@ type EagerBindings = IndexVec<ScopedEagerBindingsId, SymbolBindings>;
#[derive(Debug)]
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
pub(crate) constraints: &'map Constraints<'db>,
pub(crate) predicates: &'map Predicates<'db>,
pub(crate) narrowing_constraints: &'map NarrowingConstraints,
pub(crate) visibility_constraints: &'map VisibilityConstraints,
inner: LiveBindingsIterator<'map>,
}
@@ -424,15 +431,17 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
type Item = BindingWithConstraints<'map, 'db>;
fn next(&mut self) -> Option<Self::Item> {
let constraints = self.constraints;
let predicates = self.predicates;
let narrowing_constraints = self.narrowing_constraints;
self.inner
.next()
.map(|live_binding| BindingWithConstraints {
binding: self.all_definitions[live_binding.binding],
narrowing_constraints: ConstraintsIterator {
constraints,
constraint_ids: live_binding.narrowing_constraints.iter(),
narrowing_constraint: ConstraintsIterator {
predicates,
constraint_ids: narrowing_constraints
.iter_predicates(live_binding.narrowing_constraint),
},
visibility_constraint: live_binding.visibility_constraint,
})
@@ -443,22 +452,22 @@ impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
pub(crate) struct BindingWithConstraints<'map, 'db> {
pub(crate) binding: Option<Definition<'db>>,
pub(crate) narrowing_constraints: ConstraintsIterator<'map, 'db>,
pub(crate) narrowing_constraint: ConstraintsIterator<'map, 'db>,
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
}
pub(crate) struct ConstraintsIterator<'map, 'db> {
constraints: &'map Constraints<'db>,
constraint_ids: ConstraintIndexIterator<'map>,
predicates: &'map Predicates<'db>,
constraint_ids: NarrowingConstraintsIterator<'map>,
}
impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
type Item = Constraint<'db>;
type Item = Predicate<'db>;
fn next(&mut self) -> Option<Self::Item> {
self.constraint_ids
.next()
.map(|constraint_id| self.constraints[ScopedConstraintId::from_u32(constraint_id)])
.map(|narrowing_constraint| self.predicates[narrowing_constraint.predicate()])
}
}
@@ -466,7 +475,7 @@ impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
pub(crate) struct DeclarationsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
pub(crate) constraints: &'map Constraints<'db>,
pub(crate) predicates: &'map Predicates<'db>,
pub(crate) visibility_constraints: &'map VisibilityConstraints,
inner: LiveDeclarationsIterator<'map>,
}
@@ -508,8 +517,11 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Definition`].
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
/// Builder of constraints.
constraints: ConstraintsBuilder<'db>,
/// Builder of predicates.
pub(super) predicates: PredicatesBuilder<'db>,
/// Builder of narrowing constraints.
pub(super) narrowing_constraints: NarrowingConstraintsBuilder,
/// Builder of visibility constraints.
pub(super) visibility_constraints: VisibilityConstraintsBuilder,
@@ -541,7 +553,8 @@ impl Default for UseDefMapBuilder<'_> {
fn default() -> Self {
Self {
all_definitions: IndexVec::from_iter([None]),
constraints: ConstraintsBuilder::default(),
predicates: PredicatesBuilder::default(),
narrowing_constraints: NarrowingConstraintsBuilder::default(),
visibility_constraints: VisibilityConstraintsBuilder::default(),
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
bindings_by_use: IndexVec::new(),
@@ -573,22 +586,18 @@ impl<'db> UseDefMapBuilder<'db> {
symbol_state.record_binding(def_id, self.scope_start_visibility);
}
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
self.constraints.add_constraint(constraint)
pub(super) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
self.predicates.add_predicate(predicate)
}
pub(super) fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
pub(super) fn record_narrowing_constraint(&mut self, predicate: ScopedPredicateId) {
let narrowing_constraint = predicate.into();
for state in &mut self.symbol_states {
state.record_constraint(constraint);
state
.record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint);
}
}
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
let new_constraint_id = self.add_constraint(constraint);
self.record_constraint_id(new_constraint_id);
new_constraint_id
}
pub(super) fn record_visibility_constraint(
&mut self,
constraint: ScopedVisibilityConstraintId,
@@ -737,10 +746,15 @@ impl<'db> UseDefMapBuilder<'db> {
let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter();
for current in &mut self.symbol_states {
if let Some(snapshot) = snapshot_definitions_iter.next() {
current.merge(snapshot, &mut self.visibility_constraints);
current.merge(
snapshot,
&mut self.narrowing_constraints,
&mut self.visibility_constraints,
);
} else {
current.merge(
SymbolState::undefined(snapshot.scope_start_visibility),
&mut self.narrowing_constraints,
&mut self.visibility_constraints,
);
// Symbol not present in snapshot, so it's unbound/undeclared from that path.
@@ -762,7 +776,8 @@ impl<'db> UseDefMapBuilder<'db> {
UseDefMap {
all_definitions: self.all_definitions,
constraints: self.constraints.build(),
predicates: self.predicates.build(),
narrowing_constraints: self.narrowing_constraints.build(),
visibility_constraints: self.visibility_constraints.build(),
bindings_by_use: self.bindings_by_use,
public_symbols: self.symbol_states,

View File

@@ -1,234 +0,0 @@
/// Ordered set of `u32`.
///
/// Uses an inline bit-set for small values (up to 64 * B), falls back to heap allocated vector of
/// blocks for larger values.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum BitSet<const B: usize> {
/// Bit-set (in 64-bit blocks) for the first 64 * B entries.
Inline([u64; B]),
/// Overflow beyond 64 * B.
Heap(Vec<u64>),
}
impl<const B: usize> Default for BitSet<B> {
fn default() -> Self {
// B * 64 must fit in a u32, or else we have unusable bits; this assertion makes the
// truncating casts to u32 below safe. This would be better as a const assertion, but
// that's not possible on stable with const generic params. (B should never really be
// anywhere close to this large.)
assert!(B * 64 < (u32::MAX as usize));
// This implementation requires usize >= 32 bits.
static_assertions::const_assert!(usize::BITS >= 32);
Self::Inline([0; B])
}
}
impl<const B: usize> BitSet<B> {
/// Convert from Inline to Heap, if needed, and resize the Heap vector, if needed.
fn resize(&mut self, value: u32) {
let num_blocks_needed = (value / 64) + 1;
self.resize_blocks(num_blocks_needed as usize);
}
fn resize_blocks(&mut self, num_blocks_needed: usize) {
match self {
Self::Inline(blocks) => {
let mut vec = blocks.to_vec();
vec.resize(num_blocks_needed, 0);
*self = Self::Heap(vec);
}
Self::Heap(vec) => {
vec.resize(num_blocks_needed, 0);
}
}
}
fn blocks_mut(&mut self) -> &mut [u64] {
match self {
Self::Inline(blocks) => blocks.as_mut_slice(),
Self::Heap(blocks) => blocks.as_mut_slice(),
}
}
fn blocks(&self) -> &[u64] {
match self {
Self::Inline(blocks) => blocks.as_slice(),
Self::Heap(blocks) => blocks.as_slice(),
}
}
/// Insert a value into the [`BitSet`].
///
/// Return true if the value was newly inserted, false if already present.
pub(super) fn insert(&mut self, value: u32) -> bool {
let value_usize = value as usize;
let (block, index) = (value_usize / 64, value_usize % 64);
if block >= self.blocks().len() {
self.resize(value);
}
let blocks = self.blocks_mut();
let missing = blocks[block] & (1 << index) == 0;
blocks[block] |= 1 << index;
missing
}
/// Intersect in-place with another [`BitSet`].
pub(super) fn intersect(&mut self, other: &BitSet<B>) {
let my_blocks = self.blocks_mut();
let other_blocks = other.blocks();
let min_len = my_blocks.len().min(other_blocks.len());
for i in 0..min_len {
my_blocks[i] &= other_blocks[i];
}
for block in my_blocks.iter_mut().skip(min_len) {
*block = 0;
}
}
/// Return an iterator over the values (in ascending order) in this [`BitSet`].
pub(super) fn iter(&self) -> BitSetIterator<'_, B> {
let blocks = self.blocks();
BitSetIterator {
blocks,
current_block_index: 0,
current_block: blocks[0],
}
}
}
/// Iterator over values in a [`BitSet`].
#[derive(Debug)]
pub(super) struct BitSetIterator<'a, const B: usize> {
/// The blocks we are iterating over.
blocks: &'a [u64],
/// The index of the block we are currently iterating through.
current_block_index: usize,
/// The block we are currently iterating through (and zeroing as we go.)
current_block: u64,
}
impl<const B: usize> Iterator for BitSetIterator<'_, B> {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
while self.current_block == 0 {
if self.current_block_index + 1 >= self.blocks.len() {
return None;
}
self.current_block_index += 1;
self.current_block = self.blocks[self.current_block_index];
}
let lowest_bit_set = self.current_block.trailing_zeros();
// reset the lowest set bit, without a data dependency on `lowest_bit_set`
self.current_block &= self.current_block.wrapping_sub(1);
// SAFETY: `lowest_bit_set` cannot be more than 64, `current_block_index` cannot be more
// than `B - 1`, and we check above that `B * 64 < u32::MAX`. So both `64 *
// current_block_index` and the final value here must fit in u32.
#[allow(clippy::cast_possible_truncation)]
Some(lowest_bit_set + (64 * self.current_block_index) as u32)
}
}
impl<const B: usize> std::iter::FusedIterator for BitSetIterator<'_, B> {}
#[cfg(test)]
mod tests {
use super::BitSet;
impl<const B: usize> BitSet<B> {
/// Create and return a new [`BitSet`] with a single `value` inserted.
pub(super) fn with(value: u32) -> Self {
let mut bitset = Self::default();
bitset.insert(value);
bitset
}
}
fn assert_bitset<const B: usize>(bitset: &BitSet<B>, contents: &[u32]) {
assert_eq!(bitset.iter().collect::<Vec<_>>(), contents);
}
#[test]
fn iter() {
let mut b = BitSet::<1>::with(3);
b.insert(27);
b.insert(6);
assert!(matches!(b, BitSet::Inline(_)));
assert_bitset(&b, &[3, 6, 27]);
}
#[test]
fn iter_overflow() {
let mut b = BitSet::<1>::with(140);
b.insert(100);
b.insert(129);
assert!(matches!(b, BitSet::Heap(_)));
assert_bitset(&b, &[100, 129, 140]);
}
#[test]
fn intersect() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(23);
b2.insert(5);
b1.intersect(&b2);
assert_bitset(&b1, &[4]);
}
#[test]
fn intersect_mixed_1() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(89);
b2.insert(5);
b1.intersect(&b2);
assert_bitset(&b1, &[4]);
}
#[test]
fn intersect_mixed_2() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(23);
b2.insert(89);
b1.intersect(&b2);
assert_bitset(&b1, &[4]);
}
#[test]
fn intersect_heap() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(89);
b2.insert(90);
b1.intersect(&b2);
assert_bitset(&b1, &[4]);
}
#[test]
fn intersect_heap_2() {
let mut b1 = BitSet::<1>::with(89);
let mut b2 = BitSet::<1>::with(89);
b1.insert(91);
b2.insert(90);
b1.intersect(&b2);
assert_bitset(&b1, &[89]);
}
#[test]
fn multiple_blocks() {
let mut b = BitSet::<2>::with(120);
b.insert(45);
assert!(matches!(b, BitSet::Inline(_)));
assert_bitset(&b, &[45, 120]);
}
}

View File

@@ -36,10 +36,8 @@
//! dominates, but it does dominate the `x = 1 if flag2 else None` binding, so we have to keep
//! track of that.
//!
//! The data structures used here ([`BitSet`] and [`smallvec::SmallVec`]) optimize for keeping all
//! data inline (avoiding lots of scattered allocations) in small-to-medium cases, and falling back
//! to heap allocation to be able to scale to arbitrary numbers of live bindings and constraints
//! when needed.
//! The data structures use `IndexVec` arenas to store all data compactly and contiguously, while
//! supporting very cheap clones.
//!
//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very
//! similar to tracking live bindings.
@@ -48,10 +46,12 @@ use itertools::{EitherOrBoth, Itertools};
use ruff_index::newtype_index;
use smallvec::{smallvec, SmallVec};
use crate::semantic_index::constraint::ScopedConstraintId;
use crate::semantic_index::use_def::bitset::{BitSet, BitSetIterator};
use crate::semantic_index::use_def::VisibilityConstraintsBuilder;
use crate::visibility_constraints::ScopedVisibilityConstraintId;
use crate::semantic_index::narrowing_constraints::{
NarrowingConstraintsBuilder, ScopedNarrowingConstraintId, ScopedNarrowingConstraintPredicate,
};
use crate::semantic_index::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraintsBuilder,
};
/// A newtype-index for a definition in a particular scope.
#[newtype_index]
@@ -67,18 +67,10 @@ impl ScopedDefinitionId {
pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0);
}
/// Can reference this * 64 total constraints inline; more will fall back to the heap.
const INLINE_CONSTRAINT_BLOCKS: usize = 2;
/// Can keep inline this many live bindings or declarations per symbol at a given time; more will
/// go to heap.
const INLINE_DEFINITIONS_PER_SYMBOL: usize = 4;
/// Which constraints apply to a given binding?
type Constraints = BitSet<INLINE_CONSTRAINT_BLOCKS>;
pub(super) type ConstraintIndexIterator<'a> = BitSetIterator<'a, INLINE_CONSTRAINT_BLOCKS>;
/// Live declarations for a single symbol at some point in control flow, with their
/// corresponding visibility constraints.
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
@@ -197,7 +189,7 @@ pub(super) struct SymbolBindings {
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct LiveBinding {
pub(super) binding: ScopedDefinitionId,
pub(super) narrowing_constraints: Constraints,
pub(super) narrowing_constraint: Option<ScopedNarrowingConstraintId>,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
}
@@ -207,7 +199,7 @@ impl SymbolBindings {
fn unbound(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
let initial_binding = LiveBinding {
binding: ScopedDefinitionId::UNBOUND,
narrowing_constraints: Constraints::default(),
narrowing_constraint: None,
visibility_constraint: scope_start_visibility,
};
Self {
@@ -226,15 +218,20 @@ impl SymbolBindings {
self.live_bindings.clear();
self.live_bindings.push(LiveBinding {
binding,
narrowing_constraints: Constraints::default(),
narrowing_constraint: None,
visibility_constraint,
});
}
/// Add given constraint to all live bindings.
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
pub(super) fn record_narrowing_constraint(
&mut self,
narrowing_constraints: &mut NarrowingConstraintsBuilder,
predicate: ScopedNarrowingConstraintPredicate,
) {
for binding in &mut self.live_bindings {
binding.narrowing_constraints.insert(constraint_id.into());
binding.narrowing_constraint = narrowing_constraints
.add_predicate_to_constraint(binding.narrowing_constraint, predicate);
}
}
@@ -273,7 +270,12 @@ impl SymbolBindings {
}
}
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
fn merge(
&mut self,
b: Self,
narrowing_constraints: &mut NarrowingConstraintsBuilder,
visibility_constraints: &mut VisibilityConstraintsBuilder,
) {
let a = std::mem::take(self);
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
@@ -289,8 +291,8 @@ impl SymbolBindings {
// If the same definition is visible through both paths, any constraint
// that applies on only one path is irrelevant to the resulting type from
// unioning the two paths, so we intersect the constraints.
let mut narrowing_constraints = a.narrowing_constraints;
narrowing_constraints.intersect(&b.narrowing_constraints);
let narrowing_constraint = narrowing_constraints
.intersect_constraints(a.narrowing_constraint, b.narrowing_constraint);
// For visibility constraints, we merge them using a ternary OR operation:
let visibility_constraint = visibility_constraints
@@ -298,7 +300,7 @@ impl SymbolBindings {
self.live_bindings.push(LiveBinding {
binding: a.binding,
narrowing_constraints,
narrowing_constraint,
visibility_constraint,
});
}
@@ -338,8 +340,13 @@ impl SymbolState {
}
/// Add given constraint to all live bindings.
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
self.bindings.record_constraint(constraint_id);
pub(super) fn record_narrowing_constraint(
&mut self,
narrowing_constraints: &mut NarrowingConstraintsBuilder,
constraint: ScopedNarrowingConstraintPredicate,
) {
self.bindings
.record_narrowing_constraint(narrowing_constraints, constraint);
}
/// Add given visibility constraint to all live bindings.
@@ -373,9 +380,11 @@ impl SymbolState {
pub(super) fn merge(
&mut self,
b: SymbolState,
narrowing_constraints: &mut NarrowingConstraintsBuilder,
visibility_constraints: &mut VisibilityConstraintsBuilder,
) {
self.bindings.merge(b.bindings, visibility_constraints);
self.bindings
.merge(b.bindings, narrowing_constraints, visibility_constraints);
self.declarations
.merge(b.declarations, visibility_constraints);
}
@@ -393,8 +402,14 @@ impl SymbolState {
mod tests {
use super::*;
use crate::semantic_index::predicate::ScopedPredicateId;
#[track_caller]
fn assert_bindings(symbol: &SymbolState, expected: &[&str]) {
fn assert_bindings(
narrowing_constraints: &NarrowingConstraintsBuilder,
symbol: &SymbolState,
expected: &[&str],
) {
let actual = symbol
.bindings()
.iter()
@@ -405,13 +420,12 @@ mod tests {
} else {
def_id.as_u32().to_string()
};
let constraints = live_binding
.narrowing_constraints
.iter()
.map(|idx| idx.to_string())
let predicates = narrowing_constraints
.iter_predicates(live_binding.narrowing_constraint)
.map(|idx| idx.as_u32().to_string())
.collect::<Vec<_>>()
.join(", ");
format!("{def}<{constraints}>")
format!("{def}<{predicates}>")
})
.collect::<Vec<_>>();
assert_eq!(actual, expected);
@@ -440,36 +454,41 @@ mod tests {
#[test]
fn unbound() {
let narrowing_constraints = NarrowingConstraintsBuilder::default();
let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
assert_bindings(&sym, &["unbound<>"]);
assert_bindings(&narrowing_constraints, &sym, &["unbound<>"]);
}
#[test]
fn with() {
let narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
assert_bindings(&sym, &["1<>"]);
assert_bindings(&narrowing_constraints, &sym, &["1<>"]);
}
#[test]
fn record_constraint() {
let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym.record_constraint(ScopedConstraintId::from_u32(0));
let predicate = ScopedPredicateId::from_u32(0).into();
sym.record_narrowing_constraint(&mut narrowing_constraints, predicate);
assert_bindings(&sym, &["1<0>"]);
assert_bindings(&narrowing_constraints, &sym, &["1<0>"]);
}
#[test]
fn merge() {
let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
// merging the same definition with the same constraint keeps the constraint
@@ -478,18 +497,24 @@ mod tests {
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1a.record_constraint(ScopedConstraintId::from_u32(0));
let predicate = ScopedPredicateId::from_u32(0).into();
sym1a.record_narrowing_constraint(&mut narrowing_constraints, predicate);
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1b.record_constraint(ScopedConstraintId::from_u32(0));
let predicate = ScopedPredicateId::from_u32(0).into();
sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate);
sym1a.merge(sym1b, &mut visibility_constraints);
sym1a.merge(
sym1b,
&mut narrowing_constraints,
&mut visibility_constraints,
);
let mut sym1 = sym1a;
assert_bindings(&sym1, &["1<0>"]);
assert_bindings(&narrowing_constraints, &sym1, &["1<0>"]);
// merging the same definition with differing constraints drops all constraints
let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
@@ -497,18 +522,24 @@ mod tests {
ScopedDefinitionId::from_u32(2),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym2a.record_constraint(ScopedConstraintId::from_u32(1));
let predicate = ScopedPredicateId::from_u32(1).into();
sym2a.record_narrowing_constraint(&mut narrowing_constraints, predicate);
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(
ScopedDefinitionId::from_u32(2),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
let predicate = ScopedPredicateId::from_u32(2).into();
sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate);
sym2a.merge(sym1b, &mut visibility_constraints);
sym2a.merge(
sym1b,
&mut narrowing_constraints,
&mut visibility_constraints,
);
let sym2 = sym2a;
assert_bindings(&sym2, &["2<>"]);
assert_bindings(&narrowing_constraints, &sym2, &["2<>"]);
// merging a constrained definition with unbound keeps both
let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
@@ -516,18 +547,27 @@ mod tests {
ScopedDefinitionId::from_u32(3),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym3a.record_constraint(ScopedConstraintId::from_u32(3));
let predicate = ScopedPredicateId::from_u32(3).into();
sym3a.record_narrowing_constraint(&mut narrowing_constraints, predicate);
let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym3a.merge(sym2b, &mut visibility_constraints);
sym3a.merge(
sym2b,
&mut narrowing_constraints,
&mut visibility_constraints,
);
let sym3 = sym3a;
assert_bindings(&sym3, &["unbound<>", "3<3>"]);
assert_bindings(&narrowing_constraints, &sym3, &["unbound<>", "3<3>"]);
// merging different definitions keeps them each with their existing constraints
sym1.merge(sym3, &mut visibility_constraints);
sym1.merge(
sym3,
&mut narrowing_constraints,
&mut visibility_constraints,
);
let sym = sym1;
assert_bindings(&sym, &["unbound<>", "1<0>", "3<3>"]);
assert_bindings(&narrowing_constraints, &sym, &["unbound<>", "1<0>", "3<3>"]);
}
#[test]
@@ -556,6 +596,7 @@ mod tests {
#[test]
fn record_declaration_merge() {
let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
@@ -563,20 +604,29 @@ mod tests {
let mut sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym2.record_declaration(ScopedDefinitionId::from_u32(2));
sym.merge(sym2, &mut visibility_constraints);
sym.merge(
sym2,
&mut narrowing_constraints,
&mut visibility_constraints,
);
assert_declarations(&sym, &["1", "2"]);
}
#[test]
fn record_declaration_merge_partial_undeclared() {
let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
let sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.merge(sym2, &mut visibility_constraints);
sym.merge(
sym2,
&mut narrowing_constraints,
&mut visibility_constraints,
);
assert_declarations(&sym, &["undeclared", "1"]);
}

View File

@@ -178,8 +178,8 @@ use std::cmp::Ordering;
use ruff_index::{Idx, IndexVec};
use rustc_hash::FxHashMap;
use crate::semantic_index::constraint::{
Constraint, ConstraintNode, Constraints, PatternConstraintKind, ScopedConstraintId,
use crate::semantic_index::predicate::{
PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId,
};
use crate::types::{infer_expression_type, Truthiness};
use crate::Db;
@@ -188,7 +188,7 @@ use crate::Db;
/// is just like a boolean formula, but with `Ambiguous` as a third potential result. See the
/// module documentation for more details.)
///
/// The primitive atoms of the formula are [`Constraint`]s, which express some property of the
/// The primitive atoms of the formula are [`Predicate`]s, which express some property of the
/// runtime state of the code that we are analyzing.
///
/// We assume that each atom has a stable value each time that the formula is evaluated. An atom
@@ -197,7 +197,7 @@ use crate::Db;
/// allows us to perform simplifications like `A !A → true` and `A ∧ !A → false`.
///
/// That means that when you are constructing a formula, you might need to create distinct atoms
/// for a particular [`Constraint`], if your formula needs to consider how a particular runtime
/// for a particular [`Predicate`], if your formula needs to consider how a particular runtime
/// property might be different at different points in the execution of the program.
///
/// Visibility constraints are normalized, so equivalent constraints are guaranteed to have equal
@@ -225,7 +225,7 @@ impl std::fmt::Debug for ScopedVisibilityConstraintId {
//
// There are 3 terminals, with hard-coded constraint IDs: true, ambiguous, and false.
//
// _Atoms_ are the underlying Constraints, which are the variables that are evaluated by the
// _Atoms_ are the underlying Predicates, which are the variables that are evaluated by the
// ternary function.
//
// _Interior nodes_ provide the TDD structure for the formula. Interior nodes are stored in an
@@ -234,9 +234,9 @@ impl std::fmt::Debug for ScopedVisibilityConstraintId {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct InteriorNode {
/// A "variable" that is evaluated as part of a TDD ternary function. For visibility
/// constraints, this is a `Constraint` that represents some runtime property of the Python
/// constraints, this is a `Predicate` that represents some runtime property of the Python
/// code that we are evaluating.
atom: ScopedConstraintId,
atom: ScopedPredicateId,
if_true: ScopedVisibilityConstraintId,
if_ambiguous: ScopedVisibilityConstraintId,
if_false: ScopedVisibilityConstraintId,
@@ -281,8 +281,7 @@ const AMBIGUOUS: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::AM
const ALWAYS_FALSE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_FALSE;
const SMALLEST_TERMINAL: ScopedVisibilityConstraintId = ALWAYS_FALSE;
/// A collection of visibility constraints. This is currently stored in `UseDefMap`, which means we
/// maintain a separate set of visibility constraints for each scope in file.
/// A collection of visibility constraints for a given scope.
#[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct VisibilityConstraints {
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
@@ -344,23 +343,23 @@ impl VisibilityConstraintsBuilder {
.or_insert_with(|| self.interiors.push(node))
}
/// Adds a new visibility constraint that checks a single [`Constraint`].
/// Adds a new visibility constraint that checks a single [`Predicate`].
///
/// [`ScopedConstraintId`]s are the “variables” that are evaluated by a TDD. A TDD variable has
/// [`ScopedPredicateId`]s are the “variables” that are evaluated by a TDD. A TDD variable has
/// the same value no matter how many times it appears in the ternary formula that the TDD
/// represents.
///
/// However, we sometimes have to model how a `Constraint` can have a different runtime
/// However, we sometimes have to model how a `Predicate` can have a different runtime
/// value at different points in the execution of the program. To handle this, you can take
/// advantage of the fact that the [`Constraints`] arena does not deduplicate `Constraint`s.
/// You can add a `Constraint` multiple times, yielding different `ScopedConstraintId`s, which
/// advantage of the fact that the [`Predicates`] arena does not deduplicate `Predicate`s.
/// You can add a `Predicate` multiple times, yielding different `ScopedPredicateId`s, which
/// you can then create separate TDD atoms for.
pub(crate) fn add_atom(
&mut self,
constraint: ScopedConstraintId,
predicate: ScopedPredicateId,
) -> ScopedVisibilityConstraintId {
self.add_interior(InteriorNode {
atom: constraint,
atom: predicate,
if_true: ALWAYS_TRUE,
if_ambiguous: AMBIGUOUS,
if_false: ALWAYS_FALSE,
@@ -535,7 +534,7 @@ impl VisibilityConstraints {
pub(crate) fn evaluate<'db>(
&self,
db: &'db dyn Db,
constraints: &Constraints<'db>,
predicates: &Predicates<'db>,
mut id: ScopedVisibilityConstraintId,
) -> Truthiness {
loop {
@@ -545,8 +544,8 @@ impl VisibilityConstraints {
ALWAYS_FALSE => return Truthiness::AlwaysFalse,
_ => self.interiors[id],
};
let constraint = &constraints[node.atom];
match Self::analyze_single(db, constraint) {
let predicate = &predicates[node.atom];
match Self::analyze_single(db, predicate) {
Truthiness::AlwaysTrue => id = node.if_true,
Truthiness::Ambiguous => id = node.if_ambiguous,
Truthiness::AlwaysFalse => id = node.if_false,
@@ -554,14 +553,14 @@ impl VisibilityConstraints {
}
}
fn analyze_single(db: &dyn Db, constraint: &Constraint) -> Truthiness {
match constraint.node {
ConstraintNode::Expression(test_expr) => {
fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness {
match predicate.node {
PredicateNode::Expression(test_expr) => {
let ty = infer_expression_type(db, test_expr);
ty.bool(db).negate_if(!constraint.is_positive)
ty.bool(db).negate_if(!predicate.is_positive)
}
ConstraintNode::Pattern(inner) => match inner.kind(db) {
PatternConstraintKind::Value(value, guard) => {
PredicateNode::Pattern(inner) => match inner.kind(db) {
PatternPredicateKind::Value(value, guard) => {
let subject_expression = inner.subject(db);
let subject_ty = infer_expression_type(db, subject_expression);
let value_ty = infer_expression_type(db, *value);
@@ -580,9 +579,9 @@ impl VisibilityConstraints {
Truthiness::Ambiguous
}
}
PatternConstraintKind::Singleton(..)
| PatternConstraintKind::Class(..)
| PatternConstraintKind::Unsupported => Truthiness::Ambiguous,
PatternPredicateKind::Singleton(..)
| PatternPredicateKind::Class(..)
| PatternPredicateKind::Unsupported => Truthiness::Ambiguous,
},
}
}

View File

@@ -205,11 +205,11 @@ System site-packages will not be used for module resolution.",
#[derive(Debug, thiserror::Error)]
pub(crate) enum SitePackagesDiscoveryError {
#[error("Invalid --venv-path argument: {0} could not be canonicalized")]
#[error("Invalid --python argument: `{0}` could not be canonicalized")]
VenvDirCanonicalizationError(SystemPathBuf, #[source] io::Error),
#[error("Invalid --venv-path argument: {0} does not point to a directory on disk")]
#[error("Invalid --python argument: `{0}` does not point to a directory on disk")]
VenvDirIsNotADirectory(SystemPathBuf),
#[error("--venv-path points to a broken venv with no pyvenv.cfg file")]
#[error("--python points to a broken venv with no pyvenv.cfg file")]
NoPyvenvCfgFile(#[source] io::Error),
#[error("Failed to parse the pyvenv.cfg file at {0} because {1}")]
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),

View File

@@ -8,7 +8,7 @@ use crate::semantic_index::{
symbol_table, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
};
use crate::types::{
binding_type, declaration_type, narrowing_constraint, todo_type, IntersectionBuilder,
binding_type, declaration_type, infer_narrowing_constraint, todo_type, IntersectionBuilder,
KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType,
};
use crate::{resolve_module, Db, KnownModule, Module, Program};
@@ -184,6 +184,40 @@ pub(crate) fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> S
symbol_impl(db, scope, name, RequiresExplicitReExport::No)
}
/// Infer the public type of a class symbol (its type as seen from outside its scope) in the given
/// `scope`.
pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
symbol_table(db, scope)
.symbol_id_by_name(name)
.map(|symbol| {
let symbol_and_quals = symbol_by_id(db, scope, symbol, RequiresExplicitReExport::No);
if symbol_and_quals.is_class_var() {
// For declared class vars we do not need to check if they have bindings,
// we just trust the declaration.
return symbol_and_quals.0;
}
if let SymbolAndQualifiers(Symbol::Type(ty, _), _) = symbol_and_quals {
// Otherwise, we need to check if the symbol has bindings
let use_def = use_def_map(db, scope);
let bindings = use_def.public_bindings(symbol);
let inferred =
symbol_from_bindings_impl(db, bindings, RequiresExplicitReExport::No);
// TODO: we should not need to calculate inferred type second time. This is a temporary
// solution until the notion of Boundness and Declaredness is split. See #16036, #16264
match inferred {
Symbol::Unbound => Symbol::Unbound,
Symbol::Type(_, boundness) => Symbol::Type(ty, boundness),
}
} else {
Symbol::Unbound
}
})
.unwrap_or(Symbol::Unbound)
}
/// Infers the public type of an explicit module-global symbol as seen from within the same file.
///
/// Note that all global scopes also include various "implicit globals" such as `__name__`,
@@ -348,7 +382,7 @@ pub(crate) type SymbolFromDeclarationsResult<'db> =
/// that this comes with a [`CLASS_VAR`] type qualifier.
///
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(crate) struct SymbolAndQualifiers<'db>(pub(crate) Symbol<'db>, pub(crate) TypeQualifiers);
impl SymbolAndQualifiers<'_> {
@@ -364,11 +398,6 @@ impl SymbolAndQualifiers<'_> {
pub(crate) fn is_class_var(&self) -> bool {
self.1.contains(TypeQualifiers::CLASS_VAR)
}
/// Returns `true` if the symbol has a `Final` type qualifier.
pub(crate) fn is_final(&self) -> bool {
self.1.contains(TypeQualifiers::FINAL)
}
}
impl<'db> From<Symbol<'db>> for SymbolAndQualifiers<'db> {
@@ -377,6 +406,91 @@ impl<'db> From<Symbol<'db>> for SymbolAndQualifiers<'db> {
}
}
#[salsa::tracked]
fn symbol_by_id<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
symbol_id: ScopedSymbolId,
requires_explicit_reexport: RequiresExplicitReExport,
) -> SymbolAndQualifiers<'db> {
let use_def = use_def_map(db, scope);
// If the symbol is declared, the public type is based on declarations; otherwise, it's based
// on inference from bindings.
let declarations = use_def.public_declarations(symbol_id);
let declared = symbol_from_declarations_impl(db, declarations, requires_explicit_reexport);
match declared {
// Symbol is declared, trust the declared type
Ok(symbol_and_quals @ SymbolAndQualifiers(Symbol::Type(_, Boundness::Bound), _)) => {
symbol_and_quals
}
// Symbol is possibly declared
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::PossiblyUnbound), quals)) => {
let bindings = use_def.public_bindings(symbol_id);
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
let symbol = match inferred {
// Symbol is possibly undeclared and definitely unbound
Symbol::Unbound => {
// TODO: We probably don't want to report `Bound` here. This requires a bit of
// design work though as we might want a different behavior for stubs and for
// normal modules.
Symbol::Type(declared_ty, Boundness::Bound)
}
// Symbol is possibly undeclared and (possibly) bound
Symbol::Type(inferred_ty, boundness) => Symbol::Type(
UnionType::from_elements(db, [inferred_ty, declared_ty]),
boundness,
),
};
SymbolAndQualifiers(symbol, quals)
}
// Symbol is undeclared, return the union of `Unknown` with the inferred type
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
let bindings = use_def.public_bindings(symbol_id);
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
// modified externally, but those changes do not take effect. We therefore issue
// a diagnostic if we see it being modified externally. In type inference, we
// can assign a "narrow" type to it even if it is not *declared*. This means, we
// do not have to call [`widen_type_for_undeclared_public_symbol`].
let is_considered_non_modifiable =
symbol_table(db, scope).symbol(symbol_id).name() == "__slots__";
widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable)
.into()
}
// Symbol has conflicting declared types
Err((declared_ty, _)) => {
// Intentionally ignore conflicting declared types; that's not our problem,
// it's the problem of the module we are importing from.
SymbolAndQualifiers(
Symbol::bound(declared_ty.inner_type()),
declared_ty.qualifiers(),
)
}
}
// TODO (ticket: https://github.com/astral-sh/ruff/issues/14297) Our handling of boundness
// currently only depends on bindings, and ignores declarations. This is inconsistent, since
// we only look at bindings if the symbol may be undeclared. Consider the following example:
// ```py
// x: int
//
// if flag:
// y: int
// else
// y = 3
// ```
// If we import from this module, we will currently report `x` as a definitely-bound symbol
// (even though it has no bindings at all!) but report `y` as possibly-unbound (even though
// every path has either a binding or a declaration for it.)
}
/// Implementation of [`symbol`].
fn symbol_impl<'db>(
db: &'db dyn Db,
@@ -384,85 +498,6 @@ fn symbol_impl<'db>(
name: &str,
requires_explicit_reexport: RequiresExplicitReExport,
) -> Symbol<'db> {
#[salsa::tracked]
fn symbol_by_id<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
symbol_id: ScopedSymbolId,
requires_explicit_reexport: RequiresExplicitReExport,
) -> Symbol<'db> {
let use_def = use_def_map(db, scope);
// If the symbol is declared, the public type is based on declarations; otherwise, it's based
// on inference from bindings.
let declarations = use_def.public_declarations(symbol_id);
let declared = symbol_from_declarations_impl(db, declarations, requires_explicit_reexport);
let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final);
let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol);
match declared {
// Symbol is declared, trust the declared type
Ok(symbol @ Symbol::Type(_, Boundness::Bound)) => symbol,
// Symbol is possibly declared
Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => {
let bindings = use_def.public_bindings(symbol_id);
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
match inferred {
// Symbol is possibly undeclared and definitely unbound
Symbol::Unbound => {
// TODO: We probably don't want to report `Bound` here. This requires a bit of
// design work though as we might want a different behavior for stubs and for
// normal modules.
Symbol::Type(declared_ty, Boundness::Bound)
}
// Symbol is possibly undeclared and (possibly) bound
Symbol::Type(inferred_ty, boundness) => Symbol::Type(
UnionType::from_elements(db, [inferred_ty, declared_ty]),
boundness,
),
}
}
// Symbol is undeclared, return the union of `Unknown` with the inferred type
Ok(Symbol::Unbound) => {
let bindings = use_def.public_bindings(symbol_id);
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
// modified externally, but those changes do not take effect. We therefore issue
// a diagnostic if we see it being modified externally. In type inference, we
// can assign a "narrow" type to it even if it is not *declared*. This means, we
// do not have to call [`widen_type_for_undeclared_public_symbol`].
let is_considered_non_modifiable =
is_final || symbol_table(db, scope).symbol(symbol_id).name() == "__slots__";
widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable)
}
// Symbol has conflicting declared types
Err((declared_ty, _)) => {
// Intentionally ignore conflicting declared types; that's not our problem,
// it's the problem of the module we are importing from.
Symbol::bound(declared_ty.inner_type())
}
}
// TODO (ticket: https://github.com/astral-sh/ruff/issues/14297) Our handling of boundness
// currently only depends on bindings, and ignores declarations. This is inconsistent, since
// we only look at bindings if the symbol may be undeclared. Consider the following example:
// ```py
// x: int
//
// if flag:
// y: int
// else
// y = 3
// ```
// If we import from this module, we will currently report `x` as a definitely-bound symbol
// (even though it has no bindings at all!) but report `y` as possibly-unbound (even though
// every path has either a binding or a declaration for it.)
}
let _span = tracing::trace_span!("symbol", ?name).entered();
// We don't need to check for `typing_extensions` here, because `typing_extensions.TYPE_CHECKING`
@@ -489,7 +524,7 @@ fn symbol_impl<'db>(
symbol_table(db, scope)
.symbol_id_by_name(name)
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport))
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport).0)
.unwrap_or(Symbol::Unbound)
}
@@ -503,7 +538,7 @@ fn symbol_from_bindings_impl<'db>(
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
requires_explicit_reexport: RequiresExplicitReExport,
) -> Symbol<'db> {
let constraints = bindings_with_constraints.constraints;
let predicates = bindings_with_constraints.predicates;
let visibility_constraints = bindings_with_constraints.visibility_constraints;
let mut bindings_with_constraints = bindings_with_constraints.peekable();
@@ -515,9 +550,9 @@ fn symbol_from_bindings_impl<'db>(
Some(BindingWithConstraints {
binding,
visibility_constraint,
narrowing_constraints: _,
narrowing_constraint: _,
}) if binding.map_or(true, is_non_exported) => {
visibility_constraints.evaluate(db, constraints, *visibility_constraint)
visibility_constraints.evaluate(db, predicates, *visibility_constraint)
}
_ => Truthiness::AlwaysFalse,
};
@@ -525,7 +560,7 @@ fn symbol_from_bindings_impl<'db>(
let mut types = bindings_with_constraints.filter_map(
|BindingWithConstraints {
binding,
narrowing_constraints,
narrowing_constraint,
visibility_constraint,
}| {
let binding = binding?;
@@ -535,27 +570,29 @@ fn symbol_from_bindings_impl<'db>(
}
let static_visibility =
visibility_constraints.evaluate(db, constraints, visibility_constraint);
visibility_constraints.evaluate(db, predicates, visibility_constraint);
if static_visibility.is_always_false() {
return None;
}
let mut constraint_tys = narrowing_constraints
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
.peekable();
let constraint_tys: Vec<_> = narrowing_constraint
.filter_map(|constraint| infer_narrowing_constraint(db, constraint, binding))
.collect();
let binding_ty = binding_type(db, binding);
if constraint_tys.peek().is_some() {
if constraint_tys.is_empty() {
Some(binding_ty)
} else {
let intersection_ty = constraint_tys
.into_iter()
.rev()
.fold(
IntersectionBuilder::new(db).add_positive(binding_ty),
IntersectionBuilder::add_positive,
)
.build();
Some(intersection_ty)
} else {
Some(binding_ty)
}
},
);
@@ -592,7 +629,7 @@ fn symbol_from_declarations_impl<'db>(
declarations: DeclarationsIterator<'_, 'db>,
requires_explicit_reexport: RequiresExplicitReExport,
) -> SymbolFromDeclarationsResult<'db> {
let constraints = declarations.constraints;
let predicates = declarations.predicates;
let visibility_constraints = declarations.visibility_constraints;
let mut declarations = declarations.peekable();
@@ -605,7 +642,7 @@ fn symbol_from_declarations_impl<'db>(
declaration,
visibility_constraint,
}) if declaration.map_or(true, is_non_exported) => {
visibility_constraints.evaluate(db, constraints, *visibility_constraint)
visibility_constraints.evaluate(db, predicates, *visibility_constraint)
}
_ => Truthiness::AlwaysFalse,
};
@@ -622,7 +659,7 @@ fn symbol_from_declarations_impl<'db>(
}
let static_visibility =
visibility_constraints.evaluate(db, constraints, visibility_constraint);
visibility_constraints.evaluate(db, predicates, visibility_constraint);
if static_visibility.is_always_false() {
None

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@
//! eliminate the supertype from the intersection).
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType};
use crate::types::{IntersectionType, KnownClass, Type, UnionType};
use crate::{Db, FxOrderSet};
use smallvec::SmallVec;
@@ -43,6 +43,10 @@ impl<'db> UnionBuilder<'db> {
}
}
pub(crate) fn is_empty(&self) -> bool {
self.elements.is_empty()
}
/// Collapse the union to a single type: `object`.
fn collapse_to_object(mut self) -> Self {
self.elements.clear();
@@ -299,7 +303,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
_ => {
let known_instance = new_positive
.into_instance()
.and_then(|instance| instance.class.known(db));
.and_then(|instance| instance.class().known(db));
if known_instance == Some(KnownClass::Object) {
// `object & T` -> `T`; it is always redundant to add `object` to an intersection
@@ -318,8 +322,8 @@ impl<'db> InnerIntersectionBuilder<'db> {
Type::AlwaysFalsy if addition_is_bool_instance => {
new_positive = Type::BooleanLiteral(false);
}
Type::Instance(InstanceType { class })
if class.is_known(db, KnownClass::Bool) =>
Type::Instance(instance)
if instance.class().is_known(db, KnownClass::Bool) =>
{
match new_positive {
// `bool & AlwaysTruthy` -> `Literal[True]`
@@ -413,7 +417,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.positive
.iter()
.filter_map(|ty| ty.into_instance())
.filter_map(|instance| instance.class.known(db))
.filter_map(|instance| instance.class().known(db))
.any(KnownClass::is_bool)
};
@@ -429,7 +433,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
Type::Never => {
// Adding ~Never to an intersection is a no-op.
}
Type::Instance(instance) if instance.class.is_object(db) => {
Type::Instance(instance) if instance.class().is_object(db) => {
// Adding ~object to an intersection results in Never.
*self = Self::default();
self.positive.insert(Type::Never);

View File

@@ -54,14 +54,14 @@ impl<'db> CallOutcome<'db> {
Ok(CallOutcome::Union(bindings.into()))
} else if bindings.is_empty() && not_callable {
Err(CallError::NotCallable {
not_callable_ty: Type::Union(union),
not_callable_type: Type::Union(union),
})
} else {
Err(CallError::Union {
Err(CallError::Union(UnionCallError {
errors: errors.into(),
bindings: bindings.into(),
called_ty: Type::Union(union),
})
called_type: Type::Union(union),
}))
}
}
@@ -89,23 +89,14 @@ pub(super) enum CallError<'db> {
/// The type is not callable.
NotCallable {
/// The type that can't be called.
not_callable_ty: Type<'db>,
not_callable_type: Type<'db>,
},
/// A call to a union failed because at least one variant
/// can't be called with the given arguments.
///
/// A union where all variants are not callable is represented as a `NotCallable` error.
Union {
/// The variants that can't be called with the given arguments.
errors: Box<[CallError<'db>]>,
/// The bindings for the callable variants (that have no binding errors).
bindings: Box<[CallBinding<'db>]>,
/// The union type that we tried calling.
called_ty: Type<'db>,
},
Union(UnionCallError<'db>),
/// The type has a `__call__` method but it isn't always bound.
PossiblyUnboundDunderCall {
@@ -126,9 +117,9 @@ impl<'db> CallError<'db> {
CallError::NotCallable { .. } => None,
// If some variants are callable, and some are not, return the union of the return types of the callable variants
// combined with `Type::Unknown`
CallError::Union {
errors, bindings, ..
} => Some(UnionType::from_elements(
CallError::Union(UnionCallError {
bindings, errors, ..
}) => Some(UnionType::from_elements(
db,
bindings
.iter()
@@ -156,10 +147,10 @@ impl<'db> CallError<'db> {
pub(super) fn called_type(&self) -> Type<'db> {
match self {
Self::NotCallable {
not_callable_ty, ..
} => *not_callable_ty,
Self::Union { called_ty, .. } => *called_ty,
Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type,
not_callable_type, ..
} => *not_callable_type,
Self::Union(UnionCallError { called_type, .. })
| Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type,
Self::BindingError { binding } => binding.callable_type(),
}
}
@@ -169,6 +160,40 @@ impl<'db> CallError<'db> {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct UnionCallError<'db> {
/// The variants that can't be called with the given arguments.
pub(super) errors: Box<[CallError<'db>]>,
/// The bindings for the callable variants (that have no binding errors).
pub(super) bindings: Box<[CallBinding<'db>]>,
/// The union type that we tried calling.
pub(super) called_type: Type<'db>,
}
impl UnionCallError<'_> {
/// Return `true` if this `UnionCallError` indicates that the union might not be callable at all.
/// Otherwise, return `false`.
///
/// For example, the union type `Callable[[int], int] | None` may not be callable at all,
/// because the `None` element in this union has no `__call__` method. Calling an object that
/// inhabited this union type would lead to a `UnionCallError` that would indicate that the
/// union might not be callable at all.
///
/// On the other hand, the union type `Callable[[int], int] | Callable[[str], str]` is always
/// *callable*, but it would still lead to a `UnionCallError` if an inhabitant of this type was
/// called with a single `int` argument passed in. That's because the second element in the
/// union doesn't accept an `int` when it's called: it only accepts a `str`.
pub(crate) fn indicates_type_possibly_not_callable(&self) -> bool {
self.errors.iter().any(|error| match error {
CallError::BindingError { .. } => false,
CallError::NotCallable { .. } | CallError::PossiblyUnboundDunderCall { .. } => true,
CallError::Union(union_error) => union_error.indicates_type_possibly_not_callable(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallDunderError<'db> {
/// The dunder attribute exists but it can't be called with the given arguments.
@@ -189,7 +214,7 @@ impl<'db> CallDunderError<'db> {
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::Call(error) => error.return_type(db),
Self::PossiblyUnbound(_) => None,
Self::PossiblyUnbound(call_outcome) => Some(call_outcome.return_type(db)),
Self::MethodNotAvailable => None,
}
}

View File

@@ -35,10 +35,23 @@ impl<'a, 'db> CallArguments<'a, 'db> {
self.0.first().map(Argument::ty)
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn exactly_one_argument(&self) -> Option<Type<'db>> {
match &*self.0 {
[arg] => Some(arg.ty()),
_ => None,
}
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
self.0.get(1).map(Argument::ty)
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn third_argument(&self) -> Option<Type<'db>> {
self.0.get(2).map(Argument::ty)
}
}
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {

View File

@@ -5,7 +5,7 @@ use crate::types::diagnostic::{
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
};
use crate::types::signatures::Parameter;
use crate::types::{todo_type, UnionType};
use crate::types::{todo_type, CallableType, UnionType};
use ruff_db::diagnostic::{SecondaryDiagnosticMessage, Span};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
@@ -137,6 +137,13 @@ pub(crate) fn bind_call<'db>(
}
}
/// Describes a callable for the purposes of diagnostics.
#[derive(Debug)]
pub(crate) struct CallableDescriptor<'a> {
name: &'a str,
kind: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CallBinding<'db> {
/// Type of the callable object (function, class...)
@@ -200,18 +207,43 @@ impl<'db> CallBinding<'db> {
}
}
fn callable_name(&self, db: &'db dyn Db) -> Option<&str> {
fn callable_descriptor(&self, db: &'db dyn Db) -> Option<CallableDescriptor> {
match self.callable_ty {
Type::FunctionLiteral(function) => Some(function.name(db)),
Type::ClassLiteral(class_type) => Some(class_type.class.name(db)),
Type::FunctionLiteral(function) => Some(CallableDescriptor {
kind: "function",
name: function.name(db),
}),
Type::ClassLiteral(class_type) => Some(CallableDescriptor {
kind: "class",
name: class_type.class().name(db),
}),
Type::Callable(CallableType::BoundMethod(bound_method)) => Some(CallableDescriptor {
kind: "bound method",
name: bound_method.function(db).name(db),
}),
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
Some(CallableDescriptor {
kind: "method wrapper `__get__` of function",
name: function.name(db),
})
}
Type::Callable(CallableType::WrapperDescriptorDunderGet) => Some(CallableDescriptor {
kind: "wrapper descriptor",
name: "FunctionType.__get__",
}),
_ => None,
}
}
pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
let callable_name = self.callable_name(context.db());
let callable_descriptor = self.callable_descriptor(context.db());
for error in &self.errors {
error.report_diagnostic(context, node, self.callable_ty, callable_name);
error.report_diagnostic(
context,
node,
self.callable_ty,
callable_descriptor.as_ref(),
);
}
}
@@ -304,12 +336,46 @@ pub(crate) enum CallBindingError<'db> {
}
impl<'db> CallBindingError<'db> {
fn parameter_span_from_index(
db: &'db dyn Db,
callable_ty: Type<'db>,
parameter_index: usize,
) -> Option<Span> {
match callable_ty {
Type::FunctionLiteral(function) => {
let function_scope = function.body_scope(db);
let mut span = Span::from(function_scope.file(db));
let node = function_scope.node(db);
if let Some(func_def) = node.as_function() {
let range = func_def
.parameters
.iter()
.nth(parameter_index)
.map(|param| param.range())
.unwrap_or(func_def.parameters.range);
span = span.with_range(range);
Some(span)
} else {
None
}
}
Type::Callable(CallableType::BoundMethod(bound_method)) => {
Self::parameter_span_from_index(
db,
Type::FunctionLiteral(bound_method.function(db)),
parameter_index,
)
}
_ => None,
}
}
pub(super) fn report_diagnostic(
&self,
context: &InferContext<'db>,
node: ast::AnyNodeRef,
callable_ty: Type<'db>,
callable_name: Option<&str>,
callable_descriptor: Option<&CallableDescriptor>,
) {
match self {
Self::InvalidArgumentType {
@@ -319,23 +385,13 @@ impl<'db> CallBindingError<'db> {
provided_ty,
} => {
let mut messages = vec![];
if let Some(func_lit) = callable_ty.into_function_literal() {
let func_lit_scope = func_lit.body_scope(context.db());
let mut span = Span::from(func_lit_scope.file(context.db()));
let node = func_lit_scope.node(context.db());
if let Some(func_def) = node.as_function() {
let range = func_def
.parameters
.iter()
.nth(parameter.index)
.map(|param| param.range())
.unwrap_or(func_def.parameters.range);
span = span.with_range(range);
messages.push(SecondaryDiagnosticMessage::new(
span,
"parameter declared in function definition here",
));
}
if let Some(span) =
Self::parameter_span_from_index(context.db(), callable_ty, parameter.index)
{
messages.push(SecondaryDiagnosticMessage::new(
span,
"parameter declared in function definition here",
));
}
let provided_ty_display = provided_ty.display(context.db());
@@ -346,8 +402,8 @@ impl<'db> CallBindingError<'db> {
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}`")
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
format!(" of {kind} `{name}`")
} else {
String::new()
}
@@ -367,8 +423,8 @@ impl<'db> CallBindingError<'db> {
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}`")
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
format!(" to {kind} `{name}`")
} else {
String::new()
}
@@ -383,8 +439,8 @@ impl<'db> CallBindingError<'db> {
node,
format_args!(
"No argument{s} provided for required parameter{s} {parameters}{}",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
format!(" of {kind} `{name}`")
} else {
String::new()
}
@@ -401,8 +457,8 @@ impl<'db> CallBindingError<'db> {
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}`")
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
format!(" of {kind} `{name}`")
} else {
String::new()
}
@@ -419,8 +475,8 @@ impl<'db> CallBindingError<'db> {
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}`")
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
format!(" of {kind} `{name}`")
} else {
String::new()
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,4 @@
use crate::types::{
todo_type, Class, ClassLiteralType, DynamicType, KnownClass, KnownInstanceType, Type,
};
use crate::types::{todo_type, Class, DynamicType, KnownClass, KnownInstanceType, Type};
use crate::Db;
use itertools::Either;
@@ -10,28 +8,28 @@ use itertools::Either;
/// all types that would be invalid to have as a class base are
/// transformed into [`ClassBase::unknown`]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
pub enum ClassBase<'db> {
pub(crate) enum ClassBase<'db> {
Dynamic(DynamicType),
Class(Class<'db>),
}
impl<'db> ClassBase<'db> {
pub const fn any() -> Self {
pub(crate) const fn any() -> Self {
Self::Dynamic(DynamicType::Any)
}
pub const fn unknown() -> Self {
pub(crate) const fn unknown() -> Self {
Self::Dynamic(DynamicType::Unknown)
}
pub const fn is_dynamic(self) -> bool {
pub(crate) 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 {
pub(crate) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
struct Display<'db> {
base: ClassBase<'db>,
db: &'db dyn Db,
@@ -54,9 +52,7 @@ impl<'db> ClassBase<'db> {
KnownClass::Object
.to_class_literal(db)
.into_class_literal()
.map_or(Self::unknown(), |ClassLiteralType { class }| {
Self::Class(class)
})
.map_or(Self::unknown(), |literal| Self::Class(literal.class()))
}
/// Attempt to resolve `ty` into a `ClassBase`.
@@ -65,7 +61,7 @@ impl<'db> ClassBase<'db> {
pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
match ty {
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
Type::ClassLiteral(literal) => Some(Self::Class(literal.class())),
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?

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