Compare commits

..

7 Commits

Author SHA1 Message Date
Ibraheem Ahmed
531ca7e47a improve generic call inference performance 2025-12-11 10:21:43 -05:00
Ibraheem Ahmed
caed80df5e avoid dataclass field regression 2025-12-10 18:33:00 -05:00
Ibraheem Ahmed
5d07d58d59 improve performance of find_type_var_from 2025-12-10 17:53:00 -05:00
Ibraheem Ahmed
19c5cd8018 improve signature of tuple constructor 2025-12-02 17:01:14 -05:00
Ibraheem Ahmed
12e72041c6 more tests 2025-12-02 17:01:09 -05:00
Ibraheem Ahmed
be78ea3851 avoid quadratic behavior in find_type_var_from 2025-12-02 16:30:22 -05:00
Ibraheem Ahmed
623210232e simple subtyping for bidirectional inference 2025-12-02 15:36:42 -05:00
270 changed files with 9606 additions and 23506 deletions

View File

@@ -75,6 +75,14 @@
matchManagers: ["cargo"],
enabled: false,
},
{
// `mkdocs-material` requires a manual update to keep the version in sync
// with `mkdocs-material-insider`.
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
matchManagers: ["pip_requirements"],
matchPackageNames: ["mkdocs-material"],
enabled: false,
},
{
groupName: "pre-commit dependencies",
matchManagers: ["pre-commit"],

View File

@@ -24,8 +24,6 @@ env:
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.14"
NEXTEST_PROFILE: ci
# Enable mdtests that require external dependencies
MDTEST_EXTERNAL: "1"
jobs:
determine_changes:
@@ -781,6 +779,8 @@ jobs:
name: "mkdocs"
runs-on: ubuntu-latest
timeout-minutes: 10
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -788,6 +788,11 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
@@ -795,7 +800,11 @@ jobs:
with:
python-version: 3.13
activate-environment: true
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt
- name: "Install dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: uv pip install -r docs/requirements.txt
- name: "Update README File"
run: python scripts/transform_readme.py --target mkdocs
@@ -803,8 +812,12 @@ jobs:
run: python scripts/generate_mkdocs.py
- name: "Check docs formatting"
run: python scripts/check_docs_formatted.py
- name: "Build Insiders docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: mkdocs build --strict -f mkdocs.insiders.yml
- name: "Build docs"
run: mkdocs build --strict -f mkdocs.yml
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.public.yml
check-formatter-instability-and-black-similarity:
name: "formatter instabilities and black similarity"

View File

@@ -20,6 +20,8 @@ on:
jobs:
mkdocs:
runs-on: ubuntu-latest
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -57,12 +59,23 @@ jobs:
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> "$GITHUB_ENV"
echo "timestamp=$timestamp" >> "$GITHUB_ENV"
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: pip install -r docs/requirements-insiders.txt
- name: "Install dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: pip install -r docs/requirements.txt
- name: "Copy README File"
@@ -70,8 +83,13 @@ jobs:
python scripts/transform_readme.py --target mkdocs
python scripts/generate_mkdocs.py
- name: "Build Insiders docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: mkdocs build --strict -f mkdocs.insiders.yml
- name: "Build docs"
run: mkdocs build --strict -f mkdocs.yml
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.public.yml
- name: "Clone docs repo"
run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs

View File

@@ -18,8 +18,7 @@ jobs:
environment:
name: release
permissions:
# For PyPI's trusted publishing.
id-token: write
id-token: write # For PyPI's trusted publishing + PEP 740 attestations
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
@@ -28,5 +27,8 @@ jobs:
pattern: wheels-*
path: wheels
merge-multiple: true
- uses: astral-sh/attest-action@2c727738cea36d6c97dd85eb133ea0e0e8fe754b # v0.0.4
with:
paths: wheels/*
- name: Publish to PyPi
run: uv publish -v wheels/*

View File

@@ -1,34 +1,5 @@
# Changelog
## 0.14.8
Released on 2025-12-04.
### Preview features
- \[`flake8-bugbear`\] Catch `yield` expressions within other statements (`B901`) ([#21200](https://github.com/astral-sh/ruff/pull/21200))
- \[`flake8-use-pathlib`\] Mark fixes unsafe for return type changes (`PTH104`, `PTH105`, `PTH109`, `PTH115`) ([#21440](https://github.com/astral-sh/ruff/pull/21440))
### Bug fixes
- Fix syntax error false positives for `await` outside functions ([#21763](https://github.com/astral-sh/ruff/pull/21763))
- \[`flake8-simplify`\] Fix truthiness assumption for non-iterable arguments in tuple/list/set calls (`SIM222`, `SIM223`) ([#21479](https://github.com/astral-sh/ruff/pull/21479))
### Documentation
- Suggest using `--output-file` option in GitLab integration ([#21706](https://github.com/astral-sh/ruff/pull/21706))
### Other changes
- [syntax-error] Default type parameter followed by non-default type parameter ([#21657](https://github.com/astral-sh/ruff/pull/21657))
### Contributors
- [@kieran-ryan](https://github.com/kieran-ryan)
- [@11happy](https://github.com/11happy)
- [@danparizher](https://github.com/danparizher)
- [@ntBre](https://github.com/ntBre)
## 0.14.7
Released on 2025-11-28.

View File

@@ -331,6 +331,13 @@ you addressed them.
## MkDocs
> [!NOTE]
>
> The documentation uses Material for MkDocs Insiders, which is closed-source software.
> This means only members of the Astral organization can preview the documentation exactly as it
> will appear in production.
> Outside contributors can still preview the documentation, but there will be some differences. Consult [the Material for MkDocs documentation](https://squidfunk.github.io/mkdocs-material/insiders/benefits/#features) for which features are exclusively available in the insiders version.
To preview any changes to the documentation locally:
1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install).
@@ -344,7 +351,11 @@ To preview any changes to the documentation locally:
1. Run the development server with:
```shell
uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.yml
# For contributors.
uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.public.yml
# For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
uvx --with-requirements docs/requirements-insiders.txt -- mkdocs serve -f mkdocs.insiders.yml
```
The documentation should then be available locally at

8
Cargo.lock generated
View File

@@ -2859,7 +2859,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.8"
version = "0.14.7"
dependencies = [
"anyhow",
"argfile",
@@ -3117,14 +3117,13 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.8"
version = "0.14.7"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.10.0",
"clap",
"colored 3.0.0",
"compact_str",
"fern",
"glob",
"globset",
@@ -3473,7 +3472,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.8"
version = "0.14.7"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -4557,7 +4556,6 @@ dependencies = [
"anyhow",
"camino",
"colored 3.0.0",
"dunce",
"insta",
"memchr",
"path-slash",

View File

@@ -272,12 +272,6 @@ large_stack_arrays = "allow"
lto = "fat"
codegen-units = 16
# Profile to build a minimally sized binary for ruff/ty
[profile.minimal-size]
inherits = "release"
opt-level = "z"
codegen-units = 1
# Some crates don't change as much but benefit more from
# more expensive optimization passes, so we selectively
# decrease codegen-units in some cases.

View File

@@ -147,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.8/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.8/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.7/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -181,7 +181,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.8
rev: v0.14.7
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -166,8 +166,28 @@ impl Diagnostic {
/// Returns the primary message for this diagnostic.
///
/// A diagnostic always has a message, but it may be empty.
///
/// NOTE: At present, this routine will return the first primary
/// annotation's message as the primary message when the main diagnostic
/// message is empty. This is meant to facilitate an incremental migration
/// in ty over to the new diagnostic data model. (The old data model
/// didn't distinguish between messages on the entire diagnostic and
/// messages attached to a particular span.)
pub fn primary_message(&self) -> &str {
self.inner.message.as_str()
if !self.inner.message.as_str().is_empty() {
return self.inner.message.as_str();
}
// FIXME: As a special case, while we're migrating ty
// to the new diagnostic data model, we'll look for a primary
// message from the primary annotation. This is because most
// ty diagnostics are created with an empty diagnostic
// message and instead attach the message to the annotation.
// Fixing this will require touching basically every diagnostic
// in ty, so we do it this way for now to match the old
// semantics. ---AG
self.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default()
}
/// Introspects this diagnostic and returns what kind of "primary" message
@@ -179,6 +199,18 @@ impl Diagnostic {
/// contains *essential* information or context for understanding the
/// diagnostic.
///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
@@ -192,10 +224,11 @@ impl Diagnostic {
.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default();
if annotation.is_empty() {
ConciseMessage::MainDiagnostic(main)
} else {
ConciseMessage::Both { main, annotation }
match (main.is_empty(), annotation.is_empty()) {
(false, true) => ConciseMessage::MainDiagnostic(main),
(true, false) => ConciseMessage::PrimaryAnnotation(annotation),
(false, false) => ConciseMessage::Both { main, annotation },
(true, true) => ConciseMessage::Empty,
}
}
@@ -660,6 +693,18 @@ impl SubDiagnostic {
/// contains *essential* information or context for understanding the
/// diagnostic.
///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
@@ -669,10 +714,11 @@ impl SubDiagnostic {
.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default();
if annotation.is_empty() {
ConciseMessage::MainDiagnostic(main)
} else {
ConciseMessage::Both { main, annotation }
match (main.is_empty(), annotation.is_empty()) {
(false, true) => ConciseMessage::MainDiagnostic(main),
(true, false) => ConciseMessage::PrimaryAnnotation(annotation),
(false, false) => ConciseMessage::Both { main, annotation },
(true, true) => ConciseMessage::Empty,
}
}
}
@@ -842,10 +888,6 @@ impl Annotation {
pub fn hide_snippet(&mut self, yes: bool) {
self.hide_snippet = yes;
}
pub fn is_primary(&self) -> bool {
self.is_primary
}
}
/// Tags that can be associated with an annotation.
@@ -1466,10 +1508,28 @@ pub enum DiagnosticFormat {
pub enum ConciseMessage<'a> {
/// A diagnostic contains a non-empty main message and an empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
MainDiagnostic(&'a str),
/// A diagnostic contains an empty main message and a non-empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "old" data model.
PrimaryAnnotation(&'a str),
/// A diagnostic contains a non-empty main message and a non-empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
Both { main: &'a str, annotation: &'a str },
/// A diagnostic contains an empty main message and an empty
/// primary annotation message.
///
/// This indicates that the diagnostic is probably using the old
/// model.
Empty,
/// A custom concise message has been provided.
Custom(&'a str),
}
@@ -1480,9 +1540,13 @@ impl std::fmt::Display for ConciseMessage<'_> {
ConciseMessage::MainDiagnostic(main) => {
write!(f, "{main}")
}
ConciseMessage::PrimaryAnnotation(annotation) => {
write!(f, "{annotation}")
}
ConciseMessage::Both { main, annotation } => {
write!(f, "{main}: {annotation}")
}
ConciseMessage::Empty => Ok(()),
ConciseMessage::Custom(message) => {
write!(f, "{message}")
}

View File

@@ -21,11 +21,7 @@ use crate::source::source_text;
/// reflected in the changed AST offsets.
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
/// for determining if a query result is unchanged.
///
/// The LRU capacity of 200 was picked without any empirical evidence that it's optimal,
/// instead it's a wild guess that it should be unlikely that incremental changes involve
/// more than 200 modules. Parsed ASTs within the same revision are never evicted by Salsa.
#[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size, lru=200)]
#[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let _span = tracing::trace_span!("parsed_module", ?file).entered();
@@ -96,9 +92,14 @@ impl ParsedModule {
self.inner.store(None);
}
/// Returns the file to which this module belongs.
pub fn file(&self) -> File {
self.file
/// Returns the pointer address of this [`ParsedModule`].
///
/// The pointer uniquely identifies the module within the current Salsa revision,
/// regardless of whether particular [`ParsedModuleRef`] instances are garbage collected.
pub fn addr(&self) -> usize {
// Note that the outer `Arc` in `inner` is stable across garbage collection, while the inner
// `Arc` within the `ArcSwap` may change.
Arc::as_ptr(&self.inner).addr()
}
}

View File

@@ -667,13 +667,6 @@ impl Deref for SystemPathBuf {
}
}
impl AsRef<Path> for SystemPathBuf {
#[inline]
fn as_ref(&self) -> &Path {
self.0.as_std_path()
}
}
impl<P: AsRef<SystemPath>> FromIterator<P> for SystemPathBuf {
fn from_iter<I: IntoIterator<Item = P>>(iter: I) -> Self {
let mut buf = SystemPathBuf::new();

View File

@@ -49,7 +49,7 @@ impl ModuleImports {
// Resolve the imports.
let mut resolved_imports = ModuleImports::default();
for import in imports {
for resolved in Resolver::new(db, path).resolve(import) {
for resolved in Resolver::new(db).resolve(import) {
if let Some(path) = resolved.as_system_path() {
resolved_imports.insert(path.to_path_buf());
}

View File

@@ -1,9 +1,5 @@
use ruff_db::files::{File, FilePath, system_path_to_file};
use ruff_db::system::SystemPath;
use ty_python_semantic::{
ModuleName, resolve_module, resolve_module_confident, resolve_real_module,
resolve_real_module_confident,
};
use ruff_db::files::FilePath;
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
use crate::ModuleDb;
use crate::collector::CollectedImport;
@@ -11,15 +7,12 @@ use crate::collector::CollectedImport;
/// Collect all imports for a given Python file.
pub(crate) struct Resolver<'a> {
db: &'a ModuleDb,
file: Option<File>,
}
impl<'a> Resolver<'a> {
/// Initialize a [`Resolver`] with a given [`ModuleDb`].
pub(crate) fn new(db: &'a ModuleDb, path: &SystemPath) -> Self {
// If we know the importing file we can potentially resolve more imports
let file = system_path_to_file(db, path).ok();
Self { db, file }
pub(crate) fn new(db: &'a ModuleDb) -> Self {
Self { db }
}
/// Resolve the [`CollectedImport`] into a [`FilePath`].
@@ -77,21 +70,13 @@ impl<'a> Resolver<'a> {
/// Resolves a module name to a module.
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = if let Some(file) = self.file {
resolve_module(self.db, file, module_name)?
} else {
resolve_module_confident(self.db, module_name)?
};
let module = resolve_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
/// Resolves a module name to a module (stubs not allowed).
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = if let Some(file) = self.file {
resolve_real_module(self.db, file, module_name)?
} else {
resolve_real_module_confident(self.db, module_name)?
};
let module = resolve_real_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.14.8"
version = "0.14.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -35,7 +35,6 @@ anyhow = { workspace = true }
bitflags = { workspace = true }
clap = { workspace = true, features = ["derive", "string"], optional = true }
colored = { workspace = true }
compact_str = { workspace = true }
fern = { workspace = true }
glob = { workspace = true }
globset = { workspace = true }

View File

@@ -28,11 +28,9 @@ yaml.load("{}", SafeLoader)
yaml.load("{}", yaml.SafeLoader)
yaml.load("{}", CSafeLoader)
yaml.load("{}", yaml.CSafeLoader)
yaml.load("{}", yaml.cyaml.CSafeLoader)
yaml.load("{}", NewSafeLoader)
yaml.load("{}", Loader=SafeLoader)
yaml.load("{}", Loader=yaml.SafeLoader)
yaml.load("{}", Loader=CSafeLoader)
yaml.load("{}", Loader=yaml.CSafeLoader)
yaml.load("{}", Loader=yaml.cyaml.CSafeLoader)
yaml.load("{}", Loader=NewSafeLoader)

View File

@@ -52,16 +52,16 @@ def not_broken5():
yield inner()
def broken3():
def not_broken6():
return (yield from [])
def broken4():
def not_broken7():
x = yield from []
return x
def broken5():
def not_broken8():
x = None
def inner(ex):
@@ -76,13 +76,3 @@ class NotBroken9(object):
def __await__(self):
yield from function()
return 42
async def broken6():
yield 1
return foo()
async def broken7():
yield 1
return [1, 2, 3]

View File

@@ -17,24 +17,3 @@ def _():
# Valid yield scope
yield 3
# await is valid in any generator, sync or async
(await cor async for cor in f()) # ok
(await cor for cor in f()) # ok
# but not in comprehensions
[await cor async for cor in f()] # F704
{await cor async for cor in f()} # F704
{await cor: 1 async for cor in f()} # F704
[await cor for cor in f()] # F704
{await cor for cor in f()} # F704
{await cor: 1 for cor in f()} # F704
# or in the iterator of an async generator, which is evaluated in the parent
# scope
(cor async for cor in await f()) # F704
(await cor async for cor in [await c for c in f()]) # F704
# this is also okay because the comprehension is within the generator scope
([await c for c in cor] async for cor in f()) # ok

View File

@@ -3,5 +3,3 @@ def func():
# Top-level await
await 1
([await c for c in cor] async for cor in func()) # ok

View File

@@ -1,24 +0,0 @@
async def gen():
yield 1
return 42
def gen(): # B901 but not a syntax error - not an async generator
yield 1
return 42
async def gen(): # ok - no value in return
yield 1
return
async def gen():
yield 1
return foo()
async def gen():
yield 1
return [1, 2, 3]
async def gen():
if True:
yield 1
return 10

View File

@@ -69,7 +69,6 @@ use crate::noqa::NoqaMapping;
use crate::package::PackageRoot;
use crate::preview::is_undefined_export_in_dunder_init_enabled;
use crate::registry::Rule;
use crate::rules::flake8_bugbear::rules::ReturnInGenerator;
use crate::rules::pyflakes::rules::{
LateFutureImport, MultipleStarredExpressions, ReturnOutsideFunction,
UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction,
@@ -730,12 +729,6 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(NonlocalWithoutBinding { name }, error.range);
}
}
SemanticSyntaxErrorKind::ReturnInGenerator => {
// B901
if self.is_rule_enabled(Rule::ReturnInGenerator) {
self.report_diagnostic(ReturnInGenerator, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
@@ -754,7 +747,6 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
| SemanticSyntaxErrorKind::TypeParameterDefaultOrder(_)
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => {
self.semantic_errors.borrow_mut().push(error);
}
@@ -788,10 +780,6 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Generator {
kind: GeneratorKind::Generator,
..
} => return true,
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
@@ -841,19 +829,14 @@ impl SemanticSyntaxContext for Checker<'_> {
self.source_type.is_ipynb()
}
fn in_generator_context(&self) -> bool {
for scope in self.semantic.current_scopes() {
if matches!(
scope.kind,
ScopeKind::Generator {
kind: GeneratorKind::Generator,
..
}
) {
return true;
fn in_generator_scope(&self) -> bool {
matches!(
&self.semantic.current_scope().kind,
ScopeKind::Generator {
kind: GeneratorKind::Generator,
..
}
}
false
)
}
fn in_loop_context(&self) -> bool {

View File

@@ -46,7 +46,6 @@ pub mod rule_selector;
pub mod rules;
pub mod settings;
pub mod source_kind;
pub mod suppression;
mod text_helpers;
pub mod upstream_categories;
mod violation;

View File

@@ -1043,7 +1043,6 @@ mod tests {
Rule::YieldFromInAsyncFunction,
Path::new("yield_from_in_async_function.py")
)]
#[test_case(Rule::ReturnInGenerator, Path::new("return_in_generator.py"))]
fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy().to_string();
let path = Path::new("resources/test/fixtures/syntax_errors").join(path);

View File

@@ -75,7 +75,6 @@ pub(crate) fn unsafe_yaml_load(checker: &Checker, call: &ast::ExprCall) {
qualified_name.segments(),
["yaml", "SafeLoader" | "CSafeLoader"]
| ["yaml", "loader", "SafeLoader" | "CSafeLoader"]
| ["yaml", "cyaml", "CSafeLoader"]
)
})
{

View File

@@ -1,5 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_python_ast::statement_visitor;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{self as ast, Expr, Stmt, StmtFunctionDef};
use ruff_text_size::TextRange;
@@ -95,11 +96,6 @@ pub(crate) fn return_in_generator(checker: &Checker, function_def: &StmtFunction
return;
}
// Async functions are flagged by the `ReturnInGenerator` semantic syntax error.
if function_def.is_async {
return;
}
let mut visitor = ReturnInGeneratorVisitor::default();
visitor.visit_body(&function_def.body);
@@ -116,9 +112,15 @@ struct ReturnInGeneratorVisitor {
has_yield: bool,
}
impl Visitor<'_> for ReturnInGeneratorVisitor {
impl StatementVisitor<'_> for ReturnInGeneratorVisitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Expr(ast::StmtExpr { value, .. }) => match **value {
Expr::Yield(_) | Expr::YieldFrom(_) => {
self.has_yield = true;
}
_ => {}
},
Stmt::FunctionDef(_) => {
// Do not recurse into nested functions; they're evaluated separately.
}
@@ -128,19 +130,8 @@ impl Visitor<'_> for ReturnInGeneratorVisitor {
node_index: _,
}) => {
self.return_ = Some(*range);
walk_stmt(self, stmt);
}
_ => walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Lambda(_) => {}
Expr::Yield(_) | Expr::YieldFrom(_) => {
self.has_yield = true;
}
_ => walk_expr(self, expr),
_ => statement_visitor::walk_stmt(self, stmt),
}
}
}

View File

@@ -21,46 +21,3 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf
37 |
38 | yield from not_broken()
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:56:5
|
55 | def broken3():
56 | return (yield from [])
| ^^^^^^^^^^^^^^^^^^^^^^
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:61:5
|
59 | def broken4():
60 | x = yield from []
61 | return x
| ^^^^^^^^
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:72:5
|
71 | inner((yield from []))
72 | return x
| ^^^^^^^^
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:83:5
|
81 | async def broken6():
82 | yield 1
83 | return foo()
| ^^^^^^^^^^^^
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:88:5
|
86 | async def broken7():
87 | yield 1
88 | return [1, 2, 3]
| ^^^^^^^^^^^^^^^^
|

View File

@@ -37,88 +37,3 @@ F704 `await` statement outside of a function
12 |
13 | def _():
|
F704 `await` statement outside of a function
--> F704.py:27:2
|
26 | # but not in comprehensions
27 | [await cor async for cor in f()] # F704
| ^^^^^^^^^
28 | {await cor async for cor in f()} # F704
29 | {await cor: 1 async for cor in f()} # F704
|
F704 `await` statement outside of a function
--> F704.py:28:2
|
26 | # but not in comprehensions
27 | [await cor async for cor in f()] # F704
28 | {await cor async for cor in f()} # F704
| ^^^^^^^^^
29 | {await cor: 1 async for cor in f()} # F704
30 | [await cor for cor in f()] # F704
|
F704 `await` statement outside of a function
--> F704.py:29:2
|
27 | [await cor async for cor in f()] # F704
28 | {await cor async for cor in f()} # F704
29 | {await cor: 1 async for cor in f()} # F704
| ^^^^^^^^^
30 | [await cor for cor in f()] # F704
31 | {await cor for cor in f()} # F704
|
F704 `await` statement outside of a function
--> F704.py:30:2
|
28 | {await cor async for cor in f()} # F704
29 | {await cor: 1 async for cor in f()} # F704
30 | [await cor for cor in f()] # F704
| ^^^^^^^^^
31 | {await cor for cor in f()} # F704
32 | {await cor: 1 for cor in f()} # F704
|
F704 `await` statement outside of a function
--> F704.py:31:2
|
29 | {await cor: 1 async for cor in f()} # F704
30 | [await cor for cor in f()] # F704
31 | {await cor for cor in f()} # F704
| ^^^^^^^^^
32 | {await cor: 1 for cor in f()} # F704
|
F704 `await` statement outside of a function
--> F704.py:32:2
|
30 | [await cor for cor in f()] # F704
31 | {await cor for cor in f()} # F704
32 | {await cor: 1 for cor in f()} # F704
| ^^^^^^^^^
33 |
34 | # or in the iterator of an async generator, which is evaluated in the parent
|
F704 `await` statement outside of a function
--> F704.py:36:23
|
34 | # or in the iterator of an async generator, which is evaluated in the parent
35 | # scope
36 | (cor async for cor in await f()) # F704
| ^^^^^^^^^
37 | (await cor async for cor in [await c for c in f()]) # F704
|
F704 `await` statement outside of a function
--> F704.py:37:30
|
35 | # scope
36 | (cor async for cor in await f()) # F704
37 | (await cor async for cor in [await c for c in f()]) # F704
| ^^^^^^^
38 |
39 | # this is also okay because the comprehension is within the generator scope
|

View File

@@ -17,6 +17,4 @@ PLE1142 `await` should be used within an async function
4 | # Top-level await
5 | await 1
| ^^^^^^^
6 |
7 | ([await c for c in cor] async for cor in func()) # ok
|

View File

@@ -1,55 +0,0 @@
---
source: crates/ruff_linter/src/linter.rs
---
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> resources/test/fixtures/syntax_errors/return_in_generator.py:3:5
|
1 | async def gen():
2 | yield 1
3 | return 42
| ^^^^^^^^^
4 |
5 | def gen(): # B901 but not a syntax error - not an async generator
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> resources/test/fixtures/syntax_errors/return_in_generator.py:7:5
|
5 | def gen(): # B901 but not a syntax error - not an async generator
6 | yield 1
7 | return 42
| ^^^^^^^^^
8 |
9 | async def gen(): # ok - no value in return
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> resources/test/fixtures/syntax_errors/return_in_generator.py:15:5
|
13 | async def gen():
14 | yield 1
15 | return foo()
| ^^^^^^^^^^^^
16 |
17 | async def gen():
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> resources/test/fixtures/syntax_errors/return_in_generator.py:19:5
|
17 | async def gen():
18 | yield 1
19 | return [1, 2, 3]
| ^^^^^^^^^^^^^^^^
20 |
21 | async def gen():
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> resources/test/fixtures/syntax_errors/return_in_generator.py:24:5
|
22 | if True:
23 | yield 1
24 | return 10
| ^^^^^^^^^
|

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,17 @@
use std::cell::RefCell;
use std::sync::{LazyLock, Mutex};
use get_size2::{GetSize, StandardTracker};
use ordermap::{OrderMap, OrderSet};
thread_local! {
pub static TRACKER: RefCell<Option<StandardTracker>>= const { RefCell::new(None) };
}
struct TrackerGuard(Option<StandardTracker>);
impl Drop for TrackerGuard {
fn drop(&mut self) {
TRACKER.set(self.0.take());
}
}
pub fn attach_tracker<R>(tracker: StandardTracker, f: impl FnOnce() -> R) -> R {
let prev = TRACKER.replace(Some(tracker));
let _guard = TrackerGuard(prev);
f()
}
fn with_tracker<F, R>(f: F) -> R
where
F: FnOnce(Option<&mut StandardTracker>) -> R,
{
TRACKER.with(|tracker| {
let mut tracker = tracker.borrow_mut();
f(tracker.as_mut())
})
}
/// Returns the memory usage of the provided object, using a global tracker to avoid
/// double-counting shared objects.
pub fn heap_size<T: GetSize>(value: &T) -> usize {
with_tracker(|tracker| {
if let Some(tracker) = tracker {
value.get_heap_size_with_tracker(tracker).0
} else {
value.get_heap_size()
}
})
static TRACKER: LazyLock<Mutex<StandardTracker>> =
LazyLock::new(|| Mutex::new(StandardTracker::new()));
value
.get_heap_size_with_tracker(&mut *TRACKER.lock().unwrap())
.0
}
/// An implementation of [`GetSize::get_heap_size`] for [`OrderSet`].

View File

@@ -11,8 +11,6 @@ use crate::ExprRef;
/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of
/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should
/// generally prefer [`parenthesized_range`].
///
/// Prefer [`crate::token::parentheses_iterator`] if you have access to [`crate::token::Tokens`].
pub fn parentheses_iterator<'a>(
expr: ExprRef<'a>,
parent: Option<AnyNodeRef>,
@@ -59,8 +57,6 @@ pub fn parentheses_iterator<'a>(
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
/// parenthesized; or `None`, if the expression is not parenthesized.
///
/// Prefer [`crate::token::parenthesized_range`] if you have access to [`crate::token::Tokens`].
pub fn parenthesized_range(
expr: ExprRef,
parent: AnyNodeRef,

View File

@@ -12,11 +12,11 @@ static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"))
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ScriptTag {
/// The content of the script before the metadata block.
pub prelude: String,
prelude: String,
/// The metadata block.
pub metadata: String,
metadata: String,
/// The content of the script after the metadata block.
pub postlude: String,
postlude: String,
}
impl ScriptTag {

View File

@@ -16,10 +16,8 @@ use crate::str_prefix::{
use crate::{AnyStringFlags, BoolOp, Operator, StringFlags, UnaryOp};
use ruff_text_size::{Ranged, TextRange};
mod parentheses;
mod tokens;
pub use parentheses::{parentheses_iterator, parenthesized_range};
pub use tokens::{TokenAt, TokenIterWithContext, Tokens};
#[derive(Clone, Copy, PartialEq, Eq)]

View File

@@ -1,58 +0,0 @@
use ruff_text_size::{Ranged, TextLen, TextRange};
use super::{TokenKind, Tokens};
use crate::{AnyNodeRef, ExprRef};
/// Returns an iterator over the ranges of the optional parentheses surrounding an expression.
///
/// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7).
///
/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of
/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should
/// generally prefer [`parenthesized_range`].
pub fn parentheses_iterator<'a>(
expr: ExprRef<'a>,
parent: Option<AnyNodeRef>,
tokens: &'a Tokens,
) -> impl Iterator<Item = TextRange> + 'a {
let after_tokens = if let Some(parent) = parent {
// If the parent is a node that brings its own parentheses, exclude the closing parenthesis
// from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which
// the open and close parentheses are part of the `Arguments` node.
let exclusive_parent_end = if parent.is_arguments() {
parent.end() - ")".text_len()
} else {
parent.end()
};
tokens.in_range(TextRange::new(expr.end(), exclusive_parent_end))
} else {
tokens.after(expr.end())
};
let right_parens = after_tokens
.iter()
.filter(|token| !token.kind().is_trivia())
.take_while(move |token| token.kind() == TokenKind::Rpar);
let left_parens = tokens
.before(expr.start())
.iter()
.rev()
.filter(|token| !token.kind().is_trivia())
.take_while(|token| token.kind() == TokenKind::Lpar);
right_parens
.zip(left_parens)
.map(|(right, left)| TextRange::new(left.start(), right.end()))
}
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
/// parenthesized; or `None`, if the expression is not parenthesized.
pub fn parenthesized_range(
expr: ExprRef,
parent: AnyNodeRef,
tokens: &Tokens,
) -> Option<TextRange> {
parentheses_iterator(expr, Some(parent), tokens).last()
}

View File

@@ -1,199 +0,0 @@
//! Tests for [`ruff_python_ast::tokens::parentheses_iterator`] and
//! [`ruff_python_ast::tokens::parenthesized_range`].
use ruff_python_ast::{
self as ast, Expr,
token::{parentheses_iterator, parenthesized_range},
};
use ruff_python_parser::parse_module;
#[test]
fn test_no_parentheses() {
let source = "x = 2 + 2";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Assign(assign) = stmt else {
panic!("expected `Assign` statement, got {stmt:?}");
};
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
assert_eq!(result, None);
}
#[test]
fn test_single_parentheses() {
let source = "x = (2 + 2)";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Assign(assign) = stmt else {
panic!("expected `Assign` statement, got {stmt:?}");
};
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
let range = result.expect("should find parentheses");
assert_eq!(&source[range], "(2 + 2)");
}
#[test]
fn test_double_parentheses() {
let source = "x = ((2 + 2))";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Assign(assign) = stmt else {
panic!("expected `Assign` statement, got {stmt:?}");
};
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
let range = result.expect("should find parentheses");
assert_eq!(&source[range], "((2 + 2))");
}
#[test]
fn test_parentheses_with_whitespace() {
let source = "x = ( 2 + 2 )";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Assign(assign) = stmt else {
panic!("expected `Assign` statement, got {stmt:?}");
};
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
let range = result.expect("should find parentheses");
assert_eq!(&source[range], "( 2 + 2 )");
}
#[test]
fn test_parentheses_with_comments() {
let source = "x = ( # comment\n 2 + 2\n)";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Assign(assign) = stmt else {
panic!("expected `Assign` statement, got {stmt:?}");
};
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
let range = result.expect("should find parentheses");
assert_eq!(&source[range], "( # comment\n 2 + 2\n)");
}
#[test]
fn test_parenthesized_range_multiple() {
let source = "x = (((2 + 2)))";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Assign(assign) = stmt else {
panic!("expected `Assign` statement, got {stmt:?}");
};
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
let range = result.expect("should find parentheses");
assert_eq!(&source[range], "(((2 + 2)))");
}
#[test]
fn test_parentheses_iterator_multiple() {
let source = "x = (((2 + 2)))";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Assign(assign) = stmt else {
panic!("expected `Assign` statement, got {stmt:?}");
};
let ranges: Vec<_> =
parentheses_iterator(assign.value.as_ref().into(), Some(stmt.into()), tokens).collect();
assert_eq!(ranges.len(), 3);
assert_eq!(&source[ranges[0]], "(2 + 2)");
assert_eq!(&source[ranges[1]], "((2 + 2))");
assert_eq!(&source[ranges[2]], "(((2 + 2)))");
}
#[test]
fn test_call_arguments_not_counted() {
let source = "f(x)";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Expr(expr_stmt) = stmt else {
panic!("expected `Expr` statement, got {stmt:?}");
};
let Expr::Call(call) = expr_stmt.value.as_ref() else {
panic!("expected Call expression, got {:?}", expr_stmt.value);
};
let arg = call
.arguments
.args
.first()
.expect("call should have an argument");
let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens);
// The parentheses belong to the call, not the argument
assert_eq!(result, None);
}
#[test]
fn test_call_with_parenthesized_argument() {
let source = "f((x))";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Expr(expr_stmt) = stmt else {
panic!("expected Expr statement, got {stmt:?}");
};
let Expr::Call(call) = expr_stmt.value.as_ref() else {
panic!("expected `Call` expression, got {:?}", expr_stmt.value);
};
let arg = call
.arguments
.args
.first()
.expect("call should have an argument");
let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens);
let range = result.expect("should find parentheses around argument");
assert_eq!(&source[range], "(x)");
}
#[test]
fn test_multiline_with_parentheses() {
let source = "x = (\n 2 + 2 + 2\n)";
let parsed = parse_module(source).expect("should parse valid python");
let tokens = parsed.tokens();
let module = parsed.syntax();
let stmt = module.body.first().expect("module should have a statement");
let ast::Stmt::Assign(assign) = stmt else {
panic!("expected `Assign` statement, got {stmt:?}");
};
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
let range = result.expect("should find parentheses");
assert_eq!(&source[range], "(\n 2 + 2 + 2\n)");
}

View File

@@ -1,3 +0,0 @@
class C[T = int, U]: ...
class C[T1, T2 = int, T3, T4]: ...
type Alias[T = int, U] = ...

View File

@@ -3,13 +3,12 @@
//! This checker is not responsible for traversing the AST itself. Instead, its
//! [`SemanticSyntaxChecker::visit_stmt`] and [`SemanticSyntaxChecker::visit_expr`] methods should
//! be called in a parent `Visitor`'s `visit_stmt` and `visit_expr` methods, respectively.
use ruff_python_ast::{
self as ast, Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr,
StmtFunctionDef, StmtImportFrom,
StmtImportFrom,
comparable::ComparableExpr,
helpers,
visitor::{Visitor, walk_expr, walk_stmt},
visitor::{Visitor, walk_expr},
};
use ruff_text_size::{Ranged, TextRange, TextSize};
use rustc_hash::{FxBuildHasher, FxHashSet};
@@ -145,16 +144,11 @@ impl SemanticSyntaxChecker {
}
}
}
Stmt::ClassDef(ast::StmtClassDef {
type_params: Some(type_params),
..
})
| Stmt::TypeAlias(ast::StmtTypeAlias {
type_params: Some(type_params),
..
}) => {
Self::duplicate_type_parameter_name(type_params, ctx);
Self::type_parameter_default_order(type_params, ctx);
Stmt::ClassDef(ast::StmtClassDef { type_params, .. })
| Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => {
if let Some(type_params) = type_params {
Self::duplicate_type_parameter_name(type_params, ctx);
}
}
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
if let [Expr::Starred(ast::ExprStarred { range, .. })] = targets.as_slice() {
@@ -617,39 +611,6 @@ impl SemanticSyntaxChecker {
}
}
fn type_parameter_default_order<Ctx: SemanticSyntaxContext>(
type_params: &ast::TypeParams,
ctx: &Ctx,
) {
let mut seen_default = false;
for type_param in type_params.iter() {
let has_default = match type_param {
ast::TypeParam::TypeVar(ast::TypeParamTypeVar { default, .. })
| ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { default, .. })
| ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { default, .. }) => {
default.is_some()
}
};
if seen_default && !has_default {
// test_err type_parameter_default_order
// class C[T = int, U]: ...
// class C[T1, T2 = int, T3, T4]: ...
// type Alias[T = int, U] = ...
Self::add_error(
ctx,
SemanticSyntaxErrorKind::TypeParameterDefaultOrder(
type_param.name().id.to_string(),
),
type_param.range(),
);
}
if has_default {
seen_default = true;
}
}
}
fn duplicate_parameter_name<Ctx: SemanticSyntaxContext>(
parameters: &ast::Parameters,
ctx: &Ctx,
@@ -740,21 +701,7 @@ impl SemanticSyntaxChecker {
self.seen_futures_boundary = true;
}
}
Stmt::FunctionDef(StmtFunctionDef { is_async, body, .. }) => {
if *is_async {
let mut visitor = ReturnVisitor::default();
visitor.visit_body(body);
if visitor.has_yield {
if let Some(return_range) = visitor.return_range {
Self::add_error(
ctx,
SemanticSyntaxErrorKind::ReturnInGenerator,
return_range,
);
}
}
}
Stmt::FunctionDef(_) => {
self.seen_futures_boundary = true;
}
_ => {
@@ -949,7 +896,7 @@ impl SemanticSyntaxChecker {
// This check is required in addition to avoiding calling this function in `visit_expr`
// because the generator scope applies to nested parts of the `Expr::Generator` that are
// visited separately.
if ctx.in_generator_context() {
if ctx.in_generator_scope() {
return;
}
Self::add_error(
@@ -1119,12 +1066,6 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::DuplicateTypeParameter => {
f.write_str("duplicate type parameter")
}
SemanticSyntaxErrorKind::TypeParameterDefaultOrder(name) => {
write!(
f,
"non default type parameter `{name}` follows default type parameter"
)
}
SemanticSyntaxErrorKind::MultipleCaseAssignment(name) => {
write!(f, "multiple assignments to name `{name}` in pattern")
}
@@ -1228,9 +1169,6 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::NonlocalWithoutBinding(name) => {
write!(f, "no binding for nonlocal `{name}` found")
}
SemanticSyntaxErrorKind::ReturnInGenerator => {
write!(f, "`return` with value in async generator")
}
}
}
}
@@ -1634,12 +1572,6 @@ pub enum SemanticSyntaxErrorKind {
/// Represents a nonlocal statement for a name that has no binding in an enclosing scope.
NonlocalWithoutBinding(String),
/// Represents a default type parameter followed by a non-default type parameter.
TypeParameterDefaultOrder(String),
/// Represents a `return` statement with a value in an asynchronous generator.
ReturnInGenerator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
@@ -1756,40 +1688,6 @@ impl Visitor<'_> for ReboundComprehensionVisitor<'_> {
}
}
#[derive(Default)]
struct ReturnVisitor {
return_range: Option<TextRange>,
has_yield: bool,
}
impl Visitor<'_> for ReturnVisitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
// Do not recurse into nested functions; they're evaluated separately.
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {}
Stmt::Return(ast::StmtReturn {
value: Some(_),
range,
..
}) => {
self.return_range = Some(*range);
walk_stmt(self, stmt);
}
_ => walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Lambda(_) => {}
Expr::Yield(_) | Expr::YieldFrom(_) => {
self.has_yield = true;
}
_ => walk_expr(self, expr),
}
}
}
struct MatchPatternVisitor<'a, Ctx> {
names: FxHashSet<&'a ast::name::Name>,
ctx: &'a Ctx,
@@ -2198,11 +2096,11 @@ pub trait SemanticSyntaxContext {
/// Returns `true` if the visitor is in a function scope.
fn in_function_scope(&self) -> bool;
/// Returns `true` if the visitor is within a generator scope.
/// Returns `true` if the visitor is in a generator scope.
///
/// Note that this refers to an `Expr::Generator` precisely, not to comprehensions more
/// generally.
fn in_generator_context(&self) -> bool;
fn in_generator_scope(&self) -> bool;
/// Returns `true` if the source file is a Jupyter notebook.
fn in_notebook(&self) -> bool;

View File

@@ -573,7 +573,7 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
true
}
fn in_generator_context(&self) -> bool {
fn in_generator_scope(&self) -> bool {
true
}

View File

@@ -375,12 +375,3 @@ Module(
4 | type X[**P = x := int] = int
5 | type X[**P = *int] = int
|
|
2 | type X[**P = yield x] = int
3 | type X[**P = yield from x] = int
4 | type X[**P = x := int] = int
| ^^^ Syntax Error: non default type parameter `int` follows default type parameter
5 | type X[**P = *int] = int
|

View File

@@ -459,12 +459,3 @@ Module(
5 | type X[T = x := int] = int
6 | type X[T: int = *int] = int
|
|
3 | type X[T = (yield x)] = int
4 | type X[T = yield from x] = int
5 | type X[T = x := int] = int
| ^^^ Syntax Error: non default type parameter `int` follows default type parameter
6 | type X[T: int = *int] = int
|

View File

@@ -384,11 +384,3 @@ Module(
| ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a TypeVarTuple default
5 | type X[*Ts = x := int] = int
|
|
3 | type X[*Ts = yield x] = int
4 | type X[*Ts = yield from x] = int
5 | type X[*Ts = x := int] = int
| ^^^ Syntax Error: non default type parameter `int` follows default type parameter
|

View File

@@ -1,277 +0,0 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/type_parameter_default_order.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..89,
body: [
ClassDef(
StmtClassDef {
node_index: NodeIndex(None),
range: 0..24,
decorator_list: [],
name: Identifier {
id: Name("C"),
range: 6..7,
node_index: NodeIndex(None),
},
type_params: Some(
TypeParams {
range: 7..19,
node_index: NodeIndex(None),
type_params: [
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 8..15,
name: Identifier {
id: Name("T"),
range: 8..9,
node_index: NodeIndex(None),
},
bound: None,
default: Some(
Name(
ExprName {
node_index: NodeIndex(None),
range: 12..15,
id: Name("int"),
ctx: Load,
},
),
),
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 17..18,
name: Identifier {
id: Name("U"),
range: 17..18,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
],
},
),
arguments: None,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 21..24,
value: EllipsisLiteral(
ExprEllipsisLiteral {
node_index: NodeIndex(None),
range: 21..24,
},
),
},
),
],
},
),
ClassDef(
StmtClassDef {
node_index: NodeIndex(None),
range: 25..59,
decorator_list: [],
name: Identifier {
id: Name("C"),
range: 31..32,
node_index: NodeIndex(None),
},
type_params: Some(
TypeParams {
range: 32..54,
node_index: NodeIndex(None),
type_params: [
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 33..35,
name: Identifier {
id: Name("T1"),
range: 33..35,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 37..45,
name: Identifier {
id: Name("T2"),
range: 37..39,
node_index: NodeIndex(None),
},
bound: None,
default: Some(
Name(
ExprName {
node_index: NodeIndex(None),
range: 42..45,
id: Name("int"),
ctx: Load,
},
),
),
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 47..49,
name: Identifier {
id: Name("T3"),
range: 47..49,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 51..53,
name: Identifier {
id: Name("T4"),
range: 51..53,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
],
},
),
arguments: None,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 56..59,
value: EllipsisLiteral(
ExprEllipsisLiteral {
node_index: NodeIndex(None),
range: 56..59,
},
),
},
),
],
},
),
TypeAlias(
StmtTypeAlias {
node_index: NodeIndex(None),
range: 60..88,
name: Name(
ExprName {
node_index: NodeIndex(None),
range: 65..70,
id: Name("Alias"),
ctx: Store,
},
),
type_params: Some(
TypeParams {
range: 70..82,
node_index: NodeIndex(None),
type_params: [
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 71..78,
name: Identifier {
id: Name("T"),
range: 71..72,
node_index: NodeIndex(None),
},
bound: None,
default: Some(
Name(
ExprName {
node_index: NodeIndex(None),
range: 75..78,
id: Name("int"),
ctx: Load,
},
),
),
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 80..81,
name: Identifier {
id: Name("U"),
range: 80..81,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
],
},
),
value: EllipsisLiteral(
ExprEllipsisLiteral {
node_index: NodeIndex(None),
range: 85..88,
},
),
},
),
],
},
)
```
## Semantic Syntax Errors
|
1 | class C[T = int, U]: ...
| ^ Syntax Error: non default type parameter `U` follows default type parameter
2 | class C[T1, T2 = int, T3, T4]: ...
3 | type Alias[T = int, U] = ...
|
|
1 | class C[T = int, U]: ...
2 | class C[T1, T2 = int, T3, T4]: ...
| ^^ Syntax Error: non default type parameter `T3` follows default type parameter
3 | type Alias[T = int, U] = ...
|
|
1 | class C[T = int, U]: ...
2 | class C[T1, T2 = int, T3, T4]: ...
| ^^ Syntax Error: non default type parameter `T4` follows default type parameter
3 | type Alias[T = int, U] = ...
|
|
1 | class C[T = int, U]: ...
2 | class C[T1, T2 = int, T3, T4]: ...
3 | type Alias[T = int, U] = ...
| ^ Syntax Error: non default type parameter `U` follows default type parameter
|

View File

@@ -33,29 +33,26 @@ impl LineIndex {
line_starts.push(TextSize::default());
let bytes = text.as_bytes();
let mut utf8 = false;
assert!(u32::try_from(bytes.len()).is_ok());
for i in memchr::memchr2_iter(b'\n', b'\r', bytes) {
// Skip `\r` in `\r\n` sequences (only count the `\n`).
if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
continue;
for (i, byte) in bytes.iter().enumerate() {
utf8 |= !byte.is_ascii();
match byte {
// Only track one line break for `\r\n`.
b'\r' if bytes.get(i + 1) == Some(&b'\n') => continue,
b'\n' | b'\r' => {
// SAFETY: Assertion above guarantees `i <= u32::MAX`
#[expect(clippy::cast_possible_truncation)]
line_starts.push(TextSize::from(i as u32) + TextSize::from(1));
}
_ => {}
}
// SAFETY: Assertion above guarantees `i <= u32::MAX`
#[expect(clippy::cast_possible_truncation)]
line_starts.push(TextSize::from(i as u32) + TextSize::from(1));
}
// Determine whether the source text is ASCII.
//
// Empirically, this simple loop is auto-vectorized by LLVM and benchmarks faster than both
// `str::is_ascii()` and hand-written SIMD.
let mut has_non_ascii = false;
for byte in bytes {
has_non_ascii |= !byte.is_ascii();
}
let kind = if has_non_ascii {
let kind = if utf8 {
IndexKind::Utf8
} else {
IndexKind::Ascii

View File

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

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

@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L135" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L137" target="_blank">View source</a>
</small>
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L179" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L181" target="_blank">View source</a>
</small>
@@ -95,7 +95,7 @@ f(int) # error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L205" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L207" target="_blank">View source</a>
</small>
@@ -126,7 +126,7 @@ a = 1
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L230" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L232" target="_blank">View source</a>
</small>
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L258" target="_blank">View source</a>
</small>
@@ -190,7 +190,7 @@ class B(A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L282" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L284" target="_blank">View source</a>
</small>
@@ -218,7 +218,7 @@ type B = A
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L343" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L345" target="_blank">View source</a>
</small>
@@ -245,7 +245,7 @@ class B(A, A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L364" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L366" target="_blank">View source</a>
</small>
@@ -357,7 +357,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L590" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L592" target="_blank">View source</a>
</small>
@@ -387,7 +387,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L614" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L616" target="_blank">View source</a>
</small>
@@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L396" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L398" target="_blank">View source</a>
</small>
@@ -502,7 +502,7 @@ an atypical memory layout.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L668" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L670" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L708" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L710" target="_blank">View source</a>
</small>
@@ -557,7 +557,7 @@ a: int = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1998" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2000" target="_blank">View source</a>
</small>
@@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L730" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L732" target="_blank">View source</a>
</small>
@@ -627,7 +627,7 @@ asyncio.run(main())
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L760" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L762" target="_blank">View source</a>
</small>
@@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L813" target="_blank">View source</a>
</small>
@@ -678,7 +678,7 @@ with 1:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L832" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L834" target="_blank">View source</a>
</small>
@@ -707,7 +707,7 @@ a: str
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L855" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L857" target="_blank">View source</a>
</small>
@@ -751,7 +751,7 @@ except ZeroDivisionError:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1668" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1670" target="_blank">View source</a>
</small>
@@ -793,7 +793,7 @@ class D(A):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L891" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L893" target="_blank">View source</a>
</small>
@@ -826,7 +826,7 @@ class C[U](Generic[T]): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L635" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L637" target="_blank">View source</a>
</small>
@@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L919" target="_blank">View source</a>
</small>
@@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1014" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1016" target="_blank">View source</a>
</small>
@@ -934,7 +934,7 @@ class B(metaclass=f): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2126" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2128" target="_blank">View source</a>
</small>
@@ -1041,7 +1041,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L542" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L544" target="_blank">View source</a>
</small>
@@ -1095,7 +1095,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L990" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L992" target="_blank">View source</a>
</small>
@@ -1125,7 +1125,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1041" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1043" target="_blank">View source</a>
</small>
@@ -1175,7 +1175,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1140" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1142" target="_blank">View source</a>
</small>
@@ -1201,7 +1201,7 @@ def f(a: int = ''): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L945" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L947" target="_blank">View source</a>
</small>
@@ -1232,7 +1232,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L478" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L480" target="_blank">View source</a>
</small>
@@ -1266,7 +1266,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1160" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1162" target="_blank">View source</a>
</small>
@@ -1315,7 +1315,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L689" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L691" target="_blank">View source</a>
</small>
@@ -1340,7 +1340,7 @@ def func() -> int:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1203" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1205" target="_blank">View source</a>
</small>
@@ -1398,7 +1398,7 @@ TODO #14889
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L969" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L971" target="_blank">View source</a>
</small>
@@ -1425,7 +1425,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1435" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1437" target="_blank">View source</a>
</small>
@@ -1472,7 +1472,7 @@ Bar[int] # error: too few arguments
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1244" target="_blank">View source</a>
</small>
@@ -1502,7 +1502,7 @@ TYPE_CHECKING = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1266" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268" target="_blank">View source</a>
</small>
@@ -1532,7 +1532,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1318" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1320" target="_blank">View source</a>
</small>
@@ -1566,7 +1566,7 @@ f(10) # Error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1292" target="_blank">View source</a>
</small>
@@ -1600,7 +1600,7 @@ class C:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1346" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1348" target="_blank">View source</a>
</small>
@@ -1635,7 +1635,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1377" target="_blank">View source</a>
</small>
@@ -1660,7 +1660,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2099" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2101" target="_blank">View source</a>
</small>
@@ -1693,7 +1693,7 @@ alice["age"] # KeyError
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1396" target="_blank">View source</a>
</small>
@@ -1722,7 +1722,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1417" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1419" target="_blank">View source</a>
</small>
@@ -1746,7 +1746,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1476" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1478" target="_blank">View source</a>
</small>
@@ -1772,7 +1772,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1641" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1643" target="_blank">View source</a>
</small>
@@ -1805,7 +1805,7 @@ class B(A):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1527" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1529" target="_blank">View source</a>
</small>
@@ -1832,7 +1832,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1852" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1854" target="_blank">View source</a>
</small>
@@ -1890,7 +1890,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1974" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1976" target="_blank">View source</a>
</small>
@@ -1920,7 +1920,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1618" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1620" target="_blank">View source</a>
</small>
@@ -1949,7 +1949,7 @@ class B(A): ... # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1786" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1788" target="_blank">View source</a>
</small>
@@ -1983,7 +1983,7 @@ class F(NamedTuple):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1726" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1728" target="_blank">View source</a>
</small>
@@ -2010,7 +2010,7 @@ f("foo") # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1704" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1706" target="_blank">View source</a>
</small>
@@ -2038,7 +2038,7 @@ def _(x: int):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1747" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1749" target="_blank">View source</a>
</small>
@@ -2084,7 +2084,7 @@ class A:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1831" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1833" target="_blank">View source</a>
</small>
@@ -2111,7 +2111,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1873" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1875" target="_blank">View source</a>
</small>
@@ -2139,7 +2139,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1895" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1897" target="_blank">View source</a>
</small>
@@ -2164,7 +2164,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1914" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1916" target="_blank">View source</a>
</small>
@@ -2189,7 +2189,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1496" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1498" target="_blank">View source</a>
</small>
@@ -2226,7 +2226,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1933" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1935" target="_blank">View source</a>
</small>
@@ -2254,7 +2254,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1955" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1957" target="_blank">View source</a>
</small>
@@ -2279,7 +2279,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L507" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L509" target="_blank">View source</a>
</small>
@@ -2320,7 +2320,7 @@ class SubProto(BaseProto, Protocol):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L322" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L324" target="_blank">View source</a>
</small>
@@ -2408,7 +2408,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1548" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1550" target="_blank">View source</a>
</small>
@@ -2436,7 +2436,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L153" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L155" target="_blank">View source</a>
</small>
@@ -2468,7 +2468,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1570" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1572" target="_blank">View source</a>
</small>
@@ -2500,7 +2500,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2026" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2028" target="_blank">View source</a>
</small>
@@ -2527,7 +2527,7 @@ cast(int, f()) # Redundant
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1813" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1815" target="_blank">View source</a>
</small>
@@ -2551,7 +2551,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2047" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2049" target="_blank">View source</a>
</small>
@@ -2609,7 +2609,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L778" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L780" target="_blank">View source</a>
</small>
@@ -2648,7 +2648,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086" target="_blank">View source</a>
</small>
@@ -2711,7 +2711,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L304" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L306" target="_blank">View source</a>
</small>
@@ -2735,7 +2735,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1596" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1598" target="_blank">View source</a>
</small>

View File

@@ -90,22 +90,6 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
})?
};
let system = OsSystem::new(&cwd);
// If we see a single path, check if it's a PEP-723 script
let mut script_project = None;
if let [path] = &*args.paths {
match ProjectMetadata::discover_script(path, &system) {
Ok(project) => {
script_project = Some(project);
}
Err(ty_project::ProjectMetadataError::NotAScript(_)) => {
// This is fine
}
Err(e) => tracing::info!("Issue reading script at `{path}`: {e}"),
}
}
let project_path = args
.project
.as_ref()
@@ -127,6 +111,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
.map(|path| SystemPath::absolute(path, &cwd))
.collect();
let system = OsSystem::new(&cwd);
let watch = args.watch;
let exit_zero = args.exit_zero;
let config_file = args
@@ -136,13 +121,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let mut project_metadata = match &config_file {
Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), &system)?,
None => {
if let Some(project) = script_project {
project
} else {
ProjectMetadata::discover(&project_path, &system)?
}
}
None => ProjectMetadata::discover(&project_path, &system)?,
};
project_metadata.apply_configuration_files(&system)?;

View File

@@ -43,7 +43,7 @@ fn config_override_python_version() -> anyhow::Result<()> {
|
2 | [tool.ty.environment]
3 | python-version = "3.11"
| ^^^^^^ Python version configuration
| ^^^^^^ Python 3.11 assumed due to this configuration setting
|
info: rule `unresolved-attribute` is enabled by default
@@ -143,7 +143,7 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -159,14 +159,14 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any
|
2 | [tool.ty.environment]
3 | python-version = "3.8"
| ^^^^^ Python version configuration
| ^^^^^ Python 3.8 assumed due to this configuration setting
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
"#);
"###);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r###"
success: false
@@ -772,7 +772,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -787,7 +787,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
--> venv/pyvenv.cfg:2:11
|
2 | version = 3.8
| ^^^ Virtual environment metadata
| ^^^ Python version inferred from virtual environment metadata file
3 | home = foo/bar/bin
|
info: No Python version was specified on the command line or in a configuration file
@@ -796,7 +796,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
Found 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -831,7 +831,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -846,7 +846,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
--> venv/pyvenv.cfg:4:23
|
4 | version = 3.8
| ^^^ Virtual environment metadata
| ^^^ Python version inferred from virtual environment metadata file
|
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-reference` is enabled by default
@@ -854,7 +854,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
Found 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -898,7 +898,7 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
|
2 | [project]
3 | requires-python = ">=3.8"
| ^^^^^^^ Python version configuration
| ^^^^^^^ Python 3.8 assumed due to this configuration setting
|
Found 1 diagnostic
@@ -1206,7 +1206,7 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
2 | [environment]
3 | python-version = "3.10"
| ^^^^^^ Python version configuration
| ^^^^^^ Python 3.10 assumed due to this configuration setting
4 | python-platform = "linux"
|
info: rule `unresolved-attribute` is enabled by default
@@ -1225,7 +1225,7 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
2 | [environment]
3 | python-version = "3.10"
| ^^^^^^ Python version configuration
| ^^^^^^ Python 3.10 assumed due to this configuration setting
4 | python-platform = "linux"
|
info: rule `unresolved-import` is enabled by default

View File

@@ -15,7 +15,7 @@ use ty_project::metadata::pyproject::{PyProject, Tool};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module_confident};
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module};
struct TestCase {
db: ProjectDatabase,
@@ -232,8 +232,7 @@ impl TestCase {
}
fn module<'c>(&'c self, name: &str) -> Module<'c> {
resolve_module_confident(self.db(), &ModuleName::new(name).unwrap())
.expect("module to be present")
resolve_module(self.db(), &ModuleName::new(name).unwrap()).expect("module to be present")
}
fn sorted_submodule_names(&self, parent_module_name: &str) -> Vec<String> {
@@ -812,8 +811,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
.with_context(|| "Failed to create __init__.py")?;
std::fs::write(a_original_path.as_std_path(), "").with_context(|| "Failed to create a.py")?;
let sub_a_module =
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap());
let sub_a_module = resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap());
assert_eq!(sub_a_module, None);
case.assert_indexed_project_files([bar]);
@@ -834,9 +832,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
.expect("a.py to exist");
// `import sub.a` should now resolve
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
);
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
case.assert_indexed_project_files([bar, init_file, a_file]);
@@ -852,9 +848,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
])?;
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
);
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
let sub_path = case.project_path("sub");
let init_file = case
@@ -876,9 +870,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
case.apply_changes(changes, None);
// `import sub.a` should no longer resolve
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
);
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
assert!(!init_file.exists(case.db()));
assert!(!a_file.exists(case.db()));
@@ -898,12 +890,8 @@ fn directory_renamed() -> anyhow::Result<()> {
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
);
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_none()
);
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_none());
let sub_path = case.project_path("sub");
let sub_init = case
@@ -927,13 +915,9 @@ fn directory_renamed() -> anyhow::Result<()> {
case.apply_changes(changes, None);
// `import sub.a` should no longer resolve
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
);
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
// `import foo.baz` should now resolve
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some()
);
assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some());
// The old paths are no longer tracked
assert!(!sub_init.exists(case.db()));
@@ -966,9 +950,7 @@ fn directory_deleted() -> anyhow::Result<()> {
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
);
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
let sub_path = case.project_path("sub");
@@ -988,9 +970,7 @@ fn directory_deleted() -> anyhow::Result<()> {
case.apply_changes(changes, None);
// `import sub.a` should no longer resolve
assert!(
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
);
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
assert!(!init_file.exists(case.db()));
assert!(!a_file.exists(case.db()));
@@ -1019,7 +999,7 @@ fn search_path() -> anyhow::Result<()> {
let site_packages = case.root_path().join("site_packages");
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new("a").unwrap()),
resolve_module(case.db(), &ModuleName::new("a").unwrap()),
None
);
@@ -1029,7 +1009,7 @@ fn search_path() -> anyhow::Result<()> {
case.apply_changes(changes, None);
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]);
Ok(())
@@ -1042,7 +1022,7 @@ fn add_search_path() -> anyhow::Result<()> {
let site_packages = case.project_path("site_packages");
std::fs::create_dir_all(site_packages.as_std_path())?;
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
// Register site-packages as a search path.
case.update_options(Options {
@@ -1060,7 +1040,7 @@ fn add_search_path() -> anyhow::Result<()> {
case.apply_changes(changes, None);
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
Ok(())
}
@@ -1192,7 +1172,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
// Unset the custom typeshed directory.
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()),
resolve_module(case.db(), &ModuleName::new("os").unwrap()),
None
);
@@ -1207,7 +1187,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
case.apply_changes(changes, None);
assert!(resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()).is_some());
assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some());
Ok(())
}
@@ -1430,7 +1410,7 @@ mod unix {
Ok(())
})?;
let baz = resolve_module_confident(case.db(), &ModuleName::new_static("bar.baz").unwrap())
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
.expect("Expected bar.baz to exist in site-packages.");
let baz_project = case.project_path("bar/baz.py");
let baz_file = baz.file(case.db()).unwrap();
@@ -1506,7 +1486,7 @@ mod unix {
Ok(())
})?;
let baz = resolve_module_confident(case.db(), &ModuleName::new_static("bar.baz").unwrap())
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
.expect("Expected bar.baz to exist in site-packages.");
let baz_file = baz.file(case.db()).unwrap();
let bar_baz = case.project_path("bar/baz.py");
@@ -1611,7 +1591,7 @@ mod unix {
Ok(())
})?;
let baz = resolve_module_confident(case.db(), &ModuleName::new_static("bar.baz").unwrap())
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
.expect("Expected bar.baz to exist in site-packages.");
let baz_site_packages_path =
case.project_path(".venv/lib/python3.12/site-packages/bar/baz.py");
@@ -1874,11 +1854,11 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
let mut case = setup([("lib.py", "class Foo: ...")])?;
assert!(
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
resolve_module(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
"Expected `lib` module to exist."
);
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()),
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()),
None,
"Expected `Lib` module not to exist"
);
@@ -1911,13 +1891,13 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
// Resolving `lib` should now fail but `Lib` should now succeed
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()),
resolve_module(case.db(), &ModuleName::new("lib").unwrap()),
None,
"Expected `lib` module to no longer exist."
);
assert!(
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
"Expected `Lib` module to exist"
);

View File

@@ -1,7 +1,4 @@
name,file,index,rank
auto-import-includes-modules,main.py,0,1
auto-import-includes-modules,main.py,1,7
auto-import-includes-modules,main.py,2,1
auto-import-skips-current-module,main.py,0,1
fstring-completions,main.py,0,1
higher-level-symbols-preferred,main.py,0,
@@ -14,9 +11,9 @@ import-deprioritizes-type_check_only,main.py,2,1
import-deprioritizes-type_check_only,main.py,3,2
import-deprioritizes-type_check_only,main.py,4,3
import-keyword-completion,main.py,0,1
internal-typeshed-hidden,main.py,0,2
internal-typeshed-hidden,main.py,0,4
none-completion,main.py,0,2
numpy-array,main.py,0,159
numpy-array,main.py,0,
numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,1
object-attr-instance-methods,main.py,1,1
@@ -26,6 +23,6 @@ scope-existing-over-new-import,main.py,0,1
scope-prioritize-closer,main.py,0,2
scope-simple-long-identifier,main.py,0,1
tstring-completions,main.py,0,1
ty-extensions-lower-stdlib,main.py,0,9
ty-extensions-lower-stdlib,main.py,0,8
type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,251
type-var-typing-over-ast,main.py,1,275
1 name file index rank
auto-import-includes-modules main.py 0 1
auto-import-includes-modules main.py 1 7
auto-import-includes-modules main.py 2 1
2 auto-import-skips-current-module main.py 0 1
3 fstring-completions main.py 0 1
4 higher-level-symbols-preferred main.py 0
11 import-deprioritizes-type_check_only main.py 3 2
12 import-deprioritizes-type_check_only main.py 4 3
13 import-keyword-completion main.py 0 1
14 internal-typeshed-hidden main.py 0 2 4
15 none-completion main.py 0 2
16 numpy-array main.py 0 159
17 numpy-array main.py 1 1
18 object-attr-instance-methods main.py 0 1
19 object-attr-instance-methods main.py 1 1
23 scope-prioritize-closer main.py 0 2
24 scope-simple-long-identifier main.py 0 1
25 tstring-completions main.py 0 1
26 ty-extensions-lower-stdlib main.py 0 9 8
27 type-var-typing-over-ast main.py 0 3
28 type-var-typing-over-ast main.py 1 251 275

View File

@@ -506,21 +506,9 @@ struct CompletionAnswer {
impl CompletionAnswer {
/// Returns true when this answer matches the completion given.
fn matches(&self, completion: &Completion) -> bool {
if let Some(ref qualified) = completion.qualified {
if qualified.as_str() == self.qualified() {
return true;
}
}
self.symbol == completion.name.as_str()
&& self.module.as_deref() == completion.module_name.map(ModuleName::as_str)
}
fn qualified(&self) -> String {
self.module
.as_ref()
.map(|module| format!("{module}.{}", self.symbol))
.unwrap_or_else(|| self.symbol.clone())
}
}
/// Copy the Python project from `src_dir` to `dst_dir`.

View File

@@ -1,2 +0,0 @@
[settings]
auto-import = true

View File

@@ -1,3 +0,0 @@
multiprocess<CURSOR: multiprocessing>
collect<CURSOR: collections>
collabc<CURSOR: collections.abc>

View File

@@ -1,5 +0,0 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -1,8 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@@ -2,10 +2,7 @@ use ruff_db::files::File;
use ty_project::Db;
use ty_python_semantic::{Module, ModuleName, all_modules, resolve_real_shadowable_module};
use crate::{
SymbolKind,
symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only},
};
use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
/// Get all symbols matching the query string.
///
@@ -23,7 +20,7 @@ pub fn all_symbols<'db>(
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
let is_typing_extensions_available = importing_from.is_stub(db)
|| resolve_real_shadowable_module(db, importing_from, &typing_extensions).is_some();
|| resolve_real_shadowable_module(db, &typing_extensions).is_some();
let results = std::sync::Mutex::new(Vec::new());
{
@@ -39,39 +36,18 @@ pub fn all_symbols<'db>(
let Some(file) = module.file(&*db) else {
continue;
};
// By convention, modules starting with an underscore
// are generally considered unexported. However, we
// should consider first party modules fair game.
//
// Note that we apply this recursively. e.g.,
// `numpy._core.multiarray` is considered private
// because it's a child of `_core`.
if module.name(&*db).components().any(|c| c.starts_with('_'))
&& module
.search_path(&*db)
.is_none_or(|sp| !sp.is_first_party())
{
continue;
}
// TODO: also make it available in `TYPE_CHECKING` blocks
// (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well)
if !is_typing_extensions_available && module.name(&*db) == &typing_extensions {
continue;
}
s.spawn(move |_| {
if query.is_match_symbol_name(module.name(&*db)) {
results.lock().unwrap().push(AllSymbolInfo {
symbol: None,
module,
file,
});
}
for (_, symbol) in symbols_for_file_global_only(&*db, file).search(query) {
// It seems like we could do better here than
// locking `results` for every single symbol,
// but this works pretty well as it is.
results.lock().unwrap().push(AllSymbolInfo {
symbol: Some(symbol.to_owned()),
symbol: symbol.to_owned(),
module,
file,
});
@@ -83,16 +59,8 @@ pub fn all_symbols<'db>(
let mut results = results.into_inner().unwrap();
results.sort_by(|s1, s2| {
let key1 = (
s1.name_in_file()
.unwrap_or_else(|| s1.module().name(db).as_str()),
s1.file.path(db).as_str(),
);
let key2 = (
s2.name_in_file()
.unwrap_or_else(|| s2.module().name(db).as_str()),
s2.file.path(db).as_str(),
);
let key1 = (&s1.symbol.name, s1.file.path(db).as_str());
let key2 = (&s2.symbol.name, s2.file.path(db).as_str());
key1.cmp(&key2)
});
results
@@ -103,53 +71,14 @@ pub fn all_symbols<'db>(
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllSymbolInfo<'db> {
/// The symbol information.
///
/// When absent, this implies the symbol is the module itself.
symbol: Option<SymbolInfo<'static>>,
pub symbol: SymbolInfo<'static>,
/// The module containing the symbol.
module: Module<'db>,
pub module: Module<'db>,
/// The file containing the symbol.
///
/// This `File` is guaranteed to be the same
/// as the `File` underlying `module`.
file: File,
}
impl<'db> AllSymbolInfo<'db> {
/// Returns the name of this symbol as it exists in a file.
///
/// When absent, there is no concrete symbol in a module
/// somewhere. Instead, this represents importing a module.
/// In this case, if the caller needs a symbol name, they
/// should use `AllSymbolInfo::module().name()`.
pub fn name_in_file(&self) -> Option<&str> {
self.symbol.as_ref().map(|symbol| &*symbol.name)
}
/// Returns the "kind" of this symbol.
///
/// The kind of a symbol in the context of auto-import is
/// determined on a best effort basis. It may be imprecise
/// in some cases, e.g., reporting a module as a variable.
pub fn kind(&self) -> SymbolKind {
self.symbol
.as_ref()
.map(|symbol| symbol.kind)
.unwrap_or(SymbolKind::Module)
}
/// Returns the module this symbol is exported from.
pub fn module(&self) -> Module<'db> {
self.module
}
/// Returns the `File` corresponding to the module.
///
/// This is always equivalent to
/// `AllSymbolInfo::module().file().unwrap()`.
pub fn file(&self) -> File {
self.file
}
pub file: File,
}
#[cfg(test)]
@@ -233,31 +162,25 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
return "No symbols found".to_string();
}
self.render_diagnostics(symbols.into_iter().map(|symbol_info| AllSymbolDiagnostic {
db: &self.db,
symbol_info,
}))
self.render_diagnostics(symbols.into_iter().map(AllSymbolDiagnostic::new))
}
}
struct AllSymbolDiagnostic<'db> {
db: &'db dyn Db,
symbol_info: AllSymbolInfo<'db>,
}
impl<'db> AllSymbolDiagnostic<'db> {
fn new(symbol_info: AllSymbolInfo<'db>) -> Self {
Self { symbol_info }
}
}
impl IntoDiagnostic for AllSymbolDiagnostic<'_> {
fn into_diagnostic(self) -> Diagnostic {
let symbol_kind_str = self.symbol_info.kind().to_string();
let symbol_kind_str = self.symbol_info.symbol.kind.to_string();
let info_text = format!(
"{} {}",
symbol_kind_str,
self.symbol_info.name_in_file().unwrap_or_else(|| self
.symbol_info
.module()
.name(self.db)
.as_str())
);
let info_text = format!("{} {}", symbol_kind_str, self.symbol_info.symbol.name);
let sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, info_text);
@@ -266,12 +189,9 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
Severity::Info,
"AllSymbolInfo".to_string(),
);
let mut span = Span::from(self.symbol_info.file());
if let Some(ref symbol) = self.symbol_info.symbol {
span = span.with_range(symbol.name_range);
}
main.annotate(Annotation::primary(span));
main.annotate(Annotation::primary(
Span::from(self.symbol_info.file).with_range(self.symbol_info.symbol.name_range),
));
main.sub(sub);
main

View File

@@ -74,7 +74,7 @@ impl<'db> Completions<'db> {
.into_iter()
.filter_map(|item| {
Some(ImportEdit {
label: format!("import {}", item.qualified?),
label: format!("import {}.{}", item.module_name?, item.name),
edit: item.import?,
})
})
@@ -160,10 +160,6 @@ impl<'db> Extend<Completion<'db>> for Completions<'db> {
pub struct Completion<'db> {
/// The label shown to the user for this suggestion.
pub name: Name,
/// The fully qualified name, when available.
///
/// This is only set when `module_name` is available.
pub qualified: Option<Name>,
/// The text that should be inserted at the cursor
/// when the completion is selected.
///
@@ -229,7 +225,6 @@ impl<'db> Completion<'db> {
let is_type_check_only = semantic.is_type_check_only(db);
Completion {
name: semantic.name,
qualified: None,
insert: None,
ty: semantic.ty,
kind: None,
@@ -311,7 +306,6 @@ impl<'db> Completion<'db> {
fn keyword(name: &str) -> Self {
Completion {
name: name.into(),
qualified: None,
insert: None,
ty: None,
kind: Some(CompletionKind::Keyword),
@@ -327,7 +321,6 @@ impl<'db> Completion<'db> {
fn value_keyword(name: &str, ty: Type<'db>) -> Completion<'db> {
Completion {
name: name.into(),
qualified: None,
insert: None,
ty: Some(ty),
kind: Some(CompletionKind::Keyword),
@@ -544,22 +537,12 @@ fn add_unimported_completions<'db>(
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
for symbol in all_symbols(db, file, &completions.query) {
if symbol.file() == file || symbol.module().is_known(db, KnownModule::Builtins) {
if symbol.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
{
continue;
}
let module_name = symbol.module().name(db);
let (name, qualified, request) = symbol
.name_in_file()
.map(|name| {
let qualified = format!("{module_name}.{name}");
(name, qualified, create_import_request(module_name, name))
})
.unwrap_or_else(|| {
let name = module_name.as_str();
let qualified = name.to_string();
(name, qualified, ImportRequest::module(name))
});
let request = create_import_request(symbol.module.name(db), &symbol.symbol.name);
// FIXME: `all_symbols` doesn't account for wildcard imports.
// Since we're looking at every module, this is probably
// "fine," but it might mean that we import a symbol from the
@@ -568,12 +551,11 @@ fn add_unimported_completions<'db>(
// N.B. We use `add` here because `all_symbols` already
// takes our query into account.
completions.force_add(Completion {
name: ast::name::Name::new(name),
qualified: Some(ast::name::Name::new(qualified)),
name: ast::name::Name::new(&symbol.symbol.name),
insert: Some(import_action.symbol_text().into()),
ty: None,
kind: symbol.kind().to_completion_kind(),
module_name: Some(module_name),
kind: symbol.symbol.kind.to_completion_kind(),
module_name: Some(symbol.module.name(db)),
import: import_action.import().cloned(),
builtin: false,
// TODO: `is_type_check_only` requires inferring the type of the symbol
@@ -4368,7 +4350,7 @@ from os.<CURSOR>
.build()
.snapshot();
assert_snapshot!(snapshot, @r"
Kadabra :: Literal[1] :: <no import required>
Kadabra :: Literal[1] :: Current module
AbraKadabra :: Unavailable :: package
");
}
@@ -5552,7 +5534,7 @@ def foo(param: s<CURSOR>)
// Even though long_namea is alphabetically before long_nameb,
// long_nameb is currently imported and should be preferred.
assert_snapshot!(snapshot, @r"
long_nameb :: Literal[1] :: <no import required>
long_nameb :: Literal[1] :: Current module
long_namea :: Unavailable :: foo
");
}
@@ -5822,7 +5804,7 @@ from .imp<CURSOR>
#[test]
fn typing_extensions_excluded_from_import() {
let builder = completion_test_builder("from typing<CURSOR>").module_names();
assert_snapshot!(builder.build().snapshot(), @"typing :: <no import required>");
assert_snapshot!(builder.build().snapshot(), @"typing :: Current module");
}
#[test]
@@ -5830,7 +5812,13 @@ from .imp<CURSOR>
let builder = completion_test_builder("deprecated<CURSOR>")
.auto_import()
.module_names();
assert_snapshot!(builder.build().snapshot(), @"deprecated :: warnings");
assert_snapshot!(builder.build().snapshot(), @r"
Deprecated :: importlib.metadata
DeprecatedList :: importlib.metadata
DeprecatedNonAbstract :: importlib.metadata
DeprecatedTuple :: importlib.metadata
deprecated :: warnings
");
}
#[test]
@@ -5841,8 +5829,8 @@ from .imp<CURSOR>
.completion_test_builder()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
typing :: <no import required>
typing_extensions :: <no import required>
typing :: Current module
typing_extensions :: Current module
");
}
@@ -5855,6 +5843,10 @@ from .imp<CURSOR>
.auto_import()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
Deprecated :: importlib.metadata
DeprecatedList :: importlib.metadata
DeprecatedNonAbstract :: importlib.metadata
DeprecatedTuple :: importlib.metadata
deprecated :: typing_extensions
deprecated :: warnings
");
@@ -5867,8 +5859,8 @@ from .imp<CURSOR>
.completion_test_builder()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
typing :: <no import required>
typing_extensions :: <no import required>
typing :: Current module
typing_extensions :: Current module
");
}
@@ -5880,284 +5872,15 @@ from .imp<CURSOR>
.auto_import()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
Deprecated :: importlib.metadata
DeprecatedList :: importlib.metadata
DeprecatedNonAbstract :: importlib.metadata
DeprecatedTuple :: importlib.metadata
deprecated :: typing_extensions
deprecated :: warnings
");
}
#[test]
fn reexport_simple_import_noauto() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
import foo
foo.ZQ<CURSOR>
"#,
)
.source("foo.py", r#"from bar import ZQZQ"#)
.source("bar.py", r#"ZQZQ = 1"#)
.completion_test_builder()
.module_names()
.build()
.snapshot();
assert_snapshot!(snapshot, @"ZQZQ :: <no import required>");
}
#[test]
fn reexport_simple_import_auto() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
ZQ<CURSOR>
"#,
)
.source("foo.py", r#"from bar import ZQZQ"#)
.source("bar.py", r#"ZQZQ = 1"#)
.completion_test_builder()
.auto_import()
.module_names()
.build()
.snapshot();
// We're specifically looking for `ZQZQ` in `bar`
// here but *not* in `foo`. Namely, in `foo`,
// `ZQZQ` is a "regular" import that is not by
// convention considered a re-export.
assert_snapshot!(snapshot, @"ZQZQ :: bar");
}
#[test]
fn reexport_redundant_convention_import_noauto() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
import foo
foo.ZQ<CURSOR>
"#,
)
.source("foo.py", r#"from bar import ZQZQ as ZQZQ"#)
.source("bar.py", r#"ZQZQ = 1"#)
.completion_test_builder()
.module_names()
.build()
.snapshot();
assert_snapshot!(snapshot, @"ZQZQ :: <no import required>");
}
#[test]
fn reexport_redundant_convention_import_auto() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
ZQ<CURSOR>
"#,
)
.source("foo.py", r#"from bar import ZQZQ as ZQZQ"#)
.source("bar.py", r#"ZQZQ = 1"#)
.completion_test_builder()
.auto_import()
.module_names()
.build()
.snapshot();
assert_snapshot!(snapshot, @r"
ZQZQ :: bar
ZQZQ :: foo
");
}
#[test]
fn auto_import_respects_all() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
ZQ<CURSOR>
"#,
)
.source(
"bar.py",
r#"
ZQZQ1 = 1
ZQZQ2 = 1
__all__ = ['ZQZQ1']
"#,
)
.completion_test_builder()
.auto_import()
.module_names()
.build()
.snapshot();
// We specifically do not want `ZQZQ2` here, since
// it is not part of `__all__`.
assert_snapshot!(snapshot, @r"
ZQZQ1 :: bar
");
}
// This test confirms current behavior (as of 2025-12-04), but
// it's not consistent with auto-import. That is, it doesn't
// strictly respect `__all__` on `bar`, but perhaps it should.
//
// See: https://github.com/astral-sh/ty/issues/1757
#[test]
fn object_attr_ignores_all() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
import bar
bar.ZQ<CURSOR>
"#,
)
.source(
"bar.py",
r#"
ZQZQ1 = 1
ZQZQ2 = 1
__all__ = ['ZQZQ1']
"#,
)
.completion_test_builder()
.auto_import()
.module_names()
.build()
.snapshot();
// We specifically do not want `ZQZQ2` here, since
// it is not part of `__all__`.
assert_snapshot!(snapshot, @r"
ZQZQ1 :: <no import required>
ZQZQ2 :: <no import required>
");
}
#[test]
fn auto_import_ignores_modules_with_leading_underscore() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
Quitter<CURSOR>
"#,
)
.completion_test_builder()
.auto_import()
.module_names()
.build()
.snapshot();
// There is a `Quitter` in `_sitebuiltins` in the standard
// library. But this is skipped by auto-import because it's
// 1) not first party and 2) starts with an `_`.
assert_snapshot!(snapshot, @"<No completions found>");
}
#[test]
fn auto_import_includes_modules_with_leading_underscore_in_first_party() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
ZQ<CURSOR>
"#,
)
.source(
"bar.py",
r#"
ZQZQ1 = 1
"#,
)
.source(
"_foo.py",
r#"
ZQZQ1 = 1
"#,
)
.completion_test_builder()
.auto_import()
.module_names()
.build()
.snapshot();
assert_snapshot!(snapshot, @r"
ZQZQ1 :: _foo
ZQZQ1 :: bar
");
}
#[test]
fn auto_import_includes_stdlib_modules_as_suggestions() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
multiprocess<CURSOR>
"#,
)
.completion_test_builder()
.auto_import()
.build()
.snapshot();
assert_snapshot!(snapshot, @r"
multiprocessing
multiprocessing.connection
multiprocessing.context
multiprocessing.dummy
multiprocessing.dummy.connection
multiprocessing.forkserver
multiprocessing.heap
multiprocessing.managers
multiprocessing.pool
multiprocessing.popen_fork
multiprocessing.popen_forkserver
multiprocessing.popen_spawn_posix
multiprocessing.popen_spawn_win32
multiprocessing.process
multiprocessing.queues
multiprocessing.reduction
multiprocessing.resource_sharer
multiprocessing.resource_tracker
multiprocessing.shared_memory
multiprocessing.sharedctypes
multiprocessing.spawn
multiprocessing.synchronize
multiprocessing.util
");
}
#[test]
fn auto_import_includes_first_party_modules_as_suggestions() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
zqzqzq<CURSOR>
"#,
)
.source("zqzqzqzqzq.py", "")
.completion_test_builder()
.auto_import()
.build()
.snapshot();
assert_snapshot!(snapshot, @"zqzqzqzqzq");
}
#[test]
fn auto_import_includes_sub_modules_as_suggestions() {
let snapshot = CursorTest::builder()
.source(
"main.py",
r#"
collabc<CURSOR>
"#,
)
.completion_test_builder()
.auto_import()
.build()
.snapshot();
assert_snapshot!(snapshot, @"collections.abc");
}
/// A way to create a simple single-file (named `main.py`) completion test
/// builder.
///
@@ -6332,7 +6055,7 @@ collabc<CURSOR>
let module_name = c
.module_name
.map(ModuleName::as_str)
.unwrap_or("<no import required>");
.unwrap_or("Current module");
snapshot = format!("{snapshot} :: {module_name}");
}
snapshot

View File

@@ -230,58 +230,10 @@ calc = Calculator()
"
def test():
# Cursor on a position with no symbol
<CURSOR>
<CURSOR>
",
);
assert_snapshot!(test.document_highlights(), @"No highlights found");
}
// TODO: Should only highlight the last use and the last declaration
#[test]
fn redeclarations() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
a: str = "test"
a: int = 10
print(a<CURSOR>)
"#,
)
.build();
assert_snapshot!(test.document_highlights(), @r#"
info[document_highlights]: Highlight 1 (Write)
--> main.py:2:1
|
2 | a: str = "test"
| ^
3 |
4 | a: int = 10
|
info[document_highlights]: Highlight 2 (Write)
--> main.py:4:1
|
2 | a: str = "test"
3 |
4 | a: int = 10
| ^
5 |
6 | print(a)
|
info[document_highlights]: Highlight 3 (Read)
--> main.py:6:7
|
4 | a: int = 10
5 |
6 | print(a)
| ^
|
"#);
}
}

View File

@@ -824,12 +824,12 @@ mod tests {
Check out this great example code::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
You love to see it.
@@ -862,12 +862,12 @@ mod tests {
Check out this great example code ::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
You love to see it.
@@ -901,12 +901,12 @@ mod tests {
::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
You love to see it.
@@ -939,12 +939,12 @@ mod tests {
let docstring = r#"
Check out this great example code::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
You love to see it.
"#;
@@ -975,12 +975,12 @@ mod tests {
Check out this great example code::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")"#;
let docstring = Docstring::new(docstring.to_owned());

View File

@@ -898,42 +898,6 @@ cls = MyClass
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_string_annotation_recursive() {
let test = cursor_test(
r#"
ab: "a<CURSOR>b"
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:2:1
|
2 | ab: "ab"
| ^^
|
info[references]: Reference 2
--> main.py:2:6
|
2 | ab: "ab"
| ^^
|
"#);
}
#[test]
fn references_string_annotation_unknown() {
let test = cursor_test(
r#"
x: "foo<CURSOR>bar"
"#,
);
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_match_name_stmt() {
let test = cursor_test(
@@ -1906,259 +1870,4 @@ func<CURSOR>_alias()
|
");
}
#[test]
fn references_submodule_import_from_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.submod import val
x = sub<CURSOR>pkg
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// TODO(submodule-imports): this should light up both instances of `subpkg`
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> mypackage/__init__.py:4:5
|
2 | from .subpkg.submod import val
3 |
4 | x = subpkg
| ^^^^^^
|
");
}
#[test]
fn references_submodule_import_from_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .sub<CURSOR>pkg.submod import val
x = subpkg
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// TODO(submodule-imports): this should light up both instances of `subpkg`
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_submodule_import_from_wrong_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.submod import val
x = sub<CURSOR>mod
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// No references is actually correct (or it should only see itself)
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_submodule_import_from_wrong_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.sub<CURSOR>mod import val
x = submod
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// No references is actually correct (or it should only see itself)
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_submodule_import_from_confusing_shadowed_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .sub<CURSOR>pkg import subpkg
x = subpkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// No references is actually correct (or it should only see itself)
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_submodule_import_from_confusing_real_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg import sub<CURSOR>pkg
x = subpkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> mypackage/__init__.py:2:21
|
2 | from .subpkg import subpkg
| ^^^^^^
3 |
4 | x = subpkg
|
info[references]: Reference 2
--> mypackage/__init__.py:4:5
|
2 | from .subpkg import subpkg
3 |
4 | x = subpkg
| ^^^^^^
|
info[references]: Reference 3
--> mypackage/subpkg/__init__.py:2:1
|
2 | subpkg: int = 10
| ^^^^^^
|
");
}
#[test]
fn references_submodule_import_from_confusing_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg import subpkg
x = sub<CURSOR>pkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// TODO: this should also highlight the RHS subpkg in the import
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> mypackage/__init__.py:4:5
|
2 | from .subpkg import subpkg
3 |
4 | x = subpkg
| ^^^^^^
|
");
}
// TODO: Should only return references to the last declaration
#[test]
fn declarations() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
a: str = "test"
a: int = 10
print(a<CURSOR>)
"#,
)
.build();
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:2:1
|
2 | a: str = "test"
| ^
3 |
4 | a: int = 10
|
info[references]: Reference 2
--> main.py:4:1
|
2 | a: str = "test"
3 |
4 | a: int = 10
| ^
5 |
6 | print(a)
|
info[references]: Reference 3
--> main.py:6:7
|
4 | a: int = 10
5 |
6 | print(a)
| ^
|
"#);
}
}

View File

@@ -73,29 +73,19 @@ pub(crate) enum GotoTarget<'a> {
/// ```
ImportModuleAlias {
alias: &'a ast::Alias,
asname: &'a ast::Identifier,
},
/// In an import statement, the named under which the symbol is exported
/// in the imported file.
///
/// ```py
/// from foo import bar as baz
/// ^^^
/// ```
ImportExportedName {
alias: &'a ast::Alias,
import_from: &'a ast::StmtImportFrom,
},
/// Import alias in from import statement
/// ```py
/// from foo import bar as baz
/// ^^^
/// from foo import bar as baz
/// ^^^
/// ```
ImportSymbolAlias {
alias: &'a ast::Alias,
asname: &'a ast::Identifier,
range: TextRange,
import_from: &'a ast::StmtImportFrom,
},
/// Go to on the exception handler variable
@@ -300,9 +290,8 @@ impl GotoTarget<'_> {
GotoTarget::FunctionDef(function) => function.inferred_type(model),
GotoTarget::ClassDef(class) => class.inferred_type(model),
GotoTarget::Parameter(parameter) => parameter.inferred_type(model),
GotoTarget::ImportSymbolAlias { alias, .. }
| GotoTarget::ImportModuleAlias { alias, .. }
| GotoTarget::ImportExportedName { alias, .. } => alias.inferred_type(model),
GotoTarget::ImportSymbolAlias { alias, .. } => alias.inferred_type(model),
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
GotoTarget::KeywordArgument { keyword, .. } => keyword.value.inferred_type(model),
// When asking the type of a callable, usually you want the callable itself?
@@ -389,9 +378,7 @@ impl GotoTarget<'_> {
alias_resolution: ImportAliasResolution,
) -> Option<Definitions<'db>> {
let definitions = match self {
GotoTarget::Expression(expression) => {
definitions_for_expression(model, *expression, alias_resolution)
}
GotoTarget::Expression(expression) => definitions_for_expression(model, *expression),
// For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => Some(vec![ResolvedDefinition::Definition(
function.definition(model),
@@ -406,21 +393,22 @@ impl GotoTarget<'_> {
)]),
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
GotoTarget::ImportSymbolAlias { asname, .. } => Some(definitions_for_name(
model,
asname.as_str(),
AnyNodeRef::from(*asname),
alias_resolution,
)),
GotoTarget::ImportExportedName { alias, import_from } => {
let symbol_name = alias.name.as_str();
Some(definitions_for_imported_symbol(
model,
import_from,
symbol_name,
alias_resolution,
))
GotoTarget::ImportSymbolAlias {
alias, import_from, ..
} => {
if let Some(asname) = alias.asname.as_ref()
&& alias_resolution == ImportAliasResolution::PreserveAliases
{
Some(definitions_for_name(model, asname.as_str(), asname.into()))
} else {
let symbol_name = alias.name.as_str();
Some(definitions_for_imported_symbol(
model,
import_from,
symbol_name,
alias_resolution,
))
}
}
GotoTarget::ImportModuleComponent {
@@ -435,12 +423,15 @@ impl GotoTarget<'_> {
}
// Handle import aliases (offset within 'z' in "import x.y as z")
GotoTarget::ImportModuleAlias { asname, .. } => Some(definitions_for_name(
model,
asname.as_str(),
AnyNodeRef::from(*asname),
alias_resolution,
)),
GotoTarget::ImportModuleAlias { alias } => {
if let Some(asname) = alias.asname.as_ref()
&& alias_resolution == ImportAliasResolution::PreserveAliases
{
Some(definitions_for_name(model, asname.as_str(), asname.into()))
} else {
definitions_for_module(model, Some(alias.name.as_str()), 0)
}
}
// Handle keyword arguments in call expressions
GotoTarget::KeywordArgument {
@@ -463,22 +454,12 @@ impl GotoTarget<'_> {
// because they're not expressions
GotoTarget::PatternMatchRest(pattern_mapping) => {
pattern_mapping.rest.as_ref().map(|name| {
definitions_for_name(
model,
name.as_str(),
AnyNodeRef::Identifier(name),
alias_resolution,
)
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
})
}
GotoTarget::PatternMatchAsName(pattern_as) => pattern_as.name.as_ref().map(|name| {
definitions_for_name(
model,
name.as_str(),
AnyNodeRef::Identifier(name),
alias_resolution,
)
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
}),
GotoTarget::PatternKeywordArgument(pattern_keyword) => {
@@ -487,18 +468,12 @@ impl GotoTarget<'_> {
model,
name.as_str(),
AnyNodeRef::Identifier(name),
alias_resolution,
))
}
GotoTarget::PatternMatchStarName(pattern_star) => {
pattern_star.name.as_ref().map(|name| {
definitions_for_name(
model,
name.as_str(),
AnyNodeRef::Identifier(name),
alias_resolution,
)
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
})
}
@@ -506,18 +481,9 @@ impl GotoTarget<'_> {
//
// Prefer the function impl over the callable so that its docstrings win if defined.
GotoTarget::Call { callable, call } => {
let mut definitions = Vec::new();
// We prefer the specific overload for hover, go-to-def etc. However,
// `definitions_for_callable` always resolves import aliases. That's why we
// skip it in cases import alias resolution is turned of (rename, highlight references).
if alias_resolution == ImportAliasResolution::ResolveAliases {
definitions.extend(definitions_for_callable(model, call));
}
let mut definitions = definitions_for_callable(model, call);
let expr_definitions =
definitions_for_expression(model, *callable, alias_resolution)
.unwrap_or_default();
definitions_for_expression(model, *callable).unwrap_or_default();
definitions.extend(expr_definitions);
if definitions.is_empty() {
@@ -551,7 +517,7 @@ impl GotoTarget<'_> {
let subexpr = covering_node(subast.syntax().into(), *subrange)
.node()
.as_expr_ref()?;
definitions_for_expression(&submodel, subexpr, alias_resolution)
definitions_for_expression(&submodel, subexpr)
}
// nonlocal and global are essentially loads, but again they're statements,
@@ -561,7 +527,6 @@ impl GotoTarget<'_> {
model,
identifier.as_str(),
AnyNodeRef::Identifier(identifier),
alias_resolution,
))
}
@@ -572,7 +537,6 @@ impl GotoTarget<'_> {
model,
name.as_str(),
AnyNodeRef::Identifier(name),
alias_resolution,
))
}
@@ -582,7 +546,6 @@ impl GotoTarget<'_> {
model,
name.as_str(),
AnyNodeRef::Identifier(name),
alias_resolution,
))
}
@@ -592,7 +555,6 @@ impl GotoTarget<'_> {
model,
name.as_str(),
AnyNodeRef::Identifier(name),
alias_resolution,
))
}
};
@@ -618,9 +580,12 @@ impl GotoTarget<'_> {
GotoTarget::FunctionDef(function) => Some(Cow::Borrowed(function.name.as_str())),
GotoTarget::ClassDef(class) => Some(Cow::Borrowed(class.name.as_str())),
GotoTarget::Parameter(parameter) => Some(Cow::Borrowed(parameter.name.as_str())),
GotoTarget::ImportSymbolAlias { asname, .. } => Some(Cow::Borrowed(asname.as_str())),
GotoTarget::ImportExportedName { alias, .. } => {
Some(Cow::Borrowed(alias.name.as_str()))
GotoTarget::ImportSymbolAlias { alias, .. } => {
if let Some(asname) = &alias.asname {
Some(Cow::Borrowed(asname.as_str()))
} else {
Some(Cow::Borrowed(alias.name.as_str()))
}
}
GotoTarget::ImportModuleComponent {
module_name,
@@ -634,7 +599,13 @@ impl GotoTarget<'_> {
Some(Cow::Borrowed(module_name))
}
}
GotoTarget::ImportModuleAlias { asname, .. } => Some(Cow::Borrowed(asname.as_str())),
GotoTarget::ImportModuleAlias { alias } => {
if let Some(asname) = &alias.asname {
Some(Cow::Borrowed(asname.as_str()))
} else {
Some(Cow::Borrowed(alias.name.as_str()))
}
}
GotoTarget::ExceptVariable(except) => {
Some(Cow::Borrowed(except.name.as_ref()?.as_str()))
}
@@ -696,7 +667,7 @@ impl GotoTarget<'_> {
// Is the offset within the alias name (asname) part?
if let Some(asname) = &alias.asname {
if asname.range.contains_inclusive(offset) {
return Some(GotoTarget::ImportModuleAlias { alias, asname });
return Some(GotoTarget::ImportModuleAlias { alias });
}
}
@@ -728,13 +699,21 @@ impl GotoTarget<'_> {
// Is the offset within the alias name (asname) part?
if let Some(asname) = &alias.asname {
if asname.range.contains_inclusive(offset) {
return Some(GotoTarget::ImportSymbolAlias { alias, asname });
return Some(GotoTarget::ImportSymbolAlias {
alias,
range: asname.range,
import_from,
});
}
}
// Is the offset in the original name part?
if alias.name.range.contains_inclusive(offset) {
return Some(GotoTarget::ImportExportedName { alias, import_from });
return Some(GotoTarget::ImportSymbolAlias {
alias,
range: alias.name.range,
import_from,
});
}
None
@@ -914,13 +893,12 @@ impl Ranged for GotoTarget<'_> {
GotoTarget::FunctionDef(function) => function.name.range,
GotoTarget::ClassDef(class) => class.name.range,
GotoTarget::Parameter(parameter) => parameter.name.range,
GotoTarget::ImportSymbolAlias { asname, .. } => asname.range,
Self::ImportExportedName { alias, .. } => alias.name.range,
GotoTarget::ImportSymbolAlias { range, .. } => *range,
GotoTarget::ImportModuleComponent {
component_range, ..
} => *component_range,
GotoTarget::StringAnnotationSubexpr { subrange, .. } => *subrange,
GotoTarget::ImportModuleAlias { asname, .. } => asname.range,
GotoTarget::ImportModuleAlias { alias } => alias.asname.as_ref().unwrap().range,
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
GotoTarget::KeywordArgument { keyword, .. } => keyword.arg.as_ref().unwrap().range,
GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range,
@@ -977,14 +955,12 @@ fn convert_resolved_definitions_to_targets<'db>(
fn definitions_for_expression<'db>(
model: &SemanticModel<'db>,
expression: ruff_python_ast::ExprRef<'_>,
alias_resolution: ImportAliasResolution,
) -> Option<Vec<ResolvedDefinition<'db>>> {
match expression {
ast::ExprRef::Name(name) => Some(definitions_for_name(
model,
name.id.as_str(),
expression.into(),
alias_resolution,
)),
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
model, attribute,

View File

@@ -273,7 +273,7 @@ mod tests {
r#"
class A:
x = 1
def method(self):
def inner():
return <CURSOR>x # Should NOT find class variable x
@@ -1073,41 +1073,6 @@ def another_helper(path):
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_string_annotation_recursive() {
let test = cursor_test(
r#"
ab: "a<CURSOR>b"
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:2:1
|
2 | ab: "ab"
| ^^
|
info: Source
--> main.py:2:6
|
2 | ab: "ab"
| ^^
|
"#);
}
#[test]
fn goto_declaration_string_annotation_unknown() {
let test = cursor_test(
r#"
x: "foo<CURSOR>bar"
"#,
);
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_nested_instance_attribute() {
let test = cursor_test(
@@ -1255,12 +1220,12 @@ x: i<CURSOR>nt = 42
r#"
def outer():
x = "outer_value"
def inner():
nonlocal x
x = "modified"
return x<CURSOR> # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
@@ -1295,12 +1260,12 @@ def outer():
r#"
def outer():
xy = "outer_value"
def inner():
nonlocal x<CURSOR>y
xy = "modified"
return x # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
@@ -1636,7 +1601,7 @@ def function():
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
@@ -1675,7 +1640,7 @@ def function():
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
@@ -1713,7 +1678,7 @@ def function():
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
@@ -1751,7 +1716,7 @@ def function():
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
@@ -1919,7 +1884,7 @@ def function():
class C:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
@@ -2029,7 +1994,7 @@ def function():
r#"
class MyClass:
ClassType = int
def generic_method[T](self, value: Class<CURSOR>Type) -> T:
return value
"#,
@@ -2602,378 +2567,6 @@ def ab(a: int, *, c: int): ...
");
}
#[test]
fn goto_declaration_submodule_import_from_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.submod import val
x = sub<CURSOR>pkg
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// TODO(submodule-imports): this should only highlight `subpkg` in the import statement
// This happens because DefinitionKind::ImportFromSubmodule claims the entire ImportFrom node,
// which is correct but unhelpful. Unfortunately even if it only claimed the LHS identifier it
// would highlight `subpkg.submod` which is strictly better but still isn't what we want.
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mypackage/__init__.py:2:1
|
2 | from .subpkg.submod import val
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | x = subpkg
|
info: Source
--> mypackage/__init__.py:4:5
|
2 | from .subpkg.submod import val
3 |
4 | x = subpkg
| ^^^^^^
|
");
}
#[test]
fn goto_declaration_submodule_import_from_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .sub<CURSOR>pkg.submod import val
x = subpkg
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// TODO(submodule-imports): I don't *think* this is what we want..?
// It's a bit confusing because this symbol is essentially the LHS *and* RHS of
// `subpkg = mypackage.subpkg`. As in, it's both defining a local `subpkg` and
// loading the module `mypackage.subpkg`, so, it's understandable to get confused!
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mypackage/subpkg/__init__.py:1:1
|
|
info: Source
--> mypackage/__init__.py:2:7
|
2 | from .subpkg.submod import val
| ^^^^^^
3 |
4 | x = subpkg
|
");
}
#[test]
fn goto_declaration_submodule_import_from_wrong_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.submod import val
x = sub<CURSOR>mod
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// No result is correct!
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_submodule_import_from_wrong_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.sub<CURSOR>mod import val
x = submod
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// Going to the submod module is correct!
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mypackage/subpkg/submod.py:1:1
|
1 |
| ^
2 | val: int = 0
|
info: Source
--> mypackage/__init__.py:2:14
|
2 | from .subpkg.submod import val
| ^^^^^^
3 |
4 | x = submod
|
");
}
#[test]
fn goto_declaration_submodule_import_from_confusing_shadowed_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .sub<CURSOR>pkg import subpkg
x = subpkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// Going to the subpkg module is correct!
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mypackage/subpkg/__init__.py:1:1
|
1 |
| ^
2 | subpkg: int = 10
|
info: Source
--> mypackage/__init__.py:2:7
|
2 | from .subpkg import subpkg
| ^^^^^^
3 |
4 | x = subpkg
|
");
}
#[test]
fn goto_declaration_submodule_import_from_confusing_real_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg import sub<CURSOR>pkg
x = subpkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// Going to the subpkg `int` is correct!
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mypackage/subpkg/__init__.py:2:1
|
2 | subpkg: int = 10
| ^^^^^^
|
info: Source
--> mypackage/__init__.py:2:21
|
2 | from .subpkg import subpkg
| ^^^^^^
3 |
4 | x = subpkg
|
");
}
#[test]
fn goto_declaration_submodule_import_from_confusing_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg import subpkg
x = sub<CURSOR>pkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// TODO(submodule-imports): Ok this one is FASCINATING and it's kinda right but confusing!
//
// So there's 3 relevant definitions here:
//
// * `subpkg: int = 10` in the other file is in fact the original definition
//
// * the LHS `subpkg` in the import is an instance of `subpkg = ...`
// because it's a `DefinitionKind::ImportFromSubmodle`.
// This is the span that covers the entire import.
//
// * `the RHS `subpkg` in the import is a second instance of `subpkg = ...`
// that *immediately* overwrites the `ImportFromSubmodule`'s definition
// This span seemingly doesn't appear at all!? Is it getting hidden by the LHS span?
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mypackage/__init__.py:2:1
|
2 | from .subpkg import subpkg
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | x = subpkg
|
info: Source
--> mypackage/__init__.py:4:5
|
2 | from .subpkg import subpkg
3 |
4 | x = subpkg
| ^^^^^^
|
info[goto-declaration]: Declaration
--> mypackage/subpkg/__init__.py:2:1
|
2 | subpkg: int = 10
| ^^^^^^
|
info: Source
--> mypackage/__init__.py:4:5
|
2 | from .subpkg import subpkg
3 |
4 | x = subpkg
| ^^^^^^
|
");
}
// TODO: Should only return `a: int`
#[test]
fn redeclarations() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
a: str = "test"
a: int = 10
print(a<CURSOR>)
a: bool = True
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:2:1
|
2 | a: str = "test"
| ^
3 |
4 | a: int = 10
|
info: Source
--> main.py:6:7
|
4 | a: int = 10
5 |
6 | print(a)
| ^
7 |
8 | a: bool = True
|
info[goto-declaration]: Declaration
--> main.py:4:1
|
2 | a: str = "test"
3 |
4 | a: int = 10
| ^
5 |
6 | print(a)
|
info: Source
--> main.py:6:7
|
4 | a: int = 10
5 |
6 | print(a)
| ^
7 |
8 | a: bool = True
|
info[goto-declaration]: Declaration
--> main.py:8:1
|
6 | print(a)
7 |
8 | a: bool = True
| ^
|
info: Source
--> main.py:6:7
|
4 | a: int = 10
5 |
6 | print(a)
| ^
7 |
8 | a: bool = True
|
"#);
}
impl CursorTest {
fn goto_declaration(&self) -> String {
let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset)

View File

@@ -1714,86 +1714,6 @@ Traceb<CURSOR>ackType
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
// TODO: Should only list `a: int`
#[test]
fn redeclarations() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
a: str = "test"
a: int = 10
print(a<CURSOR>)
a: bool = True
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> main.py:2:1
|
2 | a: str = "test"
| ^
3 |
4 | a: int = 10
|
info: Source
--> main.py:6:7
|
4 | a: int = 10
5 |
6 | print(a)
| ^
7 |
8 | a: bool = True
|
info[goto-definition]: Definition
--> main.py:4:1
|
2 | a: str = "test"
3 |
4 | a: int = 10
| ^
5 |
6 | print(a)
|
info: Source
--> main.py:6:7
|
4 | a: int = 10
5 |
6 | print(a)
| ^
7 |
8 | a: bool = True
|
info[goto-definition]: Definition
--> main.py:8:1
|
6 | print(a)
7 |
8 | a: bool = True
| ^
|
info: Source
--> main.py:6:7
|
4 | a: int = 10
5 |
6 | print(a)
| ^
7 |
8 | a: bool = True
|
"#);
}
impl CursorTest {
fn goto_definition(&self) -> String {
let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset)

View File

@@ -145,14 +145,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/typing.pyi:781:1
--> stdlib/typing.pyi:770:1
|
779 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...
780 |
781 | Generic: type[_Generic]
768 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...
769 |
770 | Generic: type[_Generic]
| ^^^^^^^
782 |
783 | class _ProtocolMeta(ABCMeta):
771 |
772 | class _ProtocolMeta(ABCMeta):
|
info: Source
--> main.py:4:1
@@ -964,60 +964,6 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_string_annotation_recursive() {
let test = cursor_test(
r#"
ab: "a<CURSOR>b"
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:2:6
|
2 | ab: "ab"
| ^^
|
"#);
}
#[test]
fn goto_type_string_annotation_unknown() {
let test = cursor_test(
r#"
x: "foo<CURSOR>bar"
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:2:5
|
2 | x: "foobar"
| ^^^^^^
|
"#);
}
#[test]
fn goto_type_match_name_stmt() {
let test = cursor_test(
@@ -1111,7 +1057,7 @@ mod tests {
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
@@ -1131,7 +1077,7 @@ mod tests {
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
@@ -1151,7 +1097,7 @@ mod tests {
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
@@ -1189,7 +1135,7 @@ mod tests {
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
@@ -1398,12 +1344,12 @@ f(**kwargs<CURSOR>)
r#"
def outer():
x = "outer_value"
def inner():
nonlocal x
x = "modified"
return x<CURSOR> # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
@@ -1438,12 +1384,12 @@ def outer():
r#"
def outer():
xy = "outer_value"
def inner():
nonlocal x<CURSOR>y
xy = "modified"
return x # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
@@ -1672,283 +1618,6 @@ def function():
"#);
}
#[test]
fn goto_type_submodule_import_from_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.submod import val
x = sub<CURSOR>pkg
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// The module is the correct type definition
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> mypackage/subpkg/__init__.py:1:1
|
|
info: Source
--> mypackage/__init__.py:4:5
|
2 | from .subpkg.submod import val
3 |
4 | x = subpkg
| ^^^^^^
|
");
}
#[test]
fn goto_type_submodule_import_from_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .sub<CURSOR>pkg.submod import val
x = subpkg
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// The module is the correct type definition
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> mypackage/subpkg/__init__.py:1:1
|
|
info: Source
--> mypackage/__init__.py:2:7
|
2 | from .subpkg.submod import val
| ^^^^^^
3 |
4 | x = subpkg
|
");
}
#[test]
fn goto_type_submodule_import_from_wrong_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.submod import val
x = sub<CURSOR>mod
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// Unknown is correct, `submod` is not in scope
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> mypackage/__init__.py:4:5
|
2 | from .subpkg.submod import val
3 |
4 | x = submod
| ^^^^^^
|
");
}
#[test]
fn goto_type_submodule_import_from_wrong_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.sub<CURSOR>mod import val
x = submod
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// The module is correct
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> mypackage/subpkg/submod.py:1:1
|
1 | /
2 | | val: int = 0
| |_____________^
|
info: Source
--> mypackage/__init__.py:2:14
|
2 | from .subpkg.submod import val
| ^^^^^^
3 |
4 | x = submod
|
");
}
#[test]
fn goto_type_submodule_import_from_confusing_shadowed_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .sub<CURSOR>pkg import subpkg
x = subpkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// The module is correct
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> mypackage/subpkg/__init__.py:1:1
|
1 | /
2 | | subpkg: int = 10
| |_________________^
|
info: Source
--> mypackage/__init__.py:2:7
|
2 | from .subpkg import subpkg
| ^^^^^^
3 |
4 | x = subpkg
|
");
}
#[test]
fn goto_type_submodule_import_from_confusing_real_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg import sub<CURSOR>pkg
x = subpkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// `int` is correct
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> mypackage/__init__.py:2:21
|
2 | from .subpkg import subpkg
| ^^^^^^
3 |
4 | x = subpkg
|
"#);
}
#[test]
fn goto_type_submodule_import_from_confusing_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg import subpkg
x = sub<CURSOR>pkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// `int` is correct
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> mypackage/__init__.py:4:5
|
2 | from .subpkg import subpkg
3 |
4 | x = subpkg
| ^^^^^^
|
"#);
}
impl CursorTest {
fn goto_type_definition(&self) -> String {
let Some(targets) =

View File

@@ -1089,60 +1089,6 @@ mod tests {
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_string_annotation_recursive() {
let test = cursor_test(
r#"
ab: "a<CURSOR>b"
"#,
);
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:6
|
2 | ab: "ab"
| ^-
| ||
| |Cursor offset
| source
|
"#);
}
#[test]
fn hover_string_annotation_unknown() {
let test = cursor_test(
r#"
x: "foo<CURSOR>bar"
"#,
);
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | x: "foobar"
| ^^^-^^
| | |
| | Cursor offset
| source
|
"#);
}
#[test]
fn hover_overload_type_disambiguated1() {
let test = CursorTest::builder()
@@ -1708,12 +1654,12 @@ def ab(a: int, *, c: int):
r#"
def outer():
x = "outer_value"
def inner():
nonlocal x
x = "modified"
return x<CURSOR> # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
@@ -1747,12 +1693,12 @@ def outer():
r#"
def outer():
xy = "outer_value"
def inner():
nonlocal x<CURSOR>y
xy = "modified"
return x # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
@@ -1960,7 +1906,7 @@ def function():
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
@@ -1980,7 +1926,7 @@ def function():
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
@@ -2018,7 +1964,7 @@ def function():
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
@@ -2057,7 +2003,7 @@ def function():
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
@@ -2143,13 +2089,15 @@ def function():
"#,
);
// TODO: This should just be `**AB@Alias2 (<variance>)`
// https://github.com/astral-sh/ty/issues/1581
assert_snapshot!(test.hover(), @r"
(**AB@Alias2) -> tuple[AB@Alias2]
(
...
) -> tuple[typing.ParamSpec]
---------------------------------------------
```python
(**AB@Alias2) -> tuple[AB@Alias2]
(
...
) -> tuple[typing.ParamSpec]
```
---------------------------------------------
info[hover]: Hovered content is
@@ -2290,12 +2238,12 @@ def function():
"#,
);
// TODO: Should this be constravariant instead?
// TODO: This should be `P@Alias (<variance>)`
assert_snapshot!(test.hover(), @r"
P@Alias (bivariant)
typing.ParamSpec
---------------------------------------------
```python
P@Alias (bivariant)
typing.ParamSpec
```
---------------------------------------------
info[hover]: Hovered content is
@@ -3319,297 +3267,6 @@ def function():
");
}
#[test]
fn hover_submodule_import_from_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.submod import val
x = sub<CURSOR>pkg
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// The module is correct
assert_snapshot!(test.hover(), @r"
<module 'mypackage.subpkg'>
---------------------------------------------
```python
<module 'mypackage.subpkg'>
```
---------------------------------------------
info[hover]: Hovered content is
--> mypackage/__init__.py:4:5
|
2 | from .subpkg.submod import val
3 |
4 | x = subpkg
| ^^^-^^
| | |
| | Cursor offset
| source
|
");
}
#[test]
fn hover_submodule_import_from_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .sub<CURSOR>pkg.submod import val
x = subpkg
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// The module is correct
assert_snapshot!(test.hover(), @r"
<module 'mypackage.subpkg'>
---------------------------------------------
```python
<module 'mypackage.subpkg'>
```
---------------------------------------------
info[hover]: Hovered content is
--> mypackage/__init__.py:2:7
|
2 | from .subpkg.submod import val
| ^^^-^^
| | |
| | Cursor offset
| source
3 |
4 | x = subpkg
|
");
}
#[test]
fn hover_submodule_import_from_wrong_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.submod import val
x = sub<CURSOR>mod
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// Unknown is correct
assert_snapshot!(test.hover(), @r"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> mypackage/__init__.py:4:5
|
2 | from .subpkg.submod import val
3 |
4 | x = submod
| ^^^-^^
| | |
| | Cursor offset
| source
|
");
}
#[test]
fn hover_submodule_import_from_wrong_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg.sub<CURSOR>mod import val
x = submod
"#,
)
.source("mypackage/subpkg/__init__.py", r#""#)
.source(
"mypackage/subpkg/submod.py",
r#"
val: int = 0
"#,
)
.build();
// The submodule is correct
assert_snapshot!(test.hover(), @r"
<module 'mypackage.subpkg.submod'>
---------------------------------------------
```python
<module 'mypackage.subpkg.submod'>
```
---------------------------------------------
info[hover]: Hovered content is
--> mypackage/__init__.py:2:14
|
2 | from .subpkg.submod import val
| ^^^-^^
| | |
| | Cursor offset
| source
3 |
4 | x = submod
|
");
}
#[test]
fn hover_submodule_import_from_confusing_shadowed_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .sub<CURSOR>pkg import subpkg
x = subpkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// The module is correct
assert_snapshot!(test.hover(), @r"
<module 'mypackage.subpkg'>
---------------------------------------------
```python
<module 'mypackage.subpkg'>
```
---------------------------------------------
info[hover]: Hovered content is
--> mypackage/__init__.py:2:7
|
2 | from .subpkg import subpkg
| ^^^-^^
| | |
| | Cursor offset
| source
3 |
4 | x = subpkg
|
");
}
#[test]
fn hover_submodule_import_from_confusing_real_def() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg import sub<CURSOR>pkg
x = subpkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// int is correct
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
```python
int
```
---------------------------------------------
info[hover]: Hovered content is
--> mypackage/__init__.py:2:21
|
2 | from .subpkg import subpkg
| ^^^-^^
| | |
| | Cursor offset
| source
3 |
4 | x = subpkg
|
");
}
#[test]
fn hover_submodule_import_from_confusing_use() {
let test = CursorTest::builder()
.source(
"mypackage/__init__.py",
r#"
from .subpkg import subpkg
x = sub<CURSOR>pkg
"#,
)
.source(
"mypackage/subpkg/__init__.py",
r#"
subpkg: int = 10
"#,
)
.build();
// int is correct
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
```python
int
```
---------------------------------------------
info[hover]: Hovered content is
--> mypackage/__init__.py:4:5
|
2 | from .subpkg import subpkg
3 |
4 | x = subpkg
| ^^^-^^
| | |
| | Cursor offset
| source
|
");
}
impl CursorTest {
fn hover(&self) -> String {
use std::fmt::Write;

View File

@@ -145,7 +145,7 @@ impl<'a> Importer<'a> {
members: &MembersInScope,
) -> ImportAction {
let request = request.avoid_conflicts(self.db, self.file, members);
let mut symbol_text: Box<str> = request.member.unwrap_or(request.module).into();
let mut symbol_text: Box<str> = request.member.into();
let Some(response) = self.find(&request, members.at) else {
let insertion = if let Some(future) = self.find_last_future_import(members.at) {
Insertion::end_of_statement(future.stmt, self.source, self.stylist)
@@ -157,27 +157,14 @@ impl<'a> Importer<'a> {
Insertion::start_of_file(self.parsed.suite(), self.source, self.stylist, range)
};
let import = insertion.into_edit(&request.to_string());
if let Some(member) = request.member
&& matches!(request.style, ImportStyle::Import)
{
symbol_text = format!("{}.{}", request.module, member).into();
if matches!(request.style, ImportStyle::Import) {
symbol_text = format!("{}.{}", request.module, request.member).into();
}
return ImportAction {
import: Some(import),
symbol_text,
};
};
// When we just have a request to import a module (and not
// any members from that module), then the only way we can be
// here is if we found a pre-existing import that definitively
// satisfies the request. So we're done.
let Some(member) = request.member else {
return ImportAction {
import: None,
symbol_text,
};
};
match response.kind {
ImportResponseKind::Unqualified { ast, alias } => {
let member = alias.asname.as_ref().unwrap_or(&alias.name).as_str();
@@ -202,10 +189,13 @@ impl<'a> Importer<'a> {
let import = if let Some(insertion) =
Insertion::existing_import(response.import.stmt, self.tokens)
{
insertion.into_edit(member)
insertion.into_edit(request.member)
} else {
Insertion::end_of_statement(response.import.stmt, self.source, self.stylist)
.into_edit(&format!("from {} import {member}", request.module))
.into_edit(&format!(
"from {} import {}",
request.module, request.member
))
};
ImportAction {
import: Some(import),
@@ -337,7 +327,9 @@ impl<'ast> MembersInScope<'ast> {
.members_in_scope_at(node)
.into_iter()
.map(|(name, memberdef)| {
let def = memberdef.first_reachable_definition;
let Some(def) = memberdef.definition else {
return (name, MemberInScope::other(memberdef.ty));
};
let kind = match *def.kind(db) {
DefinitionKind::Import(ref kind) => {
MemberImportKind::Imported(AstImportKind::Import(kind.import(parsed)))
@@ -491,17 +483,6 @@ impl<'ast> AstImportKind<'ast> {
Some(ImportResponseKind::Qualified { ast, alias })
}
AstImportKind::ImportFrom(ast) => {
// If the request is for a module itself, then we
// assume that it can never be satisfies by a
// `from ... import ...` statement. For example, a
// `request for collections.abc` needs an
// `import collections.abc`. Now, there could be a
// `from collections import abc`, and we could
// plausibly consider that a match and return a
// symbol text of `abc`. But it's not clear if that's
// the right choice or not.
let member = request.member?;
if request.force_style && !matches!(request.style, ImportStyle::ImportFrom) {
return None;
}
@@ -513,7 +494,9 @@ impl<'ast> AstImportKind<'ast> {
let kind = ast
.names
.iter()
.find(|alias| alias.name.as_str() == "*" || alias.name.as_str() == member)
.find(|alias| {
alias.name.as_str() == "*" || alias.name.as_str() == request.member
})
.map(|alias| ImportResponseKind::Unqualified { ast, alias })
.unwrap_or_else(|| ImportResponseKind::Partial(ast));
Some(kind)
@@ -529,10 +512,7 @@ pub(crate) struct ImportRequest<'a> {
/// `foo`, in `from foo import bar`).
module: &'a str,
/// The member to import (e.g., `bar`, in `from foo import bar`).
///
/// When `member` is absent, then this request reflects an import
/// of the module itself. i.e., `import module`.
member: Option<&'a str>,
member: &'a str,
/// The preferred style to use when importing the symbol (e.g.,
/// `import foo` or `from foo import bar`).
///
@@ -554,7 +534,7 @@ impl<'a> ImportRequest<'a> {
pub(crate) fn import(module: &'a str, member: &'a str) -> Self {
Self {
module,
member: Some(member),
member,
style: ImportStyle::Import,
force_style: false,
}
@@ -567,26 +547,12 @@ impl<'a> ImportRequest<'a> {
pub(crate) fn import_from(module: &'a str, member: &'a str) -> Self {
Self {
module,
member: Some(member),
member,
style: ImportStyle::ImportFrom,
force_style: false,
}
}
/// Create a new [`ImportRequest`] for bringing the given module
/// into scope.
///
/// This is for just importing the module itself, always via an
/// `import` statement.
pub(crate) fn module(module: &'a str) -> Self {
Self {
module,
member: None,
style: ImportStyle::Import,
force_style: false,
}
}
/// Causes this request to become a command. This will force the
/// requested import style, even if another style would be more
/// appropriate generally.
@@ -601,13 +567,7 @@ impl<'a> ImportRequest<'a> {
/// of an import conflict are minimized (although not always reduced
/// to zero).
fn avoid_conflicts(self, db: &dyn Db, importing_file: File, members: &MembersInScope) -> Self {
let Some(member) = self.member else {
return Self {
style: ImportStyle::Import,
..self
};
};
match (members.map.get(self.module), members.map.get(member)) {
match (members.map.get(self.module), members.map.get(self.member)) {
// Neither symbol exists, so we can just proceed as
// normal.
(None, None) => self,
@@ -672,10 +632,7 @@ impl std::fmt::Display for ImportRequest<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.style {
ImportStyle::Import => write!(f, "import {}", self.module),
ImportStyle::ImportFrom => match self.member {
None => write!(f, "import {}", self.module),
Some(member) => write!(f, "from {} import {member}", self.module),
},
ImportStyle::ImportFrom => write!(f, "from {} import {}", self.module, self.member),
}
}
}
@@ -888,10 +845,6 @@ mod tests {
self.add(ImportRequest::import_from(module, member))
}
fn module(&self, module: &str) -> String {
self.add(ImportRequest::module(module))
}
fn add(&self, request: ImportRequest<'_>) -> String {
let node = covering_node(
self.cursor.parsed.syntax().into(),
@@ -1938,13 +1891,13 @@ else:
"#);
assert_snapshot!(
test.import_from("foo", "MAGIC"), @r#"
from foo import MAGIC
import foo
if os.getenv("WHATEVER"):
from foo import MAGIC
else:
from bar import MAGIC
(MAGIC)
(foo.MAGIC)
"#);
}
@@ -2155,13 +2108,13 @@ except ImportError:
");
assert_snapshot!(
test.import_from("foo", "MAGIC"), @r"
from foo import MAGIC
import foo
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(MAGIC)
(foo.MAGIC)
");
}
@@ -2205,73 +2158,4 @@ except ImportError:
(bar.MAGIC)
");
}
#[test]
fn import_module_blank() {
let test = cursor_test(
"\
<CURSOR>
",
);
assert_snapshot!(
test.module("collections"), @r"
import collections
collections
");
}
#[test]
fn import_module_exists() {
let test = cursor_test(
"\
import collections
<CURSOR>
",
);
assert_snapshot!(
test.module("collections"), @r"
import collections
collections
");
}
#[test]
fn import_module_from_exists() {
let test = cursor_test(
"\
from collections import defaultdict
<CURSOR>
",
);
assert_snapshot!(
test.module("collections"), @r"
import collections
from collections import defaultdict
collections
");
}
// This test is working as intended. That is,
// `abc` is already in scope, so requesting an
// import for `collections.abc` could feasibly
// reuse the import and rewrite the symbol text
// to just `abc`. But for now it seems better
// to respect what has been written and add the
// `import collections.abc`. This behavior could
// plausibly be changed.
#[test]
fn import_module_from_via_member_exists() {
let test = cursor_test(
"\
from collections import abc
<CURSOR>
",
);
assert_snapshot!(
test.module("collections.abc"), @r"
import collections.abc
from collections import abc
collections.abc
");
}
}

View File

@@ -19,22 +19,11 @@ pub struct InlayHint {
}
impl InlayHint {
fn variable_type(
expr: &Expr,
rhs: &Expr,
ty: Type,
db: &dyn Db,
allow_edits: bool,
) -> Option<Self> {
fn variable_type(expr: &Expr, ty: Type, db: &dyn Db, allow_edits: bool) -> Self {
let position = expr.range().end();
// Render the type to a string, and get subspans for all the types that make it up
let details = ty.display(db).to_string_parts();
// Filter out a reptitive hints like `x: T = T()`
if call_matches_name(rhs, &details.label) {
return None;
}
// Ok so the idea here is that we potentially have a random soup of spans here,
// and each byte of the string can have at most one target associate with it.
// Thankfully, they were generally pushed in print order, with the inner smaller types
@@ -84,12 +73,12 @@ impl InlayHint {
vec![]
};
Some(Self {
Self {
position,
kind: InlayHintKind::Type,
label: InlayHintLabel { parts: label_parts },
text_edits,
})
}
}
fn call_argument_name(
@@ -261,7 +250,7 @@ struct InlayHintVisitor<'a, 'db> {
db: &'db dyn Db,
model: SemanticModel<'db>,
hints: Vec<InlayHint>,
assignment_rhs: Option<&'a Expr>,
in_assignment: bool,
range: TextRange,
settings: &'a InlayHintSettings,
in_no_edits_allowed: bool,
@@ -273,21 +262,21 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
db,
model: SemanticModel::new(db, file),
hints: Vec::new(),
assignment_rhs: None,
in_assignment: false,
range,
settings,
in_no_edits_allowed: false,
}
}
fn add_type_hint(&mut self, expr: &Expr, rhs: &Expr, ty: Type<'db>, allow_edits: bool) {
fn add_type_hint(&mut self, expr: &Expr, ty: Type<'db>, allow_edits: bool) {
if !self.settings.variable_types {
return;
}
if let Some(inlay_hint) = InlayHint::variable_type(expr, rhs, ty, self.db, allow_edits) {
self.hints.push(inlay_hint);
}
let inlay_hint = InlayHint::variable_type(expr, ty, self.db, allow_edits);
self.hints.push(inlay_hint);
}
fn add_call_argument_name(
@@ -310,8 +299,8 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
}
}
impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal {
if self.range.intersect(node.range()).is_some() {
TraversalSignal::Traverse
} else {
@@ -319,7 +308,7 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
}
}
fn visit_stmt(&mut self, stmt: &'a Stmt) {
fn visit_stmt(&mut self, stmt: &Stmt) {
let node = AnyNodeRef::from(stmt);
if !self.enter_node(node).is_traverse() {
@@ -328,9 +317,7 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
match stmt {
Stmt::Assign(assign) => {
if !type_hint_is_excessive_for_expr(&assign.value) {
self.assignment_rhs = Some(&*assign.value);
}
self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value);
if !annotations_are_valid_syntax(assign) {
self.in_no_edits_allowed = true;
}
@@ -338,7 +325,7 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
self.visit_expr(target);
}
self.in_no_edits_allowed = false;
self.assignment_rhs = None;
self.in_assignment = false;
self.visit_expr(&assign.value);
@@ -357,22 +344,22 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
source_order::walk_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &'a Expr) {
fn visit_expr(&mut self, expr: &'_ Expr) {
match expr {
Expr::Name(name) => {
if let Some(rhs) = self.assignment_rhs {
if self.in_assignment {
if name.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed);
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
}
}
source_order::walk_expr(self, expr);
}
Expr::Attribute(attribute) => {
if let Some(rhs) = self.assignment_rhs {
if self.in_assignment {
if attribute.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed);
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
}
}
source_order::walk_expr(self, expr);
@@ -429,26 +416,6 @@ fn arg_matches_name(arg_or_keyword: &ArgOrKeyword, name: &str) -> bool {
}
}
/// Given a function call, check if the expression is the "same name"
/// as the function being called.
///
/// This allows us to filter out reptitive inlay hints like `x: T = T(...)`.
/// While still allowing non-trivial ones like `x: T[U] = T()`.
fn call_matches_name(expr: &Expr, name: &str) -> bool {
// Only care about function calls
let Expr::Call(call) = expr else {
return false;
};
match &*call.func {
// `x: T = T()` is a match
Expr::Name(expr_name) => expr_name.id.as_str() == name,
// `x: T = a.T()` is a match
Expr::Attribute(expr_attribute) => expr_attribute.attr.as_str() == name,
_ => false,
}
}
/// Given an expression that's the RHS of an assignment, would it be excessive to
/// emit an inlay type hint for the variable assigned to it?
///
@@ -1862,16 +1829,35 @@ mod tests {
",
);
assert_snapshot!(test.inlay_hints(), @r"
assert_snapshot!(test.inlay_hints(), @r#"
class A:
def __init__(self, y):
self.x = int(1)
self.x[: int] = int(1)
self.y[: Unknown] = y
a = A([y=]2)
a.y = int(3)
a[: A] = A([y=]2)
a.y[: int] = int(3)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:4:18
|
2 | class A:
3 | def __init__(self, y):
4 | self.x[: int] = int(1)
| ^^^
5 | self.y[: Unknown] = y
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/ty_extensions.pyi:20:1
|
@@ -1885,11 +1871,29 @@ mod tests {
--> main2.py:5:18
|
3 | def __init__(self, y):
4 | self.x = int(1)
4 | self.x[: int] = int(1)
5 | self.y[: Unknown] = y
| ^^^^^^^
6 |
7 | a = A([y=]2)
7 | a[: A] = A([y=]2)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:7
|
2 | class A:
| ^
3 | def __init__(self, y):
4 | self.x = int(1)
|
info: Source
--> main2.py:7:5
|
5 | self.y[: Unknown] = y
6 |
7 | a[: A] = A([y=]2)
| ^
8 | a.y[: int] = int(3)
|
info[inlay-hint-location]: Inlay Hint Target
@@ -1902,13 +1906,30 @@ mod tests {
5 | self.y = y
|
info: Source
--> main2.py:7:8
--> main2.py:7:13
|
5 | self.y[: Unknown] = y
6 |
7 | a = A([y=]2)
| ^
8 | a.y = int(3)
7 | a[: A] = A([y=]2)
| ^
8 | a.y[: int] = int(3)
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:8:7
|
7 | a[: A] = A([y=]2)
8 | a.y[: int] = int(3)
| ^^^
|
---------------------------------------------
@@ -1917,12 +1938,12 @@ mod tests {
class A:
def __init__(self, y):
self.x = int(1)
self.x: int = int(1)
self.y: Unknown = y
a = A(2)
a.y = int(3)
");
a: A = A(2)
a.y: int = int(3)
"#);
}
#[test]
@@ -1991,7 +2012,7 @@ mod tests {
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
@@ -2916,12 +2937,31 @@ mod tests {
def __init__(self):
self.x: int = 1
x = MyClass()
x[: MyClass] = MyClass()
y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
a[: MyClass], b[: MyClass] = MyClass(), MyClass()
c[: MyClass], d[: MyClass] = (MyClass(), MyClass())
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:7
|
2 | class MyClass:
| ^^^^^^^
3 | def __init__(self):
4 | self.x: int = 1
|
info: Source
--> main2.py:6:5
|
4 | self.x: int = 1
5 |
6 | x[: MyClass] = MyClass()
| ^^^^^^^
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2695:7
|
@@ -2933,7 +2973,7 @@ mod tests {
info: Source
--> main2.py:7:5
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
| ^^^^^
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
@@ -2951,7 +2991,7 @@ mod tests {
info: Source
--> main2.py:7:11
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
| ^^^^^^^
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
@@ -2969,7 +3009,7 @@ mod tests {
info: Source
--> main2.py:7:20
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
| ^^^^^^^
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
@@ -2987,7 +3027,7 @@ mod tests {
info: Source
--> main2.py:8:5
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
| ^^^^^^^
@@ -3005,7 +3045,7 @@ mod tests {
info: Source
--> main2.py:8:19
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
| ^^^^^^^
@@ -3054,7 +3094,7 @@ mod tests {
def __init__(self):
self.x: int = 1
x = MyClass()
x: MyClass = MyClass()
y: tuple[MyClass, MyClass] = (MyClass(), MyClass())
a, b = MyClass(), MyClass()
c, d = (MyClass(), MyClass())
@@ -4057,11 +4097,31 @@ mod tests {
def __init__(self):
self.x: int = 1
self.y: int = 2
val = MyClass()
val[: MyClass] = MyClass()
foo(val.x)
foo([x=]val.y)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:7
|
2 | def foo(x: int): pass
3 | class MyClass:
| ^^^^^^^
4 | def __init__(self):
5 | self.x: int = 1
|
info: Source
--> main2.py:7:7
|
5 | self.x: int = 1
6 | self.y: int = 2
7 | val[: MyClass] = MyClass()
| ^^^^^^^
8 |
9 | foo(val.x)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:9
|
@@ -4077,6 +4137,20 @@ mod tests {
10 | foo([x=]val.y)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
self.x: int = 1
self.y: int = 2
val: MyClass = MyClass()
foo(val.x)
foo(val.y)
");
}
@@ -4102,11 +4176,31 @@ mod tests {
def __init__(self):
self.x: int = 1
self.y: int = 2
x = MyClass()
x[: MyClass] = MyClass()
foo(x.x)
foo([x=]x.y)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:7
|
2 | def foo(x: int): pass
3 | class MyClass:
| ^^^^^^^
4 | def __init__(self):
5 | self.x: int = 1
|
info: Source
--> main2.py:7:5
|
5 | self.x: int = 1
6 | self.y: int = 2
7 | x[: MyClass] = MyClass()
| ^^^^^^^
8 |
9 | foo(x.x)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:9
|
@@ -4122,6 +4216,20 @@ mod tests {
10 | foo([x=]x.y)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
self.x: int = 1
self.y: int = 2
x: MyClass = MyClass()
foo(x.x)
foo(x.y)
");
}
@@ -4150,11 +4258,31 @@ mod tests {
return 1
def y() -> int:
return 2
val = MyClass()
val[: MyClass] = MyClass()
foo(val.x())
foo([x=]val.y())
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:7
|
2 | def foo(x: int): pass
3 | class MyClass:
| ^^^^^^^
4 | def __init__(self):
5 | def x() -> int:
|
info: Source
--> main2.py:9:7
|
7 | def y() -> int:
8 | return 2
9 | val[: MyClass] = MyClass()
| ^^^^^^^
10 |
11 | foo(val.x())
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:9
|
@@ -4170,6 +4298,22 @@ mod tests {
12 | foo([x=]val.y())
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
def x() -> int:
return 1
def y() -> int:
return 2
val: MyClass = MyClass()
foo(val.x())
foo(val.y())
");
}
@@ -4202,11 +4346,31 @@ mod tests {
return 1
def y() -> List[int]:
return 2
val = MyClass()
val[: MyClass] = MyClass()
foo(val.x()[0])
foo([x=]val.y()[1])
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:5:7
|
4 | def foo(x: int): pass
5 | class MyClass:
| ^^^^^^^
6 | def __init__(self):
7 | def x() -> List[int]:
|
info: Source
--> main2.py:11:7
|
9 | def y() -> List[int]:
10 | return 2
11 | val[: MyClass] = MyClass()
| ^^^^^^^
12 |
13 | foo(val.x()[0])
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:4:9
|
@@ -4224,6 +4388,24 @@ mod tests {
14 | foo([x=]val.y()[1])
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
from typing import List
def foo(x: int): pass
class MyClass:
def __init__(self):
def x() -> List[int]:
return 1
def y() -> List[int]:
return 2
val: MyClass = MyClass()
foo(val.x()[0])
foo(val.y()[1])
");
}
@@ -4515,7 +4697,7 @@ mod tests {
class Foo:
def __init__(self, x: int): pass
Foo([x=]1)
f = Foo([x=]1)
f[: Foo] = Foo([x=]1)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:24
@@ -4533,7 +4715,24 @@ mod tests {
3 | def __init__(self, x: int): pass
4 | Foo([x=]1)
| ^
5 | f = Foo([x=]1)
5 | f[: Foo] = Foo([x=]1)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:7
|
2 | class Foo:
| ^^^
3 | def __init__(self, x: int): pass
4 | Foo(1)
|
info: Source
--> main2.py:5:5
|
3 | def __init__(self, x: int): pass
4 | Foo([x=]1)
5 | f[: Foo] = Foo([x=]1)
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
@@ -4546,13 +4745,22 @@ mod tests {
5 | f = Foo(1)
|
info: Source
--> main2.py:5:10
--> main2.py:5:17
|
3 | def __init__(self, x: int): pass
4 | Foo([x=]1)
5 | f = Foo([x=]1)
| ^
5 | f[: Foo] = Foo([x=]1)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class Foo:
def __init__(self, x: int): pass
Foo(1)
f: Foo = Foo(1)
");
}
@@ -4570,7 +4778,7 @@ mod tests {
class Foo:
def __new__(cls, x: int): pass
Foo([x=]1)
f = Foo([x=]1)
f[: Foo] = Foo([x=]1)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:22
@@ -4588,7 +4796,24 @@ mod tests {
3 | def __new__(cls, x: int): pass
4 | Foo([x=]1)
| ^
5 | f = Foo([x=]1)
5 | f[: Foo] = Foo([x=]1)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:7
|
2 | class Foo:
| ^^^
3 | def __new__(cls, x: int): pass
4 | Foo(1)
|
info: Source
--> main2.py:5:5
|
3 | def __new__(cls, x: int): pass
4 | Foo([x=]1)
5 | f[: Foo] = Foo([x=]1)
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
@@ -4601,13 +4826,22 @@ mod tests {
5 | f = Foo(1)
|
info: Source
--> main2.py:5:10
--> main2.py:5:17
|
3 | def __new__(cls, x: int): pass
4 | Foo([x=]1)
5 | f = Foo([x=]1)
| ^
5 | f[: Foo] = Foo([x=]1)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class Foo:
def __new__(cls, x: int): pass
Foo(1)
f: Foo = Foo(1)
");
}
@@ -6194,11 +6428,11 @@ mod tests {
a = Literal['a', 'b', 'c']",
);
assert_snapshot!(test.inlay_hints(), @r#"
assert_snapshot!(test.inlay_hints(), @r"
from typing import Literal
a[: <special form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
"#);
a[: <typing.Literal special form>] = Literal['a', 'b', 'c']
");
}
struct InlayHintLocationDiagnostic {

View File

@@ -37,38 +37,6 @@ pub enum ReferencesMode {
DocumentHighlights,
}
impl ReferencesMode {
pub(super) fn to_import_alias_resolution(self) -> ImportAliasResolution {
match self {
// Resolve import aliases for find references:
// ```py
// from warnings import deprecated as my_deprecated
//
// @my_deprecated
// def foo
// ```
//
// When finding references on `my_deprecated`, we want to find all usages of `deprecated` across the entire
// project.
Self::References | Self::ReferencesSkipDeclaration => {
ImportAliasResolution::ResolveAliases
}
// For rename, don't resolve import aliases.
//
// ```py
// from warnings import deprecated as my_deprecated
//
// @my_deprecated
// def foo
// ```
// When renaming `my_deprecated`, only rename the alias, but not the original definition in `warnings`.
Self::Rename | Self::RenameMultiFile | Self::DocumentHighlights => {
ImportAliasResolution::PreserveAliases
}
}
}
}
/// Find all references to a symbol at the given position.
/// Search for references across all files in the project.
pub(crate) fn references(
@@ -77,9 +45,12 @@ pub(crate) fn references(
goto_target: &GotoTarget,
mode: ReferencesMode,
) -> Option<Vec<ReferenceTarget>> {
// Get the definitions for the symbol at the cursor position
// When finding references, do not resolve any local aliases.
let model = SemanticModel::new(db, file);
let target_definitions = goto_target
.get_definition_targets(&model, mode.to_import_alias_resolution())?
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)?
.declaration_targets(db)?;
// Extract the target text from the goto target for fast comparison
@@ -347,7 +318,7 @@ impl LocalReferencesFinder<'_> {
{
// Get the definitions for this goto target
if let Some(current_definitions) = goto_target
.get_definition_targets(self.model, self.mode.to_import_alias_resolution())
.get_definition_targets(self.model, ImportAliasResolution::PreserveAliases)
.and_then(|definitions| definitions.declaration_targets(self.model.db()))
{
// Check if any of the current definitions match our target definitions

File diff suppressed because it is too large Load Diff

View File

@@ -259,11 +259,7 @@ impl<'db> SemanticTokenVisitor<'db> {
fn classify_name(&self, name: &ast::ExprName) -> (SemanticTokenType, SemanticTokenModifier) {
// First try to classify the token based on its definition kind.
let definition = definition_for_name(
self.model,
name,
ty_python_semantic::ImportAliasResolution::ResolveAliases,
);
let definition = definition_for_name(self.model, name);
if let Some(definition) = definition {
let name_str = name.id.as_str();

File diff suppressed because it is too large Load Diff

View File

@@ -41,7 +41,7 @@ rustc-hash = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_json = { workspace = true, optional = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
@@ -55,6 +55,7 @@ default = ["zstd"]
deflate = ["ty_vendored/deflate"]
schemars = [
"dep:schemars",
"dep:serde_json",
"ruff_db/schemars",
"ruff_python_ast/schemars",
"ty_python_semantic/schemars",

View File

@@ -7,7 +7,6 @@ pub use self::changes::ChangeResult;
use crate::CollectReporter;
use crate::metadata::settings::file_settings;
use crate::{ProgressReporter, Project, ProjectMetadata};
use get_size2::StandardTracker;
use ruff_db::Db as SourceDb;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files};
@@ -130,10 +129,7 @@ impl ProjectDatabase {
/// Returns a [`SalsaMemoryDump`] that can be use to dump Salsa memory usage information
/// to the CLI after a typechecker run.
pub fn salsa_memory_dump(&self) -> SalsaMemoryDump {
let memory_usage = ruff_memory_usage::attach_tracker(StandardTracker::new(), || {
<dyn salsa::Database>::memory_usage(self)
});
let memory_usage = <dyn salsa::Database>::memory_usage(self);
let mut ingredients = memory_usage
.structs
.into_iter()

View File

@@ -9,15 +9,13 @@ use ty_python_semantic::ProgramSettings;
use crate::metadata::options::ProjectOptionsOverrides;
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
use crate::metadata::script::{Pep723Error, Pep723Metadata};
use crate::metadata::value::{RelativePathBuf, ValueSource};
use crate::metadata::value::ValueSource;
pub use options::Options;
use options::TyTomlError;
mod configuration_file;
pub mod options;
pub mod pyproject;
pub mod script;
pub mod settings;
pub mod value;
@@ -87,32 +85,6 @@ impl ProjectMetadata {
)
}
/// Loads a project from a `pyproject.toml` file.
pub(crate) fn from_script(
script: Pep723Metadata,
script_path: &SystemPath,
) -> Result<Self, ResolveRequiresPythonError> {
let project = Some(&script.to_project());
let parent_dir = script_path
.parent()
.map(ToOwned::to_owned)
.unwrap_or_default();
let mut metadata = Self::from_options(
script.tool.and_then(|tool| tool.ty).unwrap_or_default(),
parent_dir,
project,
)?;
// Try to get `uv sync --script` to setup the venv for us
if let Some(python) = script::uv_sync_script(script_path) {
let mut environment = metadata.options.environment.unwrap_or_default();
environment.python = Some(RelativePathBuf::new(python, ValueSource::Cli));
metadata.options.environment = Some(environment);
}
Ok(metadata)
}
/// Loads a project from a set of options with an optional pyproject-project table.
pub fn from_options(
mut options: Options,
@@ -148,46 +120,6 @@ impl ProjectMetadata {
})
}
pub fn discover_script(
path: &SystemPath,
system: &dyn System,
) -> Result<ProjectMetadata, ProjectMetadataError> {
tracing::debug!("Searching for a PEP-723 Script in '{path}'");
if !system.is_file(path) {
return Err(ProjectMetadataError::NotAScript(path.to_path_buf()));
}
let script_metadata = if let Ok(script_str) = system.read_to_string(path) {
match Pep723Metadata::from_script_str(
script_str.as_bytes(),
ValueSource::File(Arc::new(path.to_owned())),
) {
Ok(Some(pyproject)) => Some(pyproject),
Ok(None) => None,
Err(error) => {
return Err(ProjectMetadataError::InvalidScript {
path: path.to_owned(),
source: Box::new(error),
});
}
}
} else {
None
};
let Some(script_metadata) = script_metadata else {
return Err(ProjectMetadataError::NotAScript(path.to_path_buf()));
};
let metadata = ProjectMetadata::from_script(script_metadata, path).map_err(|err| {
ProjectMetadataError::InvalidRequiresPythonConstraint {
source: err,
path: path.to_owned(),
}
})?;
Ok(metadata)
}
/// Discovers the closest project at `path` and returns its metadata.
///
/// The algorithm traverses upwards in the `path`'s ancestor chain and uses the following precedence
@@ -387,21 +319,12 @@ pub enum ProjectMetadataError {
#[error("project path '{0}' is not a directory")]
NotADirectory(SystemPathBuf),
#[error("project path '{0}' is not a PEP-723 script")]
NotAScript(SystemPathBuf),
#[error("{path} is not a valid `pyproject.toml`: {source}")]
InvalidPyProject {
source: Box<PyProjectError>,
path: SystemPathBuf,
},
#[error("{path} is not a valid PEP-723 script: {source}")]
InvalidScript {
source: Box<Pep723Error>,
path: SystemPathBuf,
},
#[error("{path} is not a valid `ty.toml`: {source}")]
InvalidTyToml {
source: Box<TyTomlError>,

View File

@@ -1,157 +0,0 @@
use std::{io, process::Command, str::FromStr};
use camino::Utf8PathBuf;
use pep440_rs::VersionSpecifiers;
use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_python_ast::script::ScriptTag;
use serde::Deserialize;
use thiserror::Error;
use crate::metadata::{
pyproject::{Project, Tool},
value::{RangedValue, ValueSource, ValueSourceGuard},
};
/// PEP 723 metadata as parsed from a `script` comment block.
///
/// See: <https://peps.python.org/pep-0723/>
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Pep723Metadata {
pub dependencies: Option<RangedValue<Vec<toml::Value>>>,
pub requires_python: Option<RangedValue<VersionSpecifiers>>,
pub tool: Option<Tool>,
/// The raw unserialized document.
#[serde(skip)]
pub raw: String,
}
#[derive(Debug, Error)]
pub enum Pep723Error {
#[error(
"An opening tag (`# /// script`) was found without a closing tag (`# ///`). Ensure that every line between the opening and closing tags (including empty lines) starts with a leading `#`."
)]
UnclosedBlock,
#[error("The PEP 723 metadata block is missing from the script.")]
MissingTag,
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Utf8(#[from] std::str::Utf8Error),
#[error(transparent)]
Toml(#[from] toml::de::Error),
#[error("Invalid filename `{0}` supplied")]
InvalidFilename(String),
}
impl Pep723Metadata {
/// Parse the PEP 723 metadata from `stdin`.
pub fn from_script_str(
contents: &[u8],
source: ValueSource,
) -> Result<Option<Self>, Pep723Error> {
let _guard = ValueSourceGuard::new(source, true);
// Extract the `script` tag.
let Some(ScriptTag { metadata, .. }) = ScriptTag::parse(contents) else {
return Ok(None);
};
// Parse the metadata.
Ok(Some(Self::from_str(&metadata)?))
}
pub fn to_project(&self) -> Project {
Project {
name: None,
version: None,
requires_python: self.requires_python.clone(),
}
}
}
/*
{
"schema": {
"version": "preview"
},
"target": "script",
"script": {
"path": "/Users/myuser/code/myproj/scripts/load-test.py"
},
"sync": {
"environment": {
"path": "/Users/myuser/.cache/uv/environments-v2/load-test-d6edaf5bfab110a8",
"python": {
"path": "/Users/myuser/.cache/uv/environments-v2/load-test-d6edaf5bfab110a8/bin/python3",
"version": "3.14.0",
"implementation": "cpython"
}
},
"action": "check"
},
"lock": null,
"dry_run": false
}
*/
/// The output of `uv sync --output-format=json --script ...`
#[derive(Debug, Clone, Deserialize)]
struct UvMetadata {
sync: Option<UvSync>,
}
#[derive(Debug, Clone, Deserialize)]
struct UvSync {
environment: Option<UvEnvironment>,
}
#[derive(Debug, Clone, Deserialize)]
struct UvEnvironment {
path: Option<String>,
}
/// Ask `uv` to sync the script's venv to some temp dir so we can analyze dependencies properly
///
/// Returns the path to the venv on success
pub fn uv_sync_script(script_path: &SystemPath) -> Option<SystemPathBuf> {
tracing::info!("Asking uv to sync the script's venv");
let mut command = Command::new("uv");
command
.arg("sync")
.arg("--output-format=json")
.arg("--script")
.arg(script_path.as_str());
let output = command
.output()
.inspect_err(|e| {
tracing::info!(
"failed to run `uv sync --output-format=json --script {script_path}`: {e}"
);
})
.ok()?;
let metadata: UvMetadata = serde_json::from_slice(&output.stdout)
.inspect_err(|e| {
tracing::info!(
"failed to parse `uv sync --output-format=json --script {script_path}`: {e}"
);
})
.ok()?;
let env_path = metadata.sync?.environment?.path?;
let utf8_path = Utf8PathBuf::from(env_path);
Some(SystemPathBuf::from_utf8_path_buf(utf8_path))
}
impl FromStr for Pep723Metadata {
type Err = toml::de::Error;
/// Parse `Pep723Metadata` from a raw TOML string.
fn from_str(raw: &str) -> Result<Self, Self::Err> {
let metadata = toml::from_str(raw)?;
Ok(Self {
raw: raw.to_string(),
..metadata
})
}
}

View File

@@ -37,16 +37,14 @@ class MDTestRunner:
mdtest_executable: Path | None
console: Console
filters: list[str]
enable_external: bool
def __init__(self, filters: list[str] | None, enable_external: bool) -> None:
def __init__(self, filters: list[str] | None = None) -> None:
self.mdtest_executable = None
self.console = Console()
self.filters = [
f.removesuffix(".md").replace("/", "_").replace("-", "_")
for f in (filters or [])
]
self.enable_external = enable_external
def _run_cargo_test(self, *, message_format: Literal["human", "json"]) -> str:
return subprocess.check_output(
@@ -122,7 +120,6 @@ class MDTestRunner:
CLICOLOR_FORCE="1",
INSTA_FORCE_PASS="1",
INSTA_OUTPUT="none",
MDTEST_EXTERNAL="1" if self.enable_external else "0",
),
capture_output=capture_output,
text=True,
@@ -269,19 +266,11 @@ def main() -> None:
nargs="*",
help="Partial paths or mangled names, e.g., 'loops/for.md' or 'loops_for'",
)
parser.add_argument(
"--enable-external",
"-e",
action="store_true",
help="Enable tests with external dependencies",
)
args = parser.parse_args()
try:
runner = MDTestRunner(
filters=args.filters, enable_external=args.enable_external
)
runner = MDTestRunner(filters=args.filters)
runner.watch()
except KeyboardInterrupt:
print()

