Compare commits

...

33 Commits

Author SHA1 Message Date
Jack O'Connor
84b07e0f23 refactor globals_by_scope and nonlocals_by_scope into not_locals_by_scope and NotLocalVariableKind 2025-07-09 18:35:10 -07:00
Jack O'Connor
eb67413a3e multiple nonlocal declarations of the same variable 2025-07-09 18:30:41 -07:00
Jack O'Connor
b31102a9ee use a for-loop instead of loop/break 2025-07-09 18:08:50 -07:00
Jack O'Connor
d473f0e1dc general test reorganization 2025-07-09 17:53:54 -07:00
Jack O'Connor
db61d3c69c missing period
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-09 17:03:06 -07:00
Jack O'Connor
7f3cd6352e remove a reference to "infer_place_load only looks at bindings"
That's true, but I think it's actually a bug in `infer_place_load`:
https://github.com/astral-sh/ty/issues/793
2025-07-09 11:35:02 -07:00
Jack O'Connor
71cd7bb170 clear a TODO in final.md that now fails as expected 2025-07-09 11:31:38 -07:00
Jack O'Connor
062342c03b [ty] add support for nonlocal statements 2025-07-09 11:31:38 -07:00
GiGaGon
a18f76158d [flake8-bandit] Make example error out-of-the-box (S412) (#19241)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #18972

This PR makes [suspicious-httpoxy-import
(S412)](https://docs.astral.sh/ruff/rules/suspicious-httpoxy-import/#suspicious-httpoxy-import-s412)'s
example error out-of-the-box. Since the checked imports are classes
instead of modules, the example isn't valid. See #19009 for more details
```
PS ~>py -c "import wsgiref.handlers.CGIHandler"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import wsgiref.handlers.CGIHandler
ModuleNotFoundError: No module named 'wsgiref.handlers.CGIHandler'; 'wsgiref.handlers' is not a package
PS ~>py -c "from wsgiref.handlers import CGIHandler"
PS ~>
```

[Old example](https://play.ruff.rs/bf48c901-6a46-4795-ba1d-c6af79d5c96e)
```py
import wsgiref.handlers.CGIHandler
```

[New example](https://play.ruff.rs/1f0e1e60-1f0f-484a-9a17-2d0290a68f2a)
```py
from wsgiref.handlers import CGIHandler
```

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-09 14:25:27 -04:00
GiGaGon
8f400bb37a [pydoclint] Make example error out-of-the-box (DOC501) (#19218)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #18972

This PR makes [docstring-missing-exception
(DOC501)](https://docs.astral.sh/ruff/rules/docstring-missing-exception/#docstring-missing-exception-doc501)'s
example error out-of-the-box. Since the exceptions in the function body
need to undergo name resolution to figure out if one of them is
`NotImplementedError`, `DOC501` won't lint if the raised name is not
defined. This could be considered a limitation, but should be fine since
`F821` already covers undefined names. I did discover a different edge
case, but it's not relevant to the example.

[Old example](https://play.ruff.rs/d213e87d-e5c7-49d8-a908-931f61f06055)
```py
def calculate_speed(distance: float, time: float) -> float:
    """Calculate speed as distance divided by time.

    Args:
        distance: Distance traveled.
        time: Time spent traveling.

    Returns:
        Speed as distance divided by time.
    """
    try:
        return distance / time
    except ZeroDivisionError as exc:
        raise FasterThanLightError from exc
```

[New example](https://play.ruff.rs/cb41e0b7-b950-4fa0-842d-cecab9c8e842)
```py
class FasterThanLightError(ArithmeticError): ...


def calculate_speed(distance: float, time: float) -> float:
    """Calculate speed as distance divided by time.

    Args:
        distance: Distance traveled.
        time: Time spent traveling.

    Returns:
        Speed as distance divided by time.
    """
    try:
        return distance / time
    except ZeroDivisionError as exc:
        raise FasterThanLightError from exc
```

The "Use instead" section was also updated similarly.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-09 12:59:31 -04:00
Andrew Gallant
1eff0300d3 [ty] Add "kind" to completion suggestions
This makes use of the new `Type` field on `Completion` to figure out the
"kind" of a `Completion`.

The mapping here is perhaps a little suspect for some cases.

Closes astral-sh/ty#775
2025-07-09 12:03:56 -04:00
Andrew Gallant
fea84e8777 [ty] Add type information to all_members API
Since we generally need (so far) to get the type information of each
suggestion to figure out its boundness anyway, we might as well expose
it here. Completions want to use this information to enhance the
metadata on each suggestion for a more pleasant user experience.

For the most part, this was pretty straight-forward. The most exciting
part was in computing the types for instance attributes. I'm not 100%
sure it's correct or is the best way to do it.
2025-07-09 12:03:56 -04:00
Andrew Gallant
79fe538458 [ty] Expand API of all_members to return a struct
This commit doesn't change any behavior, but makes it so `all_members`
returns a `Vec<Member>` instead of `Vec<Name>`, where a `Member`
contains a `Name`. This gives us an expansion point to include other
data (such as the type of the `Name`).
2025-07-09 12:03:56 -04:00
David Peter
f7234cb474 [ty] Ecosystem analyzer PR comment workflow (#19237)
## Summary

Add PR comment workflow as a prerequisite for
https://github.com/astral-sh/ruff/pull/19234

## Test Plan

Not yet tested. Need to merge this first.
2025-07-09 18:02:05 +02:00
Micha Reiser
35a33f045e [ty] Merge ty_macros into ruff_macros (#19229) 2025-07-09 11:28:21 +00:00
Matthew Mckee
f32f7a3b48 [ty] Fix ClassLiteral.into_callable for dataclasses (#19192)
## Summary

Change `ClassLiteral.into_callable` to also look for `__init__` functions
of type `Type::Callable` (such as synthesized `__init__` functions of
dataclasses).

Fixes https://github.com/astral-sh/ty/issues/760

## Test Plan

Add subtype test

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-09 10:04:55 +02:00
David Peter
68106dd631 [ty] dataclasses.field support (#19140)
## Summary

Add an initial set of tests for `dataclasses.field`.
2025-07-09 09:18:08 +02:00
David Peter
ab3af924ef [ty] Fix panic for attribute expressions with empty value (#19069)
## Summary

closes https://github.com/astral-sh/ty/issues/738

## Test Plan

Added corpus test
2025-07-09 08:46:33 +02:00
Matthew Mckee
05139a323b [ty] Return CallableType from BoundMethodType.into_callable_type (#19193) 2025-07-08 20:33:43 +01:00
Dan Parizher
5eb5ec987d [flake8-bugbear] Support non-context-manager calls in B017 (#19063)
## Summary

Fixes #19050

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-07-08 15:04:55 -04:00
David Peter
1a099886ab [ty] Improved diagnostic for reassignments of Final symbols (#19214)
## Summary

Implement [this
suggestion](https://github.com/astral-sh/ruff/pull/19178#discussion_r2192658146)
by @AlexWaygood.


![image](https://github.com/user-attachments/assets/f183d691-ef6e-43a2-b005-3a32205bc408)
2025-07-08 20:29:07 +02:00
David Peter
a8f2c26143 [ty] Use full range for assignment definitions (#19211)
## Summary

Fix the `full_range` function for (annotated) assignment definition
kinds.

## Test Plan

Update snapshot tests
2025-07-08 19:51:09 +02:00
Junhson Jean-Baptiste
fda188953f [pylint] Update missing-maxsplit-arg docs and error to suggest proper usage (PLC0207) (#18949)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Fix #18383 by updating the documentation and error message to explain
that users should use `rsplit` in order to access the last element of
the result with `maxsplit=1`

## Test Plan

<!-- How was it tested? -->

Only documentation and an error message was changed. As such, snapshots
were updated to reflect the new error message. With this change, all
existing tests pass.
2025-07-08 12:53:23 -04:00
Ibraheem Ahmed
546f1b7b39 [ty] Add set -eu to mypy-primer script (#19212)
## Summary

So that the CI job fails if ty panics.
2025-07-08 12:16:09 -04:00
Alex Waygood
7533a0bfdb [ty] Upgrade mypy_primer (#19207) 2025-07-08 15:56:54 +01:00
Charlie Marsh
3ee3434187 Auto-generate environment variable references for ty (#19205)
## Summary

This PR mirrors the environment variable implementation we have in uv:
efc361223c/crates/uv-static/src/env_vars.rs (L6-L7).

See: https://github.com/astral-sh/ty/issues/773.
2025-07-08 10:48:31 -04:00
David Peter
149350bf39 [ty] Enforce typing.Final (#19178)
## Summary

Emit a diagnostic when a `Final`-qualified symbol is modified. This
first iteration only works for name targets. Tests with TODO comments
were added for attribute assignments as well.

related ticket: https://github.com/astral-sh/ty/issues/158

## Ecosystem impact

Correctly identified [modification of a `Final`
symbol](7b4164a5f2/sphinx/__init__.py (L44))
(behind a `# type: ignore`):
```diff
- warning[unused-ignore-comment] sphinx/__init__.py:44:56: Unused blanket `type: ignore` directive
```
And the same
[here](5471a37e82/src/trio/_core/_run.py (L128)):
```diff
- warning[unused-ignore-comment] src/trio/_core/_run.py:128:45: Unused blanket `type: ignore` directive
```

## Test Plan

New Markdown tests
2025-07-08 16:26:09 +02:00
Aria Desires
6a42d28867 [ty] Do not report settings diagnostics in check_file (#19206)
This is the trivial first part of
https://github.com/astral-sh/ty/issues/613

Ideally we should surface these elsewhere, but this is definitely Not
the place to surface them.
2025-07-08 10:18:32 -04:00
David Peter
ce2bdb9357 [ty] Conditionally defined dataclass fields (#19197)
## Summary

Fixes a bug where conditionally defined dataclass fields were previously
ignored.

Thanks to @lipefree for reporting this.

## Test Plan

New Markdown tests
2025-07-08 16:16:50 +02:00
GiGaGon
d78d10dd94 [pycodestyle] Make example not raise unnecessary SyntaxError (E114) (#19190)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #18972

This PR makes [indentation-with-invalid-multiple-comment
(E114)](https://docs.astral.sh/ruff/rules/indentation-with-invalid-multiple-comment/#indentation-with-invalid-multiple-comment-e114)'s
example not raise a syntax error by adding a 4 space indented `...`. The
example still gave `E114` without this, but adding the `...` both makes
the change in indentation of the comment clearer, and makes it not give
a `SyntaxError`.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-08 10:00:14 -04:00
GiGaGon
36276143be [pycodestyle] Make example error out-of-the-box (E272) (#19191)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #18972

This PR makes [multiple-spaces-before-keyword
(E272)](https://docs.astral.sh/ruff/rules/multiple-spaces-before-keyword/#multiple-spaces-before-keyword-e272)'s
example error out-of-the-box. Since `True` is also a keyword, the old
example raises `E271` instead.

[Old example](https://play.ruff.rs/23ec3774-5038-471c-be3f-1c1e36f85cbb)
```py
True  and False
```

[New example](https://play.ruff.rs/d77432e2-fd99-4db2-9cd0-bc08675c0aca)
```py
x  and y
```

The "Use instead" section was also updated similarly.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-08 09:58:04 -04:00
Brent Westbrook
2643dc5b7a Rename Diagnostic::syntax_error methods, separate Ord implementation (#19179)
## Summary

This PR addresses some additional feedback on #19053:

- Renaming the `syntax_error` methods to `invalid_syntax` to match the
lint id
- Moving the standalone `diagnostic_from_violation` function to
`Violation::into_diagnostic`
- Removing the `Ord` and `PartialOrd` implementations from `Diagnostic`
in favor of `Diagnostic::start_ordering`

## Test Plan

Existing tests

## Additional Follow-ups

Besides these, I also put the following comments on my todo list, but
they seemed like they might be big enough to have their own PRs:

- [Use `LintId::IOError` for IO
errors](https://github.com/astral-sh/ruff/pull/19053#discussion_r2189425922)
- [Move `Fix` and
`Edit`](https://github.com/astral-sh/ruff/pull/19053#discussion_r2189448647)
- [Avoid so many
unwraps](https://github.com/astral-sh/ruff/pull/19053#discussion_r2189465980)
2025-07-08 09:54:19 -04:00
justin
738692baff [ty] Fix __setattr__ call check precedence during attribute assignment (#18347)
## Summary

Related:

- https://github.com/astral-sh/ty/issues/111
- https://github.com/astral-sh/ruff/pull/17974#discussion_r2108527106

Previously, when validating an attribute assignment, a `__setattr__`
call check was only done if the attribute wasn't found as either a class
member or instance member

This PR changes the `__setattr__` call check to be attempted first,
prior to the "[normal
mechanism](https://docs.python.org/3/reference/datamodel.html#object.__setattr__)",
as a defined `__setattr__` should take precedence over setting an
attribute on the instance dictionary directly.

if the return type of `__setattr__` is `Never`, an `invalid-assignment`
diagnostic is emitted

Once this is merged, a subsequent PR will synthesize a `__setattr__`
method with a `Never` return type for frozen dataclasses.

## Test Plan

Existing tests + mypy_primer

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-08 15:34:34 +02:00
94 changed files with 2791 additions and 643 deletions

View File

@@ -0,0 +1,85 @@
name: PR comment (ty ecosystem-analyzer)
on: # zizmor: ignore[dangerous-triggers]
workflow_run:
workflows: [ty ecosystem-analyzer]
types: [completed]
workflow_dispatch:
inputs:
workflow_run_id:
description: The ty ecosystem-analyzer workflow that triggers the workflow run
required: true
jobs:
comment:
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download PR number
with:
name: pr-number
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true
- name: Parse pull request number
id: pr-number
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download comment.md"
id: download-comment
if: steps.pr-number.outputs.pr-number
with:
name: comment.md
workflow: ty-ecosystem-analyzer.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/comment
workflow_conclusion: completed
if_no_artifact_found: ignore
allow_forks: true
- name: Generate comment content
id: generate-comment
if: ${{ steps.download-comment.outputs.found_artifact == 'true' }}
run: |
# Guard against malicious ty ecosystem-analyzer results that symlink to a secret
# file on this runner
if [[ -L pr/comment/comment.md ]]
then
echo "Error: comment.md cannot be a symlink"
exit 1
fi
# Note: this identifier is used to find the comment to update on subsequent runs
echo '<!-- generated-comment ty ecosystem-analyzer -->' > comment.md
echo >> comment.md
cat pr/comment/comment.md >> comment.md
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.md >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
issue-number: ${{ steps.pr-number.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: "<!-- generated-comment ty ecosystem-analyzer -->"
- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}
body-path: comment.md
edit-mode: replace

View File

@@ -6,7 +6,7 @@ exclude: |
crates/ty_vendored/vendor/.*|
crates/ty_project/resources/.*|
crates/ty_python_semantic/resources/corpus/.*|
crates/ty/docs/(configuration|rules|cli).md|
crates/ty/docs/(configuration|rules|cli|environment).md|
crates/ruff_benchmark/resources/.*|
crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*|

12
Cargo.lock generated
View File

@@ -2874,6 +2874,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"tracing-subscriber",
"ty_static",
"web-time",
"zip",
]
@@ -2917,6 +2918,7 @@ dependencies = [
"tracing-subscriber",
"ty",
"ty_project",
"ty_static",
"url",
]
@@ -4165,6 +4167,7 @@ dependencies = [
"ty_project",
"ty_python_semantic",
"ty_server",
"ty_static",
"wild",
]
@@ -4268,6 +4271,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"ty_python_semantic",
"ty_static",
"ty_test",
"ty_vendored",
]
@@ -4299,6 +4303,13 @@ dependencies = [
"ty_vendored",
]
[[package]]
name = "ty_static"
version = "0.0.1"
dependencies = [
"ruff_macros",
]
[[package]]
name = "ty_test"
version = "0.0.0"
@@ -4327,6 +4338,7 @@ dependencies = [
"toml",
"tracing",
"ty_python_semantic",
"ty_static",
"ty_vendored",
]

View File

@@ -44,6 +44,7 @@ ty_ide = { path = "crates/ty_ide" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
ty_server = { path = "crates/ty_server" }
ty_static = { path = "crates/ty_static" }
ty_test = { path = "crates/ty_test" }
ty_vendored = { path = "crates/ty_vendored" }
@@ -83,7 +84,7 @@ get-size2 = { version = "0.5.0", features = [
"derive",
"smallvec",
"hashbrown",
"compact-str"
"compact-str",
] }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
@@ -173,7 +174,7 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features =
"env-filter",
"fmt",
"ansi",
"smallvec"
"smallvec",
] }
tryfn = { version = "0.2.1" }
typed-arena = { version = "2.0.2" }
@@ -183,11 +184,7 @@ unicode-width = { version = "0.2.0" }
unicode_names2 = { version = "1.2.2" }
unicode-normalization = { version = "0.1.23" }
url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = [
"v4",
"fast-rng",
"macro-diagnostics",
] }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] }
walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.92" }
wasm-bindgen-test = { version = "0.3.42" }
@@ -222,8 +219,8 @@ must_use_candidate = "allow"
similar_names = "allow"
single_match_else = "allow"
too_many_lines = "allow"
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
needless_raw_string_hashes = "allow"
# Disallowed restriction lints

View File

@@ -681,7 +681,7 @@ mod tests {
UnsafeFixes::Enabled,
)
.unwrap();
if diagnostics.inner.iter().any(Diagnostic::is_syntax_error) {
if diagnostics.inner.iter().any(Diagnostic::is_invalid_syntax) {
parse_errors.push(path.clone());
}
paths.push(path);

View File

@@ -9,15 +9,15 @@ use ignore::Error;
use log::{debug, error, warn};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use ruff_linter::message::diagnostic_from_violation;
use rustc_hash::FxHashMap;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::panic::catch_unwind;
use ruff_linter::package::PackageRoot;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{LinterSettings, flags};
use ruff_linter::{IOError, fs, warn_user_once};
use ruff_linter::{IOError, Violation, fs, warn_user_once};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::TextRange;
use ruff_workspace::resolver::{
@@ -129,11 +129,7 @@ pub(crate) fn check(
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
Diagnostics::new(
vec![diagnostic_from_violation(
IOError { message },
TextRange::default(),
&dummy,
)],
vec![IOError { message }.into_diagnostic(TextRange::default(), &dummy)],
FxHashMap::default(),
)
} else {
@@ -166,7 +162,9 @@ pub(crate) fn check(
|a, b| (a.0 + b.0, a.1 + b.1),
);
all_diagnostics.inner.sort();
all_diagnostics
.inner
.sort_by(Diagnostic::ruff_start_ordering);
// Store the caches.
caches.persist()?;

View File

@@ -1,6 +1,7 @@
use std::path::Path;
use anyhow::Result;
use ruff_db::diagnostic::Diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::packaging;
use ruff_linter::settings::flags;
@@ -52,6 +53,8 @@ pub(crate) fn check_stdin(
noqa,
fix_mode,
)?;
diagnostics.inner.sort_unstable();
diagnostics
.inner
.sort_unstable_by(Diagnostic::ruff_start_ordering);
Ok(diagnostics)
}

View File

@@ -13,13 +13,13 @@ use log::{debug, warn};
use ruff_db::diagnostic::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
use ruff_linter::message::{create_syntax_error_diagnostic, diagnostic_from_violation};
use ruff_linter::message::create_syntax_error_diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{LinterSettings, flags};
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::{IOError, fs};
use ruff_linter::{IOError, Violation, fs};
use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
use ruff_source_file::SourceFileBuilder;
@@ -62,13 +62,12 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let source_file = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![diagnostic_from_violation(
vec![
IOError {
message: err.to_string(),
},
TextRange::default(),
&source_file,
)],
}
.into_diagnostic(TextRange::default(), &source_file),
],
FxHashMap::default(),
)
} else {

View File

@@ -20,6 +20,7 @@ ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true, features = ["get-size"] }
ruff_text_size = { workspace = true }
ty_static = { workspace = true }
anstyle = { workspace = true }
arc-swap = { workspace = true }

View File

@@ -83,7 +83,7 @@ impl Diagnostic {
///
/// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic
/// message.
pub fn syntax_error(
pub fn invalid_syntax(
span: impl Into<Span>,
message: impl IntoDiagnosticMessage,
range: impl Ranged,
@@ -365,7 +365,7 @@ impl Diagnostic {
}
/// Returns `true` if `self` is a syntax error message.
pub fn is_syntax_error(&self) -> bool {
pub fn is_invalid_syntax(&self) -> bool {
self.id().is_invalid_syntax()
}
@@ -381,7 +381,7 @@ impl Diagnostic {
/// Returns the URL for the rule documentation, if it exists.
pub fn to_url(&self) -> Option<String> {
if self.is_syntax_error() {
if self.is_invalid_syntax() {
None
} else {
Some(format!(
@@ -447,20 +447,16 @@ impl Diagnostic {
pub fn expect_range(&self) -> TextRange {
self.range().expect("Expected a range for the primary span")
}
}
impl Ord for Diagnostic {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
}
}
impl PartialOrd for Diagnostic {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(
(self.ruff_source_file()?, self.range()?.start())
.cmp(&(other.ruff_source_file()?, other.range()?.start())),
)
/// Returns the ordering of diagnostics based on the start of their ranges, if they have any.
///
/// Panics if either diagnostic has no primary span, if the span has no range, or if its file is
/// not a `SourceFile`.
pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
(self.expect_ruff_source_file(), self.expect_range().start()).cmp(&(
other.expect_ruff_source_file(),
other.expect_range().start(),
))
}
}

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::PythonVersion;
use rustc_hash::FxHasher;
use std::hash::BuildHasherDefault;
use std::num::NonZeroUsize;
use ty_static::EnvVars;
pub mod diagnostic;
pub mod display;
@@ -50,8 +51,8 @@ pub trait Db: salsa::Database {
/// ty can still spawn more threads for other tasks, e.g. to wait for a Ctrl+C signal or
/// watching the files for changes.
pub fn max_parallelism() -> NonZeroUsize {
std::env::var("TY_MAX_PARALLELISM")
.or_else(|_| std::env::var("RAYON_NUM_THREADS"))
std::env::var(EnvVars::TY_MAX_PARALLELISM)
.or_else(|_| std::env::var(EnvVars::RAYON_NUM_THREADS))
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| {

View File

@@ -13,6 +13,7 @@ license = { workspace = true }
[dependencies]
ty = { workspace = true }
ty_project = { workspace = true, features = ["schemars"] }
ty_static = { workspace = true }
ruff = { workspace = true }
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true, features = ["schemars"] }

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use crate::{
generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference,
generate_ty_options, generate_ty_rules, generate_ty_schema,
generate_ty_env_vars_reference, generate_ty_options, generate_ty_rules, generate_ty_schema,
};
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
@@ -44,5 +44,8 @@ pub(crate) fn main(args: &Args) -> Result<()> {
generate_ty_options::main(&generate_ty_options::Args { mode: args.mode })?;
generate_ty_rules::main(&generate_ty_rules::Args { mode: args.mode })?;
generate_ty_cli_reference::main(&generate_ty_cli_reference::Args { mode: args.mode })?;
generate_ty_env_vars_reference::main(&generate_ty_env_vars_reference::Args {
mode: args.mode,
})?;
Ok(())
}

View File

@@ -0,0 +1,119 @@
//! Generate the environment variables reference from `ty_static::EnvVars`.
use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;
use anyhow::bail;
use pretty_assertions::StrComparison;
use ty_static::EnvVars;
use crate::generate_all::Mode;
#[derive(clap::Args)]
pub(crate) struct Args {
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
let reference_string = generate();
let filename = "environment.md";
let reference_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("crates")
.join("ty")
.join("docs")
.join(filename);
match args.mode {
Mode::DryRun => {
println!("{reference_string}");
}
Mode::Check => match fs::read_to_string(&reference_path) {
Ok(current) => {
if current == reference_string {
println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &reference_string);
bail!(
"{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{comparison}"
);
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
bail!(
"{filename} not found, please run `cargo dev generate-ty-env-vars-reference`"
);
}
Err(err) => {
bail!(
"{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{err}"
);
}
},
Mode::Write => {
// Ensure the docs directory exists
if let Some(parent) = reference_path.parent() {
fs::create_dir_all(parent)?;
}
match fs::read_to_string(&reference_path) {
Ok(current) => {
if current == reference_string {
println!("Up-to-date: {filename}");
} else {
println!("Updating: {filename}");
fs::write(&reference_path, reference_string.as_bytes())?;
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
println!("Updating: {filename}");
fs::write(&reference_path, reference_string.as_bytes())?;
}
Err(err) => {
bail!(
"{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{err}"
);
}
}
}
}
Ok(())
}
fn generate() -> String {
let mut output = String::new();
output.push_str("# Environment variables\n\n");
// Partition and sort environment variables into TY_ and external variables.
let (ty_vars, external_vars): (BTreeSet<_>, BTreeSet<_>) = EnvVars::metadata()
.iter()
.partition(|(var, _)| var.starts_with("TY_"));
output.push_str("ty defines and respects the following environment variables:\n\n");
for (var, doc) in ty_vars {
output.push_str(&render(var, doc));
}
output.push_str("## Externally-defined variables\n\n");
output.push_str("ty also reads the following externally defined environment variables:\n\n");
for (var, doc) in external_vars {
output.push_str(&render(var, doc));
}
output
}
/// Render an environment variable and its documentation.
fn render(var: &str, doc: &str) -> String {
format!("### `{var}`\n\n{doc}\n\n")
}

View File

@@ -18,6 +18,7 @@ mod generate_json_schema;
mod generate_options;
mod generate_rules_table;
mod generate_ty_cli_reference;
mod generate_ty_env_vars_reference;
mod generate_ty_options;
mod generate_ty_rules;
mod generate_ty_schema;
@@ -53,6 +54,8 @@ enum Command {
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions,
GenerateTyOptions(generate_ty_options::Args),
/// Generate environment variables reference for ty.
GenerateTyEnvVarsReference(generate_ty_env_vars_reference::Args),
/// Generate CLI help.
GenerateCliHelp(generate_cli_help::Args),
/// Generate Markdown docs.
@@ -98,6 +101,7 @@ fn main() -> Result<ExitCode> {
Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?,
Command::GenerateOptions => println!("{}", generate_options::generate()),
Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?,
Command::GenerateTyEnvVarsReference(args) => generate_ty_env_vars_reference::main(&args)?,
Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,
Command::GenerateDocs(args) => generate_docs::main(&args)?,
Command::PrintAST(args) => print_ast::main(&args)?,

View File

@@ -1,10 +1,10 @@
"""
Should emit:
B017 - on lines 23 and 41
B017 - on lines 24, 28, 46, 49, 52, and 58
"""
import asyncio
import unittest
import pytest
import pytest, contextlib
CONSTANT = True

View File

@@ -0,0 +1,28 @@
"""
Should emit:
B017 - on lines 20, 21, 25, and 26
"""
import unittest
import pytest
def something_else() -> None:
for i in (1, 2, 3):
print(i)
class Foo:
pass
class Foobar(unittest.TestCase):
def call_form_raises(self) -> None:
self.assertRaises(Exception, something_else)
self.assertRaises(BaseException, something_else)
def test_pytest_call_form() -> None:
pytest.raises(Exception, something_else)
pytest.raises(BaseException, something_else)
pytest.raises(Exception, something_else, match="hello")

View File

@@ -7,7 +7,9 @@ use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_optional_as_none_in_union_enabled;
use crate::preview::{
is_assert_raises_exception_call_enabled, is_optional_as_none_in_union_enabled,
};
use crate::registry::Rule;
use crate::rules::{
airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
@@ -1236,6 +1238,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::NonOctalPermissions) {
ruff::rules::non_octal_permissions(checker, call);
}
if checker.is_rule_enabled(Rule::AssertRaisesException)
&& is_assert_raises_exception_call_enabled(checker.settings())
{
flake8_bugbear::rules::assert_raises_exception_call(checker, call);
}
}
Expr::Dict(dict) => {
if checker.any_rule_enabled(&[

View File

@@ -64,7 +64,6 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::annotation::AnnotationContext;
use crate::docstrings::extraction::ExtractionTarget;
use crate::importer::{ImportRequest, Importer, ResolutionError};
use crate::message::diagnostic_from_violation;
use crate::noqa::NoqaMapping;
use crate::package::PackageRoot;
use crate::preview::is_undefined_export_in_dunder_init_enabled;
@@ -671,7 +670,11 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::InvalidStarExpression
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
| SemanticSyntaxErrorKind::DuplicateParameter(_)
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => {
self.semantic_errors.borrow_mut().push(error);
}
}
@@ -3158,7 +3161,7 @@ impl<'a> LintContext<'a> {
) -> DiagnosticGuard<'chk, 'a> {
DiagnosticGuard {
context: self,
diagnostic: Some(diagnostic_from_violation(kind, range, &self.source_file)),
diagnostic: Some(kind.into_diagnostic(range, &self.source_file)),
rule: T::rule(),
}
}
@@ -3177,7 +3180,7 @@ impl<'a> LintContext<'a> {
if self.is_rule_enabled(rule) {
Some(DiagnosticGuard {
context: self,
diagnostic: Some(diagnostic_from_violation(kind, range, &self.source_file)),
diagnostic: Some(kind.into_diagnostic(range, &self.source_file)),
rule,
})
} else {

View File

@@ -618,8 +618,7 @@ mod tests {
use crate::fix::edits::{
add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon,
};
use crate::message::diagnostic_from_violation;
use crate::{Edit, Fix, Locator};
use crate::{Edit, Fix, Locator, Violation};
/// Parse the given source using [`Mode::Module`] and return the first statement.
fn parse_first_stmt(source: &str) -> Result<Stmt> {
@@ -750,8 +749,8 @@ x = 1 \
let diag = {
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
let mut iter = edits.into_iter();
let mut diagnostic = diagnostic_from_violation(
MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary.
// The choice of rule here is arbitrary.
let mut diagnostic = MissingNewlineAtEndOfFile.into_diagnostic(
TextRange::default(),
&SourceFileBuilder::new("<filename>", "<code>").finish(),
);

View File

@@ -172,11 +172,10 @@ mod tests {
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{Ranged, TextSize};
use crate::Locator;
use crate::fix::{FixResult, apply_fixes};
use crate::message::diagnostic_from_violation;
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
use crate::{Edit, Fix};
use crate::{Locator, Violation};
use ruff_db::diagnostic::Diagnostic;
fn create_diagnostics(
@@ -187,8 +186,7 @@ mod tests {
edit.into_iter()
.map(|edit| {
// The choice of rule here is arbitrary.
let mut diagnostic = diagnostic_from_violation(
MissingNewlineAtEndOfFile,
let mut diagnostic = MissingNewlineAtEndOfFile.into_diagnostic(
edit.range(),
&SourceFileBuilder::new(filename, source).finish(),
);

View File

@@ -514,7 +514,7 @@ pub fn lint_only(
LinterResult {
has_valid_syntax: parsed.has_valid_syntax(),
has_no_syntax_errors: !diagnostics.iter().any(Diagnostic::is_syntax_error),
has_no_syntax_errors: !diagnostics.iter().any(Diagnostic::is_invalid_syntax),
diagnostics,
}
}
@@ -629,7 +629,7 @@ pub fn lint_fix<'a>(
if iterations == 0 {
has_valid_syntax = parsed.has_valid_syntax();
has_no_syntax_errors = !diagnostics.iter().any(Diagnostic::is_syntax_error);
has_no_syntax_errors = !diagnostics.iter().any(Diagnostic::is_invalid_syntax);
} else {
// If the source code had no syntax errors on the first pass, but
// does on a subsequent pass, then we've introduced a

View File

@@ -24,7 +24,6 @@ pub use sarif::SarifEmitter;
pub use text::TextEmitter;
use crate::Fix;
use crate::Violation;
use crate::registry::Rule;
mod azure;
@@ -108,28 +107,6 @@ where
diagnostic
}
// TODO(brent) We temporarily allow this to avoid updating all of the call sites to add
// references. I expect this method to go away or change significantly with the rest of the
// diagnostic refactor, but if it still exists in this form at the end of the refactor, we
// should just update the call sites.
#[expect(clippy::needless_pass_by_value)]
pub fn diagnostic_from_violation<T: Violation>(
kind: T,
range: TextRange,
file: &SourceFile,
) -> Diagnostic {
create_lint_diagnostic(
Violation::message(&kind),
Violation::fix_title(&kind),
range,
None,
None,
file.clone(),
None,
T::rule(),
)
}
struct MessageWithLocation<'a> {
message: &'a Diagnostic,
start_location: LineColumn,

View File

@@ -1225,8 +1225,6 @@ mod tests {
use ruff_source_file::{LineEnding, SourceFileBuilder};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::Edit;
use crate::message::diagnostic_from_violation;
use crate::noqa::{
Directive, LexicalError, NoqaLexerOutput, NoqaMapping, add_noqa_inner, lex_codes,
lex_file_exemption, lex_inline_noqa,
@@ -1234,6 +1232,7 @@ mod tests {
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
use crate::{Edit, Violation};
use crate::{Locator, generate_noqa_edits};
fn assert_lexed_ranges_match_slices(
@@ -2832,10 +2831,10 @@ mod tests {
assert_eq!(output, format!("{contents}"));
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
let messages = [diagnostic_from_violation(
UnusedVariable {
name: "x".to_string(),
},
let messages = [UnusedVariable {
name: "x".to_string(),
}
.into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
)];
@@ -2856,15 +2855,14 @@ mod tests {
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
let messages = [
diagnostic_from_violation(
AmbiguousVariableName("x".to_string()),
AmbiguousVariableName("x".to_string()).into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
),
diagnostic_from_violation(
UnusedVariable {
name: "x".to_string(),
},
UnusedVariable {
name: "x".to_string(),
}
.into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
),
@@ -2887,15 +2885,14 @@ mod tests {
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
let messages = [
diagnostic_from_violation(
AmbiguousVariableName("x".to_string()),
AmbiguousVariableName("x".to_string()).into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
),
diagnostic_from_violation(
UnusedVariable {
name: "x".to_string(),
},
UnusedVariable {
name: "x".to_string(),
}
.into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
),
@@ -2931,11 +2928,8 @@ print(
"#;
let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect();
let source_file = SourceFileBuilder::new(path.to_string_lossy(), source).finish();
let messages = [diagnostic_from_violation(
PrintfStringFormatting,
TextRange::new(12.into(), 79.into()),
&source_file,
)];
let messages = [PrintfStringFormatting
.into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)];
let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits(
path,
@@ -2964,11 +2958,8 @@ foo;
bar =
";
let source_file = SourceFileBuilder::new(path.to_string_lossy(), source).finish();
let messages = [diagnostic_from_violation(
UselessSemicolon,
TextRange::new(4.into(), 5.into()),
&source_file,
)];
let messages =
[UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)];
let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits(

View File

@@ -125,3 +125,8 @@ pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19063
pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -6,11 +6,10 @@ use ruff_text_size::{TextRange, TextSize};
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::SourceFile;
use crate::IOError;
use crate::message::diagnostic_from_violation;
use crate::registry::Rule;
use crate::rules::ruff::rules::InvalidPyprojectToml;
use crate::settings::LinterSettings;
use crate::{IOError, Violation};
/// RUF200
pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings) -> Vec<Diagnostic> {
@@ -30,11 +29,8 @@ pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings)
source_file.name(),
);
if settings.rules.enabled(Rule::IOError) {
let diagnostic = diagnostic_from_violation(
IOError { message },
TextRange::default(),
source_file,
);
let diagnostic =
IOError { message }.into_diagnostic(TextRange::default(), source_file);
messages.push(diagnostic);
} else {
warn!(
@@ -56,11 +52,8 @@ pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings)
if settings.rules.enabled(Rule::InvalidPyprojectToml) {
let toml_err = err.message().to_string();
let diagnostic = diagnostic_from_violation(
InvalidPyprojectToml { message: toml_err },
range,
source_file,
);
let diagnostic =
InvalidPyprojectToml { message: toml_err }.into_diagnostic(range, source_file);
messages.push(diagnostic);
}

View File

@@ -283,7 +283,7 @@ impl Violation for SuspiciousXmlrpcImport {
///
/// ## Example
/// ```python
/// import wsgiref.handlers.CGIHandler
/// from wsgiref.handlers import CGIHandler
/// ```
///
/// ## References

View File

@@ -16,11 +16,14 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::settings::types::PreviewMode;
use ruff_python_ast::PythonVersion;
#[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))]
#[test_case(Rule::AssertFalse, Path::new("B011.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))]
#[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))]
#[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))]
#[test_case(Rule::ClassAsDataStructure, Path::new("class_as_data_structure.py"))]
@@ -174,4 +177,23 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
use std::fmt;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr, WithItem};
use ruff_python_ast::{self as ast, Arguments, Expr, WithItem};
use ruff_text_size::Ranged;
use crate::Violation;
@@ -56,6 +56,48 @@ impl fmt::Display for ExceptionKind {
}
}
fn detect_blind_exception(
semantic: &ruff_python_semantic::SemanticModel<'_>,
func: &Expr,
arguments: &Arguments,
) -> Option<ExceptionKind> {
let is_assert_raises = matches!(
func,
&Expr::Attribute(ast::ExprAttribute { ref attr, .. }) if attr.as_str() == "assertRaises"
);
let is_pytest_raises = semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"]));
if !(is_assert_raises || is_pytest_raises) {
return None;
}
if is_pytest_raises {
if arguments.find_keyword("match").is_some() {
return None;
}
if arguments
.find_positional(1)
.is_some_and(|arg| matches!(arg, Expr::StringLiteral(_) | Expr::BytesLiteral(_)))
{
return None;
}
}
let first_arg = arguments.args.first()?;
let builtin_symbol = semantic.resolve_builtin_symbol(first_arg)?;
match builtin_symbol {
"Exception" => Some(ExceptionKind::Exception),
"BaseException" => Some(ExceptionKind::BaseException),
_ => None,
}
}
/// B017
pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) {
for item in items {
@@ -73,33 +115,31 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) {
continue;
}
let [arg] = &*arguments.args else {
continue;
};
let semantic = checker.semantic();
let Some(builtin_symbol) = semantic.resolve_builtin_symbol(arg) else {
continue;
};
let exception = match builtin_symbol {
"Exception" => ExceptionKind::Exception,
"BaseException" => ExceptionKind::BaseException,
_ => continue,
};
if !(matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
|| semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["pytest", "raises"])
})
&& arguments.find_keyword("match").is_none())
if let Some(exception) =
detect_blind_exception(checker.semantic(), func.as_ref(), arguments)
{
continue;
checker.report_diagnostic(AssertRaisesException { exception }, item.range());
}
checker.report_diagnostic(AssertRaisesException { exception }, item.range());
}
}
/// B017 (call form)
pub(crate) fn assert_raises_exception_call(
checker: &Checker,
ast::ExprCall {
func,
arguments,
range,
node_index: _,
}: &ast::ExprCall,
) {
let semantic = checker.semantic();
if arguments.args.len() < 2 && arguments.find_argument("func", 1).is_none() {
return;
}
if let Some(exception) = detect_blind_exception(semantic, func.as_ref(), arguments) {
checker.report_diagnostic(AssertRaisesException { exception }, *range);
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017.py:23:14: B017 Do not assert blind exception: `Exception`
B017_0.py:23:14: B017 Do not assert blind exception: `Exception`
|
21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None:
@@ -10,7 +10,7 @@ B017.py:23:14: B017 Do not assert blind exception: `Exception`
24 | raise Exception("Evil I say!")
|
B017.py:27:14: B017 Do not assert blind exception: `BaseException`
B017_0.py:27:14: B017 Do not assert blind exception: `BaseException`
|
26 | def also_evil_raises(self) -> None:
27 | with self.assertRaises(BaseException):
@@ -18,7 +18,7 @@ B017.py:27:14: B017 Do not assert blind exception: `BaseException`
28 | raise Exception("Evil I say!")
|
B017.py:45:10: B017 Do not assert blind exception: `Exception`
B017_0.py:45:10: B017 Do not assert blind exception: `Exception`
|
44 | def test_pytest_raises():
45 | with pytest.raises(Exception):
@@ -26,7 +26,7 @@ B017.py:45:10: B017 Do not assert blind exception: `Exception`
46 | raise ValueError("Hello")
|
B017.py:48:10: B017 Do not assert blind exception: `Exception`
B017_0.py:48:10: B017 Do not assert blind exception: `Exception`
|
46 | raise ValueError("Hello")
47 |
@@ -35,7 +35,7 @@ B017.py:48:10: B017 Do not assert blind exception: `Exception`
49 | raise ValueError("Hello")
|
B017.py:57:36: B017 Do not assert blind exception: `Exception`
B017_0.py:57:36: B017 Do not assert blind exception: `Exception`
|
55 | raise ValueError("This is also fine")
56 |

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---

View File

@@ -0,0 +1,45 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017_0.py:23:14: B017 Do not assert blind exception: `Exception`
|
21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None:
23 | with self.assertRaises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
24 | raise Exception("Evil I say!")
|
B017_0.py:27:14: B017 Do not assert blind exception: `BaseException`
|
26 | def also_evil_raises(self) -> None:
27 | with self.assertRaises(BaseException):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
28 | raise Exception("Evil I say!")
|
B017_0.py:45:10: B017 Do not assert blind exception: `Exception`
|
44 | def test_pytest_raises():
45 | with pytest.raises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
46 | raise ValueError("Hello")
|
B017_0.py:48:10: B017 Do not assert blind exception: `Exception`
|
46 | raise ValueError("Hello")
47 |
48 | with pytest.raises(Exception), pytest.raises(ValueError):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
49 | raise ValueError("Hello")
|
B017_0.py:57:36: B017 Do not assert blind exception: `Exception`
|
55 | raise ValueError("This is also fine")
56 |
57 | with contextlib.nullcontext(), pytest.raises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
58 | raise ValueError("Multiple context managers")
|

View File

@@ -0,0 +1,37 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017_1.py:20:9: B017 Do not assert blind exception: `Exception`
|
18 | class Foobar(unittest.TestCase):
19 | def call_form_raises(self) -> None:
20 | self.assertRaises(Exception, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
21 | self.assertRaises(BaseException, something_else)
|
B017_1.py:21:9: B017 Do not assert blind exception: `BaseException`
|
19 | def call_form_raises(self) -> None:
20 | self.assertRaises(Exception, something_else)
21 | self.assertRaises(BaseException, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
B017_1.py:25:5: B017 Do not assert blind exception: `Exception`
|
24 | def test_pytest_call_form() -> None:
25 | pytest.raises(Exception, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
26 | pytest.raises(BaseException, something_else)
|
B017_1.py:26:5: B017 Do not assert blind exception: `BaseException`
|
24 | def test_pytest_call_form() -> None:
25 | pytest.raises(Exception, something_else)
26 | pytest.raises(BaseException, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
27 |
28 | pytest.raises(Exception, something_else, match="hello")
|

View File

@@ -60,12 +60,14 @@ impl Violation for IndentationWithInvalidMultiple {
/// ```python
/// if True:
/// # a = 1
/// ...
/// ```
///
/// Use instead:
/// ```python
/// if True:
/// # a = 1
/// ...
/// ```
///
/// ## Formatter compatibility

View File

@@ -43,12 +43,12 @@ impl AlwaysFixableViolation for MultipleSpacesAfterKeyword {
///
/// ## Example
/// ```python
/// True and False
/// x and y
/// ```
///
/// Use instead:
/// ```python
/// True and False
/// x and y
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct MultipleSpacesBeforeKeyword;

View File

@@ -238,6 +238,9 @@ impl Violation for DocstringExtraneousYields {
///
/// ## Example
/// ```python
/// class FasterThanLightError(ArithmeticError): ...
///
///
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///
@@ -256,6 +259,9 @@ impl Violation for DocstringExtraneousYields {
///
/// Use instead:
/// ```python
/// class FasterThanLightError(ArithmeticError): ...
///
///
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///

View File

@@ -774,7 +774,7 @@ mod tests {
messages.sort_by_key(|diagnostic| diagnostic.expect_range().start());
let actual = messages
.iter()
.filter(|msg| !msg.is_syntax_error())
.filter(|msg| !msg.is_invalid_syntax())
.map(Diagnostic::name)
.collect::<Vec<_>>();
let expected: Vec<_> = expected.iter().map(|rule| rule.name().as_str()).collect();

View File

@@ -10,11 +10,11 @@ use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for access to the first or last element of `str.split()` without
/// Checks for access to the first or last element of `str.split()` or `str.rsplit()` without
/// `maxsplit=1`
///
/// ## Why is this bad?
/// Calling `str.split()` without `maxsplit` set splits on every delimiter in the
/// Calling `str.split()` or `str.rsplit()` without passing `maxsplit=1` splits on every delimiter in the
/// string. When accessing only the first or last element of the result, it
/// would be more efficient to only split once.
///
@@ -29,14 +29,44 @@ use crate::checkers::ast::Checker;
/// url = "www.example.com"
/// prefix = url.split(".", maxsplit=1)[0]
/// ```
///
/// To access the last element, use `str.rsplit()` instead of `str.split()`:
/// ```python
/// url = "www.example.com"
/// suffix = url.rsplit(".", maxsplit=1)[-1]
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct MissingMaxsplitArg;
pub(crate) struct MissingMaxsplitArg {
index: SliceBoundary,
actual_split_type: String,
}
/// Represents the index of the slice used for this rule (which can only be 0 or -1)
enum SliceBoundary {
First,
Last,
}
impl Violation for MissingMaxsplitArg {
#[derive_message_formats]
fn message(&self) -> String {
"Accessing only the first or last element of `str.split()` without setting `maxsplit=1`"
.to_string()
let MissingMaxsplitArg {
index,
actual_split_type,
} = self;
let suggested_split_type = match index {
SliceBoundary::First => "split",
SliceBoundary::Last => "rsplit",
};
if actual_split_type == suggested_split_type {
format!("Pass `maxsplit=1` into `str.{actual_split_type}()`")
} else {
format!(
"Instead of `str.{actual_split_type}()`, call `str.{suggested_split_type}()` and pass `maxsplit=1`",
)
}
}
}
@@ -82,9 +112,11 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
_ => return,
};
if !matches!(index, Some(0 | -1)) {
return;
}
let slice_boundary = match index {
Some(0) => SliceBoundary::First,
Some(-1) => SliceBoundary::Last,
_ => return,
};
let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() else {
return;
@@ -129,5 +161,11 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
}
}
checker.report_diagnostic(MissingMaxsplitArg, expr.range());
checker.report_diagnostic(
MissingMaxsplitArg {
index: slice_boundary,
actual_split_type: attr.to_string(),
},
expr.range(),
);
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
missing_maxsplit_arg.py:14:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:14:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
12 | # Errors
13 | ## Test split called directly on string literal
@@ -11,7 +11,7 @@ missing_maxsplit_arg.py:14:1: PLC0207 Accessing only the first or last element o
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:15:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:15:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
13 | ## Test split called directly on string literal
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
@@ -21,7 +21,7 @@ missing_maxsplit_arg.py:15:1: PLC0207 Accessing only the first or last element o
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:16:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:16:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
@@ -30,7 +30,7 @@ missing_maxsplit_arg.py:16:1: PLC0207 Accessing only the first or last element o
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:17:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:17:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
@@ -40,7 +40,7 @@ missing_maxsplit_arg.py:17:1: PLC0207 Accessing only the first or last element o
19 | ## Test split called on string variable
|
missing_maxsplit_arg.py:20:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:20:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
19 | ## Test split called on string variable
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
@@ -49,7 +49,7 @@ missing_maxsplit_arg.py:20:1: PLC0207 Accessing only the first or last element o
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:21:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:21:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
19 | ## Test split called on string variable
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
@@ -59,7 +59,7 @@ missing_maxsplit_arg.py:21:1: PLC0207 Accessing only the first or last element o
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:22:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:22:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
@@ -68,7 +68,7 @@ missing_maxsplit_arg.py:22:1: PLC0207 Accessing only the first or last element o
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:23:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:23:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -78,7 +78,7 @@ missing_maxsplit_arg.py:23:1: PLC0207 Accessing only the first or last element o
25 | ## Test split called on class attribute
|
missing_maxsplit_arg.py:26:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:26:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
25 | ## Test split called on class attribute
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
@@ -87,7 +87,7 @@ missing_maxsplit_arg.py:26:1: PLC0207 Accessing only the first or last element o
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:27:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:27:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
25 | ## Test split called on class attribute
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
@@ -97,7 +97,7 @@ missing_maxsplit_arg.py:27:1: PLC0207 Accessing only the first or last element o
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:28:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:28:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
@@ -106,7 +106,7 @@ missing_maxsplit_arg.py:28:1: PLC0207 Accessing only the first or last element o
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:29:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:29:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -116,7 +116,7 @@ missing_maxsplit_arg.py:29:1: PLC0207 Accessing only the first or last element o
31 | ## Test split called on sliced string
|
missing_maxsplit_arg.py:32:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:32:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
31 | ## Test split called on sliced string
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -125,7 +125,7 @@ missing_maxsplit_arg.py:32:1: PLC0207 Accessing only the first or last element o
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:33:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:33:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
31 | ## Test split called on sliced string
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -135,7 +135,7 @@ missing_maxsplit_arg.py:33:1: PLC0207 Accessing only the first or last element o
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:34:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:34:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -145,7 +145,7 @@ missing_maxsplit_arg.py:34:1: PLC0207 Accessing only the first or last element o
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:35:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:35:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
@@ -155,7 +155,7 @@ missing_maxsplit_arg.py:35:1: PLC0207 Accessing only the first or last element o
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:36:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:36:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
@@ -165,7 +165,7 @@ missing_maxsplit_arg.py:36:1: PLC0207 Accessing only the first or last element o
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:37:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:37:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
@@ -174,7 +174,7 @@ missing_maxsplit_arg.py:37:1: PLC0207 Accessing only the first or last element o
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:38:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:38:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
@@ -184,7 +184,7 @@ missing_maxsplit_arg.py:38:1: PLC0207 Accessing only the first or last element o
40 | ## Test sep given as named argument
|
missing_maxsplit_arg.py:41:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:41:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
40 | ## Test sep given as named argument
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
@@ -193,7 +193,7 @@ missing_maxsplit_arg.py:41:1: PLC0207 Accessing only the first or last element o
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:42:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:42:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
40 | ## Test sep given as named argument
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
@@ -203,7 +203,7 @@ missing_maxsplit_arg.py:42:1: PLC0207 Accessing only the first or last element o
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:43:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:43:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
@@ -212,7 +212,7 @@ missing_maxsplit_arg.py:43:1: PLC0207 Accessing only the first or last element o
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:44:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:44:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
@@ -222,7 +222,7 @@ missing_maxsplit_arg.py:44:1: PLC0207 Accessing only the first or last element o
46 | ## Special cases
|
missing_maxsplit_arg.py:47:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:47:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
46 | ## Special cases
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
@@ -231,7 +231,7 @@ missing_maxsplit_arg.py:47:1: PLC0207 Accessing only the first or last element o
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:48:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:48:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
46 | ## Special cases
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
@@ -240,7 +240,7 @@ missing_maxsplit_arg.py:48:1: PLC0207 Accessing only the first or last element o
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:49:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:49:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
@@ -250,7 +250,7 @@ missing_maxsplit_arg.py:49:1: PLC0207 Accessing only the first or last element o
51 | ## Test class attribute named split
|
missing_maxsplit_arg.py:52:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:52:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
51 | ## Test class attribute named split
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
@@ -259,7 +259,7 @@ missing_maxsplit_arg.py:52:1: PLC0207 Accessing only the first or last element o
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:53:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:53:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
51 | ## Test class attribute named split
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
@@ -269,7 +269,7 @@ missing_maxsplit_arg.py:53:1: PLC0207 Accessing only the first or last element o
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:54:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:54:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
@@ -278,7 +278,7 @@ missing_maxsplit_arg.py:54:1: PLC0207 Accessing only the first or last element o
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:55:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:55:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -288,14 +288,14 @@ missing_maxsplit_arg.py:55:1: PLC0207 Accessing only the first or last element o
57 | ## Test unpacked dict literal kwargs
|
missing_maxsplit_arg.py:58:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:58:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
57 | ## Test unpacked dict literal kwargs
58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
|
missing_maxsplit_arg.py:179:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:179:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
177 | # Errors
178 | kwargs_without_maxsplit = {"seq": ","}
@@ -305,7 +305,7 @@ missing_maxsplit_arg.py:179:1: PLC0207 Accessing only the first or last element
181 | kwargs_with_maxsplit = {"maxsplit": 1}
|
missing_maxsplit_arg.py:182:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:182:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
180 | # OK
181 | kwargs_with_maxsplit = {"maxsplit": 1}
@@ -315,7 +315,7 @@ missing_maxsplit_arg.py:182:1: PLC0207 Accessing only the first or last element
184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
|
missing_maxsplit_arg.py:184:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:184:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}

View File

@@ -292,7 +292,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
.chain(parsed.errors().iter().map(|parse_error| {
create_syntax_error_diagnostic(source_code.clone(), &parse_error.error, parse_error)
}))
.sorted()
.sorted_by(Diagnostic::ruff_start_ordering)
.collect();
(messages, transformed)
}
@@ -317,7 +317,7 @@ fn print_syntax_errors(errors: &[ParseError], path: &Path, source: &SourceKind)
/// Print the lint diagnostics in `diagnostics`.
fn print_diagnostics(mut diagnostics: Vec<Diagnostic>, path: &Path, source: &SourceKind) -> String {
diagnostics.retain(|msg| !msg.is_syntax_error());
diagnostics.retain(|msg| !msg.is_invalid_syntax());
if let Some(notebook) = source.as_ipy_notebook() {
print_jupyter_messages(&diagnostics, path, notebook)

View File

@@ -1,6 +1,10 @@
use std::fmt::{Debug, Display};
use crate::codes::Rule;
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::SourceFile;
use ruff_text_size::TextRange;
use crate::{codes::Rule, message::create_lint_diagnostic};
#[derive(Debug, Copy, Clone)]
pub enum FixAvailability {
@@ -28,7 +32,7 @@ pub trait ViolationMetadata {
fn explain() -> Option<&'static str>;
}
pub trait Violation: ViolationMetadata {
pub trait Violation: ViolationMetadata + Sized {
/// `None` in the case a fix is never available or otherwise Some
/// [`FixAvailability`] describing the available fix.
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
@@ -48,6 +52,20 @@ pub trait Violation: ViolationMetadata {
/// Returns the format strings used by [`message`](Violation::message).
fn message_formats() -> &'static [&'static str];
/// Convert the violation into a [`Diagnostic`].
fn into_diagnostic(self, range: TextRange, file: &SourceFile) -> Diagnostic {
create_lint_diagnostic(
self.message(),
self.fix_title(),
range,
None,
None,
file.clone(),
None,
Self::rule(),
)
}
}
/// This trait exists just to make implementing the [`Violation`] trait more

View File

@@ -0,0 +1,95 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{ImplItem, ItemImpl};
pub(crate) fn attribute_env_vars_metadata(mut input: ItemImpl) -> TokenStream {
// Verify that this is an impl for EnvVars
let impl_type = &input.self_ty;
let mut env_var_entries = Vec::new();
let mut hidden_vars = Vec::new();
// Process each item in the impl block
for item in &mut input.items {
if let ImplItem::Const(const_item) = item {
// Extract the const name and value
let const_name = &const_item.ident;
let const_expr = &const_item.expr;
// Check if the const has the #[attr_hidden] attribute
let is_hidden = const_item
.attrs
.iter()
.any(|attr| attr.path().is_ident("attr_hidden"));
// Remove our custom attributes
const_item.attrs.retain(|attr| {
!attr.path().is_ident("attr_hidden")
&& !attr.path().is_ident("attr_env_var_pattern")
});
if is_hidden {
hidden_vars.push(const_name.clone());
} else {
// Extract documentation from doc comments
let doc_attrs: Vec<_> = const_item
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
if !doc_attrs.is_empty() {
// Convert doc attributes to a single string
let doc_string = extract_doc_string(&doc_attrs);
env_var_entries.push((const_name.clone(), const_expr.clone(), doc_string));
}
}
}
}
// Generate the metadata method.
let metadata_entries: Vec<_> = env_var_entries
.iter()
.map(|(_name, expr, doc)| {
quote! {
(#expr, #doc)
}
})
.collect();
let metadata_impl = quote! {
impl #impl_type {
/// Returns metadata for all non-hidden environment variables.
pub fn metadata() -> Vec<(&'static str, &'static str)> {
vec![
#(#metadata_entries),*
]
}
}
};
quote! {
#input
#metadata_impl
}
}
/// Extract documentation from doc attributes into a single string
fn extract_doc_string(attrs: &[&syn::Attribute]) -> String {
attrs
.iter()
.filter_map(|attr| {
if let syn::Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &meta.value
{
return Some(lit_str.value().trim().to_string());
}
}
None
})
.collect::<Vec<_>>()
.join("\n")
}

View File

@@ -1,4 +1,4 @@
//! This crate implements internal macros for the `ruff` library.
//! This crate implements internal macros for the `ruff` and `ty` libraries.
use crate::cache_key::derive_cache_key;
use crate::newtype_index::generate_newtype_index;
@@ -11,6 +11,7 @@ mod combine;
mod combine_options;
mod config;
mod derive_message_formats;
mod env_vars;
mod kebab_case;
mod map_codes;
mod newtype_index;
@@ -144,3 +145,15 @@ pub fn newtype_index(_metadata: TokenStream, input: TokenStream) -> TokenStream
TokenStream::from(output)
}
/// Generates metadata for environment variables declared in the impl block.
///
/// This attribute macro should be applied to an `impl EnvVars` block.
/// It will generate a `metadata()` method that returns all non-hidden
/// environment variables with their documentation.
#[proc_macro_attribute]
pub fn attribute_env_vars_metadata(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as syn::ItemImpl);
env_vars::attribute_env_vars_metadata(input).into()
}

View File

@@ -952,6 +952,9 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => {
write!(f, "name `{name}` is used prior to global declaration")
}
SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { name, start: _ } => {
write!(f, "name `{name}` is used prior to nonlocal declaration")
}
SemanticSyntaxErrorKind::InvalidStarExpression => {
f.write_str("Starred expression cannot be used here")
}
@@ -977,6 +980,15 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
write!(f, "nonlocal declaration not allowed at module level")
}
SemanticSyntaxErrorKind::NonlocalAndGlobal(name) => {
write!(f, "name `{name}` is nonlocal and global")
}
SemanticSyntaxErrorKind::AnnotatedGlobal(name) => {
write!(f, "annotated name `{name}` can't be global")
}
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
write!(f, "annotated name `{name}` can't be nonlocal")
}
}
}
}
@@ -1207,6 +1219,24 @@ pub enum SemanticSyntaxErrorKind {
/// [#111123]: https://github.com/python/cpython/issues/111123
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
/// Represents the use of a `nonlocal` variable before its `nonlocal` declaration.
///
/// ## Examples
///
/// ```python
/// def f():
/// counter = 0
/// def increment():
/// print(f"Adding 1 to {counter}")
/// nonlocal counter # SyntaxError: name 'counter' is used prior to nonlocal declaration
/// counter += 1
/// ```
///
/// ## Known Issues
///
/// See [`LoadBeforeGlobalDeclaration`][Self::LoadBeforeGlobalDeclaration].
LoadBeforeNonlocalDeclaration { name: String, start: TextSize },
/// Represents the use of a starred expression in an invalid location, such as a `return` or
/// `yield` statement.
///
@@ -1307,6 +1337,15 @@ pub enum SemanticSyntaxErrorKind {
/// Represents a nonlocal declaration at module level
NonlocalDeclarationAtModuleLevel,
/// Represents the same variable declared as both nonlocal and global
NonlocalAndGlobal(String),
/// Represents a type annotation on a variable that's been declared global
AnnotatedGlobal(String),
/// Represents a type annotation on a variable that's been declared nonlocal
AnnotatedNonlocal(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]

View File

@@ -163,7 +163,7 @@ pub(crate) fn check(
.into_iter()
.zip(noqa_edits)
.filter_map(|(message, noqa_edit)| {
if message.is_syntax_error() && !show_syntax_errors {
if message.is_invalid_syntax() && !show_syntax_errors {
None
} else {
Some(to_lsp_diagnostic(

View File

@@ -19,6 +19,7 @@ ruff_python_ast = { workspace = true }
ty_python_semantic = { workspace = true }
ty_project = { workspace = true, features = ["zstd"] }
ty_server = { workspace = true }
ty_static = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }

View File

@@ -0,0 +1,55 @@
# Environment variables
ty defines and respects the following environment variables:
### `TY_LOG`
If set, ty will use this value as the log level for its `--verbose` output.
Accepts any filter compatible with the `tracing_subscriber` crate.
For example:
- `TY_LOG=uv=debug` is the equivalent of `-vv` to the command line
- `TY_LOG=trace` will enable all trace-level logging.
See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
for more.
### `TY_LOG_PROFILE`
If set to `"1"` or `"true"`, ty will enable flamegraph profiling.
This creates a `tracing.folded` file that can be used to generate flame graphs
for performance analysis.
### `TY_MAX_PARALLELISM`
Specifies an upper limit for the number of tasks ty is allowed to run in parallel.
For example, how many files should be checked in parallel.
This isn't the same as a thread limit. ty may spawn additional threads
when necessary, e.g. to watch for file system changes or a dedicated UI thread.
## Externally-defined variables
ty also reads the following externally defined environment variables:
### `CONDA_PREFIX`
Used to detect an activated Conda environment location.
If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred.
### `RAYON_NUM_THREADS`
Specifies an upper limit for the number of threads ty uses when performing work in parallel.
Equivalent to `TY_MAX_PARALLELISM`.
This is a standard Rayon environment variable.
### `VIRTUAL_ENV`
Used to detect an activated virtual environment.
### `XDG_CONFIG_HOME`
Path to user-level configuration directory on Unix systems.

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

@@ -36,7 +36,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L98)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L99)
</small>
**What it does**
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L142)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L143)
</small>
**What it does**
@@ -88,7 +88,7 @@ f(int) # error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L168)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
</small>
**What it does**
@@ -117,7 +117,7 @@ a = 1
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L193)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L194)
</small>
**What it does**
@@ -147,7 +147,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L219)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L220)
</small>
**What it does**
@@ -177,7 +177,7 @@ class B(A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L263)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L264)
</small>
**What it does**
@@ -202,7 +202,7 @@ class B(A, A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L284)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285)
</small>
**What it does**
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L426)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L427)
</small>
**What it does**
@@ -334,7 +334,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L450)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L451)
</small>
**What it does**
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L316)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L317)
</small>
**What it does**
@@ -445,7 +445,7 @@ an atypical memory layout.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L470)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471)
</small>
**What it does**
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L510)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L511)
</small>
**What it does**
@@ -496,7 +496,7 @@ a: int = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1514)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1515)
</small>
**What it does**
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533)
</small>
**What it does**
@@ -550,7 +550,7 @@ class A(42): ... # error: [invalid-base]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L583)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L584)
</small>
**What it does**
@@ -575,7 +575,7 @@ with 1:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L604)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L605)
</small>
**What it does**
@@ -602,7 +602,7 @@ a: str
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628)
</small>
**What it does**
@@ -644,7 +644,7 @@ except ZeroDivisionError:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L663)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L664)
</small>
**What it does**
@@ -675,7 +675,7 @@ class C[U](Generic[T]): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L689)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L690)
</small>
**What it does**
@@ -708,7 +708,7 @@ def f(t: TypeVar("U")): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L738)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
</small>
**What it does**
@@ -735,12 +735,35 @@ class B(metaclass=f): ...
- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
## `invalid-nonlocal`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-nonlocal) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564)
</small>
**What it does**
Detects `nonlocal` statements that don't match a binding in any enclosing scope.
**Why is this bad?**
Unmatched `nonlocal` statements will raise a `SyntaxError` at runtime.
**Example**
```python
def f():
nonlocal x # error: no binding for nonlocal 'x' found
```
## `invalid-overload`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766)
</small>
**What it does**
@@ -788,7 +811,7 @@ def foo(x: int) -> int: ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L808)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809)
</small>
**What it does**
@@ -812,7 +835,7 @@ def f(a: int = ''): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L398)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L399)
</small>
**What it does**
@@ -844,7 +867,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L828)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L829)
</small>
Checks for `raise` statements that raise non-exceptions or use invalid
@@ -891,7 +914,7 @@ def g():
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L491)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L492)
</small>
**What it does**
@@ -914,7 +937,7 @@ def func() -> int:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L871)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L872)
</small>
**What it does**
@@ -968,7 +991,7 @@ TODO #14889
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L717)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L718)
</small>
**What it does**
@@ -993,7 +1016,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L910)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911)
</small>
**What it does**
@@ -1021,7 +1044,7 @@ TYPE_CHECKING = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L934)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L935)
</small>
**What it does**
@@ -1049,7 +1072,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L986)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L987)
</small>
**What it does**
@@ -1081,7 +1104,7 @@ f(10) # Error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L958)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959)
</small>
**What it does**
@@ -1113,7 +1136,7 @@ class C:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1014)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1015)
</small>
**What it does**
@@ -1146,7 +1169,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1043)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1044)
</small>
**What it does**
@@ -1169,7 +1192,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063)
</small>
**What it does**
@@ -1196,7 +1219,7 @@ func("string") # error: [no-matching-overload]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086)
</small>
**What it does**
@@ -1218,7 +1241,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1103)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1104)
</small>
**What it does**
@@ -1242,7 +1265,7 @@ for i in 34: # TypeError: 'int' object is not iterable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1154)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1155)
</small>
**What it does**
@@ -1296,7 +1319,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1490)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1491)
</small>
**What it does**
@@ -1324,7 +1347,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1245)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
</small>
**What it does**
@@ -1351,7 +1374,7 @@ class B(A): ... # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291)
</small>
**What it does**
@@ -1376,7 +1399,7 @@ f("foo") # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1269)
</small>
**What it does**
@@ -1402,7 +1425,7 @@ def _(x: int):
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1311)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
</small>
**What it does**
@@ -1446,7 +1469,7 @@ class A:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1368)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1369)
</small>
**What it does**
@@ -1471,7 +1494,7 @@ f(x=1, y=2) # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1389)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1390)
</small>
**What it does**
@@ -1497,7 +1520,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412)
</small>
**What it does**
@@ -1520,7 +1543,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1430)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1431)
</small>
**What it does**
@@ -1543,7 +1566,7 @@ print(x) # NameError: name 'x' is not defined
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1123)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1124)
</small>
**What it does**
@@ -1578,7 +1601,7 @@ b1 < b2 < b1 # exception raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1449)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1450)
</small>
**What it does**
@@ -1604,7 +1627,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472)
</small>
**What it does**
@@ -1655,7 +1678,7 @@ a = 20 / 0 # type: ignore
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1175)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176)
</small>
**What it does**
@@ -1681,7 +1704,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L116)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L117)
</small>
**What it does**
@@ -1711,7 +1734,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198)
</small>
**What it does**
@@ -1741,7 +1764,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1543)
</small>
**What it does**
@@ -1766,7 +1789,7 @@ cast(int, f()) # Redundant
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1350)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351)
</small>
**What it does**
@@ -1817,7 +1840,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L550)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L551)
</small>
**What it does**
@@ -1854,7 +1877,7 @@ class D(C): ... # error: [unsupported-base]
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L245)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L246)
</small>
**What it does**
@@ -1876,7 +1899,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1223)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1224)
</small>
**What it does**