View File

@@ -1,14 +0,0 @@
from typing import Protocol
class A(Protocol):
@property
def f(self): ...
type Recursive = int | tuple[Recursive, ...]
class B[T: A]: ...
class C[T: A](A):
x: tuple[Recursive, ...]
class D(B[C]): ...

View File

@@ -1,4 +0,0 @@
from __future__ import annotations
class MyClass:
type: type = str

View File

@@ -1,6 +0,0 @@
# This is a regression test for `store_expression_type`.
# ref: https://github.com/astral-sh/ty/issues/1688
x: int
type x[T] = x[T, U]

View File

@@ -1,6 +0,0 @@
class C[T: (A, B)]:
def f(foo: T):
try:
pass
except foo:
pass

View File

@@ -169,13 +169,13 @@ def f(x: Any[int]):
`Any` cannot be called (this leads to a `TypeError` at runtime):
```py
Any() # error: [call-non-callable] "Object of type `<special form 'typing.Any'>` is not callable"
Any() # error: [call-non-callable] "Object of type `typing.Any` is not callable"
```
`Any` also cannot be used as a metaclass (under the hood, this leads to an implicit call to `Any`):
```py
class F(metaclass=Any): ... # error: [invalid-metaclass] "Metaclass type `<special form 'typing.Any'>` is not callable"
class F(metaclass=Any): ... # error: [invalid-metaclass] "Metaclass type `typing.Any` is not callable"
```
And `Any` cannot be used in `isinstance()` checks:

View File

@@ -307,10 +307,12 @@ Using a `ParamSpec` in a `Callable` annotation:
from typing_extensions import Callable
def _[**P1](c: Callable[P1, int]):
reveal_type(P1.args) # revealed: P1@_.args
reveal_type(P1.kwargs) # revealed: P1@_.kwargs
# TODO: Should reveal `ParamSpecArgs` and `ParamSpecKwargs`
reveal_type(P1.args) # revealed: @Todo(ParamSpecArgs / ParamSpecKwargs)
reveal_type(P1.kwargs) # revealed: @Todo(ParamSpecArgs / ParamSpecKwargs)
reveal_type(c) # revealed: (**P1@_) -> int
# TODO: Signature should be (**P1) -> int
reveal_type(c) # revealed: (...) -> int
```
And, using the legacy syntax:
@@ -320,8 +322,9 @@ from typing_extensions import ParamSpec
P2 = ParamSpec("P2")
# TODO: argument list should not be `...` (requires `ParamSpec` support)
def _(c: Callable[P2, int]):
reveal_type(c) # revealed: (**P2@_) -> int
reveal_type(c) # revealed: (...) -> int
```
## Using `typing.Unpack`

View File

@@ -59,7 +59,7 @@ python-version = "3.11"
```py
from typing import Never
reveal_type(Never) # revealed: <special form 'typing.Never'>
reveal_type(Never) # revealed: typing.Never
```
### Python 3.10