View File

@@ -4,6 +4,7 @@ mod python_version;
mod version;
pub use args::Cli;
use ty_static::EnvVars;
use std::io::{self, BufWriter, Write, stdout};
use std::process::{ExitCode, Termination};
@@ -144,7 +145,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
};
let mut stdout = stdout().lock();
match std::env::var("TY_MEMORY_REPORT").as_deref() {
match std::env::var(EnvVars::TY_MEMORY_REPORT).as_deref() {
Ok("short") => write!(stdout, "{}", db.salsa_memory_dump().display_short())?,
Ok("mypy_primer") => write!(stdout, "{}", db.salsa_memory_dump().display_mypy_primer())?,
Ok("full") => write!(stdout, "{}", db.salsa_memory_dump().display_full())?,

View File

@@ -12,6 +12,7 @@ use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::registry::LookupSpan;
use ty_static::EnvVars;
/// Logging flags to `#[command(flatten)]` into your CLI
#[derive(clap::Args, Debug, Clone, Default)]
@@ -84,7 +85,7 @@ pub(crate) fn setup_tracing(
use tracing_subscriber::prelude::*;
// The `TY_LOG` environment variable overrides the default log level.
let filter = if let Ok(log_env_variable) = std::env::var("TY_LOG") {
let filter = if let Ok(log_env_variable) = std::env::var(EnvVars::TY_LOG) {
EnvFilter::builder()
.parse(log_env_variable)
.context("Failed to parse directives specified in TY_LOG environment variable.")?
@@ -165,7 +166,7 @@ fn setup_profile<S>() -> (
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
if let Ok("1" | "true") = std::env::var("TY_LOG_PROFILE").as_deref() {
if let Ok("1" | "true") = std::env::var(EnvVars::TY_LOG_PROFILE).as_deref() {
let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded")
.expect("Flame layer to be created");
(Some(layer), Some(guard))

View File

@@ -10,7 +10,7 @@ use ty_python_semantic::{Completion, NameKind, SemanticModel};
use crate::Db;
use crate::find_node::covering_node;
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion> {
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion<'_>> {
let parsed = parsed_module(db, file).load(db);
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
@@ -1223,33 +1223,33 @@ quux.<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins(), @r"
bar
baz
foo
__annotations__
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__getattribute__
__getstate__
__hash__
__init__
__init_subclass__
__module__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
bar :: Unknown | Literal[2]
baz :: Unknown | Literal[3]
foo :: Unknown | Literal[1]
__annotations__ :: dict[str, Any]
__class__ :: type
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
__dict__ :: dict[str, Any]
__dir__ :: bound method object.__dir__() -> Iterable[str]
__doc__ :: str | None
__eq__ :: bound method object.__eq__(value: object, /) -> bool
__format__ :: bound method object.__format__(format_spec: str, /) -> str
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
__getstate__ :: bound method object.__getstate__() -> object
__hash__ :: bound method object.__hash__() -> int
__init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ :: bound method object.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method object.__ne__(value: object, /) -> bool
__new__ :: bound method object.__new__() -> Self
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method object.__repr__() -> str
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method object.__sizeof__() -> int
__str__ :: bound method object.__str__() -> str
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
");
}
@@ -1268,33 +1268,33 @@ quux.b<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins(), @r"
bar
baz
foo
__annotations__
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__getattribute__
__getstate__
__hash__
__init__
__init_subclass__
__module__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
bar :: Unknown | Literal[2]
baz :: Unknown | Literal[3]
foo :: Unknown | Literal[1]
__annotations__ :: dict[str, Any]
__class__ :: type
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
__dict__ :: dict[str, Any]
__dir__ :: bound method object.__dir__() -> Iterable[str]
__doc__ :: str | None
__eq__ :: bound method object.__eq__(value: object, /) -> bool
__format__ :: bound method object.__format__(format_spec: str, /) -> str
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
__getstate__ :: bound method object.__getstate__() -> object
__hash__ :: bound method object.__hash__() -> int
__init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ :: bound method object.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method object.__ne__(value: object, /) -> bool
__new__ :: bound method object.__new__() -> Self
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method object.__repr__() -> str
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method object.__sizeof__() -> int
__str__ :: bound method object.__str__() -> str
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
");
}
@@ -1321,6 +1321,89 @@ class Quux:
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
}
#[test]
fn class_attributes1() {
let test = cursor_test(
"\
class Quux:
some_attribute: int = 1
def __init__(self):
self.foo = 1
self.bar = 2
self.baz = 3
def some_method(self) -> int:
return 1
@property
def some_property(self) -> int:
return 1
@classmethod
def some_class_method(self) -> int:
return 1
@staticmethod
def some_static_method(self) -> int:
return 1
Quux.<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
mro :: def mro(self) -> list[type]
some_attribute :: int
some_class_method :: bound method <class 'Quux'>.some_class_method() -> int
some_method :: def some_method(self) -> int
some_property :: property
some_static_method :: def some_static_method(self) -> int
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
__basicsize__ :: int
__call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any
__class__ :: <class 'type'>
__delattr__ :: def __delattr__(self, name: str, /) -> None
__dict__ :: MappingProxyType[str, Any]
__dictoffset__ :: int
__dir__ :: def __dir__(self) -> Iterable[str]
__doc__ :: str | None
__eq__ :: def __eq__(self, value: object, /) -> bool
__flags__ :: int
__format__ :: def __format__(self, format_spec: str, /) -> str
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: def __init__(self) -> Unknown
__init_subclass__ :: def __init_subclass__(cls) -> None
__instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool
__itemsize__ :: int
__module__ :: str
__mro__ :: tuple[<class 'type'>, <class 'object'>]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self
__or__ :: def __or__(self, value: Any, /) -> UnionType
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str
__ror__ :: def __ror__(self, value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool
__subclasses__ :: def __subclasses__(self: Self) -> list[Self]
__subclasshook__ :: bound method <class 'object'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
__weakrefoffset__ :: int
");
}
// We don't yet take function parameters into account.
#[test]
fn call_prefix1() {
@@ -2366,7 +2449,22 @@ importlib.<CURSOR>
self.completions_if(|c| !c.builtin)
}
fn completions_without_builtins_with_types(&self) -> String {
self.completions_if_snapshot(
|c| !c.builtin,
|c| format!("{} :: {}", c.name, c.ty.display(&self.db)),
)
}
fn completions_if(&self, predicate: impl Fn(&Completion) -> bool) -> String {
self.completions_if_snapshot(predicate, |c| c.name.as_str().to_string())
}
fn completions_if_snapshot(
&self,
predicate: impl Fn(&Completion) -> bool,
snapshot: impl Fn(&Completion) -> String,
) -> String {
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
if completions.is_empty() {
return "<No completions found>".to_string();
@@ -2374,7 +2472,7 @@ importlib.<CURSOR>
let included = completions
.iter()
.filter(|label| predicate(label))
.map(|completion| completion.name.as_str().to_string())
.map(snapshot)
.collect::<Vec<String>>();
if included.is_empty() {
// It'd be nice to include the actual number of

View File

@@ -285,16 +285,7 @@ impl Project {
return Vec::new();
}
let mut file_diagnostics: Vec<_> = self
.settings_diagnostics(db)
.iter()
.map(OptionDiagnostic::to_diagnostic)
.collect();
let check_diagnostics = self.check_file_impl(db, file);
file_diagnostics.extend(check_diagnostics);
file_diagnostics
self.check_file_impl(db, file)
}
/// Opens a file in the project.
@@ -500,11 +491,11 @@ impl Project {
parsed_ref
.errors()
.iter()
.map(|error| Diagnostic::syntax_error(file, &error.error, error)),
.map(|error| Diagnostic::invalid_syntax(file, &error.error, error)),
);
diagnostics.extend(parsed_ref.unsupported_syntax_errors().iter().map(|error| {
let mut error = Diagnostic::syntax_error(file, error, error);
let mut error = Diagnostic::invalid_syntax(file, error, error);
add_inferred_python_version_hint_to_diagnostic(db, &mut error, "parsing syntax");
error
}));

View File

@@ -22,6 +22,7 @@ ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_trivia = { workspace = true }
ty_static = { workspace = true }
anyhow = { workspace = true }
bitflags = { workspace = true }
@@ -52,6 +53,7 @@ strum_macros = { workspace = true }
ruff_db = { workspace = true, features = ["testing", "os"] }
ruff_python_parser = { workspace = true }
ty_python_semantic = { workspace = true, features = ["testing"] }
ty_static = { workspace = true }
ty_test = { workspace = true }
ty_vendored = { workspace = true }

View File

@@ -1791,6 +1791,80 @@ date.year = 2025
date.tz = "UTC"
```
### Return type of `__setattr__`
If the return type of the `__setattr__` method is `Never`, we do not allow any attribute assignments
on instances of that class:
```py
from typing_extensions import Never
class Frozen:
existing: int = 1
def __setattr__(self, name, value) -> Never:
raise AttributeError("Attributes can not be modified")
instance = Frozen()
instance.non_existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `non_existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
instance.existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
```
### `__setattr__` on `object`
`object` has a custom `__setattr__` implementation, but we still emit an error if a non-existing
attribute is assigned on an `object` instance.
```py
obj = object()
obj.non_existing = 1 # error: [unresolved-attribute]
```
### Setting attributes on `Never` / `Any`
Setting attributes on `Never` itself should be allowed (even though it has a `__setattr__` attribute
of type `Never`):
```py
from typing_extensions import Never, Any
def _(n: Never):
reveal_type(n.__setattr__) # revealed: Never
# No error:
n.non_existing = 1
```
And similarly for `Any`:
```py
def _(a: Any):
reveal_type(a.__setattr__) # revealed: Any
# No error:
a.non_existing = 1
```
### Possibly unbound `__setattr__` method
If a `__setattr__` method is only partially bound, the behavior is still the same:
```py
from typing_extensions import Never
def flag() -> bool:
return True
class Frozen:
if flag():
def __setattr__(self, name, value) -> Never:
raise AttributeError("Attributes can not be modified")
instance = Frozen()
instance.non_existing = 2 # error: [invalid-assignment]
instance.existing = 2 # error: [invalid-assignment]
```
### `argparse.Namespace`
A standard library example of a class with a custom `__setattr__` method is `argparse.Namespace`:

View File

@@ -558,6 +558,50 @@ class C(Base):
reveal_type(C.__init__) # revealed: (self: C, x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
```
## Conditionally defined fields
### Statically known conditions
Fields that are defined in always-reachable branches are always present in the synthesized
`__init__` method. Fields that are defined in never-reachable branches are not present:
```py
from dataclasses import dataclass
@dataclass
class C:
normal: int
if 1 + 2 == 3:
always_present: str
if 1 + 2 == 4:
never_present: bool
reveal_type(C.__init__) # revealed: (self: C, normal: int, always_present: str) -> None
```
### Dynamic conditions
If a field is conditionally defined, we currently assume that it is always present. A more complex
alternative here would be to synthesized a union of all possible `__init__` signatures:
```py
from dataclasses import dataclass
def flag() -> bool:
return True
@dataclass
class C:
normal: int
if flag():
conditionally_present: str
reveal_type(C.__init__) # revealed: (self: C, normal: int, conditionally_present: str) -> None
```
## Generic dataclasses
```toml
@@ -789,6 +833,23 @@ class Fails: # error: [duplicate-kw-only]
reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None
```
This also works if `KW_ONLY` is used in a conditional branch:
```py
def flag() -> bool:
return True
@dataclass
class D: # error: [duplicate-kw-only]
x: int
_1: KW_ONLY
if flag():
y: str
_2: KW_ONLY
z: float
```
## Other special cases
### `dataclasses.dataclass`

View File

@@ -0,0 +1,35 @@
# Dataclass fields
## Basic
```py
from dataclasses import dataclass, field
@dataclass
class Member:
name: str
role: str = field(default="user")
tag: str | None = field(default=None, init=False)
# TODO: this should not include the `tag` parameter, since it has `init=False` set
# revealed: (self: Member, name: str, role: str = Unknown, tag: str | None = Unknown) -> None
reveal_type(Member.__init__)
alice = Member(name="Alice", role="admin")
reveal_type(alice.role) # revealed: str
alice.role = "moderator"
# TODO: this should be an error, `tag` has `init=False`
bob = Member(name="Bob", tag="VIP")
```
## The `field` function
```py
from dataclasses import field
# TODO: this should be `Literal[1]`. This is currently blocked on enum support, because
# the `dataclasses.field` overloads make use of a `_MISSING_TYPE` enum, for which we
# infer a @Todo type, and therefore pick the wrong overload.
reveal_type(field(default=1)) # revealed: Unknown
```

View File

@@ -1304,7 +1304,7 @@ scope of the name that was declared `global`, can add a symbol to the global nam
def f():
global g, h
g: bool = True
g = True
f()
```

View File

@@ -83,7 +83,7 @@ def f():
x = 1
def g() -> None:
nonlocal x
global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global"
global x # error: [invalid-syntax] "name `x` is nonlocal and global"
x = None
```
@@ -209,5 +209,18 @@ x: int = 1
def f():
global x
x: str = "foo" # TODO: error: [invalid-syntax] "annotated name 'x' can't be global"
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be global"
```
## Global declarations affect the inferred type of the binding
Even if the `global` declaration isn't used in an assignment, we conservatively assume it could be:
```py
x = 1
def f():
global x
# TODO: reveal_type(x) # revealed: Unknown | Literal["1"]
```

View File

@@ -43,3 +43,295 @@ def f():
def h():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## The `nonlocal` keyword
Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in
enclosing scopes. This example isn't a type error, because the inner `x` shadows the outer one:
```py
def f():
x: int = 1
def g():
x = "hello" # allowed
```
With `nonlocal` it is a type error, because `x` refers to the same place in both scopes:
```py
def f():
x: int = 1
def g():
nonlocal x
x = "hello" # error: [invalid-assignment] "Object of type `Literal["hello"]` is not assignable to `int`"
```
## Local variable bindings "look ahead" to any assignment in the current scope
The binding `x = 2` in `g` causes the earlier read of `x` to refer to `g`'s not-yet-initialized
binding, rather than to `x = 1` in `f`'s scope:
```py
def f():
x = 1
def g():
if x == 1: # error: [unresolved-reference] "Name `x` used when not defined"
x = 2
```
The `nonlocal` keyword makes this example legal (and makes the assignment `x = 2` affect the outer
scope):
```py
def f():
x = 1
def g():
nonlocal x
if x == 1:
x = 2
```
For the same reason, using the `+=` operator in an inner scope is an error without `nonlocal`
(unless you shadow the outer variable first):
```py
def f():
x = 1
def g():
x += 1 # error: [unresolved-reference] "Name `x` used when not defined"
def f():
x = 1
def g():
x = 1
x += 1 # allowed, but doesn't affect the outer scope
def f():
x = 1
def g():
nonlocal x
x += 1 # allowed, and affects the outer scope
```
## `nonlocal` declarations must match an outer binding
`nonlocal x` isn't allowed when there's no binding for `x` in an enclosing scope:
```py
def f():
def g():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
def f():
x = 1
def g():
nonlocal x, y # error: [invalid-nonlocal] "no binding for nonlocal `y` found"
```
A global `x` doesn't work. The target must be in a function-like scope:
```py
x = 1
def f():
def g():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
def f():
global x
def g():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
```
A class-scoped `x` also doesn't work:
```py
class Foo:
x = 1
@staticmethod
def f():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
```
## `nonlocal` uses the closest binding
```py
def f():
x = 1
def g():
x = 2
def h():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[2]
```
## `nonlocal` "chaining"
Multiple `nonlocal` statements can "chain" through nested scopes:
```py
def f():
x = 1
def g():
nonlocal x
def h():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
```
And the `nonlocal` chain can skip over a scope that doesn't bind the variable:
```py
def f1():
x = 1
def f2():
nonlocal x
def f3():
# No binding; this scope gets skipped.
def f4():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
```
But a `global` statement breaks the chain:
```py
def f():
x = 1
def g():
global x
def h():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
```
## `nonlocal` bindings respect declared types from the defining scope, even without a binding
```py
def f():
x: int
def g():
nonlocal x
x = "string" # error: [invalid-assignment] "Object of type `Literal["string"]` is not assignable to `int`"
```
## A complicated mixture of `nonlocal` chaining, empty scopes, and the `global` keyword
```py
def f1():
# The original bindings of `x`, `y`, and `z` with type declarations.
x: int = 1
y: int = 1
z: int = 1
def f2():
# This scope doesn't touch `x`, `y`, or `z` at all.
def f3():
# This scope treats declares `x` nonlocal and `y` as global, and it shadows `z` without
# giving it a new type declaration.
nonlocal x
x = 2
global y
y = 2
z = 2
def f4():
# This scope sees `x` from `f1` and `z` from `f3`, but it doesn't see `y` at all,
# because of the `global` keyword above.
nonlocal x, y, z # error: [invalid-nonlocal] "no binding for nonlocal `y` found"
x = "string" # error: [invalid-assignment]
z = "string" # not an error
```
## TODO: `nonlocal` affects the inferred type in the outer scope
Without `nonlocal`, `g` can't write to `x`, and the inferred type of `x` in `f`'s scope isn't
affected by `g`:
```py
def f():
x = 1
def g():
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(x) # revealed: Literal[1]
```
But with `nonlocal`, `g` could write to `x`, and that affects its inferred type in `f`. That's true
regardless of whether `g` actually writes to `x`. With a write:
```py
def f():
x = 1
def g():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
x += 1
reveal_type(x) # revealed: Unknown | Literal[2]
# TODO: should be `Unknown | Literal[1]`
reveal_type(x) # revealed: Literal[1]
```
Without a write:
```py
def f():
x = 1
def g():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
# TODO: should be `Unknown | Literal[1]`
reveal_type(x) # revealed: Literal[1]
```
## Annotating a `nonlocal` binding is a syntax error
```py
def f():
x: int = 1
def g():
nonlocal x
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be nonlocal"
```
## Use before `nonlocal`
Using a name prior to its `nonlocal` declaration in the same scope is a syntax error:
```py
def f():
x = 1
def g():
x = 2
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
```
This is true even if there are multiple `nonlocal` declarations of the same variable, as long as any
of them come after the usage:
```py
def f():
x = 1
def g():
nonlocal x
x = 2
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
def f():
x = 1
def g():
nonlocal x
nonlocal x
x = 2 # allowed
```
## `nonlocal` before outer initialization
`nonlocal x` works even if `x` isn't bound in the enclosing scope until afterwards:
```py
def f():
def g():
# This is allowed, because of the subsequent definition of `x`.
nonlocal x
x = 1
```

View File

@@ -37,6 +37,18 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.
23 | e: bytes
24 |
25 | reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None
26 | def flag() -> bool:
27 | return True
28 |
29 | @dataclass
30 | class D: # error: [duplicate-kw-only]
31 | x: int
32 | _1: KW_ONLY
33 |
34 | if flag():
35 | y: str
36 | _2: KW_ONLY
37 | z: float
```
# Diagnostics
@@ -109,6 +121,23 @@ info[revealed-type]: Revealed type
24 |
25 | reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None
| ^^^^^^^^^^^^^^ `(self: Fails, a: int, *, c: str, e: bytes) -> None`
26 | def flag() -> bool:
27 | return True
|
```
```
error[duplicate-kw-only]: Dataclass has more than one field annotated with `KW_ONLY`
--> src/mdtest_snippet.py:30:7
|
29 | @dataclass
30 | class D: # error: [duplicate-kw-only]
| ^
31 | x: int
32 | _1: KW_ONLY
|
info: `KW_ONLY` fields: `_1`, `_2`
info: rule `duplicate-kw-only` is enabled by default
```

View File

@@ -0,0 +1,59 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: final.md - `typing.Final` - Full diagnostics
mdtest path: crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import Final
2 |
3 | MY_CONSTANT: Final[int] = 1
4 |
5 | # more code
6 |
7 | MY_CONSTANT = 2 # error: [invalid-assignment]
8 | from _stat import ST_INO
9 |
10 | ST_INO = 1 # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Reassignment of `Final` symbol `MY_CONSTANT` is not allowed
--> src/mdtest_snippet.py:3:14
|
1 | from typing import Final
2 |
3 | MY_CONSTANT: Final[int] = 1
| ---------- Symbol declared as `Final` here
4 |
5 | # more code
6 |
7 | MY_CONSTANT = 2 # error: [invalid-assignment]
| ^^^^^^^^^^^^^^^ Symbol later reassigned here
8 | from _stat import ST_INO
|
info: rule `invalid-assignment` is enabled by default
```
```
error[invalid-assignment]: Reassignment of `Final` symbol `ST_INO` is not allowed
--> src/mdtest_snippet.py:10:1
|
8 | from _stat import ST_INO
9 |
10 | ST_INO = 1 # error: [invalid-assignment]
| ^^^^^^^^^^ Reassignment of `Final` symbol
|
info: rule `invalid-assignment` is enabled by default
```

View File

@@ -82,7 +82,7 @@ info: Type variable defined here
2 | from typing_extensions import reveal_type
3 |
4 | T = TypeVar("T", bound=int)
| ^
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 |
6 | def f(x: T) -> T:
|

View File

@@ -97,7 +97,7 @@ info: Type variable defined here
2 | from typing_extensions import reveal_type
3 |
4 | T = TypeVar("T", int, None)
| ^
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 |
6 | def f(x: T) -> T:
|

View File

@@ -1774,6 +1774,25 @@ static_assert(is_subtype_of(type[B], Callable[[str], B]))
static_assert(not is_subtype_of(type[B], Callable[[int], B]))
```
### Dataclasses
Dataclasses synthesize a `__init__` method.
```py
from typing import Callable
from ty_extensions import TypeOf, static_assert, is_subtype_of
from dataclasses import dataclass
@dataclass
class A:
x: "A" | None
static_assert(is_subtype_of(type[A], Callable[[A], A]))
static_assert(is_subtype_of(type[A], Callable[[None], A]))
static_assert(is_subtype_of(type[A], Callable[[A | None], A]))
static_assert(not is_subtype_of(type[A], Callable[[int], A]))
```
### Bound methods
```py

View File

@@ -100,9 +100,13 @@ reveal_type(C().FINAL_D) # revealed: Unknown
## Not modifiable
### Names
Symbols qualified with `Final` cannot be reassigned, and attempting to do so will result in an
error:
`mod.py`:
```py
from typing import Final, Annotated
@@ -114,13 +118,96 @@ FINAL_E: Final[int]
FINAL_E = 1
FINAL_F: Final = 1
# TODO: all of these should be errors
FINAL_A = 2
FINAL_B = 2
FINAL_C = 2
FINAL_D = 2
FINAL_E = 2
FINAL_F = 2
FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed"
FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed"
FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed"
FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed"
FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed"
FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed"
def global_use():
global FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F
FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed"
FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed"
FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed"
FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed"
FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed"
FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed"
def local_use():
# These are not errors, because they refer to local variables
FINAL_A = 2
FINAL_B = 2
FINAL_C = 2
FINAL_D = 2
FINAL_E = 2
FINAL_F = 2
def nonlocal_use():
X: Final[int] = 1
def inner():
nonlocal X
X = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `X` is not allowed: Reassignment of `Final` symbol"
```
`main.py`:
```py
from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F
FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed"
FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed"
FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed"
FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed"
FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed"
FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed"
```
### Attributes
Assignments to attributes qualified with `Final` are also not allowed:
```py
from typing import Final
class C:
FINAL_A: Final[int] = 1
FINAL_B: Final = 1
def __init__(self):
self.FINAL_C: Final[int] = 1
self.FINAL_D: Final = 1
# TODO: these should be errors (that mention `Final`)
C.FINAL_A = 2
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
C.FINAL_B = 2
# TODO: these should be errors (that mention `Final`)
c = C()
c.FINAL_A = 2
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
c.FINAL_B = 2
c.FINAL_C = 2
c.FINAL_D = 2
```
## Mutability
Objects qualified with `Final` *can be modified*. `Final` represents a constant reference to an
object, but that object itself may still be mutable:
```py
from typing import Final
class C:
x: int = 1
FINAL_C_INSTANCE: Final[C] = C()
FINAL_C_INSTANCE.x = 2
FINAL_LIST: Final[list[int]] = [1, 2, 3]
FINAL_LIST[0] = 4
```
## Too many arguments
@@ -168,4 +255,28 @@ class C:
NO_RHS: Final
```
## Full diagnostics
<!-- snapshot-diagnostics -->
Annotated assignment:
```py
from typing import Final
MY_CONSTANT: Final[int] = 1
# more code
MY_CONSTANT = 2 # error: [invalid-assignment]
```
Imported `Final` symbol:
```py
from _stat import ST_INO
ST_INO = 1 # error: [invalid-assignment]
```
[`typing.final`]: https://docs.python.org/3/library/typing.html#typing.Final

View File

@@ -20,3 +20,4 @@ spack # slow, success, but mypy-primer hangs processing the output
spark # too many iterations
steam.py # hangs (single threaded)
xarray # too many iterations
zope.interface # https://github.com/astral-sh/ty/issues/764

View File

@@ -1,4 +1,5 @@
AutoSplit
DateType
Expression
PyGithub
PyWinCtl

View File

@@ -15,7 +15,7 @@ pub use program::{
PythonVersionWithSource, SearchPathSettings,
};
pub use python_platform::PythonPlatform;
pub use semantic_model::{Completion, HasType, NameKind, SemanticModel};
pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;

View File

@@ -1421,7 +1421,7 @@ impl RequiresExplicitReExport {
/// ```py
/// def _():
/// x = 1
///
///
/// x = 2
///
/// if flag():

View File

@@ -193,6 +193,12 @@ pub(crate) enum EagerSnapshotResult<'map, 'db> {
NoLongerInEagerContext,
}
#[derive(Debug, Update, get_size2::GetSize)]
pub(crate) enum NotLocalVariableKind {
Nonlocal,
Global,
}
/// The place tables and use-def maps for all scopes in a file.
#[derive(Debug, Update, get_size2::GetSize)]
pub(crate) struct SemanticIndex<'db> {
@@ -217,8 +223,8 @@ pub(crate) struct SemanticIndex<'db> {
/// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`].
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
/// Map from the file-local [`FileScopeId`] to the set of explicit-global symbols it contains.
globals_by_scope: FxHashMap<FileScopeId, FxHashSet<ScopedPlaceId>>,
/// Map from the file-local [`FileScopeId`] to the set of explicit-nonlocal symbols it contains.
not_locals_by_scope: FxHashMap<FileScopeId, FxHashMap<ScopedPlaceId, NotLocalVariableKind>>,
/// Use-def map for each scope in this file.
use_def_maps: IndexVec<FileScopeId, ArcUseDefMap<'db>>,
@@ -308,9 +314,21 @@ impl<'db> SemanticIndex<'db> {
symbol: ScopedPlaceId,
scope: FileScopeId,
) -> bool {
self.globals_by_scope
.get(&scope)
.is_some_and(|globals| globals.contains(&symbol))
let Some(scope) = self.not_locals_by_scope.get(&scope) else {
return false;
};
matches!(scope.get(&symbol), Some(NotLocalVariableKind::Global))
}
pub(crate) fn symbol_is_nonlocal_in_scope(
&self,
symbol: ScopedPlaceId,
scope: FileScopeId,
) -> bool {
let Some(scope) = self.not_locals_by_scope.get(&scope) else {
return false;
};
matches!(scope.get(&symbol), Some(NotLocalVariableKind::Nonlocal))
}
/// Returns the id of the parent scope.

View File

@@ -45,7 +45,7 @@ use crate::semantic_index::reachability_constraints::{
use crate::semantic_index::use_def::{
EagerSnapshotKey, FlowSnapshot, ScopedEagerSnapshotId, UseDefMapBuilder,
};
use crate::semantic_index::{ArcUseDefMap, SemanticIndex};
use crate::semantic_index::{ArcUseDefMap, NotLocalVariableKind, SemanticIndex};
use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue};
use crate::{Db, Program};
@@ -103,7 +103,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
globals_by_scope: FxHashMap<FileScopeId, FxHashSet<ScopedPlaceId>>,
not_locals_by_scope: FxHashMap<FileScopeId, FxHashMap<ScopedPlaceId, NotLocalVariableKind>>,
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
imported_modules: FxHashSet<ModuleName>,
@@ -141,7 +141,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
scopes_by_node: FxHashMap::default(),
definitions_by_node: FxHashMap::default(),
expressions_by_node: FxHashMap::default(),
globals_by_scope: FxHashMap::default(),
not_locals_by_scope: FxHashMap::default(),
imported_modules: FxHashSet::default(),
generator_functions: FxHashSet::default(),
@@ -1046,7 +1046,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
self.scopes_by_node.shrink_to_fit();
self.generator_functions.shrink_to_fit();
self.eager_snapshots.shrink_to_fit();
self.globals_by_scope.shrink_to_fit();
self.not_locals_by_scope.shrink_to_fit();
SemanticIndex {
place_tables,
@@ -1054,7 +1054,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
definitions_by_node: self.definitions_by_node,
expressions_by_node: self.expressions_by_node,
scope_ids_by_scope: self.scope_ids_by_scope,
globals_by_scope: self.globals_by_scope,
not_locals_by_scope: self.not_locals_by_scope,
ast_ids,
scopes_by_expression: self.scopes_by_expression,
scopes_by_node: self.scopes_by_node,
@@ -1422,6 +1422,32 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
self.visit_expr(value);
}
if let ast::Expr::Name(name) = &*node.target {
let symbol_id = self.add_symbol(name.id.clone());
let scope_id = self.current_scope();
// Check whether the variable has been declared global or nonlocal.
if let Some(not_locals) = self.not_locals_by_scope.get(&scope_id) {
if let Some(not_local_kind) = not_locals.get(&symbol_id) {
self.report_semantic_error(SemanticSyntaxError {
kind: match not_local_kind {
NotLocalVariableKind::Global => {
SemanticSyntaxErrorKind::AnnotatedGlobal(
name.id.as_str().into(),
)
}
NotLocalVariableKind::Nonlocal => {
SemanticSyntaxErrorKind::AnnotatedNonlocal(
name.id.as_str().into(),
)
}
},
range: name.range,
python_version: self.python_version,
});
}
}
}
// See https://docs.python.org/3/library/ast.html#ast.AnnAssign
if matches!(
*node.target,
@@ -1864,6 +1890,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
let symbol_id = self.add_symbol(name.id.clone());
let symbol_table = self.current_place_table();
let symbol = symbol_table.place_expr(symbol_id);
// Check whether the variable has already been accessed in this scope.
if symbol.is_bound() || symbol.is_declared() || symbol.is_used() {
self.report_semantic_error(SemanticSyntaxError {
kind: SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration {
@@ -1875,10 +1902,61 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
});
}
let scope_id = self.current_scope();
self.globals_by_scope
// Check whether the variable has also been declared nonlocal.
if let Some(not_locals) = self.not_locals_by_scope.get(&scope_id) {
if let Some(NotLocalVariableKind::Nonlocal) = not_locals.get(&symbol_id) {
self.report_semantic_error(SemanticSyntaxError {
kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()),
range: name.range,
python_version: self.python_version,
});
}
}
self.not_locals_by_scope
.entry(scope_id)
.or_default()
.insert(symbol_id);
.insert(symbol_id, NotLocalVariableKind::Global);
}
walk_stmt(self, stmt);
}
ast::Stmt::Nonlocal(ast::StmtNonlocal {
range: _,
node_index: _,
names,
}) => {
for name in names {
let symbol_id = self.add_symbol(name.id.clone());
let symbol_table = self.current_place_table();
let symbol = symbol_table.place_expr(symbol_id);
// Make sure the variable exists in an enclosing scope. But note that its
// definition might come below the inner scope.
// Check whether the variable has already been accessed in this scope.
if symbol.is_bound() || symbol.is_declared() || symbol.is_used() {
self.report_semantic_error(SemanticSyntaxError {
kind: SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration {
name: name.to_string(),
start: name.range.start(),
},
range: name.range,
python_version: self.python_version,
});
}
let scope_id = self.current_scope();
// Check whether the variable has also been declared global.
if let Some(not_locals) = self.not_locals_by_scope.get(&scope_id) {
if let Some(NotLocalVariableKind::Global) = not_locals.get(&symbol_id) {
self.report_semantic_error(SemanticSyntaxError {
kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()),
range: name.range,
python_version: self.python_version,
});
}
}
self.not_locals_by_scope
.entry(scope_id)
.or_default()
.insert(symbol_id, NotLocalVariableKind::Nonlocal);
}
walk_stmt(self, stmt);
}

View File

@@ -618,6 +618,15 @@ impl DefinitionKind<'_> {
}
}
pub(crate) fn is_import(&self) -> bool {
matches!(
self,
DefinitionKind::Import(_)
| DefinitionKind::ImportFrom(_)
| DefinitionKind::StarImport(_)
)
}
/// Returns the [`TextRange`] of the definition target.
///
/// A definition target would mainly be the node representing the place being defined i.e.,
@@ -668,8 +677,20 @@ impl DefinitionKind<'_> {
DefinitionKind::Class(class) => class.node(module).range(),
DefinitionKind::TypeAlias(type_alias) => type_alias.node(module).range(),
DefinitionKind::NamedExpression(named) => named.node(module).range(),
DefinitionKind::Assignment(assignment) => assignment.target.node(module).range(),
DefinitionKind::AnnotatedAssignment(assign) => assign.target.node(module).range(),
DefinitionKind::Assignment(assign) => {
let target_range = assign.target.node(module).range();
let value_range = assign.value.node(module).range();
target_range.cover(value_range)
}
DefinitionKind::AnnotatedAssignment(assign) => {
let target_range = assign.target.node(module).range();
if let Some(ref value) = assign.value {
let value_range = value.node(module).range();
target_range.cover(value_range)
} else {
target_range
}
}
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.node(module).range(),
DefinitionKind::For(for_stmt) => for_stmt.target.node(module).range(),
DefinitionKind::Comprehension(comp) => comp.target(module).range(),

View File

@@ -665,7 +665,7 @@ impl PlaceTable {
}
/// Returns the place named `name`.
#[allow(unused)] // used in tests
#[cfg(test)]
pub(crate) fn place_by_name(&self, name: &str) -> Option<&PlaceExprWithFlags> {
let id = self.place_id_by_name(name)?;
Some(self.place_expr(id))

View File

@@ -306,7 +306,10 @@ pub(crate) struct UseDefMap<'db> {
/// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then
/// we don't actually need anything here, all we'll need to validate is that our own RHS is a
/// valid assignment to our own annotation.
bindings_by_declaration: FxHashMap<Definition<'db>, Bindings>,
///
/// If we see a binding to a `Final`-qualified symbol, we also need this map to find previous
/// bindings to that symbol. If there are any, the assignment is invalid.
bindings_by_definition: FxHashMap<Definition<'db>, Bindings>,
/// [`PlaceState`] visible at end of scope for each place.
end_of_scope_places: IndexVec<ScopedPlaceId, PlaceState>,
@@ -448,12 +451,12 @@ impl<'db> UseDefMap<'db> {
}
}
pub(crate) fn bindings_at_declaration(
pub(crate) fn bindings_at_definition(
&self,
declaration: Definition<'db>,
definition: Definition<'db>,
) -> BindingWithConstraintsIterator<'_, 'db> {
self.bindings_iterator(
&self.bindings_by_declaration[&declaration],
&self.bindings_by_definition[&definition],
BoundnessAnalysis::BasedOnUnboundVisibility,
)
}
@@ -744,8 +747,8 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Live declarations for each so-far-recorded binding.
declarations_by_binding: FxHashMap<Definition<'db>, Declarations>,
/// Live bindings for each so-far-recorded declaration.
bindings_by_declaration: FxHashMap<Definition<'db>, Bindings>,
/// Live bindings for each so-far-recorded definition.
bindings_by_definition: FxHashMap<Definition<'db>, Bindings>,
/// Currently live bindings and declarations for each place.
place_states: IndexVec<ScopedPlaceId, PlaceState>,
@@ -772,7 +775,7 @@ impl<'db> UseDefMapBuilder<'db> {
reachability: ScopedReachabilityConstraintId::ALWAYS_TRUE,
node_reachability: FxHashMap::default(),
declarations_by_binding: FxHashMap::default(),
bindings_by_declaration: FxHashMap::default(),
bindings_by_definition: FxHashMap::default(),
place_states: IndexVec::new(),
reachable_definitions: IndexVec::new(),
eager_snapshots: EagerSnapshots::default(),
@@ -808,6 +811,9 @@ impl<'db> UseDefMapBuilder<'db> {
binding: Definition<'db>,
is_place_name: bool,
) {
self.bindings_by_definition
.insert(binding, self.place_states[place].bindings().clone());
let def_id = self.all_definitions.push(DefinitionState::Defined(binding));
let place_state = &mut self.place_states[place];
self.declarations_by_binding
@@ -942,7 +948,7 @@ impl<'db> UseDefMapBuilder<'db> {
.all_definitions
.push(DefinitionState::Defined(declaration));
let place_state = &mut self.place_states[place];
self.bindings_by_declaration
self.bindings_by_definition
.insert(declaration, place_state.bindings().clone());
place_state.record_declaration(def_id, self.reachability);
@@ -1119,7 +1125,7 @@ impl<'db> UseDefMapBuilder<'db> {
self.bindings_by_use.shrink_to_fit();
self.node_reachability.shrink_to_fit();
self.declarations_by_binding.shrink_to_fit();
self.bindings_by_declaration.shrink_to_fit();
self.bindings_by_definition.shrink_to_fit();
self.eager_snapshots.shrink_to_fit();
UseDefMap {
@@ -1132,7 +1138,7 @@ impl<'db> UseDefMapBuilder<'db> {
end_of_scope_places: self.place_states,
reachable_definitions: self.reachable_definitions,
declarations_by_binding: self.declarations_by_binding,
bindings_by_declaration: self.bindings_by_declaration,
bindings_by_definition: self.bindings_by_definition,
eager_snapshots: self.eager_snapshots,
end_of_scope_reachability: self.reachability,
}

View File

@@ -45,7 +45,7 @@ impl<'db> SemanticModel<'db> {
&self,
import: &ast::StmtImportFrom,
_name: Option<usize>,
) -> Vec<Completion> {
) -> Vec<Completion<'db>> {
let module_name = match ModuleName::from_import_statement(self.db, self.file, import) {
Ok(module_name) => module_name,
Err(err) => {
@@ -62,7 +62,7 @@ impl<'db> SemanticModel<'db> {
/// Returns completions for symbols available in the given module as if
/// it were imported by this model's `File`.
fn module_completions(&self, module_name: &ModuleName) -> Vec<Completion> {
fn module_completions(&self, module_name: &ModuleName) -> Vec<Completion<'db>> {
let Some(module) = resolve_module(self.db, module_name) else {
tracing::debug!("Could not resolve module from `{module_name:?}`");
return vec![];
@@ -71,17 +71,22 @@ impl<'db> SemanticModel<'db> {
let builtin = module.is_known(KnownModule::Builtins);
crate::types::all_members(self.db, ty)
.into_iter()
.map(|name| Completion { name, builtin })
.map(|member| Completion {
name: member.name,
ty: member.ty,
builtin,
})
.collect()
}
/// Returns completions for symbols available in a `object.<CURSOR>` context.
pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec<Completion> {
pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec<Completion<'db>> {
let ty = node.value.inferred_type(self);
crate::types::all_members(self.db, ty)
.into_iter()
.map(|name| Completion {
name,
.map(|member| Completion {
name: member.name,
ty: member.ty,
builtin: false,
})
.collect()
@@ -92,7 +97,7 @@ impl<'db> SemanticModel<'db> {
///
/// If a scope could not be determined, then completions for the global
/// scope of this model's `File` are returned.
pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec<Completion> {
pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec<Completion<'db>> {
let index = semantic_index(self.db, self.file);
// TODO: We currently use `try_expression_scope_id` here as a hotfix for [1].
@@ -115,8 +120,9 @@ impl<'db> SemanticModel<'db> {
for (file_scope, _) in index.ancestor_scopes(file_scope) {
completions.extend(
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
.map(|name| Completion {
name,
.map(|member| Completion {
name: member.name,
ty: member.ty,
builtin: false,
}),
);
@@ -163,9 +169,11 @@ impl NameKind {
/// A suggestion for code completion.
#[derive(Clone, Debug)]
pub struct Completion {
pub struct Completion<'db> {
/// The label shown to the user for this suggestion.
pub name: Name,
/// The type of this completion.
pub ty: Type<'db>,
/// Whether this suggestion came from builtins or not.
///
/// At time of writing (2025-06-26), this information
@@ -175,6 +183,94 @@ pub struct Completion {
pub builtin: bool,
}
impl<'db> Completion<'db> {
/// Returns the "kind" of this completion.
///
/// This is meant to be a very general classification of this completion.
/// Typically, this is communicated from the LSP server to a client, and
/// the client uses this information to help improve the UX (perhaps by
/// assigning an icon of some kind to the completion).
pub fn kind(&self, db: &'db dyn Db) -> Option<CompletionKind> {
fn imp<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<CompletionKind> {
Some(match ty {
Type::FunctionLiteral(_)
| Type::DataclassDecorator(_)
| Type::WrapperDescriptor(_)
| Type::DataclassTransformer(_)
| Type::Callable(_) => CompletionKind::Function,
Type::BoundMethod(_) | Type::MethodWrapper(_) => CompletionKind::Method,
Type::ModuleLiteral(_) => CompletionKind::Module,
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => {
CompletionKind::Class
}
// This is a little weird for "struct." I'm mostly interpreting
// "struct" here as a more general "object." ---AG
Type::NominalInstance(_)
| Type::PropertyInstance(_)
| Type::Tuple(_)
| Type::BoundSuper(_) => CompletionKind::Struct,
Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::TypeIs(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_) => CompletionKind::Value,
Type::ProtocolInstance(_) => CompletionKind::Interface,
Type::TypeVar(_) => CompletionKind::TypeParameter,
Type::Union(union) => union.elements(db).iter().find_map(|&ty| imp(db, ty))?,
Type::Intersection(intersection) => {
intersection.iter_positive(db).find_map(|ty| imp(db, ty))?
}
Type::Dynamic(_)
| Type::Never
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy => return None,
})
}
imp(db, self.ty)
}
}
/// The "kind" of a completion.
///
/// This is taken directly from the LSP completion specification:
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind>
///
/// The idea here is that `Completion::kind` defines the mapping to this from
/// `Type` (and possibly other information), which might be interesting and
/// contentious. Then the outer edges map this to the LSP types, which is
/// expected to be mundane and boring.
#[derive(Clone, Copy, Debug)]
pub enum CompletionKind {
Text,
Method,
Function,
Constructor,
Field,
Variable,
Class,
Interface,
Module,
Property,
Unit,
Value,
Enum,
Keyword,
Snippet,
Color,
File,
Reference,
Folder,
EnumMember,
Constant,
Struct,
Event,
Operator,
TypeParameter,
}
pub trait HasType {
/// Returns the inferred type of `self`.
///

View File

@@ -23,6 +23,7 @@ use ruff_python_ast::PythonVersion;
use ruff_python_trivia::Cursor;
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
use ruff_text_size::{TextLen, TextRange};
use ty_static::EnvVars;
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
@@ -149,7 +150,7 @@ impl PythonEnvironment {
PythonEnvironment::new(path, origin, system)
}
if let Ok(virtual_env) = system.env_var("VIRTUAL_ENV") {
if let Ok(virtual_env) = system.env_var(EnvVars::VIRTUAL_ENV) {
return resolve_environment(
system,
SystemPath::new(&virtual_env),
@@ -158,7 +159,7 @@ impl PythonEnvironment {
.map(Some);
}
if let Ok(conda_env) = system.env_var("CONDA_PREFIX") {
if let Ok(conda_env) = system.env_var(EnvVars::CONDA_PREFIX) {
return resolve_environment(
system,
SystemPath::new(&conda_env),

View File

@@ -106,7 +106,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
index
.semantic_syntax_errors()
.iter()
.map(|error| Diagnostic::syntax_error(file, error, error)),
.map(|error| Diagnostic::invalid_syntax(file, error, error)),
);
check_suppressions(db, file, &mut diagnostics);
@@ -1104,7 +1104,9 @@ impl<'db> Type<'db> {
Type::FunctionLiteral(function_literal) => {
Some(Type::Callable(function_literal.into_callable_type(db)))
}
Type::BoundMethod(bound_method) => Some(bound_method.into_callable_type(db)),
Type::BoundMethod(bound_method) => {
Some(Type::Callable(bound_method.into_callable_type(db)))
}
Type::NominalInstance(_) | Type::ProtocolInstance(_) => {
let call_symbol = self
@@ -2613,7 +2615,7 @@ impl<'db> Type<'db> {
/// See also: [`Type::member`]
fn static_member(&self, db: &'db dyn Db, name: &str) -> Place<'db> {
if let Type::ModuleLiteral(module) = self {
module.static_member(db, name)
module.static_member(db, name).place
} else if let place @ Place::Type(_, _) = self.class_member(db, name.into()).place {
place
} else if let Some(place @ Place::Type(_, _)) =
@@ -3067,7 +3069,7 @@ impl<'db> Type<'db> {
Place::bound(Type::IntLiteral(i64::from(bool_value))).into()
}
Type::ModuleLiteral(module) => module.static_member(db, name_str).into(),
Type::ModuleLiteral(module) => module.static_member(db, name_str),
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
db,
@@ -7166,8 +7168,8 @@ fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
}
impl<'db> BoundMethodType<'db> {
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> {
Type::Callable(CallableType::new(
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
CallableType::new(
db,
CallableSignature::from_overloads(
self.function(db)
@@ -7177,7 +7179,7 @@ impl<'db> BoundMethodType<'db> {
.map(signatures::Signature::bind_self),
),
false,
))
)
}
fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self {
@@ -7511,15 +7513,14 @@ pub struct ModuleLiteralType<'db> {
impl get_size2::GetSize for ModuleLiteralType<'_> {}
impl<'db> ModuleLiteralType<'db> {
fn static_member(self, db: &'db dyn Db, name: &str) -> Place<'db> {
fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
// `__dict__` is a very special member that is never overridden by module globals;
// we should always look it up directly as an attribute on `types.ModuleType`,
// never in the global scope of the module.
if name == "__dict__" {
return KnownClass::ModuleType
.to_instance(db)
.member(db, "__dict__")
.place;
.member(db, "__dict__");
}
// If the file that originally imported the module has also imported a submodule
@@ -7538,7 +7539,8 @@ impl<'db> ModuleLiteralType<'db> {
full_submodule_name.extend(&submodule_name);
if imported_submodules.contains(&full_submodule_name) {
if let Some(submodule) = resolve_module(db, &full_submodule_name) {
return Place::bound(Type::module_literal(db, importing_file, &submodule));
return Place::bound(Type::module_literal(db, importing_file, &submodule))
.into();
}
}
}
@@ -7547,7 +7549,6 @@ impl<'db> ModuleLiteralType<'db> {
.file()
.map(|file| imported_symbol(db, file, name, None))
.unwrap_or_default()
.place
}
}

View File

@@ -668,7 +668,7 @@ impl<'db> Bindings<'db> {
ide_support::all_members(db, *ty)
.into_iter()
.sorted()
.map(|member| Type::string_literal(db, &member)),
.map(|member| Type::string_literal(db, &member.name)),
));
}
}

View File

@@ -602,7 +602,7 @@ impl<'db> ClassType<'db> {
// https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable
// by always respecting the signature of the metaclass `__call__`, rather than
// using a heuristic which makes unwarranted assumptions to sometimes ignore it.
return metaclass_dunder_call_function.into_callable_type(db);
return Type::Callable(metaclass_dunder_call_function.into_callable_type(db));
}
let dunder_new_function_symbol = self_ty
@@ -661,27 +661,33 @@ impl<'db> ClassType<'db> {
// same parameters as the `__init__` method after it is bound, and with the return type of
// the concrete type of `Self`.
let synthesized_dunder_init_callable =
if let Place::Type(Type::FunctionLiteral(dunder_init_function), _) =
dunder_init_function_symbol
{
let synthesized_signature = |signature: Signature<'db>| {
Signature::new(signature.parameters().clone(), Some(correct_return_type))
.bind_self()
if let Place::Type(ty, _) = dunder_init_function_symbol {
let signature = match ty {
Type::FunctionLiteral(dunder_init_function) => {
Some(dunder_init_function.signature(db))
}
Type::Callable(callable) => Some(callable.signatures(db)),
_ => None,
};
let synthesized_dunder_init_signature = CallableSignature::from_overloads(
dunder_init_function
.signature(db)
.overloads
.iter()
.cloned()
.map(synthesized_signature),
);
Some(Type::Callable(CallableType::new(
db,
synthesized_dunder_init_signature,
true,
)))
if let Some(signature) = signature {
let synthesized_signature = |signature: &Signature<'db>| {
Signature::new(signature.parameters().clone(), Some(correct_return_type))
.bind_self()
};
let synthesized_dunder_init_signature = CallableSignature::from_overloads(
signature.overloads.iter().map(synthesized_signature),
);
Some(Type::Callable(CallableType::new(
db,
synthesized_dunder_init_signature,
true,
)))
} else {
None
}
} else {
None
};
@@ -1650,7 +1656,7 @@ impl<'db> ClassLiteral<'db> {
if !declarations
.clone()
.all(|DeclarationWithConstraint { declaration, .. }| {
declaration.is_defined_and(|declaration| {
declaration.is_undefined_or(|declaration| {
matches!(
declaration.kind(db),
DefinitionKind::AnnotatedAssignment(..)
@@ -2042,7 +2048,11 @@ impl<'db> ClassLiteral<'db> {
/// A helper function for `instance_member` that looks up the `name` attribute only on
/// this class, not on its superclasses.
fn own_instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
pub(crate) fn own_instance_member(
self,
db: &'db dyn Db,
name: &str,
) -> PlaceAndQualifiers<'db> {
// TODO: There are many things that are not yet implemented here:
// - `typing.Final`
// - Proper diagnostics

View File

@@ -85,6 +85,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&STATIC_ASSERT_ERROR);
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
registry.register_lint(&REDUNDANT_CAST);
registry.register_lint(&INVALID_NONLOCAL);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -1560,6 +1561,25 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Detects `nonlocal` statements that don't match a binding in any enclosing scope.
///
/// ## Why is this bad?
/// Unmatched `nonlocal` statements will raise a `SyntaxError` at runtime.
///
/// ## Example
/// ```python
/// def f():
/// nonlocal x # error: no binding for nonlocal 'x' found
/// ```
pub(crate) static INVALID_NONLOCAL = {
summary: "detects unmatched `nonlocal` statements",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
/// A collection of type check diagnostics.
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
pub struct TypeCheckDiagnostics {

View File

@@ -766,7 +766,7 @@ impl<'db> FunctionType<'db> {
self.literal(db).signature(db, self.type_mappings(db))
}
/// Convert the `FunctionType` into a [`Type::Callable`].
/// Convert the `FunctionType` into a [`CallableType`].
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
CallableType::new(db, self.signature(db), false)
}

View File

@@ -1,3 +1,6 @@
use std::cmp::Ordering;
use crate::module_resolver::resolve_module;
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::place::ScopeId;
@@ -14,7 +17,7 @@ use rustc_hash::FxHashSet;
pub(crate) fn all_declarations_and_bindings<'db>(
db: &'db dyn Db,
scope_id: ScopeId<'db>,
) -> impl Iterator<Item = Name> + 'db {
) -> impl Iterator<Item = Member<'db>> + 'db {
let use_def_map = use_def_map(db, scope_id);
let table = place_table(db, scope_id);
@@ -24,10 +27,13 @@ pub(crate) fn all_declarations_and_bindings<'db>(
place_from_declarations(db, declarations)
.ok()
.and_then(|result| {
result
.place
.ignore_possibly_unbound()
.and_then(|_| table.place_expr(symbol_id).as_name().cloned())
result.place.ignore_possibly_unbound().and_then(|ty| {
table
.place_expr(symbol_id)
.as_name()
.cloned()
.map(|name| Member { name, ty })
})
})
})
.chain(
@@ -36,17 +42,23 @@ pub(crate) fn all_declarations_and_bindings<'db>(
.filter_map(move |(symbol_id, bindings)| {
place_from_bindings(db, bindings)
.ignore_possibly_unbound()
.and_then(|_| table.place_expr(symbol_id).as_name().cloned())
.and_then(|ty| {
table
.place_expr(symbol_id)
.as_name()
.cloned()
.map(|name| Member { name, ty })
})
}),
)
}
struct AllMembers {
members: FxHashSet<Name>,
struct AllMembers<'db> {
members: FxHashSet<Member<'db>>,
}
impl AllMembers {
fn of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Self {
impl<'db> AllMembers<'db> {
fn of(db: &'db dyn Db, ty: Type<'db>) -> Self {
let mut all_members = Self {
members: FxHashSet::default(),
};
@@ -54,7 +66,7 @@ impl AllMembers {
all_members
}
fn extend_with_type<'db>(&mut self, db: &'db dyn Db, ty: Type<'db>) {
fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) {
match ty {
Type::Union(union) => self.members.extend(
union
@@ -76,7 +88,6 @@ impl AllMembers {
Type::NominalInstance(instance) => {
let (class_literal, _specialization) = instance.class.class_literal(db);
self.extend_with_class_members(db, class_literal);
self.extend_with_instance_members(db, class_literal);
}
@@ -180,68 +191,143 @@ impl AllMembers {
}
}
self.members
.insert(place_table.place_expr(symbol_id).expect_name().clone());
self.members.insert(Member {
name: place_table.place_expr(symbol_id).expect_name().clone(),
ty,
});
}
let module_name = module.name();
self.members.extend(
imported_modules(db, literal.importing_file(db))
.iter()
.filter_map(|submodule_name| submodule_name.relative_to(module_name))
.filter_map(|relative_submodule_name| {
Some(Name::from(relative_submodule_name.components().next()?))
.filter_map(|submodule_name| {
let module = resolve_module(db, submodule_name)?;
let ty = Type::module_literal(db, file, &module);
Some((submodule_name, ty))
})
.filter_map(|(submodule_name, ty)| {
let relative = submodule_name.relative_to(module_name)?;
Some((relative, ty))
})
.filter_map(|(relative_submodule_name, ty)| {
let name = Name::from(relative_submodule_name.components().next()?);
Some(Member { name, ty })
}),
);
}
}
}
fn extend_with_declarations_and_bindings(&mut self, db: &dyn Db, scope_id: ScopeId) {
self.members
.extend(all_declarations_and_bindings(db, scope_id));
}
fn extend_with_class_members<'db>(
&mut self,
db: &'db dyn Db,
class_literal: ClassLiteral<'db>,
) {
fn extend_with_class_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
for parent in class_literal
.iter_mro(db, None)
.filter_map(ClassBase::into_class)
.map(|class| class.class_literal(db).0)
{
let parent_ty = Type::ClassLiteral(parent);
let parent_scope = parent.body_scope(db);
self.extend_with_declarations_and_bindings(db, parent_scope);
for Member { name, .. } in all_declarations_and_bindings(db, parent_scope) {
let result = parent_ty.member(db, name.as_str());
let Some(ty) = result.place.ignore_possibly_unbound() else {
continue;
};
self.members.insert(Member { name, ty });
}
}
}
fn extend_with_instance_members<'db>(
&mut self,
db: &'db dyn Db,
class_literal: ClassLiteral<'db>,
) {
fn extend_with_instance_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
for parent in class_literal
.iter_mro(db, None)
.filter_map(ClassBase::into_class)
.map(|class| class.class_literal(db).0)
{
let parent_instance = Type::instance(db, parent.default_specialization(db));
let class_body_scope = parent.body_scope(db);
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
for function_scope_id in attribute_scopes(db, class_body_scope) {
let place_table = index.place_table(function_scope_id);
self.members
.extend(place_table.instance_attributes().cloned());
for place_expr in place_table.places() {
let Some(name) = place_expr.as_instance_attribute() else {
continue;
};
let result = parent_instance.member(db, name.as_str());
let Some(ty) = result.place.ignore_possibly_unbound() else {
continue;
};
self.members.insert(Member {
name: name.clone(),
ty,
});
}
}
// This is very similar to `extend_with_class_members`,
// but uses the type of the class instance to query the
// class member. This gets us the right type for each
// member, e.g., `SomeClass.__delattr__` is not a bound
// method, but `instance_of_SomeClass.__delattr__` is.
for Member { name, .. } in all_declarations_and_bindings(db, class_body_scope) {
let result = parent_instance.member(db, name.as_str());
let Some(ty) = result.place.ignore_possibly_unbound() else {
continue;
};
self.members.insert(Member { name, ty });
}
}
}
}
/// A member of a type.
///
/// This represents a single item in (ideally) the list returned by
/// `dir(object)`.
///
/// The equality, comparison and hashing traits implemented for
/// this type are done so by taking only the name into account. At
/// present, this is because we assume the name is enough to uniquely
/// identify each attribute on an object. This is perhaps complicated
/// by overloads, but they only get represented by one member for
/// now. Moreover, it is convenient to be able to sort collections of
/// members, and a `Type` currently (as of 2025-07-09) has no way to do
/// ordered comparisons.
#[derive(Clone, Debug)]
pub struct Member<'db> {
pub name: Name,
pub ty: Type<'db>,
}
impl std::hash::Hash for Member<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl Eq for Member<'_> {}
impl<'db> PartialEq for Member<'db> {
fn eq(&self, rhs: &Member<'db>) -> bool {
self.name == rhs.name
}
}
impl<'db> Ord for Member<'db> {
fn cmp(&self, rhs: &Member<'db>) -> Ordering {
self.name.cmp(&rhs.name)
}
}
impl<'db> PartialOrd for Member<'db> {
fn partial_cmp(&self, rhs: &Member<'db>) -> Option<Ordering> {
Some(self.cmp(rhs))
}
}
/// List all members of a given type: anything that would be valid when accessed
/// as an attribute on an object of the given type.
pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Name> {
pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Member<'db>> {
AllMembers::of(db, ty).members
}

View File

@@ -92,15 +92,16 @@ use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
UNSUPPORTED_OPERATOR, report_implicit_return_type, report_instance_layout_conflict,
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
report_invalid_return_type, report_possibly_unbound_attribute,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_NONLOCAL, INVALID_PARAMETER_DEFAULT,
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_generator_function_return_type, report_invalid_return_type,
report_possibly_unbound_attribute,
};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
@@ -1564,24 +1565,74 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut bound_ty = ty;
let global_use_def_map = self.index.use_def_map(FileScopeId::global());
let nonlocal_use_def_map;
let place_id = binding.place(self.db());
let place = place_table.place_expr(place_id);
let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, place_id);
let declarations = if skip_non_global_scopes {
let (declarations, is_local) = if skip_non_global_scopes {
match self
.index
.place_table(FileScopeId::global())
.place_id_by_expr(&place.expr)
{
Some(id) => global_use_def_map.end_of_scope_declarations(id),
// This case is a syntax error (load before global declaration) but ignore that here
None => use_def.declarations_at_binding(binding),
Some(id) => (global_use_def_map.end_of_scope_declarations(id), false),
// This variable shows up in `global` declarations but doesn't have an explicit
// binding in the global scope.
None => (use_def.declarations_at_binding(binding), true),
}
} else if self
.index
.symbol_is_nonlocal_in_scope(place_id, file_scope_id)
{
// If we run out of ancestor scopes without finding a definition, we'll fall back to
// the local scope. This will also be a syntax error in `infer_nonlocal_statement` (no
// binding for `nonlocal` found), but ignore that here.
let mut declarations = use_def.declarations_at_binding(binding);
let mut is_local = true;
// Walk up parent scopes looking for the enclosing scope that has definition of this
// name. `ancestor_scopes` includes the current scope, so skip that one.
for (enclosing_scope_file_id, enclosing_scope) in
self.index.ancestor_scopes(file_scope_id).skip(1)
{
// Ignore class scopes and the global scope.
if !enclosing_scope.kind().is_function_like() {
continue;
}
let enclosing_place_table = self.index.place_table(enclosing_scope_file_id);
let Some(enclosing_place_id) = enclosing_place_table.place_id_by_expr(&place.expr)
else {
// This ancestor scope doesn't have a binding. Keep going.
continue;
};
if self
.index
.symbol_is_nonlocal_in_scope(enclosing_place_id, enclosing_scope_file_id)
{
// The variable is `nonlocal` in this ancestor scope. Keep going.
continue;
}
if self
.index
.symbol_is_global_in_scope(enclosing_place_id, enclosing_scope_file_id)
{
// The variable is `global` in this ancestor scope. This breaks the `nonlocal`
// chain, and it's a syntax error in `infer_nonlocal_statement`. Ignore that
// here and just bail out of this loop.
break;
}
// We found the closest definition. Note that (unlike in `infer_place_load`) this
// does *not* need to be a binding. It could be just `x: int`.
nonlocal_use_def_map = self.index.use_def_map(enclosing_scope_file_id);
declarations = nonlocal_use_def_map.end_of_scope_declarations(enclosing_place_id);
is_local = false;
break;
}
(declarations, is_local)
} else {
use_def.declarations_at_binding(binding)
(use_def.declarations_at_binding(binding), true)
};
let declared_ty = place_from_declarations(self.db(), declarations)
let (declared_ty, is_modifiable) = place_from_declarations(self.db(), declarations)
.and_then(|place_and_quals| {
Ok(
if matches!(place_and_quals.place, Place::Type(_, Boundness::Bound)) {
@@ -1600,8 +1651,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.map(
|PlaceAndQualifiers {
place: resolved_place,
..
qualifiers,
}| {
let is_modifiable = !qualifiers.contains(TypeQualifiers::FINAL);
if resolved_place.is_unbound() && !place_table.place_expr(place_id).is_name() {
if let AnyNodeRef::ExprAttribute(ast::ExprAttribute {
value, attr, ..
@@ -1611,7 +1664,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Place::Type(ty, Boundness::Bound) =
value_type.member(db, attr).place
{
return ty;
// TODO: also consider qualifiers on the attribute
return (ty, is_modifiable);
}
} else if let AnyNodeRef::ExprSubscript(ast::ExprSubscript {
value,
@@ -1623,12 +1677,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let slice_ty = self.infer_expression(slice);
let result_ty =
self.infer_subscript_expression_types(value, value_ty, slice_ty);
return result_ty;
return (result_ty, is_modifiable);
}
}
resolved_place
.ignore_possibly_unbound()
.unwrap_or(Type::unknown())
(
resolved_place
.ignore_possibly_unbound()
.unwrap_or(Type::unknown()),
is_modifiable,
)
},
)
.unwrap_or_else(|(ty, conflicting)| {
@@ -1640,8 +1697,64 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
format_enumeration(conflicting.iter().map(|ty| ty.display(db)))
));
}
ty.inner_type()
(
ty.inner_type(),
!ty.qualifiers.contains(TypeQualifiers::FINAL),
)
});
if !is_modifiable {
let mut previous_bindings = use_def.bindings_at_definition(binding);
// An assignment to a local `Final`-qualified symbol is only an error if there are prior bindings
let previous_definition = previous_bindings
.next()
.and_then(|r| r.binding.definition());
if !is_local || previous_definition.is_some() {
let place = place_table.place_expr(binding.place(db));
if let Some(builder) = self.context.report_lint(
&INVALID_ASSIGNMENT,
binding.full_range(self.db(), self.module()),
) {
let mut diagnostic = builder.into_diagnostic(format_args!(
"Reassignment of `Final` symbol `{place}` is not allowed"
));
diagnostic.set_primary_message("Reassignment of `Final` symbol");
if let Some(previous_definition) = previous_definition {
// It is not very helpful to show the previous definition if it results from
// an import. Ideally, we would show the original definition in the external
// module, but that information is currently not threaded through attribute
// lookup.
if !previous_definition.kind(db).is_import() {
if let DefinitionKind::AnnotatedAssignment(assignment) =
previous_definition.kind(db)
{
let range = assignment.annotation(self.module()).range();
diagnostic.annotate(
self.context
.secondary(range)
.message("Symbol declared as `Final` here"),
);
} else {
let range =
previous_definition.full_range(self.db(), self.module());
diagnostic.annotate(
self.context
.secondary(range)
.message("Symbol declared as `Final` here"),
);
}
diagnostic.set_primary_message("Symbol later reassigned here");
}
}
}
}
}
if !bound_ty.is_assignable_to(db, declared_ty) {
report_invalid_assignment(&self.context, node, declared_ty, bound_ty);
// allow declarations to override inference in case of invalid assignment
@@ -1721,7 +1834,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.is_declaration()
);
let use_def = self.index.use_def_map(declaration.file_scope(self.db()));
let prior_bindings = use_def.bindings_at_declaration(declaration);
let prior_bindings = use_def.bindings_at_definition(declaration);
// unbound_ty is Never because for this check we don't care about unbound
let inferred_ty = place_from_bindings(self.db(), prior_bindings)
.with_qualifiers(TypeQualifiers::empty())
@@ -2144,12 +2257,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::Stmt::Raise(raise) => self.infer_raise_statement(raise),
ast::Stmt::Return(ret) => self.infer_return_statement(ret),
ast::Stmt::Delete(delete) => self.infer_delete_statement(delete),
ast::Stmt::Nonlocal(nonlocal) => self.infer_nonlocal_statement(nonlocal),
ast::Stmt::Break(_)
| ast::Stmt::Continue(_)
| ast::Stmt::Pass(_)
| ast::Stmt::IpyEscapeCommand(_)
| ast::Stmt::Global(_)
| ast::Stmt::Nonlocal(_) => {
| ast::Stmt::Global(_) => {
// No-op
}
}
@@ -3345,167 +3458,193 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
dataclass_params.is_some_and(|params| params.contains(DataclassParams::FROZEN))
};
match object_ty.class_member(db, attribute.into()) {
meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target)
{
builder.into_diagnostic(format_args!(
"Cannot assign to ClassVar `{attribute}` \
from an instance of type `{ty}`",
ty = object_ty.display(self.db()),
));
}
}
false
}
PlaceAndQualifiers {
place: Place::Type(meta_attr_ty, meta_attr_boundness),
qualifiers: _,
} => {
if is_read_only() {
// First, try to call the `__setattr__` dunder method. If this is present/defined, overrides
// assigning the attributed by the normal mechanism.
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
db,
"__setattr__",
&mut CallArgumentTypes::positional([
Type::StringLiteral(StringLiteralType::new(db, Box::from(attribute))),
value_ty,
]),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
);
let check_setattr_return_type = |result: Bindings<'db>| -> bool {
match result.return_type(db) {
Type::Never => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
builder.into_diagnostic(format_args!(
"Property `{attribute}` defined in `{ty}` is read-only",
ty = object_ty.display(self.db()),
));
"Cannot assign to attribute `{attribute}` on type `{}` \
whose `__setattr__` method returns `Never`/`NoReturn`",
object_ty.display(db)
));
}
}
false
} else {
let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) =
meta_attr_ty.class_member(db, "__set__".into()).place
{
let successful_call = meta_dunder_set
.try_call(
db,
&CallArgumentTypes::positional([
meta_attr_ty,
object_ty,
value_ty,
]),
)
.is_ok();
if !successful_call && emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
builder.into_diagnostic(format_args!(
"Invalid assignment to data descriptor attribute \
`{attribute}` on type `{}` with custom `__set__` method",
object_ty.display(db)
));
}
}
successful_call
} else {
ensure_assignable_to(meta_attr_ty)
};
let assignable_to_instance_attribute = if meta_attr_boundness
== Boundness::PossiblyUnbound
{
let (assignable, boundness) =
if let Place::Type(instance_attr_ty, instance_attr_boundness) =
object_ty.instance_member(db, attribute).place
{
(
ensure_assignable_to(instance_attr_ty),
instance_attr_boundness,
)
} else {
(true, Boundness::PossiblyUnbound)
};
if boundness == Boundness::PossiblyUnbound {
report_possibly_unbound_attribute(
&self.context,
target,
attribute,
object_ty,
);
}
assignable
} else {
true
};
assignable_to_meta_attr && assignable_to_instance_attribute
}
_ => true,
}
};
PlaceAndQualifiers {
place: Place::Unbound,
..
} => {
if let Place::Type(instance_attr_ty, instance_attr_boundness) =
object_ty.instance_member(db, attribute).place
{
if instance_attr_boundness == Boundness::PossiblyUnbound {
report_possibly_unbound_attribute(
&self.context,
target,
attribute,
object_ty,
);
match setattr_dunder_call_result {
Ok(result) => check_setattr_return_type(result),
Err(CallDunderError::PossiblyUnbound(result)) => {
check_setattr_return_type(*result)
}
Err(CallDunderError::CallError(..)) => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
{
builder.into_diagnostic(format_args!(
"Can not assign object of type `{}` to attribute \
`{attribute}` on type `{}` with \
custom `__setattr__` method.",
value_ty.display(db),
object_ty.display(db)
));
}
if is_read_only() {
}
false
}
Err(CallDunderError::MethodNotAvailable) => {
match object_ty.class_member(db, attribute.into()) {
meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target)
{
builder.into_diagnostic(format_args!(
"Property `{attribute}` defined in `{ty}` is read-only",
"Cannot assign to ClassVar `{attribute}` \
from an instance of type `{ty}`",
ty = object_ty.display(self.db()),
));
}
}
false
} else {
ensure_assignable_to(instance_attr_ty)
}
} else {
let result = object_ty.try_call_dunder_with_policy(
db,
"__setattr__",
&mut CallArgumentTypes::positional([
Type::StringLiteral(StringLiteralType::new(
db,
Box::from(attribute),
)),
value_ty,
]),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
);
match result {
Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true,
Err(CallDunderError::CallError(..)) => {
PlaceAndQualifiers {
place: Place::Type(meta_attr_ty, meta_attr_boundness),
qualifiers: _,
} => {
if is_read_only() {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
builder.into_diagnostic(format_args!(
"Can not assign object of type `{}` to attribute \
`{attribute}` on type `{}` with \
custom `__setattr__` method.",
value_ty.display(db),
object_ty.display(db)
));
"Property `{attribute}` defined in `{ty}` is read-only",
ty = object_ty.display(self.db()),
));
}
}
false
} else {
let assignable_to_meta_attr =
if let Place::Type(meta_dunder_set, _) =
meta_attr_ty.class_member(db, "__set__".into()).place
{
let successful_call = meta_dunder_set
.try_call(
db,
&CallArgumentTypes::positional([
meta_attr_ty,
object_ty,
value_ty,
]),
)
.is_ok();
if !successful_call && emit_diagnostics {
if let Some(builder) = self
.context
.report_lint(&INVALID_ASSIGNMENT, target)
{
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
builder.into_diagnostic(format_args!(
"Invalid assignment to data descriptor attribute \
`{attribute}` on type `{}` with custom `__set__` method",
object_ty.display(db)
));
}
}
successful_call
} else {
ensure_assignable_to(meta_attr_ty)
};
let assignable_to_instance_attribute =
if meta_attr_boundness == Boundness::PossiblyUnbound {
let (assignable, boundness) = if let Place::Type(
instance_attr_ty,
instance_attr_boundness,
) =
object_ty.instance_member(db, attribute).place
{
(
ensure_assignable_to(instance_attr_ty),
instance_attr_boundness,
)
} else {
(true, Boundness::PossiblyUnbound)
};
if boundness == Boundness::PossiblyUnbound {
report_possibly_unbound_attribute(
&self.context,
target,
attribute,
object_ty,
);
}
assignable
} else {
true
};
assignable_to_meta_attr && assignable_to_instance_attribute
}
Err(CallDunderError::MethodNotAvailable) => {
}
PlaceAndQualifiers {
place: Place::Unbound,
..
} => {
if let Place::Type(instance_attr_ty, instance_attr_boundness) =
object_ty.instance_member(db, attribute).place
{
if instance_attr_boundness == Boundness::PossiblyUnbound {
report_possibly_unbound_attribute(
&self.context,
target,
attribute,
object_ty,
);
}
if is_read_only() {
if emit_diagnostics {
if let Some(builder) = self
.context
.report_lint(&INVALID_ASSIGNMENT, target)
{
builder.into_diagnostic(format_args!(
"Property `{attribute}` defined in `{ty}` is read-only",
ty = object_ty.display(self.db()),
));
}
}
false
} else {
ensure_assignable_to(instance_attr_ty)
}
} else {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
@@ -3654,7 +3793,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
Type::ModuleLiteral(module) => {
if let Place::Type(attr_ty, _) = module.static_member(db, attribute) {
if let Place::Type(attr_ty, _) = module.static_member(db, attribute).place {
let assignable = value_ty.is_assignable_to(db, attr_ty);
if assignable {
true
@@ -4401,7 +4540,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// First try loading the requested attribute from the module.
if !import_is_self_referential {
if let Place::Type(ty, boundness) = module_ty.member(self.db(), name).place {
if let PlaceAndQualifiers {
place: Place::Type(ty, boundness),
qualifiers,
} = module_ty.member(self.db(), name)
{
if &alias.name != "*" && boundness == Boundness::PossiblyUnbound {
// TODO: Consider loading _both_ the attribute and any submodule and unioning them
// together if the attribute exists but is possibly-unbound.
@@ -4417,7 +4560,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
alias.into(),
definition,
&DeclaredAndInferredType::AreTheSame(ty),
&DeclaredAndInferredType::MightBeDifferent {
declared_ty: TypeAndQualifiers {
inner: ty,
qualifiers,
},
inferred_ty: ty,
},
);
return;
}
@@ -4513,6 +4662,64 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
fn infer_nonlocal_statement(&mut self, nonlocal: &ast::StmtNonlocal) {
let ast::StmtNonlocal {
node_index: _,
range,
names,
} = nonlocal;
let db = self.db();
let scope = self.scope();
let file_scope_id = scope.file_scope_id(db);
let current_file = self.file();
'names: for name in names {
// Walk up parent scopes looking for a possible enclosing scope that may have a
// definition of this name visible to us. Note that we skip the scope containing the
// use that we are resolving, since we already looked for the place there up above.
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) {
// Class scopes are not visible to nested scopes, and `nonlocal` cannot refer to
// globals, so check only function-like scopes.
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file);
if !enclosing_scope_id.is_function_like(db) {
continue;
}
let enclosing_place_table = self.index.place_table(enclosing_scope_file_id);
let Some(enclosing_place_id) = enclosing_place_table.place_id_by_name(name) else {
// This scope doesn't define this name. Keep going.
continue;
};
// We've found a definition for this name in an enclosing function-like scope.
// Either this definition is the valid place this name refers to, or else we'll
// emit a syntax error. Either way, we won't walk any more enclosing scopes. Note
// that there are differences here compared to `infer_place_load`: A regular load
// (e.g. `print(x)`) is allowed to refer to a global variable (e.g. `x = 1` in the
// global scope), and similarly it's allowed to refer to a local variable in an
// enclosing function that's declared `global` (e.g. `global x`). However, the
// `nonlocal` keyword can't refer to global variables (that's a `SyntaxError`), and
// it also can't refer to local variables in enclosing functions that are declared
// `global` (also a `SyntaxError`).
if self
.index
.symbol_is_global_in_scope(enclosing_place_id, enclosing_scope_file_id)
{
// A "chain" of `nonlocal` statements is "broken" by a `global` statement. Stop
// looping and report that this `nonlocal` statement is invalid.
break;
}
// We found a definition. We've checked that the name isn't `global` in this scope,
// but it's ok if it's `nonlocal`. If a "chain" of `nonlocal` statements fails to
// lead to a valid binding, the outermost one will be an error; we don't need to
// walk the whole chain for each one.
continue 'names;
}
// There's no matching binding in an enclosing scope. This `nonlocal` statement is
// invalid.
if let Some(builder) = self.context.report_lint(&INVALID_NONLOCAL, range) {
builder.into_diagnostic(format_args!("no binding for nonlocal `{name}` found"));
}
}
}
fn module_type_from_name(&self, module_name: &ModuleName) -> Option<Type<'db>> {
resolve_module(self.db(), module_name)
.map(|module| Type::module_literal(self.db(), self.file(), &module))
@@ -5656,6 +5863,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
(place, None)
} else {
if expr_ref
.as_name_expr()
.is_some_and(|name| name.is_invalid())
{
return (Place::Unbound, None);
}
let use_id = expr_ref.scoped_use_id(db, scope);
let place = place_from_bindings(db, use_def.bindings_at_use(use_id));
(place, Some(use_id))
@@ -5695,13 +5909,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let current_file = self.file();
let mut is_nonlocal_binding = false;
if let Some(name) = expr.as_name() {
let skip_non_global_scopes = place_table
.place_id_by_name(name)
.is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id));
if skip_non_global_scopes {
return global_symbol(self.db(), self.file(), name);
if let Some(symbol_id) = place_table.place_id_by_name(name) {
if self.skip_non_global_scopes(file_scope_id, symbol_id) {
return global_symbol(self.db(), self.file(), name);
}
is_nonlocal_binding = self
.index
.symbol_is_nonlocal_in_scope(symbol_id, file_scope_id);
}
}
@@ -5714,7 +5930,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// a local variable or not in function-like scopes. If a variable has any bindings in a
// function-like scope, it is considered a local variable; it never references another
// scope. (At runtime, it would use the `LOAD_FAST` opcode.)
if has_bindings_in_this_scope && scope.is_function_like(db) {
if has_bindings_in_this_scope && scope.is_function_like(db) && !is_nonlocal_binding {
return Place::Unbound.into();
}

View File

@@ -1,5 +1,6 @@
use camino::Utf8Path;
use dir_test::{Fixture, dir_test};
use ty_static::EnvVars;
use ty_test::OutputFormat;
/// See `crates/ty_test/README.md` for documentation on these tests.
@@ -19,7 +20,7 @@ fn mdtest(fixture: Fixture<&str>) {
let test_name = test_name("mdtest", absolute_fixture_path);
let output_format = if std::env::var("MDTEST_GITHUB_ANNOTATIONS_FORMAT").is_ok() {
let output_format = if std::env::var(EnvVars::MDTEST_GITHUB_ANNOTATIONS_FORMAT).is_ok() {
OutputFormat::GitHub
} else {
OutputFormat::Cli

View File

@@ -1,10 +1,11 @@
use std::borrow::Cow;
use lsp_types::request::Completion;
use lsp_types::{CompletionItem, CompletionParams, CompletionResponse, Url};
use lsp_types::{CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, Url};
use ruff_db::source::{line_index, source_text};
use ty_ide::completion;
use ty_project::ProjectDatabase;
use ty_python_semantic::CompletionKind;
use crate::DocumentSnapshot;
use crate::document::PositionExt;
@@ -55,10 +56,14 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
let items: Vec<CompletionItem> = completions
.into_iter()
.enumerate()
.map(|(i, comp)| CompletionItem {
label: comp.name.into(),
sort_text: Some(format!("{i:-max_index_len$}")),
..Default::default()
.map(|(i, comp)| {
let kind = comp.kind(db).map(ty_kind_to_lsp_kind);
CompletionItem {
label: comp.name.into(),
kind,
sort_text: Some(format!("{i:-max_index_len$}")),
..Default::default()
}
})
.collect();
let response = CompletionResponse::Array(items);
@@ -69,3 +74,38 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
impl RetriableRequestHandler for CompletionRequestHandler {
const RETRY_ON_CANCELLATION: bool = true;
}
fn ty_kind_to_lsp_kind(kind: CompletionKind) -> CompletionItemKind {
// Gimme my dang globs in tight scopes!
#[allow(clippy::enum_glob_use)]
use self::CompletionKind::*;
// ref https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind
match kind {
Text => CompletionItemKind::TEXT,
Method => CompletionItemKind::METHOD,
Function => CompletionItemKind::FUNCTION,
Constructor => CompletionItemKind::CONSTRUCTOR,
Field => CompletionItemKind::FIELD,
Variable => CompletionItemKind::VARIABLE,
Class => CompletionItemKind::CLASS,
Interface => CompletionItemKind::INTERFACE,
Module => CompletionItemKind::MODULE,
Property => CompletionItemKind::PROPERTY,
Unit => CompletionItemKind::UNIT,
Value => CompletionItemKind::VALUE,
Enum => CompletionItemKind::ENUM,
Keyword => CompletionItemKind::KEYWORD,
Snippet => CompletionItemKind::SNIPPET,
Color => CompletionItemKind::COLOR,
File => CompletionItemKind::FILE,
Reference => CompletionItemKind::REFERENCE,
Folder => CompletionItemKind::FOLDER,
EnumMember => CompletionItemKind::ENUM_MEMBER,
Constant => CompletionItemKind::CONSTANT,
Struct => CompletionItemKind::STRUCT,
Event => CompletionItemKind::EVENT,
Operator => CompletionItemKind::OPERATOR,
TypeParameter => CompletionItemKind::TYPE_PARAMETER,
}
}

View File

@@ -0,0 +1,19 @@
[package]
name = "ty_static"
version = "0.0.1"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[lib]
doctest = false
[lints]
workspace = true
[dependencies]
ruff_macros = { workspace = true }

View File

@@ -0,0 +1,71 @@
use ruff_macros::attribute_env_vars_metadata;
/// Declares all environment variable used throughout `ty` and its crates.
pub struct EnvVars;
#[attribute_env_vars_metadata]
impl EnvVars {
/// If set, ty will use this value as the log level for its `--verbose` output.
/// Accepts any filter compatible with the `tracing_subscriber` crate.
///
/// For example:
///
/// - `TY_LOG=uv=debug` is the equivalent of `-vv` to the command line
/// - `TY_LOG=trace` will enable all trace-level logging.
///
/// See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
/// for more.
pub const TY_LOG: &'static str = "TY_LOG";
/// If set to `"1"` or `"true"`, ty will enable flamegraph profiling.
/// This creates a `tracing.folded` file that can be used to generate flame graphs
/// for performance analysis.
pub const TY_LOG_PROFILE: &'static str = "TY_LOG_PROFILE";
/// Control memory usage reporting format after ty execution.
///
/// Accepted values:
///
/// * `short` - Display short memory report
/// * `mypy_primer` - Display mypy_primer format and suppress workspace diagnostics
/// * `full` - Display full memory report
#[attr_hidden]
pub const TY_MEMORY_REPORT: &'static str = "TY_MEMORY_REPORT";
/// Specifies an upper limit for the number of tasks ty is allowed to run in parallel.
///
/// For example, how many files should be checked in parallel.
/// This isn't the same as a thread limit. ty may spawn additional threads
/// when necessary, e.g. to watch for file system changes or a dedicated UI thread.
pub const TY_MAX_PARALLELISM: &'static str = "TY_MAX_PARALLELISM";
/// Used to detect an activated virtual environment.
pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV";
/// Used to detect an activated Conda environment location.
/// If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred.
pub const CONDA_PREFIX: &'static str = "CONDA_PREFIX";
/// Filter which tests to run in mdtest.
///
/// Only tests whose names contain this filter string will be executed.
#[attr_hidden]
pub const MDTEST_TEST_FILTER: &'static str = "MDTEST_TEST_FILTER";
/// Switch mdtest output format to GitHub Actions annotations.
///
/// If set (to any value), mdtest will output errors in GitHub Actions format.
#[attr_hidden]
pub const MDTEST_GITHUB_ANNOTATIONS_FORMAT: &'static str = "MDTEST_GITHUB_ANNOTATIONS_FORMAT";
// Externally defined environment variables
/// Specifies an upper limit for the number of threads ty uses when performing work in parallel.
/// Equivalent to `TY_MAX_PARALLELISM`.
///
/// This is a standard Rayon environment variable.
pub const RAYON_NUM_THREADS: &'static str = "RAYON_NUM_THREADS";
/// Path to user-level configuration directory on Unix systems.
pub const XDG_CONFIG_HOME: &'static str = "XDG_CONFIG_HOME";
}

View File

@@ -0,0 +1,3 @@
pub use env_vars::*;
mod env_vars;

View File

@@ -19,6 +19,7 @@ ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_ast = { workspace = true }
ty_python_semantic = { workspace = true, features = ["serde", "testing"] }
ty_static = { workspace = true }
ty_vendored = { workspace = true }
anyhow = { workspace = true }

View File

@@ -29,7 +29,7 @@ mod diagnostic;
mod matcher;
mod parser;
const MDTEST_TEST_FILTER: &str = "MDTEST_TEST_FILTER";
use ty_static::EnvVars;
/// Run `path` as a markdown test suite with given `title`.
///
@@ -53,7 +53,7 @@ pub fn run(
let mut db = db::Db::setup();
let filter = std::env::var(MDTEST_TEST_FILTER).ok();
let filter = std::env::var(EnvVars::MDTEST_TEST_FILTER).ok();
let mut any_failures = false;
for test in suite.tests() {
if filter
@@ -105,10 +105,12 @@ pub fn run(
if output_format.is_cli() {
println!(
"\nTo rerun this specific test, set the environment variable: {MDTEST_TEST_FILTER}='{escaped_test_name}'",
"\nTo rerun this specific test, set the environment variable: {}='{escaped_test_name}'",
EnvVars::MDTEST_TEST_FILTER,
);
println!(
"{MDTEST_TEST_FILTER}='{escaped_test_name}' cargo test -p ty_python_semantic --test mdtest -- {test_name}",
"{}='{escaped_test_name}' cargo test -p ty_python_semantic --test mdtest -- {test_name}",
EnvVars::MDTEST_TEST_FILTER,
);
}
}
@@ -322,14 +324,14 @@ fn run_test(
let mut diagnostics: Vec<Diagnostic> = parsed
.errors()
.iter()
.map(|error| Diagnostic::syntax_error(test_file.file, &error.error, error))
.map(|error| Diagnostic::invalid_syntax(test_file.file, &error.error, error))
.collect();
diagnostics.extend(
parsed
.unsupported_syntax_errors()
.iter()
.map(|error| Diagnostic::syntax_error(test_file.file, error, error)),
.map(|error| Diagnostic::invalid_syntax(test_file.file, error, error)),
);
let mdtest_result = attempt_test(db, check_types, test_file, "run mdtest", None);

View File

@@ -57,6 +57,7 @@ KNOWN_FORMATTING_VIOLATIONS = [
"incorrect-blank-line-after-class",
"incorrect-blank-line-before-class",
"indentation-with-invalid-multiple",
"indentation-with-invalid-multiple-comment",
"line-too-long",
"missing-trailing-comma",
"missing-whitespace",
@@ -111,7 +112,6 @@ KNOWN_FORMATTING_VIOLATIONS = [
# For some docs, Ruff is unable to parse the example code.
KNOWN_PARSE_ERRORS = [
"blank-line-with-whitespace",
"indentation-with-invalid-multiple-comment",
"indented-form-feed",
"missing-newline-at-end-of-file",
"mixed-spaces-and-tabs",

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env bash
set -eu
echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
mkdir -p ~/.config/ty
@@ -19,7 +20,7 @@ cd ..
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@e5f55447969d33ae3c7ccdb183e2a37101867270" \
--from="git+https://github.com/hauntsaninja/mypy_primer@59509d48de6da6aaa4e3a2f5e338769bc471f2d7" \
mypy_primer \
--repo ruff \
--type-checker ty \

10
ty.schema.json generated
View File

@@ -531,6 +531,16 @@
}
]
},
"invalid-nonlocal": {
"title": "detects unmatched `nonlocal` statements",
"description": "## What it does\nDetects `nonlocal` statements that don't match a binding in any enclosing scope.\n\n## Why is this bad?\nUnmatched `nonlocal` statements will raise a `SyntaxError` at runtime.\n\n## Example\n```python\ndef f():\n nonlocal x # error: no binding for nonlocal 'x' found\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"invalid-overload": {
"title": "detects invalid `@overload` usages",
"description": "## What it does\nChecks for various invalid `@overload` usages.\n\n## Why is this bad?\nThe `@overload` decorator is used to define functions and methods that accepts different\ncombinations of arguments and return different types based on the arguments passed. This is\nmainly beneficial for type checkers. But, if the `@overload` usage is invalid, the type\nchecker may not be able to provide correct type information.\n\n## Example\n\nDefining only one overload:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo(x: int) -> int: ...\ndef foo(x: int | None) -> int | None:\n return x\n```\n\nOr, not providing an implementation for the overloaded definition:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo() -> None: ...\n@overload\ndef foo(x: int) -> int: ...\n```\n\n## References\n- [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload)",