View File

@@ -18,8 +18,9 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
def g() -> TypeGuard[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
reveal_type(args) # revealed: P@i.args
reveal_type(kwargs) # revealed: P@i.kwargs
# TODO: Should reveal a type representing `P.args` and `P.kwargs`
reveal_type(args) # revealed: tuple[@Todo(ParamSpecArgs / ParamSpecKwargs), ...]
reveal_type(kwargs) # revealed: dict[str, @Todo(ParamSpecArgs / ParamSpecKwargs)]
return callback(42, *args, **kwargs)
class Foo:
@@ -64,9 +65,8 @@ def _(
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
# error: [invalid-type-form] "Variable of type `ParamSpec` is not allowed in a type expression"
def foo(a_: e) -> None:
reveal_type(a_) # revealed: Unknown
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`)
```
## Inheritance

View File

@@ -542,8 +542,7 @@ e: list[Any] | None = [1]
reveal_type(e) # revealed: list[Any]
f: list[Any] | None = f2(1)
# TODO: Better constraint solver.
reveal_type(f) # revealed: list[int] | None
reveal_type(f) # revealed: list[Any] | None
g: list[Any] | dict[Any, Any] = f3(1)
# TODO: Better constraint solver.
@@ -600,6 +599,48 @@ reveal_type(x7) # revealed: Contravariant[Any]
reveal_type(x8) # revealed: Invariant[Any]
```
## Declared type preference sees through subtyping
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Any, Iterable, Literal, MutableSequence, Sequence
x1: Sequence[Any] = [1, 2, 3]
reveal_type(x1) # revealed: list[Any]
x2: MutableSequence[Any] = [1, 2, 3]
reveal_type(x2) # revealed: list[Any]
x3: Iterable[Any] = [1, 2, 3]
reveal_type(x3) # revealed: list[Any]
class X[T]:
value: T
def __init__(self, value: T): ...
class A[T](X[T]): ...
def a[T](value: T) -> A[T]:
return A(value)
x4: A[object] = A(1)
reveal_type(x4) # revealed: A[object]
x5: X[object] = A(1)
reveal_type(x5) # revealed: A[object]
x6: X[object] | None = A(1)
reveal_type(x6) # revealed: A[object]
x7: X[object] | None = a(1)
reveal_type(x7) # revealed: A[object]
```
## Narrow generic unions
```toml

View File

@@ -13,7 +13,7 @@ python-version = "3.10"
class A: ...
class B: ...
reveal_type(A | B) # revealed: <types.UnionType special form 'A | B'>
reveal_type(A | B) # revealed: types.UnionType
```
## Union of two classes (prior to 3.10)
@@ -43,14 +43,14 @@ class A: ...
class B: ...
def _(sub_a: type[A], sub_b: type[B]):
reveal_type(A | sub_b) # revealed: <types.UnionType special form>
reveal_type(sub_a | B) # revealed: <types.UnionType special form>
reveal_type(sub_a | sub_b) # revealed: <types.UnionType special form>
reveal_type(A | sub_b) # revealed: types.UnionType
reveal_type(sub_a | B) # revealed: types.UnionType
reveal_type(sub_a | sub_b) # revealed: types.UnionType
class C[T]: ...
class D[T]: ...
reveal_type(C | D) # revealed: <types.UnionType special form 'C[Unknown] | D[Unknown]'>
reveal_type(C | D) # revealed: types.UnionType
reveal_type(C[int] | D[str]) # revealed: <types.UnionType special form 'C[int] | D[str]'>
reveal_type(C[int] | D[str]) # revealed: types.UnionType
```

View File

@@ -227,56 +227,17 @@ def _(literals_2: Literal[0, 1], b: bool, flag: bool):
literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15]
literals_64 = 4 * literals_16 + literals_4 # Literal[0, 1, .., 63]
literals_128 = 2 * literals_64 + literals_2 # Literal[0, 1, .., 127]
literals_256 = 2 * literals_128 + literals_2 # Literal[0, 1, .., 255]
# Going beyond the MAX_NON_RECURSIVE_UNION_LITERALS limit (currently 256):
reveal_type(literals_256 if flag else 256) # revealed: int
# Going beyond the MAX_UNION_LITERALS limit (currently 200):
literals_256 = 16 * literals_16 + literals_16
reveal_type(literals_256) # revealed: int
# Going beyond the limit when another type is already part of the union
bool_and_literals_128 = b if flag else literals_128 # bool | Literal[0, 1, ..., 127]
literals_128_shifted = literals_128 + 128 # Literal[128, 129, ..., 255]
literals_256_shifted = literals_256 + 256 # Literal[256, 257, ..., 511]
# Now union the two:
two = bool_and_literals_128 if flag else literals_128_shifted
# revealed: bool | Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]
reveal_type(two)
reveal_type(two if flag else literals_256_shifted) # revealed: int
```
Recursively defined literal union types are widened earlier than non-recursively defined types for
faster convergence.
```py
class RecursiveAttr:
def __init__(self):
self.i = 0
def update(self):
self.i = self.i + 1
reveal_type(RecursiveAttr().i) # revealed: Unknown | int
# Here are some recursive but saturating examples. Because it's difficult to statically determine whether literal unions saturate or diverge,
# we widen them early, even though they may actually be convergent.
class RecursiveAttr2:
def __init__(self):
self.i = 0
def update(self):
self.i = (self.i + 1) % 9
reveal_type(RecursiveAttr2().i) # revealed: Unknown | Literal[0, 1, 2, 3, 4, 5, 6, 7, 8]
class RecursiveAttr3:
def __init__(self):
self.i = 0
def update(self):
self.i = (self.i + 1) % 10
# Going beyond the MAX_RECURSIVE_UNION_LITERALS limit:
reveal_type(RecursiveAttr3().i) # revealed: Unknown | int
reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int
```
## Simplifying gradually-equivalent types

View File

@@ -603,14 +603,12 @@ super(object, object()).__class__
# Not all objects valid in a class's bases list are valid as the first argument to `super()`.
# For example, it's valid to inherit from `typing.ChainMap`, but it's not valid as the first argument to `super()`.
#
# error: [invalid-super-argument] "`<special form 'typing.ChainMap'>` is not a valid class"
# error: [invalid-super-argument] "`typing.ChainMap` is not a valid class"
reveal_type(super(typing.ChainMap, collections.ChainMap())) # revealed: Unknown
# Meanwhile, it's not valid to inherit from unsubscripted `typing.Generic`,
# but it *is* valid as the first argument to `super()`.
#
# revealed: <super: <special form 'typing.Generic'>, <class 'SupportsInt'>>
reveal_type(super(typing.Generic, typing.SupportsInt))
reveal_type(super(typing.Generic, typing.SupportsInt)) # revealed: <super: typing.Generic, <class 'SupportsInt'>>
def _(x: type[typing.Any], y: typing.Any):
reveal_type(super(x, y)) # revealed: <super: Any, Any>

View File

@@ -1462,57 +1462,3 @@ def test_c():
c = C(1)
c.__lt__ = Mock()
```
## Imperatively calling `dataclasses.dataclass`
While we do not currently recognize the special behaviour of `dataclasses.dataclass` if it is called
imperatively, we recognize that it can be called imperatively and do not emit any false-positive
diagnostics on such calls:
```py
from dataclasses import dataclass
from typing_extensions import TypeVar, dataclass_transform
U = TypeVar("U")
@dataclass_transform(kw_only_default=True)
def sequence(cls: type[U]) -> type[U]:
d = dataclass(
repr=False,
eq=False,
match_args=False,
kw_only=True,
)(cls)
reveal_type(d) # revealed: type[U@sequence] & Any
return d
@dataclass_transform(kw_only_default=True)
def sequence2(cls: type) -> type:
d = dataclass(
repr=False,
eq=False,
match_args=False,
kw_only=True,
)(cls)
reveal_type(d) # revealed: type & Any
return d
@dataclass_transform(kw_only_default=True)
def sequence3(cls: type[U]) -> type[U]:
# TODO: should reveal `type[U@sequence3]`
return reveal_type(dataclass(cls)) # revealed: Unknown
@dataclass_transform(kw_only_default=True)
def sequence4(cls: type) -> type:
# TODO: should reveal `type`
return reveal_type(dataclass(cls)) # revealed: Unknown
class Foo: ...
ordered_foo = dataclass(order=True)(Foo)
reveal_type(ordered_foo) # revealed: type[Foo] & Any
# TODO: should be `Foo & Any`
reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call)
# TODO: should be `Any`
reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call)
```

View File

@@ -143,10 +143,6 @@ await C()
def f():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await C()
(await cor async for cor in f()) # ok
(await cor for cor in f()) # ok
([await c for c in cor] async for cor in f()) # ok
```
Generators are evaluated lazily, so `await` is allowed, even outside of a function.

View File

@@ -1,26 +0,0 @@
# Diagnostics for invalid attribute access on special forms
<!-- snapshot-diagnostics -->
```py
from typing_extensions import Any, Final, LiteralString, Self
X = Any
class Foo:
X: Final = LiteralString
a: int
b: Self
class Bar:
def __init__(self):
self.y: Final = LiteralString
X.foo # error: [unresolved-attribute]
X.aaaaooooooo # error: [unresolved-attribute]
Foo.X.startswith # error: [unresolved-attribute]
Foo.Bar().y.startswith # error: [unresolved-attribute]
# TODO: false positive (just testing the diagnostic in the meantime)
Foo().b.a # error: [unresolved-attribute]
```

View File

@@ -7,11 +7,10 @@
```py
from typing_extensions import assert_type
def _(x: int, y: bool):
def _(x: int):
assert_type(x, int) # fine
assert_type(x, str) # error: [type-assertion-failure]
assert_type(assert_type(x, int), int)
assert_type(y, int) # error: [type-assertion-failure]
```
## Narrowing

View File

@@ -1,4 +0,0 @@
# mdtests with external dependencies
This directory contains mdtests that make use of external packages. See the mdtest `README.md` for
more information.

View File

@@ -1,78 +0,0 @@
# attrs
```toml
[environment]
python-version = "3.13"
python-platform = "linux"
[project]
dependencies = ["attrs==25.4.0"]
```
## Basic class (`attr`)
```py
import attr
@attr.s
class User:
id: int = attr.ib()
name: str = attr.ib()
user = User(id=1, name="John Doe")
reveal_type(user.id) # revealed: int
reveal_type(user.name) # revealed: str
```
## Basic class (`define`)
```py
from attrs import define, field
@define
class User:
id: int = field()
internal_name: str = field(alias="name")
user = User(id=1, name="John Doe")
reveal_type(user.id) # revealed: int
reveal_type(user.internal_name) # revealed: str
```
## Usage of `field` parameters
```py
from attrs import define, field
@define
class Product:
id: int = field(init=False)
name: str = field()
price_cent: int = field(kw_only=True)
reveal_type(Product.__init__) # revealed: (self: Product, name: str, *, price_cent: int) -> None
```
## Dedicated support for the `default` decorator?
We currently do not support this:
```py
from attrs import define, field
@define
class Person:
id: int = field()
name: str = field()
# error: [call-non-callable] "Object of type `_MISSING_TYPE` is not callable"
@id.default
def _default_id(self) -> int:
raise NotImplementedError
# error: [missing-argument] "No argument provided for required parameter `id`"
person = Person(name="Alice")
reveal_type(person.id) # revealed: int
reveal_type(person.name) # revealed: str
```

View File

@@ -1,23 +0,0 @@
# numpy
```toml
[environment]
python-version = "3.13"
python-platform = "linux"
[project]
dependencies = ["numpy==2.3.0"]
```
## Basic usage
```py
import numpy as np
xs = np.array([1, 2, 3])
reveal_type(xs) # revealed: ndarray[tuple[Any, ...], dtype[Any]]
xs = np.array([1.0, 2.0, 3.0], dtype=np.float64)
# TODO: should be `ndarray[tuple[Any, ...], dtype[float64]]`
reveal_type(xs) # revealed: ndarray[tuple[Any, ...], dtype[Unknown]]
```

View File

@@ -1,48 +0,0 @@
# Pydantic
```toml
[environment]
python-version = "3.12"
python-platform = "linux"
[project]
dependencies = ["pydantic==2.12.2"]
```
## Basic model
```py
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
reveal_type(User.__init__) # revealed: (self: User, *, id: int, name: str) -> None
user = User(id=1, name="John Doe")
reveal_type(user.id) # revealed: int
reveal_type(user.name) # revealed: str
# error: [missing-argument] "No argument provided for required parameter `name`"
invalid_user = User(id=2)
```
## Usage of `Field`
```py
from pydantic import BaseModel, Field
class Product(BaseModel):
id: int = Field(init=False)
name: str = Field(..., kw_only=False, min_length=1)
internal_price_cent: int = Field(..., gt=0, alias="price_cent")
reveal_type(Product.__init__) # revealed: (self: Product, name: str = Any, *, price_cent: int = Any) -> None
product = Product("Laptop", price_cent=999_00)
reveal_type(product.id) # revealed: int
reveal_type(product.name) # revealed: str
reveal_type(product.internal_price_cent) # revealed: int
```

View File

@@ -1,27 +0,0 @@
# pytest
```toml
[environment]
python-version = "3.13"
python-platform = "linux"
[project]
dependencies = ["pytest==9.0.1"]
```
## `pytest.fail`
Make sure that we recognize `pytest.fail` calls as terminal:
```py
import pytest
def some_runtime_condition() -> bool:
return True
def test_something():
if not some_runtime_condition():
pytest.fail("Runtime condition failed")
no_error_here_this_is_unreachable
```

View File

@@ -1,210 +0,0 @@
# SQLAlchemy
```toml
[environment]
python-version = "3.13"
python-platform = "linux"
[project]
dependencies = ["SQLAlchemy==2.0.44"]
```
## ORM Model
This test makes sure that ty understands SQLAlchemy's `dataclass_transform` setup:
```py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True, init=False)
internal_name: Mapped[str] = mapped_column(alias="name")
user = User(name="John Doe")
reveal_type(user.id) # revealed: int
reveal_type(user.internal_name) # revealed: str
```
Unfortunately, SQLAlchemy overrides `__init__` and explicitly accepts all combinations of keyword
arguments. This is why we currently cannot flag invalid constructor calls:
```py
reveal_type(User.__init__) # revealed: def __init__(self, **kw: Any) -> Unknown
# TODO: this should ideally be an error
invalid_user = User(invalid_arg=42)
```
## Basic query example
First, set up a `Session`:
```py
from sqlalchemy import select, Integer, Text, Boolean
from sqlalchemy.orm import Session
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import create_engine
engine = create_engine("sqlite://example.db")
session = Session(engine)
```
And define a simple model:
```py
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(Text)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
```
Finally, we can execute queries:
```py
stmt = select(User)
reveal_type(stmt) # revealed: Select[tuple[User]]
users = session.scalars(stmt).all()
reveal_type(users) # revealed: Sequence[User]
for row in session.execute(stmt):
reveal_type(row) # revealed: Row[tuple[User]]
stmt = select(User).where(User.name == "Alice")
alice1 = session.scalars(stmt).first()
reveal_type(alice1) # revealed: User | None
alice2 = session.scalar(stmt)
reveal_type(alice2) # revealed: User | None
result = session.execute(stmt)
row = result.one_or_none()
assert row is not None
(alice3,) = row._tuple()
reveal_type(alice3) # revealed: User
```
This also works with more complex queries:
```py
stmt = select(User).where(User.is_admin == True).order_by(User.name).limit(10)
admin_users = session.scalars(stmt).all()
reveal_type(admin_users) # revealed: Sequence[User]
```
We can also specify particular columns to select:
```py
stmt = select(User.id, User.name)
# TODO: should be `Select[tuple[int, str]]`
reveal_type(stmt) # revealed: Select[tuple[Unknown, Unknown]]
ids_and_names = session.execute(stmt).all()
# TODO: should be `Sequence[Row[tuple[int, str]]]`
reveal_type(ids_and_names) # revealed: Sequence[Row[tuple[Unknown, Unknown]]]
for row in session.execute(stmt):
# TODO: should be `Row[tuple[int, str]]`
reveal_type(row) # revealed: Row[tuple[Unknown, Unknown]]
for user_id, name in session.execute(stmt).tuples():
# TODO: should be `int`
reveal_type(user_id) # revealed: Unknown
# TODO: should be `str`
reveal_type(name) # revealed: Unknown
result = session.execute(stmt)
row = result.one_or_none()
assert row is not None
(user_id, name) = row._tuple()
# TODO: should be `int`
reveal_type(user_id) # revealed: Unknown
# TODO: should be `str`
reveal_type(name) # revealed: Unknown
stmt = select(User.id).where(User.name == "Alice")
# TODO: should be `Select[tuple[int]]`
reveal_type(stmt) # revealed: Select[tuple[Unknown]]
alice_id = session.scalars(stmt).first()
# TODO: should be `int | None`
reveal_type(alice_id) # revealed: Unknown | None
alice_id = session.scalar(stmt)
# TODO: should be `int | None`
reveal_type(alice_id) # revealed: Unknown | None
```
Using the legacy `query` API also works:
```py
users_legacy = session.query(User).all()
reveal_type(users_legacy) # revealed: list[User]
query = session.query(User)
reveal_type(query) # revealed: Query[User]
reveal_type(query.all()) # revealed: list[User]
for row in query:
reveal_type(row) # revealed: User
```
And similarly when specifying particular columns:
```py
query = session.query(User.id, User.name)
# TODO: should be `RowReturningQuery[tuple[int, str]]`
reveal_type(query) # revealed: RowReturningQuery[tuple[Unknown, Unknown]]
# TODO: should be `list[Row[tuple[int, str]]]`
reveal_type(query.all()) # revealed: list[Row[tuple[Unknown, Unknown]]]
for row in query:
# TODO: should be `Row[tuple[int, str]]`
reveal_type(row) # revealed: Row[tuple[Unknown, Unknown]]
```
## Async API
The async API is supported as well:
```py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, Integer, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(Text)
async def test_async(session: AsyncSession):
stmt = select(User).where(User.name == "Alice")
alice = await session.scalar(stmt)
reveal_type(alice) # revealed: User | None
stmt = select(User.id, User.name)
result = await session.execute(stmt)
for user_id, name in result.tuples():
# TODO: should be `int`
reveal_type(user_id) # revealed: Unknown
# TODO: should be `str`
reveal_type(name) # revealed: Unknown
```

View File

@@ -1,30 +0,0 @@
# SQLModel
```toml
[environment]
python-version = "3.13"
python-platform = "linux"
[project]
dependencies = ["sqlmodel==0.0.27"]
```
## Basic model
```py
from sqlmodel import SQLModel
class User(SQLModel):
id: int
name: str
user = User(id=1, name="John Doe")
reveal_type(user.id) # revealed: int
reveal_type(user.name) # revealed: str
# TODO: this should not mention `__pydantic_self__`, and have proper parameters defined by the fields
reveal_type(User.__init__) # revealed: def __init__(__pydantic_self__, **data: Any) -> None
# TODO: this should be an error
User()
```

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