mirror of https://github.com/astral-sh/ruff
Merge remote-tracking branch 'origin/main' into dcreager/callable-return
* origin/main: (41 commits) [ty] Carry generic context through when converting class into `Callable` (#21798) [ty] Add more tests for renamings (#21810) [ty] Minor improvements to `assert_type` diagnostics (#21811) [ty] Add some attribute/method renaming test cases (#21809) Update mkdocs-material to 9.7.0 (Insiders now free) (#21797) Remove unused whitespaces in test cases (#21806) [ty] fix panic when instantiating a type variable with invalid constraints (#21663) [ty] fix build failure caused by conflicts between #21683 and #21800 (#21802) [ty] do nothing with `store_expression_type` if `inner_expression_inference_state` is `Get` (#21718) [ty] increase the limit on the number of elements in a non-recursively defined literal union (#21683) [ty] normalize typevar bounds/constraints in cycles (#21800) [ty] Update completion eval to include modules [ty] Add modules to auto-import [ty] Add support for module-only import requests [ty] Refactor auto-import symbol info [ty] Clarify the use of `SymbolKind` in auto-import [ty] Redact ranking of completions from e2e LSP tests [ty] Tweaks tests to use clearer language [ty] Update evaluation results [ty] Make auto-import ignore symbols in modules starting with a `_` ...
This commit is contained in:
commit
c0dc6cfa61
|
|
@ -75,14 +75,6 @@
|
|||
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"],
|
||||
|
|
|
|||
|
|
@ -779,8 +779,6 @@ 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,11 +786,6 @@ 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
|
||||
|
|
@ -800,11 +793,7 @@ 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
|
||||
|
|
@ -812,12 +801,8 @@ 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"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
|
||||
run: mkdocs build --strict -f mkdocs.public.yml
|
||||
run: mkdocs build --strict -f mkdocs.yml
|
||||
|
||||
check-formatter-instability-and-black-similarity:
|
||||
name: "formatter instabilities and black similarity"
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ 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:
|
||||
|
|
@ -59,23 +57,12 @@ 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"
|
||||
|
|
@ -83,13 +70,8 @@ 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"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
|
||||
run: mkdocs build --strict -f mkdocs.public.yml
|
||||
run: mkdocs build --strict -f mkdocs.yml
|
||||
|
||||
- name: "Clone docs repo"
|
||||
run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ jobs:
|
|||
environment:
|
||||
name: release
|
||||
permissions:
|
||||
id-token: write # For PyPI's trusted publishing + PEP 740 attestations
|
||||
# For PyPI's trusted publishing.
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
|
@ -27,8 +28,5 @@ 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/*
|
||||
|
|
|
|||
29
CHANGELOG.md
29
CHANGELOG.md
|
|
@ -1,5 +1,34 @@
|
|||
# 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.
|
||||
|
|
|
|||
|
|
@ -331,13 +331,6 @@ 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).
|
||||
|
|
@ -351,11 +344,7 @@ To preview any changes to the documentation locally:
|
|||
1. Run the development server with:
|
||||
|
||||
```shell
|
||||
# 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
|
||||
uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.yml
|
||||
```
|
||||
|
||||
The documentation should then be available locally at
|
||||
|
|
|
|||
|
|
@ -2859,7 +2859,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.7"
|
||||
version = "0.14.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
|
|
@ -3117,7 +3117,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.7"
|
||||
version = "0.14.8"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
|
|
@ -3473,7 +3473,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.7"
|
||||
version = "0.14.8"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
|
|
|||
|
|
@ -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.7/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.7/install.ps1 | iex"
|
||||
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"
|
||||
```
|
||||
|
||||
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.7
|
||||
rev: v0.14.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.7"
|
||||
version = "0.14.8"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -667,6 +667,13 @@ 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();
|
||||
|
|
|
|||
|
|
@ -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).resolve(import) {
|
||||
for resolved in Resolver::new(db, path).resolve(import) {
|
||||
if let Some(path) = resolved.as_system_path() {
|
||||
resolved_imports.insert(path.to_path_buf());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
use ruff_db::files::FilePath;
|
||||
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
|
||||
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 crate::ModuleDb;
|
||||
use crate::collector::CollectedImport;
|
||||
|
|
@ -7,12 +11,15 @@ 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) -> Self {
|
||||
Self { db }
|
||||
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 }
|
||||
}
|
||||
|
||||
/// Resolve the [`CollectedImport`] into a [`FilePath`].
|
||||
|
|
@ -70,13 +77,21 @@ 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 = resolve_module(self.db, module_name)?;
|
||||
let module = if let Some(file) = self.file {
|
||||
resolve_module(self.db, file, module_name)?
|
||||
} else {
|
||||
resolve_module_confident(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 = resolve_real_module(self.db, module_name)?;
|
||||
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)?
|
||||
};
|
||||
Some(module.file(self.db)?.path(self.db))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.7"
|
||||
version = "0.14.8"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -52,16 +52,16 @@ def not_broken5():
|
|||
yield inner()
|
||||
|
||||
|
||||
def not_broken6():
|
||||
def broken3():
|
||||
return (yield from [])
|
||||
|
||||
|
||||
def not_broken7():
|
||||
def broken4():
|
||||
x = yield from []
|
||||
return x
|
||||
|
||||
|
||||
def not_broken8():
|
||||
def broken5():
|
||||
x = None
|
||||
|
||||
def inner(ex):
|
||||
|
|
@ -76,3 +76,13 @@ 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]
|
||||
|
|
|
|||
24
crates/ruff_linter/resources/test/fixtures/syntax_errors/return_in_generator.py
vendored
Normal file
24
crates/ruff_linter/resources/test/fixtures/syntax_errors/return_in_generator.py
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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
|
||||
|
|
@ -69,6 +69,7 @@ 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,
|
||||
|
|
@ -729,6 +730,12 @@ 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(_)
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,7 @@ 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);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::statement_visitor;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt, StmtFunctionDef};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
|
|
@ -96,6 +95,11 @@ 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);
|
||||
|
||||
|
|
@ -112,15 +116,9 @@ struct ReturnInGeneratorVisitor {
|
|||
has_yield: bool,
|
||||
}
|
||||
|
||||
impl StatementVisitor<'_> for ReturnInGeneratorVisitor {
|
||||
impl Visitor<'_> 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.
|
||||
}
|
||||
|
|
@ -130,8 +128,19 @@ impl StatementVisitor<'_> for ReturnInGeneratorVisitor {
|
|||
node_index: _,
|
||||
}) => {
|
||||
self.return_ = Some(*range);
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
_ => statement_visitor::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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,3 +21,46 @@ 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]
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
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
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
|
|
@ -3,12 +3,13 @@
|
|||
//! 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,
|
||||
StmtImportFrom,
|
||||
StmtFunctionDef, StmtImportFrom,
|
||||
comparable::ComparableExpr,
|
||||
helpers,
|
||||
visitor::{Visitor, walk_expr},
|
||||
visitor::{Visitor, walk_expr, walk_stmt},
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
|
@ -739,7 +740,21 @@ impl SemanticSyntaxChecker {
|
|||
self.seen_futures_boundary = true;
|
||||
}
|
||||
}
|
||||
Stmt::FunctionDef(_) => {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.seen_futures_boundary = true;
|
||||
}
|
||||
_ => {
|
||||
|
|
@ -1213,6 +1228,9 @@ 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1619,6 +1637,9 @@ pub enum SemanticSyntaxErrorKind {
|
|||
|
||||
/// 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)]
|
||||
|
|
@ -1735,6 +1756,40 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.7"
|
||||
version = "0.14.8"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module_confident};
|
||||
|
||||
struct TestCase {
|
||||
db: ProjectDatabase,
|
||||
|
|
@ -232,7 +232,8 @@ impl TestCase {
|
|||
}
|
||||
|
||||
fn module<'c>(&'c self, name: &str) -> Module<'c> {
|
||||
resolve_module(self.db(), &ModuleName::new(name).unwrap()).expect("module to be present")
|
||||
resolve_module_confident(self.db(), &ModuleName::new(name).unwrap())
|
||||
.expect("module to be present")
|
||||
}
|
||||
|
||||
fn sorted_submodule_names(&self, parent_module_name: &str) -> Vec<String> {
|
||||
|
|
@ -811,7 +812,8 @@ 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(case.db(), &ModuleName::new_static("sub.a").unwrap());
|
||||
let sub_a_module =
|
||||
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap());
|
||||
|
||||
assert_eq!(sub_a_module, None);
|
||||
case.assert_indexed_project_files([bar]);
|
||||
|
|
@ -832,7 +834,9 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
|
|||
.expect("a.py to exist");
|
||||
|
||||
// `import sub.a` should now resolve
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
assert!(
|
||||
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
|
||||
);
|
||||
|
||||
case.assert_indexed_project_files([bar, init_file, a_file]);
|
||||
|
||||
|
|
@ -848,7 +852,9 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
|||
])?;
|
||||
let bar = case.system_file(case.project_path("bar.py")).unwrap();
|
||||
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
assert!(
|
||||
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
|
||||
);
|
||||
|
||||
let sub_path = case.project_path("sub");
|
||||
let init_file = case
|
||||
|
|
@ -870,7 +876,9 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
|||
case.apply_changes(changes, None);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
assert!(
|
||||
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
|
||||
);
|
||||
|
||||
assert!(!init_file.exists(case.db()));
|
||||
assert!(!a_file.exists(case.db()));
|
||||
|
|
@ -890,8 +898,12 @@ fn directory_renamed() -> anyhow::Result<()> {
|
|||
|
||||
let bar = case.system_file(case.project_path("bar.py")).unwrap();
|
||||
|
||||
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());
|
||||
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()
|
||||
);
|
||||
|
||||
let sub_path = case.project_path("sub");
|
||||
let sub_init = case
|
||||
|
|
@ -915,9 +927,13 @@ fn directory_renamed() -> anyhow::Result<()> {
|
|||
case.apply_changes(changes, None);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
assert!(
|
||||
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
|
||||
);
|
||||
// `import foo.baz` should now resolve
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some());
|
||||
assert!(
|
||||
resolve_module_confident(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some()
|
||||
);
|
||||
|
||||
// The old paths are no longer tracked
|
||||
assert!(!sub_init.exists(case.db()));
|
||||
|
|
@ -950,7 +966,9 @@ fn directory_deleted() -> anyhow::Result<()> {
|
|||
|
||||
let bar = case.system_file(case.project_path("bar.py")).unwrap();
|
||||
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
assert!(
|
||||
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
|
||||
);
|
||||
|
||||
let sub_path = case.project_path("sub");
|
||||
|
||||
|
|
@ -970,7 +988,9 @@ fn directory_deleted() -> anyhow::Result<()> {
|
|||
case.apply_changes(changes, None);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
assert!(
|
||||
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
|
||||
);
|
||||
|
||||
assert!(!init_file.exists(case.db()));
|
||||
assert!(!a_file.exists(case.db()));
|
||||
|
|
@ -999,7 +1019,7 @@ fn search_path() -> anyhow::Result<()> {
|
|||
let site_packages = case.root_path().join("site_packages");
|
||||
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), &ModuleName::new("a").unwrap()),
|
||||
resolve_module_confident(case.db(), &ModuleName::new("a").unwrap()),
|
||||
None
|
||||
);
|
||||
|
||||
|
|
@ -1009,7 +1029,7 @@ fn search_path() -> anyhow::Result<()> {
|
|||
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -1022,7 +1042,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(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
|
||||
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
|
||||
|
||||
// Register site-packages as a search path.
|
||||
case.update_options(Options {
|
||||
|
|
@ -1040,7 +1060,7 @@ fn add_search_path() -> anyhow::Result<()> {
|
|||
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1172,7 +1192,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
|||
|
||||
// Unset the custom typeshed directory.
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), &ModuleName::new("os").unwrap()),
|
||||
resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()),
|
||||
None
|
||||
);
|
||||
|
||||
|
|
@ -1187,7 +1207,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
|||
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some());
|
||||
assert!(resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()).is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1410,7 +1430,7 @@ mod unix {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
|
||||
let baz = resolve_module_confident(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();
|
||||
|
|
@ -1486,7 +1506,7 @@ mod unix {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
|
||||
let baz = resolve_module_confident(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");
|
||||
|
|
@ -1591,7 +1611,7 @@ mod unix {
|
|||
Ok(())
|
||||
})?;
|
||||
|
||||
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
|
||||
let baz = resolve_module_confident(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");
|
||||
|
|
@ -1854,11 +1874,11 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
|
|||
let mut case = setup([("lib.py", "class Foo: ...")])?;
|
||||
|
||||
assert!(
|
||||
resolve_module(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
|
||||
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
|
||||
"Expected `lib` module to exist."
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()),
|
||||
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()),
|
||||
None,
|
||||
"Expected `Lib` module not to exist"
|
||||
);
|
||||
|
|
@ -1891,13 +1911,13 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
|
|||
|
||||
// Resolving `lib` should now fail but `Lib` should now succeed
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), &ModuleName::new("lib").unwrap()),
|
||||
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()),
|
||||
None,
|
||||
"Expected `lib` module to no longer exist."
|
||||
);
|
||||
|
||||
assert!(
|
||||
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
|
||||
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
|
||||
"Expected `Lib` module to exist"
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
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,
|
||||
|
|
@ -11,9 +14,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,4
|
||||
internal-typeshed-hidden,main.py,0,2
|
||||
none-completion,main.py,0,2
|
||||
numpy-array,main.py,0,
|
||||
numpy-array,main.py,0,159
|
||||
numpy-array,main.py,1,1
|
||||
object-attr-instance-methods,main.py,0,1
|
||||
object-attr-instance-methods,main.py,1,1
|
||||
|
|
@ -23,6 +26,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,8
|
||||
ty-extensions-lower-stdlib,main.py,0,9
|
||||
type-var-typing-over-ast,main.py,0,3
|
||||
type-var-typing-over-ast,main.py,1,275
|
||||
type-var-typing-over-ast,main.py,1,251
|
||||
|
|
|
|||
|
|
|
@ -506,9 +506,21 @@ 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`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
[settings]
|
||||
auto-import = true
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
multiprocess<CURSOR: multiprocessing>
|
||||
collect<CURSOR: collections>
|
||||
collabc<CURSOR: collections.abc>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
|
|
@ -2,7 +2,10 @@ use ruff_db::files::File;
|
|||
use ty_project::Db;
|
||||
use ty_python_semantic::{Module, ModuleName, all_modules, resolve_real_shadowable_module};
|
||||
|
||||
use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
|
||||
use crate::{
|
||||
SymbolKind,
|
||||
symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only},
|
||||
};
|
||||
|
||||
/// Get all symbols matching the query string.
|
||||
///
|
||||
|
|
@ -20,7 +23,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, &typing_extensions).is_some();
|
||||
|| resolve_real_shadowable_module(db, importing_from, &typing_extensions).is_some();
|
||||
|
||||
let results = std::sync::Mutex::new(Vec::new());
|
||||
{
|
||||
|
|
@ -36,18 +39,39 @@ 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: symbol.to_owned(),
|
||||
symbol: Some(symbol.to_owned()),
|
||||
module,
|
||||
file,
|
||||
});
|
||||
|
|
@ -59,8 +83,16 @@ pub fn all_symbols<'db>(
|
|||
|
||||
let mut results = results.into_inner().unwrap();
|
||||
results.sort_by(|s1, s2| {
|
||||
let key1 = (&s1.symbol.name, s1.file.path(db).as_str());
|
||||
let key2 = (&s2.symbol.name, s2.file.path(db).as_str());
|
||||
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(),
|
||||
);
|
||||
key1.cmp(&key2)
|
||||
});
|
||||
results
|
||||
|
|
@ -71,14 +103,53 @@ pub fn all_symbols<'db>(
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AllSymbolInfo<'db> {
|
||||
/// The symbol information.
|
||||
pub symbol: SymbolInfo<'static>,
|
||||
///
|
||||
/// When absent, this implies the symbol is the module itself.
|
||||
symbol: Option<SymbolInfo<'static>>,
|
||||
/// The module containing the symbol.
|
||||
pub module: Module<'db>,
|
||||
module: Module<'db>,
|
||||
/// The file containing the symbol.
|
||||
///
|
||||
/// This `File` is guaranteed to be the same
|
||||
/// as the `File` underlying `module`.
|
||||
pub file: File,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -162,25 +233,31 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
|
|||
return "No symbols found".to_string();
|
||||
}
|
||||
|
||||
self.render_diagnostics(symbols.into_iter().map(AllSymbolDiagnostic::new))
|
||||
self.render_diagnostics(symbols.into_iter().map(|symbol_info| AllSymbolDiagnostic {
|
||||
db: &self.db,
|
||||
symbol_info,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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.symbol.kind.to_string();
|
||||
let symbol_kind_str = self.symbol_info.kind().to_string();
|
||||
|
||||
let info_text = format!("{} {}", symbol_kind_str, self.symbol_info.symbol.name);
|
||||
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 sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, info_text);
|
||||
|
||||
|
|
@ -189,9 +266,12 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
|
|||
Severity::Info,
|
||||
"AllSymbolInfo".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.symbol_info.file).with_range(self.symbol_info.symbol.name_range),
|
||||
));
|
||||
|
||||
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.sub(sub);
|
||||
|
||||
main
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ impl<'db> Completions<'db> {
|
|||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
Some(ImportEdit {
|
||||
label: format!("import {}.{}", item.module_name?, item.name),
|
||||
label: format!("import {}", item.qualified?),
|
||||
edit: item.import?,
|
||||
})
|
||||
})
|
||||
|
|
@ -160,6 +160,10 @@ 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.
|
||||
///
|
||||
|
|
@ -225,6 +229,7 @@ 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,
|
||||
|
|
@ -306,6 +311,7 @@ impl<'db> Completion<'db> {
|
|||
fn keyword(name: &str) -> Self {
|
||||
Completion {
|
||||
name: name.into(),
|
||||
qualified: None,
|
||||
insert: None,
|
||||
ty: None,
|
||||
kind: Some(CompletionKind::Keyword),
|
||||
|
|
@ -321,6 +327,7 @@ 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),
|
||||
|
|
@ -537,12 +544,22 @@ 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.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
|
||||
{
|
||||
if symbol.file() == file || symbol.module().is_known(db, KnownModule::Builtins) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let request = create_import_request(symbol.module.name(db), &symbol.symbol.name);
|
||||
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))
|
||||
});
|
||||
// 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
|
||||
|
|
@ -551,11 +568,12 @@ 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(&symbol.symbol.name),
|
||||
name: ast::name::Name::new(name),
|
||||
qualified: Some(ast::name::Name::new(qualified)),
|
||||
insert: Some(import_action.symbol_text().into()),
|
||||
ty: None,
|
||||
kind: symbol.symbol.kind.to_completion_kind(),
|
||||
module_name: Some(symbol.module.name(db)),
|
||||
kind: symbol.kind().to_completion_kind(),
|
||||
module_name: Some(module_name),
|
||||
import: import_action.import().cloned(),
|
||||
builtin: false,
|
||||
// TODO: `is_type_check_only` requires inferring the type of the symbol
|
||||
|
|
@ -4350,7 +4368,7 @@ from os.<CURSOR>
|
|||
.build()
|
||||
.snapshot();
|
||||
assert_snapshot!(snapshot, @r"
|
||||
Kadabra :: Literal[1] :: Current module
|
||||
Kadabra :: Literal[1] :: <no import required>
|
||||
AbraKadabra :: Unavailable :: package
|
||||
");
|
||||
}
|
||||
|
|
@ -5534,7 +5552,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] :: Current module
|
||||
long_nameb :: Literal[1] :: <no import required>
|
||||
long_namea :: Unavailable :: foo
|
||||
");
|
||||
}
|
||||
|
|
@ -5804,7 +5822,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 :: Current module");
|
||||
assert_snapshot!(builder.build().snapshot(), @"typing :: <no import required>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -5812,13 +5830,7 @@ from .imp<CURSOR>
|
|||
let builder = completion_test_builder("deprecated<CURSOR>")
|
||||
.auto_import()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
Deprecated :: importlib.metadata
|
||||
DeprecatedList :: importlib.metadata
|
||||
DeprecatedNonAbstract :: importlib.metadata
|
||||
DeprecatedTuple :: importlib.metadata
|
||||
deprecated :: warnings
|
||||
");
|
||||
assert_snapshot!(builder.build().snapshot(), @"deprecated :: warnings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -5829,8 +5841,8 @@ from .imp<CURSOR>
|
|||
.completion_test_builder()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
typing :: Current module
|
||||
typing_extensions :: Current module
|
||||
typing :: <no import required>
|
||||
typing_extensions :: <no import required>
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -5843,10 +5855,6 @@ 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
|
||||
");
|
||||
|
|
@ -5859,8 +5867,8 @@ from .imp<CURSOR>
|
|||
.completion_test_builder()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
typing :: Current module
|
||||
typing_extensions :: Current module
|
||||
typing :: <no import required>
|
||||
typing_extensions :: <no import required>
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -5872,15 +5880,284 @@ 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.
|
||||
///
|
||||
|
|
@ -6055,7 +6332,7 @@ from .imp<CURSOR>
|
|||
let module_name = c
|
||||
.module_name
|
||||
.map(ModuleName::as_str)
|
||||
.unwrap_or("Current module");
|
||||
.unwrap_or("<no import required>");
|
||||
snapshot = format!("{snapshot} :: {module_name}");
|
||||
}
|
||||
snapshot
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ calc = Calculator()
|
|||
"
|
||||
def test():
|
||||
# Cursor on a position with no symbol
|
||||
<CURSOR>
|
||||
<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -898,6 +898,42 @@ 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(
|
||||
|
|
@ -1870,4 +1906,211 @@ 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
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 +1073,41 @@ 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(
|
||||
|
|
@ -1220,12 +1255,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
|
||||
"#,
|
||||
);
|
||||
|
|
@ -1260,12 +1295,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
|
||||
"#,
|
||||
);
|
||||
|
|
@ -1601,7 +1636,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):
|
||||
|
|
@ -1640,7 +1675,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):
|
||||
|
|
@ -1678,7 +1713,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):
|
||||
|
|
@ -1716,7 +1751,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):
|
||||
|
|
@ -1884,7 +1919,7 @@ def function():
|
|||
class C:
|
||||
def __init__(self):
|
||||
self._value = 0
|
||||
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
|
@ -1994,7 +2029,7 @@ def function():
|
|||
r#"
|
||||
class MyClass:
|
||||
ClassType = int
|
||||
|
||||
|
||||
def generic_method[T](self, value: Class<CURSOR>Type) -> T:
|
||||
return value
|
||||
"#,
|
||||
|
|
@ -2567,6 +2602,298 @@ 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
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_declaration(&self) -> String {
|
||||
let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset)
|
||||
|
|
|
|||
|
|
@ -145,14 +145,14 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/typing.pyi:770:1
|
||||
--> stdlib/typing.pyi:781:1
|
||||
|
|
||||
768 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...
|
||||
769 |
|
||||
770 | Generic: type[_Generic]
|
||||
779 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...
|
||||
780 |
|
||||
781 | Generic: type[_Generic]
|
||||
| ^^^^^^^
|
||||
771 |
|
||||
772 | class _ProtocolMeta(ABCMeta):
|
||||
782 |
|
||||
783 | class _ProtocolMeta(ABCMeta):
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:1
|
||||
|
|
@ -964,6 +964,60 @@ 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(
|
||||
|
|
@ -1057,7 +1111,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):
|
||||
|
|
@ -1077,7 +1131,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):
|
||||
|
|
@ -1097,7 +1151,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):
|
||||
|
|
@ -1135,7 +1189,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):
|
||||
|
|
@ -1344,12 +1398,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
|
||||
"#,
|
||||
);
|
||||
|
|
@ -1384,12 +1438,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
|
||||
"#,
|
||||
);
|
||||
|
|
@ -1618,6 +1672,283 @@ 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) =
|
||||
|
|
|
|||
|
|
@ -1089,6 +1089,60 @@ 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()
|
||||
|
|
@ -1654,12 +1708,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
|
||||
"#,
|
||||
);
|
||||
|
|
@ -1693,12 +1747,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
|
||||
"#,
|
||||
);
|
||||
|
|
@ -1906,7 +1960,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):
|
||||
|
|
@ -1926,7 +1980,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):
|
||||
|
|
@ -1964,7 +2018,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):
|
||||
|
|
@ -2003,7 +2057,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):
|
||||
|
|
@ -3267,6 +3321,297 @@ 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;
|
||||
|
|
|
|||
|
|
@ -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.into();
|
||||
let mut symbol_text: Box<str> = request.member.unwrap_or(request.module).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,14 +157,27 @@ 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 matches!(request.style, ImportStyle::Import) {
|
||||
symbol_text = format!("{}.{}", request.module, request.member).into();
|
||||
if let Some(member) = request.member
|
||||
&& matches!(request.style, ImportStyle::Import)
|
||||
{
|
||||
symbol_text = format!("{}.{}", request.module, 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();
|
||||
|
|
@ -189,13 +202,10 @@ impl<'a> Importer<'a> {
|
|||
let import = if let Some(insertion) =
|
||||
Insertion::existing_import(response.import.stmt, self.tokens)
|
||||
{
|
||||
insertion.into_edit(request.member)
|
||||
insertion.into_edit(member)
|
||||
} else {
|
||||
Insertion::end_of_statement(response.import.stmt, self.source, self.stylist)
|
||||
.into_edit(&format!(
|
||||
"from {} import {}",
|
||||
request.module, request.member
|
||||
))
|
||||
.into_edit(&format!("from {} import {member}", request.module))
|
||||
};
|
||||
ImportAction {
|
||||
import: Some(import),
|
||||
|
|
@ -481,6 +491,17 @@ 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;
|
||||
}
|
||||
|
|
@ -492,9 +513,7 @@ impl<'ast> AstImportKind<'ast> {
|
|||
let kind = ast
|
||||
.names
|
||||
.iter()
|
||||
.find(|alias| {
|
||||
alias.name.as_str() == "*" || alias.name.as_str() == request.member
|
||||
})
|
||||
.find(|alias| alias.name.as_str() == "*" || alias.name.as_str() == member)
|
||||
.map(|alias| ImportResponseKind::Unqualified { ast, alias })
|
||||
.unwrap_or_else(|| ImportResponseKind::Partial(ast));
|
||||
Some(kind)
|
||||
|
|
@ -510,7 +529,10 @@ 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`).
|
||||
member: &'a str,
|
||||
///
|
||||
/// When `member` is absent, then this request reflects an import
|
||||
/// of the module itself. i.e., `import module`.
|
||||
member: Option<&'a str>,
|
||||
/// The preferred style to use when importing the symbol (e.g.,
|
||||
/// `import foo` or `from foo import bar`).
|
||||
///
|
||||
|
|
@ -532,7 +554,7 @@ impl<'a> ImportRequest<'a> {
|
|||
pub(crate) fn import(module: &'a str, member: &'a str) -> Self {
|
||||
Self {
|
||||
module,
|
||||
member,
|
||||
member: Some(member),
|
||||
style: ImportStyle::Import,
|
||||
force_style: false,
|
||||
}
|
||||
|
|
@ -545,12 +567,26 @@ impl<'a> ImportRequest<'a> {
|
|||
pub(crate) fn import_from(module: &'a str, member: &'a str) -> Self {
|
||||
Self {
|
||||
module,
|
||||
member,
|
||||
member: Some(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.
|
||||
|
|
@ -565,7 +601,13 @@ 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 {
|
||||
match (members.map.get(self.module), members.map.get(self.member)) {
|
||||
let Some(member) = self.member else {
|
||||
return Self {
|
||||
style: ImportStyle::Import,
|
||||
..self
|
||||
};
|
||||
};
|
||||
match (members.map.get(self.module), members.map.get(member)) {
|
||||
// Neither symbol exists, so we can just proceed as
|
||||
// normal.
|
||||
(None, None) => self,
|
||||
|
|
@ -630,7 +672,10 @@ 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 => write!(f, "from {} import {}", self.module, self.member),
|
||||
ImportStyle::ImportFrom => match self.member {
|
||||
None => write!(f, "import {}", self.module),
|
||||
Some(member) => write!(f, "from {} import {member}", self.module),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -843,6 +888,10 @@ 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(),
|
||||
|
|
@ -2156,4 +2205,73 @@ 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
|
||||
");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2012,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):
|
||||
|
|
@ -6428,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[: <typing.Literal special form>] = Literal['a', 'b', 'c']
|
||||
");
|
||||
a[: <special form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
"#);
|
||||
}
|
||||
|
||||
struct InlayHintLocationDiagnostic {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ mod tests {
|
|||
}
|
||||
|
||||
fn rename(&self, new_name: &str) -> String {
|
||||
let Some(_) = can_rename(&self.db, self.cursor.file, self.cursor.offset) else {
|
||||
return "Cannot rename".to_string();
|
||||
};
|
||||
|
||||
let Some(rename_results) =
|
||||
rename(&self.db, self.cursor.file, self.cursor.offset, new_name)
|
||||
else {
|
||||
|
|
@ -1182,6 +1186,7 @@ result = func(10, y=20)
|
|||
");
|
||||
}
|
||||
|
||||
// TODO Should rename the alias
|
||||
#[test]
|
||||
fn import_alias() {
|
||||
let test = CursorTest::builder()
|
||||
|
|
@ -1197,21 +1202,10 @@ result = func(10, y=20)
|
|||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("z"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:3:20
|
||||
|
|
||||
2 | import warnings
|
||||
3 | import warnings as abc
|
||||
| ^^^
|
||||
4 |
|
||||
5 | x = abc
|
||||
| ---
|
||||
6 | y = warnings
|
||||
|
|
||||
");
|
||||
assert_snapshot!(test.rename("z"), @"Cannot rename");
|
||||
}
|
||||
|
||||
// TODO Should rename the alias
|
||||
#[test]
|
||||
fn import_alias_use() {
|
||||
let test = CursorTest::builder()
|
||||
|
|
@ -1227,17 +1221,816 @@ result = func(10, y=20)
|
|||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("z"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:3:20
|
||||
assert_snapshot!(test.rename("z"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_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): we should refuse to rename this (it's the name of a module)
|
||||
assert_snapshot!(test.rename("mypkg"), @r"
|
||||
info[rename]: Rename symbol (found 1 locations)
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | import warnings
|
||||
3 | import warnings as abc
|
||||
| ^^^
|
||||
4 |
|
||||
5 | x = abc
|
||||
| ---
|
||||
6 | y = warnings
|
||||
2 | from .subpkg.submod import val
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_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();
|
||||
|
||||
// Refusing to rename is correct
|
||||
assert_snapshot!(test.rename("mypkg"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_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();
|
||||
|
||||
// Refusing to rename is good/fine here, it's an undefined reference
|
||||
assert_snapshot!(test.rename("mypkg"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_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();
|
||||
|
||||
// Refusing to rename is good here, it's a module name
|
||||
assert_snapshot!(test.rename("mypkg"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_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();
|
||||
|
||||
// Refusing to rename is good here, it's the name of a module
|
||||
assert_snapshot!(test.rename("mypkg"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_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();
|
||||
|
||||
// Renaming the integer is correct
|
||||
assert_snapshot!(test.rename("mypkg"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> mypackage/__init__.py:2:21
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ------
|
||||
|
|
||||
::: mypackage/subpkg/__init__.py:2:1
|
||||
|
|
||||
2 | subpkg: int = 10
|
||||
| ------
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_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): this is incorrect, we should rename the `subpkg` int
|
||||
// and the RHS of the import statement (but *not* rename the LHS).
|
||||
//
|
||||
// However us being cautious here *would* be good as the rename will actually
|
||||
// result in a `subpkg` variable still existing in this code, as the import's LHS
|
||||
// `DefinitionKind::ImportFromSubmodule` would stop being overwritten by the RHS!
|
||||
assert_snapshot!(test.rename("mypkg"), @r"
|
||||
info[rename]: Rename symbol (found 1 locations)
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
// TODO: This should rename all overloads
|
||||
#[test]
|
||||
fn rename_overloaded_function() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"lib1.py",
|
||||
r#"
|
||||
from typing import overload, Any
|
||||
|
||||
@overload
|
||||
def test<CURSOR>() -> None: ...
|
||||
@overload
|
||||
def test(a: str) -> str: ...
|
||||
@overload
|
||||
def test(a: int) -> int: ...
|
||||
|
||||
def test(a: Any) -> Any:
|
||||
return a
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from lib2 import test
|
||||
|
||||
test("test")
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r"
|
||||
info[rename]: Rename symbol (found 1 locations)
|
||||
--> lib1.py:5:5
|
||||
|
|
||||
4 | @overload
|
||||
5 | def test() -> None: ...
|
||||
| ^^^^
|
||||
6 | @overload
|
||||
7 | def test(a: str) -> str: ...
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_property() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"lib.py",
|
||||
r#"
|
||||
class Foo:
|
||||
@property
|
||||
def my_property<CURSOR>(self) -> int:
|
||||
return 42
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from lib import Foo
|
||||
|
||||
print(Foo().my_property)
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> lib.py:4:9
|
||||
|
|
||||
2 | class Foo:
|
||||
3 | @property
|
||||
4 | def my_property(self) -> int:
|
||||
| ^^^^^^^^^^^
|
||||
5 | return 42
|
||||
|
|
||||
::: main.py:4:13
|
||||
|
|
||||
2 | from lib import Foo
|
||||
3 |
|
||||
4 | print(Foo().my_property)
|
||||
| -----------
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
// TODO: this should rename the name of the function decorated with
|
||||
// `@my_property.setter` as well as the getter function name
|
||||
#[test]
|
||||
fn rename_property_with_setter() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"lib.py",
|
||||
r#"
|
||||
class Foo:
|
||||
@property
|
||||
def my_property<CURSOR>(self) -> int:
|
||||
return 42
|
||||
|
||||
@my_property.setter
|
||||
def my_property(self, value: int) -> None:
|
||||
pass
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from lib import Foo
|
||||
|
||||
print(Foo().my_property)
|
||||
Foo().my_property = 56
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r"
|
||||
info[rename]: Rename symbol (found 4 locations)
|
||||
--> lib.py:4:9
|
||||
|
|
||||
2 | class Foo:
|
||||
3 | @property
|
||||
4 | def my_property(self) -> int:
|
||||
| ^^^^^^^^^^^
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | @my_property.setter
|
||||
| -----------
|
||||
8 | def my_property(self, value: int) -> None:
|
||||
9 | pass
|
||||
|
|
||||
::: main.py:4:13
|
||||
|
|
||||
2 | from lib import Foo
|
||||
3 |
|
||||
4 | print(Foo().my_property)
|
||||
| -----------
|
||||
5 | Foo().my_property = 56
|
||||
| -----------
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
// TODO: this should rename the name of the function decorated with
|
||||
// `@my_property.deleter` as well as the getter function name
|
||||
#[test]
|
||||
fn rename_property_with_deleter() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"lib.py",
|
||||
r#"
|
||||
class Foo:
|
||||
@property
|
||||
def my_property<CURSOR>(self) -> int:
|
||||
return 42
|
||||
|
||||
@my_property.deleter
|
||||
def my_property(self) -> None:
|
||||
pass
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from lib import Foo
|
||||
|
||||
print(Foo().my_property)
|
||||
del Foo().my_property
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r"
|
||||
info[rename]: Rename symbol (found 4 locations)
|
||||
--> lib.py:4:9
|
||||
|
|
||||
2 | class Foo:
|
||||
3 | @property
|
||||
4 | def my_property(self) -> int:
|
||||
| ^^^^^^^^^^^
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | @my_property.deleter
|
||||
| -----------
|
||||
8 | def my_property(self) -> None:
|
||||
9 | pass
|
||||
|
|
||||
::: main.py:4:13
|
||||
|
|
||||
2 | from lib import Foo
|
||||
3 |
|
||||
4 | print(Foo().my_property)
|
||||
| -----------
|
||||
5 | del Foo().my_property
|
||||
| -----------
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
// TODO: this should rename the name of the functions decorated with
|
||||
// `@my_property.deleter` and `@my_property.deleter` as well as the
|
||||
// getter function name
|
||||
#[test]
|
||||
fn rename_property_with_setter_and_deleter() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"lib.py",
|
||||
r#"
|
||||
class Foo:
|
||||
@property
|
||||
def my_property<CURSOR>(self) -> int:
|
||||
return 42
|
||||
|
||||
@my_property.setter
|
||||
def my_property(self, value: int) -> None:
|
||||
pass
|
||||
|
||||
@my_property.deleter
|
||||
def my_property(self) -> None:
|
||||
pass
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from lib import Foo
|
||||
|
||||
print(Foo().my_property)
|
||||
Foo().my_property = 56
|
||||
del Foo().my_property
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r"
|
||||
info[rename]: Rename symbol (found 6 locations)
|
||||
--> lib.py:4:9
|
||||
|
|
||||
2 | class Foo:
|
||||
3 | @property
|
||||
4 | def my_property(self) -> int:
|
||||
| ^^^^^^^^^^^
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | @my_property.setter
|
||||
| -----------
|
||||
8 | def my_property(self, value: int) -> None:
|
||||
9 | pass
|
||||
10 |
|
||||
11 | @my_property.deleter
|
||||
| -----------
|
||||
12 | def my_property(self) -> None:
|
||||
13 | pass
|
||||
|
|
||||
::: main.py:4:13
|
||||
|
|
||||
2 | from lib import Foo
|
||||
3 |
|
||||
4 | print(Foo().my_property)
|
||||
| -----------
|
||||
5 | Foo().my_property = 56
|
||||
| -----------
|
||||
6 | del Foo().my_property
|
||||
| -----------
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_single_dispatch_function() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"foo.py",
|
||||
r#"
|
||||
from functools import singledispatch
|
||||
|
||||
@singledispatch
|
||||
def f<CURSOR>(x: object):
|
||||
raise NotImplementedError
|
||||
|
||||
@f.register
|
||||
def _(x: int) -> str:
|
||||
return "int"
|
||||
|
||||
@f.register
|
||||
def _(x: str) -> int:
|
||||
return int(x)
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r#"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> foo.py:5:5
|
||||
|
|
||||
4 | @singledispatch
|
||||
5 | def f(x: object):
|
||||
| ^
|
||||
6 | raise NotImplementedError
|
||||
7 |
|
||||
8 | @f.register
|
||||
| -
|
||||
9 | def _(x: int) -> str:
|
||||
10 | return "int"
|
||||
11 |
|
||||
12 | @f.register
|
||||
| -
|
||||
13 | def _(x: str) -> int:
|
||||
14 | return int(x)
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_single_dispatch_function_stacked_register() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"foo.py",
|
||||
r#"
|
||||
from functools import singledispatch
|
||||
|
||||
@singledispatch
|
||||
def f<CURSOR>(x):
|
||||
raise NotImplementedError
|
||||
|
||||
@f.register(int)
|
||||
@f.register(float)
|
||||
def _(x) -> float:
|
||||
return "int"
|
||||
|
||||
@f.register(str)
|
||||
def _(x) -> int:
|
||||
return int(x)
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r#"
|
||||
info[rename]: Rename symbol (found 4 locations)
|
||||
--> foo.py:5:5
|
||||
|
|
||||
4 | @singledispatch
|
||||
5 | def f(x):
|
||||
| ^
|
||||
6 | raise NotImplementedError
|
||||
7 |
|
||||
8 | @f.register(int)
|
||||
| -
|
||||
9 | @f.register(float)
|
||||
| -
|
||||
10 | def _(x) -> float:
|
||||
11 | return "int"
|
||||
12 |
|
||||
13 | @f.register(str)
|
||||
| -
|
||||
14 | def _(x) -> int:
|
||||
15 | return int(x)
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_single_dispatchmethod() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"foo.py",
|
||||
r#"
|
||||
from functools import singledispatchmethod
|
||||
|
||||
class Foo:
|
||||
@singledispatchmethod
|
||||
def f<CURSOR>(self, x: object):
|
||||
raise NotImplementedError
|
||||
|
||||
@f.register
|
||||
def _(self, x: str) -> float:
|
||||
return "int"
|
||||
|
||||
@f.register
|
||||
def _(self, x: str) -> int:
|
||||
return int(x)
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r#"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> foo.py:6:9
|
||||
|
|
||||
4 | class Foo:
|
||||
5 | @singledispatchmethod
|
||||
6 | def f(self, x: object):
|
||||
| ^
|
||||
7 | raise NotImplementedError
|
||||
8 |
|
||||
9 | @f.register
|
||||
| -
|
||||
10 | def _(self, x: str) -> float:
|
||||
11 | return "int"
|
||||
12 |
|
||||
13 | @f.register
|
||||
| -
|
||||
14 | def _(self, x: str) -> int:
|
||||
15 | return int(x)
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_single_dispatchmethod_staticmethod() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"foo.py",
|
||||
r#"
|
||||
from functools import singledispatchmethod
|
||||
|
||||
class Foo:
|
||||
@singledispatchmethod
|
||||
@staticmethod
|
||||
def f<CURSOR>(self, x):
|
||||
raise NotImplementedError
|
||||
|
||||
@f.register(str)
|
||||
@staticmethod
|
||||
def _(x: int) -> str:
|
||||
return "int"
|
||||
|
||||
@f.register
|
||||
@staticmethod
|
||||
def _(x: str) -> int:
|
||||
return int(x)
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r#"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> foo.py:7:9
|
||||
|
|
||||
5 | @singledispatchmethod
|
||||
6 | @staticmethod
|
||||
7 | def f(self, x):
|
||||
| ^
|
||||
8 | raise NotImplementedError
|
||||
9 |
|
||||
10 | @f.register(str)
|
||||
| -
|
||||
11 | @staticmethod
|
||||
12 | def _(x: int) -> str:
|
||||
13 | return "int"
|
||||
14 |
|
||||
15 | @f.register
|
||||
| -
|
||||
16 | @staticmethod
|
||||
17 | def _(x: str) -> int:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_single_dispatchmethod_classmethod() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"foo.py",
|
||||
r#"
|
||||
from functools import singledispatchmethod
|
||||
|
||||
class Foo:
|
||||
@singledispatchmethod
|
||||
@classmethod
|
||||
def f<CURSOR>(cls, x):
|
||||
raise NotImplementedError
|
||||
|
||||
@f.register(str)
|
||||
@classmethod
|
||||
def _(cls, x) -> str:
|
||||
return "int"
|
||||
|
||||
@f.register(int)
|
||||
@f.register(float)
|
||||
@staticmethod
|
||||
def _(cls, x) -> int:
|
||||
return int(x)
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r#"
|
||||
info[rename]: Rename symbol (found 4 locations)
|
||||
--> foo.py:7:9
|
||||
|
|
||||
5 | @singledispatchmethod
|
||||
6 | @classmethod
|
||||
7 | def f(cls, x):
|
||||
| ^
|
||||
8 | raise NotImplementedError
|
||||
9 |
|
||||
10 | @f.register(str)
|
||||
| -
|
||||
11 | @classmethod
|
||||
12 | def _(cls, x) -> str:
|
||||
13 | return "int"
|
||||
14 |
|
||||
15 | @f.register(int)
|
||||
| -
|
||||
16 | @f.register(float)
|
||||
| -
|
||||
17 | @staticmethod
|
||||
18 | def _(cls, x) -> int:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_attribute() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"foo.py",
|
||||
r#"
|
||||
class Test:
|
||||
attribute<CURSOR>: str
|
||||
|
||||
def __init__(self, value: str):
|
||||
self.attribute = value
|
||||
|
||||
class Child(Test):
|
||||
def test(self):
|
||||
return self.attribute
|
||||
|
||||
|
||||
c = Child("test")
|
||||
|
||||
print(c.attribute)
|
||||
c.attribute = "new_value"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r#"
|
||||
info[rename]: Rename symbol (found 5 locations)
|
||||
--> foo.py:3:5
|
||||
|
|
||||
2 | class Test:
|
||||
3 | attribute: str
|
||||
| ^^^^^^^^^
|
||||
4 |
|
||||
5 | def __init__(self, value: str):
|
||||
6 | self.attribute = value
|
||||
| ---------
|
||||
7 |
|
||||
8 | class Child(Test):
|
||||
9 | def test(self):
|
||||
10 | return self.attribute
|
||||
| ---------
|
||||
|
|
||||
::: foo.py:15:9
|
||||
|
|
||||
13 | c = Child("test")
|
||||
14 |
|
||||
15 | print(c.attribute)
|
||||
| ---------
|
||||
16 | c.attribute = "new_value"
|
||||
| ---------
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
// TODO: This should rename all attribute usages
|
||||
// Note: Pylance only renames the assignment in `__init__`.
|
||||
#[test]
|
||||
fn rename_implicit_attribute() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
class Test:
|
||||
def __init__(self, value: str):
|
||||
self.<CURSOR>attribute = value
|
||||
|
||||
class Child(Test):
|
||||
def __init__(self, value: str):
|
||||
super().__init__(value)
|
||||
self.attribute = value + "child"
|
||||
|
||||
def test(self):
|
||||
return self.attribute
|
||||
|
||||
|
||||
c = Child("test")
|
||||
|
||||
print(c.attribute)
|
||||
c.attribute = "new_value"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("better_name"), @r"
|
||||
info[rename]: Rename symbol (found 1 locations)
|
||||
--> main.py:4:14
|
||||
|
|
||||
2 | class Test:
|
||||
3 | def __init__(self, value: str):
|
||||
4 | self.attribute = value
|
||||
| ^^^^^^^^^
|
||||
5 |
|
||||
6 | class Child(Test):
|
||||
|
|
||||
");
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,14 @@
|
|||
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]): ...
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# 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]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
class C[T: (A, B)]:
|
||||
def f(foo: T):
|
||||
try:
|
||||
pass
|
||||
except foo:
|
||||
pass
|
||||
|
|
@ -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 `typing.Any` is not callable"
|
||||
Any() # error: [call-non-callable] "Object of type `<special form '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 `typing.Any` is not callable"
|
||||
class F(metaclass=Any): ... # error: [invalid-metaclass] "Metaclass type `<special form 'typing.Any'>` is not callable"
|
||||
```
|
||||
|
||||
And `Any` cannot be used in `isinstance()` checks:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ python-version = "3.11"
|
|||
```py
|
||||
from typing import Never
|
||||
|
||||
reveal_type(Never) # revealed: typing.Never
|
||||
reveal_type(Never) # revealed: <special form 'typing.Never'>
|
||||
```
|
||||
|
||||
### Python 3.10
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ python-version = "3.10"
|
|||
class A: ...
|
||||
class B: ...
|
||||
|
||||
reveal_type(A | B) # revealed: types.UnionType
|
||||
reveal_type(A | B) # revealed: <types.UnionType special form 'A | B'>
|
||||
```
|
||||
|
||||
## 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
|
||||
reveal_type(sub_a | B) # revealed: types.UnionType
|
||||
reveal_type(sub_a | sub_b) # revealed: types.UnionType
|
||||
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>
|
||||
|
||||
class C[T]: ...
|
||||
class D[T]: ...
|
||||
|
||||
reveal_type(C | D) # revealed: types.UnionType
|
||||
reveal_type(C | D) # revealed: <types.UnionType special form 'C[Unknown] | D[Unknown]'>
|
||||
|
||||
reveal_type(C[int] | D[str]) # revealed: types.UnionType
|
||||
reveal_type(C[int] | D[str]) # revealed: <types.UnionType special form 'C[int] | D[str]'>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -227,17 +227,22 @@ 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_UNION_LITERALS limit (currently 200):
|
||||
literals_256 = 16 * literals_16 + literals_16
|
||||
reveal_type(literals_256) # revealed: int
|
||||
# Going beyond the MAX_UNION_LITERALS limit (currently 512):
|
||||
literals_512 = 2 * literals_256 + literals_2 # Literal[0, 1, .., 511]
|
||||
reveal_type(literals_512 if flag else 512) # 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:
|
||||
reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int
|
||||
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
|
||||
```
|
||||
|
||||
## Simplifying gradually-equivalent types
|
||||
|
|
|
|||
|
|
@ -603,12 +603,14 @@ 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] "`typing.ChainMap` is not a valid class"
|
||||
# error: [invalid-super-argument] "`<special form '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()`.
|
||||
reveal_type(super(typing.Generic, typing.SupportsInt)) # revealed: <super: typing.Generic, <class 'SupportsInt'>>
|
||||
#
|
||||
# revealed: <super: <special form 'typing.Generic'>, <class 'SupportsInt'>>
|
||||
reveal_type(super(typing.Generic, typing.SupportsInt))
|
||||
|
||||
def _(x: type[typing.Any], y: typing.Any):
|
||||
reveal_type(super(x, y)) # revealed: <super: Any, Any>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# 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]
|
||||
```
|
||||
|
|
@ -7,10 +7,11 @@
|
|||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(x: int):
|
||||
def _(x: int, y: bool):
|
||||
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
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class Foo(Protocol):
|
|||
def f[T](self, v: T) -> T: ...
|
||||
|
||||
t = (Protocol, int)
|
||||
reveal_type(t[0]) # revealed: typing.Protocol
|
||||
reveal_type(t[0]) # revealed: <special form 'typing.Protocol'>
|
||||
|
||||
class Lorem(t[0]):
|
||||
def f(self) -> int: ...
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@ consistent with each other.
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
|
@ -308,6 +309,11 @@ class C(Generic[T]):
|
|||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
|
|
@ -318,12 +324,18 @@ wrong_innards: C[int] = C("five")
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
|
|
@ -334,6 +346,7 @@ wrong_innards: C[int] = C("five")
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
|
@ -343,6 +356,11 @@ class C(Generic[T]):
|
|||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
|
|
@ -353,6 +371,7 @@ wrong_innards: C[int] = C("five")
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
|
@ -362,6 +381,11 @@ class C(Generic[T]):
|
|||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
|
|
@ -373,6 +397,11 @@ class D(Generic[T]):
|
|||
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D(1)) # revealed: D[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `D[int | str]` is not assignable to `D[int]`"
|
||||
|
|
@ -386,6 +415,7 @@ to specialize the class.
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
|
@ -398,6 +428,11 @@ class C(Generic[T, U]):
|
|||
class D(C[V, int]):
|
||||
def __init__(self, x: V) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[V@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[V@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D(1)) # revealed: D[int]
|
||||
```
|
||||
|
||||
|
|
@ -405,6 +440,7 @@ reveal_type(D(1)) # revealed: D[int]
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
|
@ -415,6 +451,11 @@ class C(Generic[T, U]):
|
|||
class D(C[T, U]):
|
||||
pass
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(C(1, "str")) # revealed: C[int, str]
|
||||
reveal_type(D(1, "str")) # revealed: D[int, str]
|
||||
```
|
||||
|
|
@ -425,6 +466,7 @@ This is a specific example of the above, since it was reported specifically by a
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
|
@ -432,6 +474,11 @@ U = TypeVar("U")
|
|||
class D(dict[T, U]):
|
||||
pass
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D(key=1)) # revealed: D[str, int]
|
||||
```
|
||||
|
||||
|
|
@ -443,12 +490,18 @@ context. But from the user's point of view, this is another example of the above
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
class C(tuple[T, U]): ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C, U@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C, U@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C((1, 2))) # revealed: C[int, int]
|
||||
```
|
||||
|
||||
|
|
@ -480,6 +533,7 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
S = TypeVar("S")
|
||||
T = TypeVar("T")
|
||||
|
|
@ -487,6 +541,11 @@ T = TypeVar("T")
|
|||
class C(Generic[T]):
|
||||
def __init__(self, x: T, y: S) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C, S@__init__]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1, 1)) # revealed: C[int]
|
||||
reveal_type(C(1, "string")) # revealed: C[int]
|
||||
reveal_type(C(1, True)) # revealed: C[int]
|
||||
|
|
@ -499,6 +558,7 @@ wrong_innards: C[int] = C("five", 1)
|
|||
|
||||
```py
|
||||
from typing_extensions import overload, Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
|
@ -514,6 +574,11 @@ class C(Generic[T]):
|
|||
def __init__(self, x: int) -> None: ...
|
||||
def __init__(self, x: str | bytes | int) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C("string")) # revealed: C[str]
|
||||
reveal_type(C(b"bytes")) # revealed: C[bytes]
|
||||
reveal_type(C(12)) # revealed: C[Unknown]
|
||||
|
|
@ -541,6 +606,11 @@ class D(Generic[T, U]):
|
|||
def __init__(self, t: T, u: U) -> None: ...
|
||||
def __init__(self, *args) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D("string")) # revealed: D[str, str]
|
||||
reveal_type(D(1)) # revealed: D[str, int]
|
||||
reveal_type(D(1, "string")) # revealed: D[int, str]
|
||||
|
|
@ -551,6 +621,7 @@ reveal_type(D(1, "string")) # revealed: D[int, str]
|
|||
```py
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
|
@ -558,6 +629,11 @@ T = TypeVar("T")
|
|||
class A(Generic[T]):
|
||||
x: T
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@A]
|
||||
reveal_type(generic_context(A))
|
||||
# revealed: ty_extensions.GenericContext[T@A]
|
||||
reveal_type(generic_context(into_callable(A)))
|
||||
|
||||
reveal_type(A(x=1)) # revealed: A[int]
|
||||
```
|
||||
|
||||
|
|
@ -565,17 +641,28 @@ reveal_type(A(x=1)) # revealed: A[int]
|
|||
|
||||
```py
|
||||
from typing_extensions import Generic, TypeVar
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U", default=T)
|
||||
|
||||
class C(Generic[T, U]): ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C, U@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C, U@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C()) # revealed: C[Unknown, Unknown]
|
||||
|
||||
class D(Generic[T, U]):
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D()) # revealed: D[Unknown, Unknown]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ The specialization must match the generic types:
|
|||
|
||||
```py
|
||||
# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2"
|
||||
reveal_type(C[int, int]) # revealed: C[Unknown]
|
||||
reveal_type(C[int, int]) # revealed: <type alias 'C[Unknown]'>
|
||||
```
|
||||
|
||||
And non-generic types cannot be specialized:
|
||||
|
|
@ -85,19 +85,19 @@ type BoundedByUnion[T: int | str] = ...
|
|||
|
||||
class IntSubclass(int): ...
|
||||
|
||||
reveal_type(Bounded[int]) # revealed: Bounded[int]
|
||||
reveal_type(Bounded[IntSubclass]) # revealed: Bounded[IntSubclass]
|
||||
reveal_type(Bounded[int]) # revealed: <type alias 'Bounded[int]'>
|
||||
reveal_type(Bounded[IntSubclass]) # revealed: <type alias 'Bounded[IntSubclass]'>
|
||||
|
||||
# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `T@Bounded`"
|
||||
reveal_type(Bounded[str]) # revealed: Bounded[Unknown]
|
||||
reveal_type(Bounded[str]) # revealed: <type alias 'Bounded[Unknown]'>
|
||||
|
||||
# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `T@Bounded`"
|
||||
reveal_type(Bounded[int | str]) # revealed: Bounded[Unknown]
|
||||
reveal_type(Bounded[int | str]) # revealed: <type alias 'Bounded[Unknown]'>
|
||||
|
||||
reveal_type(BoundedByUnion[int]) # revealed: BoundedByUnion[int]
|
||||
reveal_type(BoundedByUnion[IntSubclass]) # revealed: BoundedByUnion[IntSubclass]
|
||||
reveal_type(BoundedByUnion[str]) # revealed: BoundedByUnion[str]
|
||||
reveal_type(BoundedByUnion[int | str]) # revealed: BoundedByUnion[int | str]
|
||||
reveal_type(BoundedByUnion[int]) # revealed: <type alias 'BoundedByUnion[int]'>
|
||||
reveal_type(BoundedByUnion[IntSubclass]) # revealed: <type alias 'BoundedByUnion[IntSubclass]'>
|
||||
reveal_type(BoundedByUnion[str]) # revealed: <type alias 'BoundedByUnion[str]'>
|
||||
reveal_type(BoundedByUnion[int | str]) # revealed: <type alias 'BoundedByUnion[int | str]'>
|
||||
```
|
||||
|
||||
If the type variable is constrained, the specialized type must satisfy those constraints:
|
||||
|
|
@ -105,20 +105,20 @@ If the type variable is constrained, the specialized type must satisfy those con
|
|||
```py
|
||||
type Constrained[T: (int, str)] = ...
|
||||
|
||||
reveal_type(Constrained[int]) # revealed: Constrained[int]
|
||||
reveal_type(Constrained[int]) # revealed: <type alias 'Constrained[int]'>
|
||||
|
||||
# TODO: error: [invalid-argument-type]
|
||||
# TODO: revealed: Constrained[Unknown]
|
||||
reveal_type(Constrained[IntSubclass]) # revealed: Constrained[IntSubclass]
|
||||
reveal_type(Constrained[IntSubclass]) # revealed: <type alias 'Constrained[IntSubclass]'>
|
||||
|
||||
reveal_type(Constrained[str]) # revealed: Constrained[str]
|
||||
reveal_type(Constrained[str]) # revealed: <type alias 'Constrained[str]'>
|
||||
|
||||
# TODO: error: [invalid-argument-type]
|
||||
# TODO: revealed: Unknown
|
||||
reveal_type(Constrained[int | str]) # revealed: Constrained[int | str]
|
||||
reveal_type(Constrained[int | str]) # revealed: <type alias 'Constrained[int | str]'>
|
||||
|
||||
# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `T@Constrained`"
|
||||
reveal_type(Constrained[object]) # revealed: Constrained[Unknown]
|
||||
reveal_type(Constrained[object]) # revealed: <type alias 'Constrained[Unknown]'>
|
||||
```
|
||||
|
||||
If the type variable has a default, it can be omitted:
|
||||
|
|
@ -126,8 +126,8 @@ If the type variable has a default, it can be omitted:
|
|||
```py
|
||||
type WithDefault[T, U = int] = ...
|
||||
|
||||
reveal_type(WithDefault[str, str]) # revealed: WithDefault[str, str]
|
||||
reveal_type(WithDefault[str]) # revealed: WithDefault[str, int]
|
||||
reveal_type(WithDefault[str, str]) # revealed: <type alias 'WithDefault[str, str]'>
|
||||
reveal_type(WithDefault[str]) # revealed: <type alias 'WithDefault[str, int]'>
|
||||
```
|
||||
|
||||
If the type alias is not specialized explicitly, it is implicitly specialized to `Unknown`:
|
||||
|
|
|
|||
|
|
@ -264,12 +264,19 @@ signatures don't count towards variance).
|
|||
### `__new__` only
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
|
|
@ -279,11 +286,18 @@ wrong_innards: C[int] = C("five")
|
|||
### `__init__` only
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
|
|
@ -293,6 +307,8 @@ wrong_innards: C[int] = C("five")
|
|||
### Identical `__new__` and `__init__` signatures
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
|
|
@ -301,6 +317,11 @@ class C[T]:
|
|||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
|
|
@ -310,6 +331,8 @@ wrong_innards: C[int] = C("five")
|
|||
### Compatible `__new__` and `__init__` signatures
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
|
|
@ -318,6 +341,11 @@ class C[T]:
|
|||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
|
|
@ -331,6 +359,11 @@ class D[T]:
|
|||
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D(1)) # revealed: D[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `D[int | str]` is not assignable to `D[int]`"
|
||||
|
|
@ -343,6 +376,8 @@ If either method comes from a generic base class, we don't currently use its inf
|
|||
to specialize the class.
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T, U]:
|
||||
def __new__(cls, *args, **kwargs) -> "C[T, U]":
|
||||
return object.__new__(cls)
|
||||
|
|
@ -350,18 +385,30 @@ class C[T, U]:
|
|||
class D[V](C[V, int]):
|
||||
def __init__(self, x: V) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[V@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[V@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D(1)) # revealed: D[Literal[1]]
|
||||
```
|
||||
|
||||
### Generic class inherits `__init__` from generic base class
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T, U]:
|
||||
def __init__(self, t: T, u: U) -> None: ...
|
||||
|
||||
class D[T, U](C[T, U]):
|
||||
pass
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(C(1, "str")) # revealed: C[Literal[1], Literal["str"]]
|
||||
reveal_type(D(1, "str")) # revealed: D[Literal[1], Literal["str"]]
|
||||
```
|
||||
|
|
@ -371,9 +418,16 @@ reveal_type(D(1, "str")) # revealed: D[Literal[1], Literal["str"]]
|
|||
This is a specific example of the above, since it was reported specifically by a user.
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class D[T, U](dict[T, U]):
|
||||
pass
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D(key=1)) # revealed: D[str, int]
|
||||
```
|
||||
|
||||
|
|
@ -384,8 +438,15 @@ for `tuple`, so we use a different mechanism to make sure it has the right inher
|
|||
context. But from the user's point of view, this is another example of the above.)
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T, U](tuple[T, U]): ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C, U@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C, U@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C((1, 2))) # revealed: C[Literal[1], Literal[2]]
|
||||
```
|
||||
|
||||
|
|
@ -409,11 +470,18 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t
|
|||
### `__init__` is itself generic
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __init__[S](self, x: T, y: S) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C, S@__init__]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C(1, 1)) # revealed: C[int]
|
||||
reveal_type(C(1, "string")) # revealed: C[int]
|
||||
reveal_type(C(1, True)) # revealed: C[int]
|
||||
|
|
@ -427,6 +495,7 @@ wrong_innards: C[int] = C("five", 1)
|
|||
```py
|
||||
from __future__ import annotations
|
||||
from typing import overload
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T]:
|
||||
# we need to use the type variable or else the class is bivariant in T, and
|
||||
|
|
@ -443,6 +512,11 @@ class C[T]:
|
|||
def __init__(self, x: int) -> None: ...
|
||||
def __init__(self, x: str | bytes | int) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C("string")) # revealed: C[str]
|
||||
reveal_type(C(b"bytes")) # revealed: C[bytes]
|
||||
reveal_type(C(12)) # revealed: C[Unknown]
|
||||
|
|
@ -470,6 +544,11 @@ class D[T, U]:
|
|||
def __init__(self, t: T, u: U) -> None: ...
|
||||
def __init__(self, *args) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D("string")) # revealed: D[str, Literal["string"]]
|
||||
reveal_type(D(1)) # revealed: D[str, Literal[1]]
|
||||
reveal_type(D(1, "string")) # revealed: D[Literal[1], Literal["string"]]
|
||||
|
|
@ -479,24 +558,42 @@ reveal_type(D(1, "string")) # revealed: D[Literal[1], Literal["string"]]
|
|||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
@dataclass
|
||||
class A[T]:
|
||||
x: T
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@A]
|
||||
reveal_type(generic_context(A))
|
||||
# revealed: ty_extensions.GenericContext[T@A]
|
||||
reveal_type(generic_context(into_callable(A)))
|
||||
|
||||
reveal_type(A(x=1)) # revealed: A[int]
|
||||
```
|
||||
|
||||
### Class typevar has another typevar as a default
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
class C[T, U = T]: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@C, U@C]
|
||||
reveal_type(generic_context(C))
|
||||
# revealed: ty_extensions.GenericContext[T@C, U@C]
|
||||
reveal_type(generic_context(into_callable(C)))
|
||||
|
||||
reveal_type(C()) # revealed: C[Unknown, Unknown]
|
||||
|
||||
class D[T, U = T]:
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(D))
|
||||
# revealed: ty_extensions.GenericContext[T@D, U@D]
|
||||
reveal_type(generic_context(into_callable(D)))
|
||||
|
||||
reveal_type(D()) # revealed: D[Unknown, Unknown]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -77,44 +77,44 @@ IntOrTypeVar = int | T
|
|||
TypeVarOrNone = T | None
|
||||
NoneOrTypeVar = None | T
|
||||
|
||||
reveal_type(IntOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes2) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes3) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes4) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes5) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes6) # revealed: types.UnionType
|
||||
reveal_type(BytesOrIntOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrNone) # revealed: types.UnionType
|
||||
reveal_type(NoneOrInt) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrNone) # revealed: types.UnionType
|
||||
reveal_type(NoneOrIntOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrAny) # revealed: types.UnionType
|
||||
reveal_type(AnyOrInt) # revealed: types.UnionType
|
||||
reveal_type(NoneOrAny) # revealed: types.UnionType
|
||||
reveal_type(AnyOrNone) # revealed: types.UnionType
|
||||
reveal_type(NeverOrAny) # revealed: types.UnionType
|
||||
reveal_type(AnyOrNever) # revealed: types.UnionType
|
||||
reveal_type(UnknownOrInt) # revealed: types.UnionType
|
||||
reveal_type(IntOrUnknown) # revealed: types.UnionType
|
||||
reveal_type(StrOrZero) # revealed: types.UnionType
|
||||
reveal_type(ZeroOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrLiteralString) # revealed: types.UnionType
|
||||
reveal_type(LiteralStringOrInt) # revealed: types.UnionType
|
||||
reveal_type(NoneOrTuple) # revealed: types.UnionType
|
||||
reveal_type(TupleOrNone) # revealed: types.UnionType
|
||||
reveal_type(IntOrAnnotated) # revealed: types.UnionType
|
||||
reveal_type(AnnotatedOrInt) # revealed: types.UnionType
|
||||
reveal_type(IntOrOptional) # revealed: types.UnionType
|
||||
reveal_type(OptionalOrInt) # revealed: types.UnionType
|
||||
reveal_type(IntOrTypeOfStr) # revealed: types.UnionType
|
||||
reveal_type(TypeOfStrOrInt) # revealed: types.UnionType
|
||||
reveal_type(IntOrCallable) # revealed: types.UnionType
|
||||
reveal_type(CallableOrInt) # revealed: types.UnionType
|
||||
reveal_type(TypeVarOrInt) # revealed: types.UnionType
|
||||
reveal_type(IntOrTypeVar) # revealed: types.UnionType
|
||||
reveal_type(TypeVarOrNone) # revealed: types.UnionType
|
||||
reveal_type(NoneOrTypeVar) # revealed: types.UnionType
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special form 'int | str'>
|
||||
reveal_type(IntOrStrOrBytes1) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes2) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes3) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes4) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes5) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes6) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(BytesOrIntOrStr) # revealed: <types.UnionType special form 'bytes | int | str'>
|
||||
reveal_type(IntOrNone) # revealed: <types.UnionType special form 'int | None'>
|
||||
reveal_type(NoneOrInt) # revealed: <types.UnionType special form 'None | int'>
|
||||
reveal_type(IntOrStrOrNone) # revealed: <types.UnionType special form 'int | str | None'>
|
||||
reveal_type(NoneOrIntOrStr) # revealed: <types.UnionType special form 'None | int | str'>
|
||||
reveal_type(IntOrAny) # revealed: <types.UnionType special form 'int | Any'>
|
||||
reveal_type(AnyOrInt) # revealed: <types.UnionType special form 'Any | int'>
|
||||
reveal_type(NoneOrAny) # revealed: <types.UnionType special form 'None | Any'>
|
||||
reveal_type(AnyOrNone) # revealed: <types.UnionType special form 'Any | None'>
|
||||
reveal_type(NeverOrAny) # revealed: <types.UnionType special form 'Any'>
|
||||
reveal_type(AnyOrNever) # revealed: <types.UnionType special form 'Any'>
|
||||
reveal_type(UnknownOrInt) # revealed: <types.UnionType special form 'Unknown | int'>
|
||||
reveal_type(IntOrUnknown) # revealed: <types.UnionType special form 'int | Unknown'>
|
||||
reveal_type(StrOrZero) # revealed: <types.UnionType special form 'str | Literal[0]'>
|
||||
reveal_type(ZeroOrStr) # revealed: <types.UnionType special form 'Literal[0] | str'>
|
||||
reveal_type(IntOrLiteralString) # revealed: <types.UnionType special form 'int | LiteralString'>
|
||||
reveal_type(LiteralStringOrInt) # revealed: <types.UnionType special form 'LiteralString | int'>
|
||||
reveal_type(NoneOrTuple) # revealed: <types.UnionType special form 'None | tuple[int, str]'>
|
||||
reveal_type(TupleOrNone) # revealed: <types.UnionType special form 'tuple[int, str] | None'>
|
||||
reveal_type(IntOrAnnotated) # revealed: <types.UnionType special form 'int | str'>
|
||||
reveal_type(AnnotatedOrInt) # revealed: <types.UnionType special form 'str | int'>
|
||||
reveal_type(IntOrOptional) # revealed: <types.UnionType special form 'int | str | None'>
|
||||
reveal_type(OptionalOrInt) # revealed: <types.UnionType special form 'str | None | int'>
|
||||
reveal_type(IntOrTypeOfStr) # revealed: <types.UnionType special form 'int | type[str]'>
|
||||
reveal_type(TypeOfStrOrInt) # revealed: <types.UnionType special form 'type[str] | int'>
|
||||
reveal_type(IntOrCallable) # revealed: <types.UnionType special form 'int | ((str, /) -> bytes)'>
|
||||
reveal_type(CallableOrInt) # revealed: <types.UnionType special form '((str, /) -> bytes) | int'>
|
||||
reveal_type(TypeVarOrInt) # revealed: <types.UnionType special form 'T@TypeVarOrInt | int'>
|
||||
reveal_type(IntOrTypeVar) # revealed: <types.UnionType special form 'int | T@IntOrTypeVar'>
|
||||
reveal_type(TypeVarOrNone) # revealed: <types.UnionType special form 'T@TypeVarOrNone | None'>
|
||||
reveal_type(NoneOrTypeVar) # revealed: <types.UnionType special form 'None | T@NoneOrTypeVar'>
|
||||
|
||||
def _(
|
||||
int_or_str: IntOrStr,
|
||||
|
|
@ -295,7 +295,7 @@ X = Foo | Bar
|
|||
# In an ideal world, perhaps we would respect `Meta.__or__` here and reveal `str`?
|
||||
# But we still need to record what the elements are, since (according to the typing spec)
|
||||
# `X` is still a valid type alias
|
||||
reveal_type(X) # revealed: types.UnionType
|
||||
reveal_type(X) # revealed: <types.UnionType special form 'Foo | Bar'>
|
||||
|
||||
def f(obj: X):
|
||||
reveal_type(obj) # revealed: Foo | Bar
|
||||
|
|
@ -391,16 +391,17 @@ MyOptional = T | None
|
|||
|
||||
reveal_type(MyList) # revealed: <class 'list[T@MyList]'>
|
||||
reveal_type(MyDict) # revealed: <class 'dict[T@MyDict, U@MyDict]'>
|
||||
reveal_type(MyType) # revealed: GenericAlias
|
||||
reveal_type(MyType) # revealed: <special form 'type[T@MyType]'>
|
||||
reveal_type(IntAndType) # revealed: <class 'tuple[int, T@IntAndType]'>
|
||||
reveal_type(Pair) # revealed: <class 'tuple[T@Pair, T@Pair]'>
|
||||
reveal_type(Sum) # revealed: <class 'tuple[T@Sum, U@Sum]'>
|
||||
reveal_type(ListOrTuple) # revealed: types.UnionType
|
||||
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
|
||||
reveal_type(ListOrTuple) # revealed: <types.UnionType special form 'list[T@ListOrTuple] | tuple[T@ListOrTuple, ...]'>
|
||||
# revealed: <types.UnionType special form 'list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...]'>
|
||||
reveal_type(ListOrTupleLegacy)
|
||||
reveal_type(MyCallable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
||||
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
|
||||
reveal_type(AnnotatedType) # revealed: <special form 'typing.Annotated[T@AnnotatedType, <metadata>]'>
|
||||
reveal_type(TransparentAlias) # revealed: typing.TypeVar
|
||||
reveal_type(MyOptional) # revealed: types.UnionType
|
||||
reveal_type(MyOptional) # revealed: <types.UnionType special form 'T@MyOptional | None'>
|
||||
|
||||
def _(
|
||||
list_of_ints: MyList[int],
|
||||
|
|
@ -456,12 +457,12 @@ AnnotatedInt = AnnotatedType[int]
|
|||
SubclassOfInt = MyType[int]
|
||||
CallableIntToStr = MyCallable[[int], str]
|
||||
|
||||
reveal_type(IntsOrNone) # revealed: types.UnionType
|
||||
reveal_type(IntsOrStrs) # revealed: types.UnionType
|
||||
reveal_type(IntsOrNone) # revealed: <types.UnionType special form 'list[int] | None'>
|
||||
reveal_type(IntsOrStrs) # revealed: <types.UnionType special form 'tuple[int, int] | tuple[str, str]'>
|
||||
reveal_type(ListOfPairs) # revealed: <class 'list[tuple[str, str]]'>
|
||||
reveal_type(ListOrTupleOfInts) # revealed: types.UnionType
|
||||
reveal_type(AnnotatedInt) # revealed: <typing.Annotated special form>
|
||||
reveal_type(SubclassOfInt) # revealed: GenericAlias
|
||||
reveal_type(ListOrTupleOfInts) # revealed: <types.UnionType special form 'list[int] | tuple[int, ...]'>
|
||||
reveal_type(AnnotatedInt) # revealed: <special form 'typing.Annotated[int, <metadata>]'>
|
||||
reveal_type(SubclassOfInt) # revealed: <special form 'type[int]'>
|
||||
reveal_type(CallableIntToStr) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
||||
|
||||
def _(
|
||||
|
|
@ -495,8 +496,8 @@ MyOtherType = MyType[T]
|
|||
TypeOrList = MyType[B] | MyList[B]
|
||||
|
||||
reveal_type(MyOtherList) # revealed: <class 'list[T@MyOtherList]'>
|
||||
reveal_type(MyOtherType) # revealed: GenericAlias
|
||||
reveal_type(TypeOrList) # revealed: types.UnionType
|
||||
reveal_type(MyOtherType) # revealed: <special form 'type[T@MyOtherType]'>
|
||||
reveal_type(TypeOrList) # revealed: <types.UnionType special form 'type[B@TypeOrList] | list[B@TypeOrList]'>
|
||||
|
||||
def _(
|
||||
list_of_ints: MyOtherList[int],
|
||||
|
|
@ -898,7 +899,7 @@ from typing import Optional
|
|||
|
||||
MyOptionalInt = Optional[int]
|
||||
|
||||
reveal_type(MyOptionalInt) # revealed: types.UnionType
|
||||
reveal_type(MyOptionalInt) # revealed: <types.UnionType special form 'int | None'>
|
||||
|
||||
def _(optional_int: MyOptionalInt):
|
||||
reveal_type(optional_int) # revealed: int | None
|
||||
|
|
@ -931,9 +932,9 @@ MyLiteralString = LiteralString
|
|||
MyNoReturn = NoReturn
|
||||
MyNever = Never
|
||||
|
||||
reveal_type(MyLiteralString) # revealed: typing.LiteralString
|
||||
reveal_type(MyNoReturn) # revealed: typing.NoReturn
|
||||
reveal_type(MyNever) # revealed: typing.Never
|
||||
reveal_type(MyLiteralString) # revealed: <special form 'typing.LiteralString'>
|
||||
reveal_type(MyNoReturn) # revealed: <special form 'typing.NoReturn'>
|
||||
reveal_type(MyNever) # revealed: <special form 'typing.Never'>
|
||||
|
||||
def _(
|
||||
ls: MyLiteralString,
|
||||
|
|
@ -986,8 +987,8 @@ from typing import Union
|
|||
IntOrStr = Union[int, str]
|
||||
IntOrStrOrBytes = Union[int, Union[str, bytes]]
|
||||
|
||||
reveal_type(IntOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes) # revealed: types.UnionType
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special form 'int | str'>
|
||||
reveal_type(IntOrStrOrBytes) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
|
||||
def _(
|
||||
int_or_str: IntOrStr,
|
||||
|
|
@ -1015,7 +1016,7 @@ An empty `typing.Union` leads to a `TypeError` at runtime, so we emit an error.
|
|||
# error: [invalid-type-form] "`typing.Union` requires at least one type argument"
|
||||
EmptyUnion = Union[()]
|
||||
|
||||
reveal_type(EmptyUnion) # revealed: types.UnionType
|
||||
reveal_type(EmptyUnion) # revealed: <types.UnionType special form 'Never'>
|
||||
|
||||
def _(empty: EmptyUnion):
|
||||
reveal_type(empty) # revealed: Never
|
||||
|
|
@ -1060,14 +1061,14 @@ SubclassOfG = type[G]
|
|||
SubclassOfGInt = type[G[int]]
|
||||
SubclassOfP = type[P]
|
||||
|
||||
reveal_type(SubclassOfA) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfAny) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfAOrB1) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfAOrB2) # revealed: types.UnionType
|
||||
reveal_type(SubclassOfAOrB3) # revealed: types.UnionType
|
||||
reveal_type(SubclassOfG) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfGInt) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfP) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfA) # revealed: <special form 'type[A]'>
|
||||
reveal_type(SubclassOfAny) # revealed: <special form 'type[Any]'>
|
||||
reveal_type(SubclassOfAOrB1) # revealed: <special form 'type[A | B]'>
|
||||
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfG) # revealed: <special form 'type[G[Unknown]]'>
|
||||
reveal_type(SubclassOfGInt) # revealed: <special form 'type[G[int]]'>
|
||||
reveal_type(SubclassOfP) # revealed: <special form 'type[P]'>
|
||||
|
||||
def _(
|
||||
subclass_of_a: SubclassOfA,
|
||||
|
|
@ -1148,14 +1149,14 @@ SubclassOfG = Type[G]
|
|||
SubclassOfGInt = Type[G[int]]
|
||||
SubclassOfP = Type[P]
|
||||
|
||||
reveal_type(SubclassOfA) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfAny) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfAOrB1) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfAOrB2) # revealed: types.UnionType
|
||||
reveal_type(SubclassOfAOrB3) # revealed: types.UnionType
|
||||
reveal_type(SubclassOfG) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfGInt) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfP) # revealed: GenericAlias
|
||||
reveal_type(SubclassOfA) # revealed: <special form 'type[A]'>
|
||||
reveal_type(SubclassOfAny) # revealed: <special form 'type[Any]'>
|
||||
reveal_type(SubclassOfAOrB1) # revealed: <special form 'type[A | B]'>
|
||||
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfG) # revealed: <special form 'type[G[Unknown]]'>
|
||||
reveal_type(SubclassOfGInt) # revealed: <special form 'type[G[int]]'>
|
||||
reveal_type(SubclassOfP) # revealed: <special form 'type[P]'>
|
||||
|
||||
def _(
|
||||
subclass_of_a: SubclassOfA,
|
||||
|
|
@ -1270,25 +1271,25 @@ DefaultDictOrNone = DefaultDict[str, int] | None
|
|||
DequeOrNone = Deque[str] | None
|
||||
OrderedDictOrNone = OrderedDict[str, int] | None
|
||||
|
||||
reveal_type(NoneOrList) # revealed: types.UnionType
|
||||
reveal_type(NoneOrSet) # revealed: types.UnionType
|
||||
reveal_type(NoneOrDict) # revealed: types.UnionType
|
||||
reveal_type(NoneOrFrozenSet) # revealed: types.UnionType
|
||||
reveal_type(NoneOrChainMap) # revealed: types.UnionType
|
||||
reveal_type(NoneOrCounter) # revealed: types.UnionType
|
||||
reveal_type(NoneOrDefaultDict) # revealed: types.UnionType
|
||||
reveal_type(NoneOrDeque) # revealed: types.UnionType
|
||||
reveal_type(NoneOrOrderedDict) # revealed: types.UnionType
|
||||
reveal_type(NoneOrList) # revealed: <types.UnionType special form 'None | list[str]'>
|
||||
reveal_type(NoneOrSet) # revealed: <types.UnionType special form 'None | set[str]'>
|
||||
reveal_type(NoneOrDict) # revealed: <types.UnionType special form 'None | dict[str, int]'>
|
||||
reveal_type(NoneOrFrozenSet) # revealed: <types.UnionType special form 'None | frozenset[str]'>
|
||||
reveal_type(NoneOrChainMap) # revealed: <types.UnionType special form 'None | ChainMap[str, int]'>
|
||||
reveal_type(NoneOrCounter) # revealed: <types.UnionType special form 'None | Counter[str]'>
|
||||
reveal_type(NoneOrDefaultDict) # revealed: <types.UnionType special form 'None | defaultdict[str, int]'>
|
||||
reveal_type(NoneOrDeque) # revealed: <types.UnionType special form 'None | deque[str]'>
|
||||
reveal_type(NoneOrOrderedDict) # revealed: <types.UnionType special form 'None | OrderedDict[str, int]'>
|
||||
|
||||
reveal_type(ListOrNone) # revealed: types.UnionType
|
||||
reveal_type(SetOrNone) # revealed: types.UnionType
|
||||
reveal_type(DictOrNone) # revealed: types.UnionType
|
||||
reveal_type(FrozenSetOrNone) # revealed: types.UnionType
|
||||
reveal_type(ChainMapOrNone) # revealed: types.UnionType
|
||||
reveal_type(CounterOrNone) # revealed: types.UnionType
|
||||
reveal_type(DefaultDictOrNone) # revealed: types.UnionType
|
||||
reveal_type(DequeOrNone) # revealed: types.UnionType
|
||||
reveal_type(OrderedDictOrNone) # revealed: types.UnionType
|
||||
reveal_type(ListOrNone) # revealed: <types.UnionType special form 'list[int] | None'>
|
||||
reveal_type(SetOrNone) # revealed: <types.UnionType special form 'set[int] | None'>
|
||||
reveal_type(DictOrNone) # revealed: <types.UnionType special form 'dict[str, int] | None'>
|
||||
reveal_type(FrozenSetOrNone) # revealed: <types.UnionType special form 'frozenset[int] | None'>
|
||||
reveal_type(ChainMapOrNone) # revealed: <types.UnionType special form 'ChainMap[str, int] | None'>
|
||||
reveal_type(CounterOrNone) # revealed: <types.UnionType special form 'Counter[str] | None'>
|
||||
reveal_type(DefaultDictOrNone) # revealed: <types.UnionType special form 'defaultdict[str, int] | None'>
|
||||
reveal_type(DequeOrNone) # revealed: <types.UnionType special form 'deque[str] | None'>
|
||||
reveal_type(OrderedDictOrNone) # revealed: <types.UnionType special form 'OrderedDict[str, int] | None'>
|
||||
|
||||
def _(
|
||||
none_or_list: NoneOrList,
|
||||
|
|
@ -1381,9 +1382,9 @@ CallableNoArgs = Callable[[], None]
|
|||
BasicCallable = Callable[[int, str], bytes]
|
||||
GradualCallable = Callable[..., str]
|
||||
|
||||
reveal_type(CallableNoArgs) # revealed: GenericAlias
|
||||
reveal_type(BasicCallable) # revealed: GenericAlias
|
||||
reveal_type(GradualCallable) # revealed: GenericAlias
|
||||
reveal_type(CallableNoArgs) # revealed: <typing.Callable special form '() -> None'>
|
||||
reveal_type(BasicCallable) # revealed: <typing.Callable special form '(int, str, /) -> bytes'>
|
||||
reveal_type(GradualCallable) # revealed: <typing.Callable special form '(...) -> str'>
|
||||
|
||||
def _(
|
||||
callable_no_args: CallableNoArgs,
|
||||
|
|
@ -1415,8 +1416,8 @@ InvalidCallable1 = Callable[[int]]
|
|||
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
InvalidCallable2 = Callable[int, str]
|
||||
|
||||
reveal_type(InvalidCallable1) # revealed: GenericAlias
|
||||
reveal_type(InvalidCallable2) # revealed: GenericAlias
|
||||
reveal_type(InvalidCallable1) # revealed: <typing.Callable special form '(...) -> Unknown'>
|
||||
reveal_type(InvalidCallable2) # revealed: <typing.Callable special form '(...) -> Unknown'>
|
||||
|
||||
def _(invalid_callable1: InvalidCallable1, invalid_callable2: InvalidCallable2):
|
||||
reveal_type(invalid_callable1) # revealed: (...) -> Unknown
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ in `import os.path as os.path` the `os.path` is not a valid identifier.
|
|||
```py
|
||||
from b import Any, Literal, foo
|
||||
|
||||
reveal_type(Any) # revealed: typing.Any
|
||||
reveal_type(Literal) # revealed: typing.Literal
|
||||
reveal_type(Any) # revealed: <special form 'typing.Any'>
|
||||
reveal_type(Literal) # revealed: <special form 'typing.Literal'>
|
||||
reveal_type(foo) # revealed: <module 'foo'>
|
||||
```
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ reveal_type(Any) # revealed: Unknown
|
|||
```pyi
|
||||
from typing import Any
|
||||
|
||||
reveal_type(Any) # revealed: typing.Any
|
||||
reveal_type(Any) # revealed: <special form 'typing.Any'>
|
||||
```
|
||||
|
||||
## Nested mixed re-export and not
|
||||
|
|
@ -169,7 +169,7 @@ reveal_type(Any) # revealed: Unknown
|
|||
```pyi
|
||||
from typing import Any
|
||||
|
||||
reveal_type(Any) # revealed: typing.Any
|
||||
reveal_type(Any) # revealed: <special form 'typing.Any'>
|
||||
```
|
||||
|
||||
## Exported as different name
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ python = "/.venv"
|
|||
`/.venv/pyvenv.cfg`:
|
||||
|
||||
```cfg
|
||||
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
|
||||
home = /do/re/mi//cpython-3.13.2-macos-aarch64-none/bin
|
||||
```
|
||||
|
||||
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
`/do/re/mi//cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
|
@ -54,11 +54,11 @@ python = "/.venv"
|
|||
`/.venv/pyvenv.cfg`:
|
||||
|
||||
```cfg
|
||||
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
|
||||
home = /do/re/mi//cpython-3.13.2-macos-aarch64-none/bin
|
||||
version = wut
|
||||
```
|
||||
|
||||
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
`/do/re/mi//cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
|
@ -87,11 +87,11 @@ python = "/.venv"
|
|||
`/.venv/pyvenv.cfg`:
|
||||
|
||||
```cfg
|
||||
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
|
||||
home = /do/re/mi//cpython-3.13.2-macos-aarch64-none/bin
|
||||
version_info = no-really-wut
|
||||
```
|
||||
|
||||
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
`/do/re/mi//cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
|
@ -132,7 +132,7 @@ python = "/.venv"
|
|||
`/.venv/pyvenv.cfg`:
|
||||
|
||||
```cfg
|
||||
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
|
||||
home = /do/re/mi//cpython-3.13.2-macos-aarch64-none/bin
|
||||
implementation = CPython
|
||||
uv = 0.7.6
|
||||
version_info = 3.13.2
|
||||
|
|
@ -141,7 +141,7 @@ prompt = ruff
|
|||
extends-environment = /.other-environment
|
||||
```
|
||||
|
||||
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
`/do/re/mi//cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
|
@ -182,12 +182,12 @@ python = "/.venv"
|
|||
`/.venv/pyvenv.cfg`:
|
||||
|
||||
```cfg
|
||||
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
|
||||
home = /do/re/mi//cpython-3.13.2-macos-aarch64-none/bin
|
||||
version_info = 3.13
|
||||
command = /.pyenv/versions/3.13.3/bin/python3.13 -m venv --without-pip --prompt="python-default/3.13.3" /somewhere-else/python/virtualenvs/python-default/3.13.3
|
||||
```
|
||||
|
||||
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
`/do/re/mi//cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1336,6 +1336,69 @@ reveal_type(g) # revealed: Unknown
|
|||
reveal_type(h) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Star-imports can affect member states
|
||||
|
||||
If a star-import pulls in a symbol that was previously defined in the importing module (e.g. `obj`),
|
||||
it can affect the state of associated member expressions (e.g. `obj.attr` or `obj[0]`). In the test
|
||||
below, note how the types of the corresponding attribute expressions change after the star import
|
||||
affects the object:
|
||||
|
||||
`common.py`:
|
||||
|
||||
```py
|
||||
class C:
|
||||
attr: int | None
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from common import C
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
should_be_imported: C = C()
|
||||
|
||||
if flag():
|
||||
might_be_imported: C = C()
|
||||
|
||||
if False:
|
||||
should_not_be_imported: C = C()
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from common import C
|
||||
|
||||
should_be_imported = C()
|
||||
might_be_imported = C()
|
||||
should_not_be_imported = C()
|
||||
|
||||
# We start with the plain attribute types:
|
||||
reveal_type(should_be_imported.attr) # revealed: int | None
|
||||
reveal_type(might_be_imported.attr) # revealed: int | None
|
||||
reveal_type(should_not_be_imported.attr) # revealed: int | None
|
||||
|
||||
# Now we narrow the types by assignment:
|
||||
should_be_imported.attr = 1
|
||||
might_be_imported.attr = 1
|
||||
should_not_be_imported.attr = 1
|
||||
|
||||
reveal_type(should_be_imported.attr) # revealed: Literal[1]
|
||||
reveal_type(might_be_imported.attr) # revealed: Literal[1]
|
||||
reveal_type(should_not_be_imported.attr) # revealed: Literal[1]
|
||||
|
||||
# This star import adds bindings for `should_be_imported` and `might_be_imported`:
|
||||
from exporter import *
|
||||
|
||||
# As expected, narrowing is "reset" for the first two variables, but not for the third:
|
||||
reveal_type(should_be_imported.attr) # revealed: int | None
|
||||
reveal_type(might_be_imported.attr) # revealed: int | None
|
||||
reveal_type(should_not_be_imported.attr) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Cyclic star imports
|
||||
|
||||
Believe it or not, this code does *not* raise an exception at runtime!
|
||||
|
|
@ -1374,7 +1437,7 @@ are present due to `*` imports.
|
|||
import collections.abc
|
||||
|
||||
reveal_type(collections.abc.Sequence) # revealed: <class 'Sequence'>
|
||||
reveal_type(collections.abc.Callable) # revealed: typing.Callable
|
||||
reveal_type(collections.abc.Callable) # revealed: <special form 'typing.Callable'>
|
||||
reveal_type(collections.abc.Set) # revealed: <class 'AbstractSet'>
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ python file in some random workspace, and so we need to be more tolerant of situ
|
|||
fly in a published package, cases where we're not configured as well as we'd like, or cases where
|
||||
two projects in a monorepo have conflicting definitions (but we want to analyze both at once).
|
||||
|
||||
In practice these tests cover what we call "desperate module resolution" which, when an import
|
||||
fails, results in us walking up the ancestor directories of the importing file and trying those as
|
||||
"desperate search-paths".
|
||||
|
||||
Currently desperate search-paths are restricted to subdirectories of the first-party search-path
|
||||
(the directory you're running `ty` in). Currently we only consider one desperate search-path: the
|
||||
closest ancestor directory containing a `pyproject.toml`. In the future we may want to try every
|
||||
ancestor `pyproject.toml` or every ancestor directory.
|
||||
|
||||
## Invalid Names
|
||||
|
||||
While you can't syntactically refer to a module with an invalid name (i.e. one with a `-`, or that
|
||||
|
|
@ -18,9 +27,10 @@ strings and does in fact allow syntactically invalid module names.
|
|||
|
||||
### Current File Is Invalid Module Name
|
||||
|
||||
Relative and absolute imports should resolve fine in a file that isn't a valid module name.
|
||||
Relative and absolute imports should resolve fine in a file that isn't a valid module name (in this
|
||||
case, it could be imported via `importlib.import_module`).
|
||||
|
||||
`my-main.py`:
|
||||
`tests/my-mod.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
|
@ -37,13 +47,13 @@ reveal_type(mod2.y) # revealed: Unknown
|
|||
reveal_type(mod3.z) # revealed: int
|
||||
```
|
||||
|
||||
`mod1.py`:
|
||||
`tests/mod1.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`mod2.py`:
|
||||
`tests/mod2.py`:
|
||||
|
||||
```py
|
||||
y: int = 2
|
||||
|
|
@ -57,13 +67,16 @@ z: int = 2
|
|||
|
||||
### Current Directory Is Invalid Module Name
|
||||
|
||||
Relative and absolute imports should resolve fine in a dir that isn't a valid module name.
|
||||
If python files are rooted in a directory with an invalid module name and they relatively import
|
||||
each other, there's probably no coherent explanation for what's going on and it's fine that the
|
||||
relative import don't resolve (but maybe we could provide some good diagnostics).
|
||||
|
||||
`my-tests/main.py`:
|
||||
This is a case that sufficient desperation might "accidentally" make work, so it's included here as
|
||||
a canary in the coal mine.
|
||||
|
||||
`my-tests/mymod.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .mod1 import x
|
||||
|
||||
|
|
@ -94,46 +107,97 @@ y: int = 2
|
|||
z: int = 2
|
||||
```
|
||||
|
||||
### Current Directory Is Invalid Package Name
|
||||
### Ancestor Directory Is Invalid Module Name
|
||||
|
||||
Relative and absolute imports should resolve fine in a dir that isn't a valid package name, even if
|
||||
it contains an `__init__.py`:
|
||||
Relative and absolute imports *could* resolve fine in the first-party search-path, even if one of
|
||||
the ancestor dirs is an invalid module. i.e. in this case we will be inclined to compute module
|
||||
names like `my-proj.tests.mymod`, but it could be that in practice the user always runs this code
|
||||
rooted in the `my-proj` directory.
|
||||
|
||||
`my-tests/__init__.py`:
|
||||
This case is hard for us to detect and handle in a principled way, but two more extreme kinds of
|
||||
desperation could handle this:
|
||||
|
||||
- try every ancestor as a desperate search-path
|
||||
- try the closest ancestor with an invalid module name as a desperate search-path
|
||||
|
||||
The second one is a bit messed up because it could result in situations where someone can get a
|
||||
worse experience because a directory happened to *not* be invalid as a module name (`myproj` or
|
||||
`my_proj`).
|
||||
|
||||
`my-proj/tests/mymod.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`my-tests/main.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
# TODO: it would be *nice* if there were no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .mod1 import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import mod2
|
||||
|
||||
# error: [unresolved-import]
|
||||
import mod3
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(mod2.y) # revealed: Unknown
|
||||
reveal_type(mod3.z) # revealed: int
|
||||
reveal_type(mod3.z) # revealed: Unknown
|
||||
```
|
||||
|
||||
`my-tests/mod1.py`:
|
||||
`my-proj/tests/mod1.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`my-tests/mod2.py`:
|
||||
`my-proj/tests/mod2.py`:
|
||||
|
||||
```py
|
||||
y: int = 2
|
||||
```
|
||||
|
||||
`mod3.py`:
|
||||
`my-proj/mod3.py`:
|
||||
|
||||
```py
|
||||
z: int = 2
|
||||
```
|
||||
|
||||
### Ancestor Directory Above `pyproject.toml` is invalid
|
||||
|
||||
Like the previous tests but with a `pyproject.toml` existing between the invalid name and the python
|
||||
files. This is an "easier" case in case we use the `pyproject.toml` as a hint about what's going on.
|
||||
|
||||
`my-proj/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "my_proj"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`my-proj/tests/main.py`:
|
||||
|
||||
```py
|
||||
from .mod1 import x
|
||||
from . import mod2
|
||||
import mod3
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(mod2.y) # revealed: int
|
||||
reveal_type(mod3.z) # revealed: int
|
||||
```
|
||||
|
||||
`my-proj/tests/mod1.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`my-proj/tests/mod2.py`:
|
||||
|
||||
```py
|
||||
y: int = 2
|
||||
```
|
||||
|
||||
`my-proj/mod3.py`:
|
||||
|
||||
```py
|
||||
z: int = 2
|
||||
|
|
@ -141,7 +205,7 @@ z: int = 2
|
|||
|
||||
## Multiple Projects
|
||||
|
||||
It's common for a monorepo to define many separate projects that may or may not depend on eachother
|
||||
It's common for a monorepo to define many separate projects that may or may not depend on each other
|
||||
and are stitched together with a package manager like `uv` or `poetry`, often as editables. In this
|
||||
case, especially when running as an LSP, we want to be able to analyze all of the projects at once,
|
||||
allowing us to reuse results between projects, without getting confused about things that only make
|
||||
|
|
@ -150,7 +214,7 @@ sense when analyzing the project separately.
|
|||
The following tests will feature two projects, `a` and `b` where the "real" packages are found under
|
||||
`src/` subdirectories (and we've been configured to understand that), but each project also contains
|
||||
other python files in their roots or subdirectories that contains python files which relatively
|
||||
import eachother and also absolutely import the main package of the project. All of these imports
|
||||
import each other and also absolutely import the main package of the project. All of these imports
|
||||
*should* resolve.
|
||||
|
||||
Often the fact that there is both an `a` and `b` project seemingly won't matter, but many possible
|
||||
|
|
@ -164,13 +228,36 @@ following examples include them in case they help.
|
|||
|
||||
Here we have fairly typical situation where there are two projects `aproj` and `bproj` where the
|
||||
"real" packages are found under `src/` subdirectories, but each project also contains a `tests/`
|
||||
directory that contains python files which relatively import eachother and also absolutely import
|
||||
directory that contains python files which relatively import each other and also absolutely import
|
||||
the package they test. All of these imports *should* resolve.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
# This is similar to what we would compute for installed editables
|
||||
extra-paths = ["aproj/src/", "bproj/src/"]
|
||||
# Setup a venv with editables for aproj/src/ and bproj/src/
|
||||
python = "/.venv"
|
||||
```
|
||||
|
||||
`/.venv/pyvenv.cfg`:
|
||||
|
||||
```cfg
|
||||
home = /do/re/mi//cpython-3.13.2-macos-aarch64-none/bin
|
||||
```
|
||||
|
||||
`/do/re/mi//cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
`/.venv/<path-to-site-packages>/a.pth`:
|
||||
|
||||
```pth
|
||||
aproj/src/
|
||||
```
|
||||
|
||||
`/.venv/<path-to-site-packages>/b.pth`:
|
||||
|
||||
```pth
|
||||
bproj/src/
|
||||
```
|
||||
|
||||
`aproj/tests/test1.py`:
|
||||
|
|
@ -239,16 +326,60 @@ version = "0.1.0"
|
|||
y: str = "20"
|
||||
```
|
||||
|
||||
### Tests Directory With Ambiguous Project Directories
|
||||
### Tests Directory With Ambiguous Project Directories Via Editables
|
||||
|
||||
The same situation as the previous test but instead of the project `a` being in a directory `aproj`
|
||||
to disambiguate, we now need to avoid getting confused about whether `a/` or `a/src/a/` is the
|
||||
package `a` while still resolving imports.
|
||||
|
||||
Unfortunately this is a quite difficult square to circle as `a/` is a namespace package of `a` and
|
||||
`a/src/a/` is a regular package of `a`. **This is a very bad situation you're not supposed to ever
|
||||
create, and we are now very sensitive to precise search-path ordering.**
|
||||
|
||||
Here the use of editables means that `a/` has higher priority than `a/src/a/`.
|
||||
|
||||
Somehow this results in `a/tests/test1.py` being able to resolve `.setup` but not `.`.
|
||||
|
||||
My best guess is that in this state we can resolve regular modules in `a/tests/` but not namespace
|
||||
packages because we have some extra validation for namespace packages conflicted by regular
|
||||
packages, but that validation isn't applied when we successfully resolve a submodule of the
|
||||
namespace package.
|
||||
|
||||
In this case, as we find that `a/tests/test1.py` matches on the first-party path as `a.tests.test1`
|
||||
and is syntactically valid. We then resolve `a.tests.test1` and because the namespace package
|
||||
(`/a/`) comes first we succeed. We then syntactically compute `.` to be `a.tests`.
|
||||
|
||||
When we go to lookup `a.tests.setup`, whatever grace that allowed `a.tests.test1` to resolve still
|
||||
works so it resolves too. However when we try to resolve `a.tests` on its own some additional
|
||||
validation rejects the namespace package conflicting with the regular package.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
# This is similar to what we would compute for installed editables
|
||||
extra-paths = ["a/src/", "b/src/"]
|
||||
# Setup a venv with editables for a/src/ and b/src/
|
||||
python = "/.venv"
|
||||
```
|
||||
|
||||
`/.venv/pyvenv.cfg`:
|
||||
|
||||
```cfg
|
||||
home = /do/re/mi//cpython-3.13.2-macos-aarch64-none/bin
|
||||
```
|
||||
|
||||
`/do/re/mi//cpython-3.13.2-macos-aarch64-none/bin/python`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
`/.venv/<path-to-site-packages>/a.pth`:
|
||||
|
||||
```pth
|
||||
a/src/
|
||||
```
|
||||
|
||||
`/.venv/<path-to-site-packages>/b.pth`:
|
||||
|
||||
```pth
|
||||
b/src/
|
||||
```
|
||||
|
||||
`a/tests/test1.py`:
|
||||
|
|
@ -256,7 +387,6 @@ extra-paths = ["a/src/", "b/src/"]
|
|||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .setup import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
|
|
@ -264,7 +394,7 @@ from . import setup
|
|||
from a import y
|
||||
import a
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(setup.x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: int
|
||||
reveal_type(a.y) # revealed: int
|
||||
|
|
@ -294,7 +424,6 @@ y: int = 10
|
|||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .setup import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
|
|
@ -302,7 +431,7 @@ from . import setup
|
|||
from b import y
|
||||
import b
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(setup.x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: str
|
||||
reveal_type(b.y) # revealed: str
|
||||
|
|
@ -327,10 +456,15 @@ version = "0.1.0"
|
|||
y: str = "20"
|
||||
```
|
||||
|
||||
### Tests Package With Ambiguous Project Directories
|
||||
### Tests Directory With Ambiguous Project Directories Via `extra-paths`
|
||||
|
||||
The same situation as the previous test but `tests/__init__.py` is also defined, in case that
|
||||
complicates the situation.
|
||||
The same situation as the previous test but instead of using editables we use `extra-paths` which
|
||||
have higher priority than the first-party search-path. Thus, `/a/src/a/` is always seen before
|
||||
`/a/`.
|
||||
|
||||
In this case everything works well because the namespace package `a.tests` (`a/tests/`) is
|
||||
completely hidden by the regular package `a` (`a/src/a/`) and so we immediately enter desperate
|
||||
resolution and use the now-unambiguous namespace package `tests`.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
|
|
@ -340,27 +474,17 @@ extra-paths = ["a/src/", "b/src/"]
|
|||
`a/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .setup import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import setup
|
||||
from a import y
|
||||
import a
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(setup.x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(setup.x) # revealed: int
|
||||
reveal_type(y) # revealed: int
|
||||
reveal_type(a.y) # revealed: int
|
||||
```
|
||||
|
||||
`a/tests/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`a/tests/setup.py`:
|
||||
|
||||
```py
|
||||
|
|
@ -383,27 +507,17 @@ y: int = 10
|
|||
`b/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .setup import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import setup
|
||||
from b import y
|
||||
import b
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(setup.x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(setup.x) # revealed: str
|
||||
reveal_type(y) # revealed: str
|
||||
reveal_type(b.y) # revealed: str
|
||||
```
|
||||
|
||||
`b/tests/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`b/tests/setup.py`:
|
||||
|
||||
```py
|
||||
|
|
@ -431,21 +545,16 @@ that `import main` and expect that to work.
|
|||
`a/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
from .setup import x
|
||||
from . import setup
|
||||
|
||||
# error: [unresolved-import]
|
||||
from main import y
|
||||
|
||||
# error: [unresolved-import]
|
||||
import main
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(setup.x) # revealed: int
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(main.y) # revealed: Unknown
|
||||
reveal_type(y) # revealed: int
|
||||
reveal_type(main.y) # revealed: int
|
||||
```
|
||||
|
||||
`a/tests/setup.py`:
|
||||
|
|
@ -470,113 +579,16 @@ y: int = 10
|
|||
`b/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
from .setup import x
|
||||
from . import setup
|
||||
|
||||
# error: [unresolved-import]
|
||||
from main import y
|
||||
|
||||
# error: [unresolved-import]
|
||||
import main
|
||||
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(setup.x) # revealed: str
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(main.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: str = "2"
|
||||
```
|
||||
|
||||
`b/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`b/main.py`:
|
||||
|
||||
```py
|
||||
y: str = "20"
|
||||
```
|
||||
|
||||
### Tests Package Absolute Importing `main.py`
|
||||
|
||||
The same as the previous case but `tests/__init__.py` exists in case that causes different issues.
|
||||
|
||||
`a/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
from .setup import x
|
||||
from . import setup
|
||||
|
||||
# error: [unresolved-import]
|
||||
from main import y
|
||||
|
||||
# error: [unresolved-import]
|
||||
import main
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(setup.x) # revealed: int
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(main.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a/tests/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`a/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`a/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`a/main.py`:
|
||||
|
||||
```py
|
||||
y: int = 10
|
||||
```
|
||||
|
||||
`b/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
from .setup import x
|
||||
from . import setup
|
||||
|
||||
# error: [unresolved-import]
|
||||
from main import y
|
||||
|
||||
# error: [unresolved-import]
|
||||
import main
|
||||
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(setup.x) # revealed: str
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(main.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b/tests/__init__.py`:
|
||||
|
||||
```py
|
||||
reveal_type(y) # revealed: str
|
||||
reveal_type(main.y) # revealed: str
|
||||
```
|
||||
|
||||
`b/tests/setup.py`:
|
||||
|
|
@ -606,16 +618,11 @@ imports it.
|
|||
`a/main.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
# error: [unresolved-import]
|
||||
from utils import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
import utils
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(utils.x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(utils.x) # revealed: int
|
||||
```
|
||||
|
||||
`a/utils/__init__.py`:
|
||||
|
|
@ -634,16 +641,11 @@ version = "0.1.0"
|
|||
`b/main.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
# error: [unresolved-import]
|
||||
from utils import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
import utils
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(utils.x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(utils.x) # revealed: str
|
||||
```
|
||||
|
||||
`b/utils/__init__.py`:
|
||||
|
|
|
|||
|
|
@ -218,8 +218,8 @@ class E(A[int]):
|
|||
def method(self, x: object) -> None: ... # fine
|
||||
|
||||
class F[T](A[T]):
|
||||
# TODO: we should emit `invalid-method-override` on this:
|
||||
# `str` is not necessarily a supertype of `T`!
|
||||
# error: [invalid-method-override]
|
||||
def method(self, x: str) -> None: ...
|
||||
|
||||
class G(A[int]):
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ class B: ...
|
|||
|
||||
EitherOr = A | B
|
||||
|
||||
# error: [invalid-base] "Invalid class base with type `types.UnionType`"
|
||||
# error: [invalid-base] "Invalid class base with type `<types.UnionType special form 'A | B'>`"
|
||||
class Foo(EitherOr): ...
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ from typing import Union
|
|||
|
||||
IntOrStr = Union[int, str]
|
||||
|
||||
reveal_type(IntOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special form 'int | str'>
|
||||
|
||||
def _(x: int | str | bytes | memoryview | range):
|
||||
if isinstance(x, IntOrStr):
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ from typing import Union
|
|||
|
||||
IntOrStr = Union[int, str]
|
||||
|
||||
reveal_type(IntOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special form 'int | str'>
|
||||
|
||||
def f(x: type[int | str | bytes | range]):
|
||||
if issubclass(x, IntOrStr):
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ MyList: TypeAlias = list[T]
|
|||
ListOrSet: TypeAlias = list[T] | set[T]
|
||||
|
||||
reveal_type(MyList) # revealed: <class 'list[T]'>
|
||||
reveal_type(ListOrSet) # revealed: types.UnionType
|
||||
reveal_type(ListOrSet) # revealed: <types.UnionType special form 'list[T] | set[T]'>
|
||||
|
||||
def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]):
|
||||
reveal_type(list_of_int) # revealed: list[int]
|
||||
|
|
@ -293,7 +293,7 @@ def _(rec: RecursiveHomogeneousTuple):
|
|||
reveal_type(rec) # revealed: tuple[Divergent, ...]
|
||||
|
||||
ClassInfo: TypeAlias = type | UnionType | tuple["ClassInfo", ...]
|
||||
reveal_type(ClassInfo) # revealed: types.UnionType
|
||||
reveal_type(ClassInfo) # revealed: <types.UnionType special form 'type | UnionType | tuple[Divergent, ...]'>
|
||||
|
||||
def my_isinstance(obj: object, classinfo: ClassInfo) -> bool:
|
||||
# TODO should be `type | UnionType | tuple[ClassInfo, ...]`
|
||||
|
|
|
|||
|
|
@ -3184,14 +3184,9 @@ from ty_extensions import reveal_protocol_interface
|
|||
reveal_protocol_interface(Foo)
|
||||
```
|
||||
|
||||
## Known panics
|
||||
## Protocols generic over TypeVars bound to forward references
|
||||
|
||||
### Protocols generic over TypeVars bound to forward references
|
||||
|
||||
This test currently panics because the `ClassLiteral::explicit_bases` query fails to converge. See
|
||||
issue <https://github.com/astral-sh/ty/issues/1587>.
|
||||
|
||||
<!-- expect-panic: execute: too many cycle iterations -->
|
||||
Protocols can have TypeVars with forward reference bounds that form cycles.
|
||||
|
||||
```py
|
||||
from typing import Any, Protocol, TypeVar
|
||||
|
|
@ -3209,6 +3204,19 @@ class A2(Protocol[T2]):
|
|||
|
||||
class B1(A1[T3], Protocol[T3]): ...
|
||||
class B2(A2[T4], Protocol[T4]): ...
|
||||
|
||||
# TODO should just be `B2[Any]`
|
||||
reveal_type(T3.__bound__) # revealed: B2[Any] | @Todo(specialized non-generic class)
|
||||
|
||||
# TODO error: [invalid-type-arguments]
|
||||
def f(x: B1[int]):
|
||||
pass
|
||||
|
||||
reveal_type(T4.__bound__) # revealed: B1[Any]
|
||||
|
||||
# error: [invalid-type-arguments]
|
||||
def g(x: B2[int]):
|
||||
pass
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_type.m
|
|||
```
|
||||
1 | from typing_extensions import assert_type
|
||||
2 |
|
||||
3 | def _(x: int):
|
||||
3 | def _(x: int, y: bool):
|
||||
4 | assert_type(x, int) # fine
|
||||
5 | assert_type(x, str) # error: [type-assertion-failure]
|
||||
6 | assert_type(assert_type(x, int), int)
|
||||
7 | assert_type(y, int) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
|
@ -26,15 +27,32 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_type.m
|
|||
error[type-assertion-failure]: Argument does not have asserted type `str`
|
||||
--> src/mdtest_snippet.py:5:5
|
||||
|
|
||||
3 | def _(x: int):
|
||||
3 | def _(x: int, y: bool):
|
||||
4 | assert_type(x, int) # fine
|
||||
5 | assert_type(x, str) # error: [type-assertion-failure]
|
||||
| ^^^^^^^^^^^^-^^^^^^
|
||||
| |
|
||||
| Inferred type of argument is `int`
|
||||
| Inferred type is `int`
|
||||
6 | assert_type(assert_type(x, int), int)
|
||||
7 | assert_type(y, int) # error: [type-assertion-failure]
|
||||
|
|
||||
info: `str` and `int` are not equivalent types
|
||||
info: rule `type-assertion-failure` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[type-assertion-failure]: Argument does not have asserted type `int`
|
||||
--> src/mdtest_snippet.py:7:5
|
||||
|
|
||||
5 | assert_type(x, str) # error: [type-assertion-failure]
|
||||
6 | assert_type(assert_type(x, int), int)
|
||||
7 | assert_type(y, int) # error: [type-assertion-failure]
|
||||
| ^^^^^^^^^^^^-^^^^^^
|
||||
| |
|
||||
| Inferred type is `bool`
|
||||
|
|
||||
info: `bool` is a subtype of `int`, but they are not equivalent
|
||||
info: rule `type-assertion-failure` is enabled by default
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -91,14 +91,14 @@ error[missing-argument]: No argument provided for required parameter `arg` of bo
|
|||
7 | from typing_extensions import deprecated
|
||||
|
|
||||
info: Parameter declared here
|
||||
--> stdlib/typing_extensions.pyi:1000:28
|
||||
--> stdlib/typing_extensions.pyi:1001:28
|
||||
|
|
||||
998 | stacklevel: int
|
||||
999 | def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
|
||||
1000 | def __call__(self, arg: _T, /) -> _T: ...
|
||||
999 | stacklevel: int
|
||||
1000 | def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
|
||||
1001 | def __call__(self, arg: _T, /) -> _T: ...
|
||||
| ^^^^^^^
|
||||
1001 |
|
||||
1002 | @final
|
||||
1002 |
|
||||
1003 | @final
|
||||
|
|
||||
info: rule `missing-argument` is enabled by default
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ error[invalid-argument-type]: Invalid second argument to `isinstance`
|
|||
10 | # error: [invalid-argument-type]
|
||||
|
|
||||
info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects
|
||||
info: Elements `<typing.Literal special form>` and `<class 'list[int]'>` in the union are not class objects
|
||||
info: Elements `<special form 'Literal[42]'>` and `<class 'list[int]'>` in the union are not class objects
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
|
@ -82,7 +82,7 @@ error[invalid-argument-type]: Invalid second argument to `isinstance`
|
|||
13 | else:
|
||||
|
|
||||
info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects
|
||||
info: Element `typing.Any` in the union, and 2 more elements, are not class objects
|
||||
info: Element `<special form 'typing.Any'>` in the union, and 2 more elements, are not class objects
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO) for class `Baz` with bases list `[typing.Protocol[T], <class 'Foo'>, <class 'Bar[T@Baz]'>]`
|
||||
error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO) for class `Baz` with bases list `[<special form 'typing.Protocol[T]'>, <class 'Foo'>, <class 'Bar[T@Baz]'>]`
|
||||
--> src/mdtest_snippet.py:7:1
|
||||
|
|
||||
5 | class Foo(Protocol): ...
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error[call-non-callable]: Object of type `typing.Protocol` is not callable
|
||||
error[call-non-callable]: Object of type `<special form 'typing.Protocol'>` is not callable
|
||||
--> src/mdtest_snippet.py:4:13
|
||||
|
|
||||
3 | # error: [call-non-callable]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: special_form_attributes.md - Diagnostics for invalid attribute access on special forms
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import Any, Final, LiteralString, Self
|
||||
2 |
|
||||
3 | X = Any
|
||||
4 |
|
||||
5 | class Foo:
|
||||
6 | X: Final = LiteralString
|
||||
7 | a: int
|
||||
8 | b: Self
|
||||
9 |
|
||||
10 | class Bar:
|
||||
11 | def __init__(self):
|
||||
12 | self.y: Final = LiteralString
|
||||
13 |
|
||||
14 | X.foo # error: [unresolved-attribute]
|
||||
15 | X.aaaaooooooo # error: [unresolved-attribute]
|
||||
16 | Foo.X.startswith # error: [unresolved-attribute]
|
||||
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
|
||||
18 |
|
||||
19 | # TODO: false positive (just testing the diagnostic in the meantime)
|
||||
20 | Foo().b.a # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Special form `typing.Any` has no attribute `foo`
|
||||
--> src/mdtest_snippet.py:14:1
|
||||
|
|
||||
12 | self.y: Final = LiteralString
|
||||
13 |
|
||||
14 | X.foo # error: [unresolved-attribute]
|
||||
| ^^^^^
|
||||
15 | X.aaaaooooooo # error: [unresolved-attribute]
|
||||
16 | Foo.X.startswith # error: [unresolved-attribute]
|
||||
|
|
||||
help: Objects with type `Any` have a `foo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any`
|
||||
help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Special form `typing.Any` has no attribute `aaaaooooooo`
|
||||
--> src/mdtest_snippet.py:15:1
|
||||
|
|
||||
14 | X.foo # error: [unresolved-attribute]
|
||||
15 | X.aaaaooooooo # error: [unresolved-attribute]
|
||||
| ^^^^^^^^^^^^^
|
||||
16 | Foo.X.startswith # error: [unresolved-attribute]
|
||||
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
|
||||
|
|
||||
help: Objects with type `Any` have an `aaaaooooooo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any`
|
||||
help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Special form `typing.LiteralString` has no attribute `startswith`
|
||||
--> src/mdtest_snippet.py:16:1
|
||||
|
|
||||
14 | X.foo # error: [unresolved-attribute]
|
||||
15 | X.aaaaooooooo # error: [unresolved-attribute]
|
||||
16 | Foo.X.startswith # error: [unresolved-attribute]
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
|
||||
|
|
||||
help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString`
|
||||
help: This error may indicate that `Foo.X` was defined as `Foo.X = typing.LiteralString` when `Foo.X: typing.LiteralString` was intended
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Special form `typing.LiteralString` has no attribute `startswith`
|
||||
--> src/mdtest_snippet.py:17:1
|
||||
|
|
||||
15 | X.aaaaooooooo # error: [unresolved-attribute]
|
||||
16 | Foo.X.startswith # error: [unresolved-attribute]
|
||||
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
18 |
|
||||
19 | # TODO: false positive (just testing the diagnostic in the meantime)
|
||||
|
|
||||
help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString`
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Special form `typing.Self` has no attribute `a`
|
||||
--> src/mdtest_snippet.py:20:1
|
||||
|
|
||||
19 | # TODO: false positive (just testing the diagnostic in the meantime)
|
||||
20 | Foo().b.a # error: [unresolved-attribute]
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
|
@ -166,7 +166,7 @@ impl<'db> DunderAllNamesCollector<'db> {
|
|||
) -> Option<&'db FxHashSet<Name>> {
|
||||
let module_name =
|
||||
ModuleName::from_import_statement(self.db, self.file, import_from).ok()?;
|
||||
let module = resolve_module(self.db, &module_name)?;
|
||||
let module = resolve_module(self.db, self.file, &module_name)?;
|
||||
dunder_all_names(self.db, module.file(self.db)?)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ pub use diagnostic::add_inferred_python_version_hint_to_diagnostic;
|
|||
pub use module_name::{ModuleName, ModuleNameResolutionError};
|
||||
pub use module_resolver::{
|
||||
KnownModule, Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules,
|
||||
list_modules, resolve_module, resolve_real_module, resolve_real_shadowable_module,
|
||||
system_module_search_paths,
|
||||
list_modules, resolve_module, resolve_module_confident, resolve_real_module,
|
||||
resolve_real_module_confident, resolve_real_shadowable_module, system_module_search_paths,
|
||||
};
|
||||
pub use program::{
|
||||
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ pub use module::Module;
|
|||
pub use path::{SearchPath, SearchPathValidationError};
|
||||
pub use resolver::SearchPaths;
|
||||
pub(crate) use resolver::file_to_module;
|
||||
pub use resolver::{resolve_module, resolve_real_module, resolve_real_shadowable_module};
|
||||
pub use resolver::{
|
||||
resolve_module, resolve_module_confident, resolve_real_module, resolve_real_module_confident,
|
||||
resolve_real_shadowable_module,
|
||||
};
|
||||
use ruff_db::system::SystemPath;
|
||||
|
||||
use crate::Db;
|
||||
|
|
|
|||
|
|
@ -594,7 +594,7 @@ impl SearchPath {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_first_party(&self) -> bool {
|
||||
pub fn is_first_party(&self) -> bool {
|
||||
matches!(&*self.0, SearchPathInner::FirstParty(_))
|
||||
}
|
||||
|
||||
|
|
@ -608,6 +608,18 @@ impl SearchPath {
|
|||
|
||||
#[must_use]
|
||||
pub(crate) fn relativize_system_path(&self, path: &SystemPath) -> Option<ModulePath> {
|
||||
self.relativize_system_path_only(path)
|
||||
.map(|relative_path| ModulePath {
|
||||
search_path: self.clone(),
|
||||
relative_path: relative_path.as_utf8_path().to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn relativize_system_path_only<'a>(
|
||||
&self,
|
||||
path: &'a SystemPath,
|
||||
) -> Option<&'a SystemPath> {
|
||||
if path
|
||||
.extension()
|
||||
.is_some_and(|extension| !self.is_valid_extension(extension))
|
||||
|
|
@ -621,14 +633,7 @@ impl SearchPath {
|
|||
| SearchPathInner::StandardLibraryCustom(search_path)
|
||||
| SearchPathInner::StandardLibraryReal(search_path)
|
||||
| SearchPathInner::SitePackages(search_path)
|
||||
| SearchPathInner::Editable(search_path) => {
|
||||
path.strip_prefix(search_path)
|
||||
.ok()
|
||||
.map(|relative_path| ModulePath {
|
||||
search_path: self.clone(),
|
||||
relative_path: relative_path.as_utf8_path().to_path_buf(),
|
||||
})
|
||||
}
|
||||
| SearchPathInner::Editable(search_path) => path.strip_prefix(search_path).ok(),
|
||||
SearchPathInner::StandardLibraryVendored(_) => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -783,7 +788,7 @@ impl fmt::Display for SearchPath {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) enum SystemOrVendoredPathRef<'db> {
|
||||
System(&'db SystemPath),
|
||||
Vendored(&'db VendoredPath),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,31 @@
|
|||
/*!
|
||||
This module principally provides two routines for resolving a particular module
|
||||
name to a `Module`: [`resolve_module`] and [`resolve_real_module`]. You'll
|
||||
usually want the former, unless you're certain you want to forbid stubs, in
|
||||
which case, use the latter.
|
||||
This module principally provides several routines for resolving a particular module
|
||||
name to a `Module`:
|
||||
|
||||
* [`file_to_module`][]: resolves the module `.<self>` (often as the first step in resolving `.`)
|
||||
* [`resolve_module`][]: resolves an absolute module name
|
||||
|
||||
You may notice that we actually provide `resolve_(real)_(shadowable)_module_(confident)`.
|
||||
You almost certainly just want [`resolve_module`][]. The other variations represent
|
||||
restrictions to answer specific kinds of questions, usually to empower IDE features.
|
||||
|
||||
* The `real` variation disallows all stub files, including the vendored typeshed.
|
||||
This enables the goto-definition ("real") vs goto-declaration ("stub or real") distinction.
|
||||
|
||||
* The `confident` variation disallows "desperate resolution", which is a fallback
|
||||
mode where we start trying to use ancestor directories of the importing file
|
||||
as search-paths, but only if we failed to resolve it with the normal search-paths.
|
||||
This is mostly just a convenience for cases where we don't want to try to define
|
||||
the importing file (resolving a `KnownModule` and tests).
|
||||
|
||||
* The `shadowable` variation disables some guards that prevents third-party code
|
||||
from shadowing any vendored non-stdlib `KnownModule`. In particular `typing_extensions`,
|
||||
which we vendor and heavily assume the contents of (and so don't ever want to shadow).
|
||||
This enables checking if the user *actually* has `typing_extensions` installed,
|
||||
in which case it's ok to suggest it in features like auto-imports.
|
||||
|
||||
There is some awkwardness to the structure of the code to specifically enable caching
|
||||
of queries, as module resolution happens a lot and involves a lot of disk access.
|
||||
|
||||
For implementors, see `import-resolution-diagram.svg` for a flow diagram that
|
||||
specifies ty's implementation of Python's import resolution algorithm.
|
||||
|
|
@ -33,14 +56,51 @@ use super::module::{Module, ModuleKind};
|
|||
use super::path::{ModulePath, SearchPath, SearchPathValidationError, SystemOrVendoredPathRef};
|
||||
|
||||
/// Resolves a module name to a module.
|
||||
pub fn resolve_module<'db>(db: &'db dyn Db, module_name: &ModuleName) -> Option<Module<'db>> {
|
||||
pub fn resolve_module<'db>(
|
||||
db: &'db dyn Db,
|
||||
importing_file: File,
|
||||
module_name: &ModuleName,
|
||||
) -> Option<Module<'db>> {
|
||||
let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::StubsAllowed);
|
||||
|
||||
resolve_module_query(db, interned_name)
|
||||
.or_else(|| desperately_resolve_module(db, importing_file, interned_name))
|
||||
}
|
||||
|
||||
/// Resolves a module name to a module, without desperate resolution available.
|
||||
///
|
||||
/// This is appropriate for resolving a `KnownModule`, or cases where for whatever reason
|
||||
/// we don't have a well-defined importing file.
|
||||
pub fn resolve_module_confident<'db>(
|
||||
db: &'db dyn Db,
|
||||
module_name: &ModuleName,
|
||||
) -> Option<Module<'db>> {
|
||||
let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::StubsAllowed);
|
||||
|
||||
resolve_module_query(db, interned_name)
|
||||
}
|
||||
|
||||
/// Resolves a module name to a module (stubs not allowed).
|
||||
pub fn resolve_real_module<'db>(db: &'db dyn Db, module_name: &ModuleName) -> Option<Module<'db>> {
|
||||
pub fn resolve_real_module<'db>(
|
||||
db: &'db dyn Db,
|
||||
importing_file: File,
|
||||
module_name: &ModuleName,
|
||||
) -> Option<Module<'db>> {
|
||||
let interned_name =
|
||||
ModuleNameIngredient::new(db, module_name, ModuleResolveMode::StubsNotAllowed);
|
||||
|
||||
resolve_module_query(db, interned_name)
|
||||
.or_else(|| desperately_resolve_module(db, importing_file, interned_name))
|
||||
}
|
||||
|
||||
/// Resolves a module name to a module, without desperate resolution available (stubs not allowed).
|
||||
///
|
||||
/// This is appropriate for resolving a `KnownModule`, or cases where for whatever reason
|
||||
/// we don't have a well-defined importing file.
|
||||
pub fn resolve_real_module_confident<'db>(
|
||||
db: &'db dyn Db,
|
||||
module_name: &ModuleName,
|
||||
) -> Option<Module<'db>> {
|
||||
let interned_name =
|
||||
ModuleNameIngredient::new(db, module_name, ModuleResolveMode::StubsNotAllowed);
|
||||
|
||||
|
|
@ -60,6 +120,7 @@ pub fn resolve_real_module<'db>(db: &'db dyn Db, module_name: &ModuleName) -> Op
|
|||
/// are involved in an import cycle with `builtins`.
|
||||
pub fn resolve_real_shadowable_module<'db>(
|
||||
db: &'db dyn Db,
|
||||
importing_file: File,
|
||||
module_name: &ModuleName,
|
||||
) -> Option<Module<'db>> {
|
||||
let interned_name = ModuleNameIngredient::new(
|
||||
|
|
@ -69,6 +130,7 @@ pub fn resolve_real_shadowable_module<'db>(
|
|||
);
|
||||
|
||||
resolve_module_query(db, interned_name)
|
||||
.or_else(|| desperately_resolve_module(db, importing_file, interned_name))
|
||||
}
|
||||
|
||||
/// Which files should be visible when doing a module query
|
||||
|
|
@ -181,6 +243,55 @@ fn resolve_module_query<'db>(
|
|||
Some(module)
|
||||
}
|
||||
|
||||
/// Like `resolve_module_query` but for cases where it failed to resolve the module
|
||||
/// and we are now Getting Desperate and willing to try the ancestor directories of
|
||||
/// the `importing_file` as potential temporary search paths that are private
|
||||
/// to this import.
|
||||
///
|
||||
/// The reason this is split out is because in 99.9% of cases `resolve_module_query`
|
||||
/// will find the right answer (or no valid answer exists), and we want it to be
|
||||
/// aggressively cached. Including the `importing_file` as part of that query would
|
||||
/// trash the caching of import resolution between files.
|
||||
///
|
||||
/// TODO: should (some) of this also be cached? If an entire directory of python files
|
||||
/// is misunderstood we'll end up in here a lot.
|
||||
fn desperately_resolve_module<'db>(
|
||||
db: &'db dyn Db,
|
||||
importing_file: File,
|
||||
module_name: ModuleNameIngredient<'db>,
|
||||
) -> Option<Module<'db>> {
|
||||
let name = module_name.name(db);
|
||||
let mode = module_name.mode(db);
|
||||
let _span = tracing::trace_span!("desperately_resolve_module", %name).entered();
|
||||
|
||||
let Some(resolved) = desperately_resolve_name(db, importing_file, name, mode) else {
|
||||
tracing::debug!("Module `{name}` not found while looking in parent dirs");
|
||||
return None;
|
||||
};
|
||||
|
||||
let module = match resolved {
|
||||
ResolvedName::FileModule(module) => {
|
||||
tracing::trace!(
|
||||
"Resolved module `{name}` to `{path}`",
|
||||
path = module.file.path(db)
|
||||
);
|
||||
Module::file_module(
|
||||
db,
|
||||
name.clone(),
|
||||
module.kind,
|
||||
module.search_path,
|
||||
module.file,
|
||||
)
|
||||
}
|
||||
ResolvedName::NamespacePackage => {
|
||||
tracing::trace!("Module `{name}` is a namespace package");
|
||||
Module::namespace_package(db, name.clone())
|
||||
}
|
||||
};
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
/// Resolves the module for the given path.
|
||||
///
|
||||
/// Returns `None` if the path is not a module locatable via any of the known search paths.
|
||||
|
|
@ -201,13 +312,33 @@ pub(crate) fn path_to_module<'db>(db: &'db dyn Db, path: &FilePath) -> Option<Mo
|
|||
/// Resolves the module for the file with the given id.
|
||||
///
|
||||
/// Returns `None` if the file is not a module locatable via any of the known search paths.
|
||||
///
|
||||
/// This function can be understood as essentially resolving `import .<self>` in the file itself,
|
||||
/// and indeed, one of its primary jobs is resolving `.<self>` to derive the module name of `.`.
|
||||
/// This intuition is particularly useful for understanding why it's correct that we pass
|
||||
/// the file itself as `importing_file` to various subroutines.
|
||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
||||
pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
|
||||
let _span = tracing::trace_span!("file_to_module", ?file).entered();
|
||||
|
||||
let path = SystemOrVendoredPathRef::try_from_file(db, file)?;
|
||||
|
||||
let module_name = search_paths(db, ModuleResolveMode::StubsAllowed).find_map(|candidate| {
|
||||
file_to_module_impl(
|
||||
db,
|
||||
file,
|
||||
path,
|
||||
search_paths(db, ModuleResolveMode::StubsAllowed),
|
||||
)
|
||||
.or_else(|| file_to_module_impl(db, file, path, desperate_search_paths(db, file).iter()))
|
||||
}
|
||||
|
||||
fn file_to_module_impl<'db, 'a>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
path: SystemOrVendoredPathRef<'a>,
|
||||
mut search_paths: impl Iterator<Item = &'a SearchPath>,
|
||||
) -> Option<Module<'db>> {
|
||||
let module_name = search_paths.find_map(|candidate: &SearchPath| {
|
||||
let relative_path = match path {
|
||||
SystemOrVendoredPathRef::System(path) => candidate.relativize_system_path(path),
|
||||
SystemOrVendoredPathRef::Vendored(path) => candidate.relativize_vendored_path(path),
|
||||
|
|
@ -219,7 +350,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
|
|||
// If it doesn't, then that means that multiple modules have the same name in different
|
||||
// root paths, but that the module corresponding to `path` is in a lower priority search path,
|
||||
// in which case we ignore it.
|
||||
let module = resolve_module(db, &module_name)?;
|
||||
let module = resolve_module(db, file, &module_name)?;
|
||||
let module_file = module.file(db)?;
|
||||
|
||||
if file.path(db) == module_file.path(db) {
|
||||
|
|
@ -230,7 +361,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
|
|||
// If a .py and .pyi are both defined, the .pyi will be the one returned by `resolve_module().file`,
|
||||
// which would make us erroneously believe the `.py` is *not* also this module (breaking things
|
||||
// like relative imports). So here we try `resolve_real_module().file` to cover both cases.
|
||||
let module = resolve_real_module(db, &module_name)?;
|
||||
let module = resolve_real_module(db, file, &module_name)?;
|
||||
let module_file = module.file(db)?;
|
||||
if file.path(db) == module_file.path(db) {
|
||||
return Some(module);
|
||||
|
|
@ -250,6 +381,58 @@ pub(crate) fn search_paths(db: &dyn Db, resolve_mode: ModuleResolveMode) -> Sear
|
|||
Program::get(db).search_paths(db).iter(db, resolve_mode)
|
||||
}
|
||||
|
||||
/// Get the search-paths that should be used for desperate resolution of imports in this file
|
||||
///
|
||||
/// Currently this is "the closest ancestor dir that contains a pyproject.toml", which is
|
||||
/// a completely arbitrary decision. We could potentially change this to return an iterator
|
||||
/// of every ancestor with a pyproject.toml or every ancestor.
|
||||
///
|
||||
/// For now this works well in common cases where we have some larger workspace that contains
|
||||
/// one or more python projects in sub-directories, and those python projects assume that
|
||||
/// absolute imports resolve relative to the pyproject.toml they live under.
|
||||
///
|
||||
/// Being so strict minimizes concerns about this going off a lot and doing random
|
||||
/// chaotic things. In particular, all files under a given pyproject.toml will currently
|
||||
/// agree on this being their desperate search-path, which is really nice.
|
||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
||||
fn desperate_search_paths(db: &dyn Db, importing_file: File) -> Option<SearchPath> {
|
||||
let system = db.system();
|
||||
let importing_path = importing_file.path(db).as_system_path()?;
|
||||
|
||||
// Only allow this if the importing_file is under the first-party search path
|
||||
let (base_path, rel_path) =
|
||||
search_paths(db, ModuleResolveMode::StubsAllowed).find_map(|search_path| {
|
||||
if !search_path.is_first_party() {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
search_path.as_system_path()?,
|
||||
search_path.relativize_system_path_only(importing_path)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
// Read the revision on the corresponding file root to
|
||||
// register an explicit dependency on this directory. When
|
||||
// the revision gets bumped, the cache that Salsa creates
|
||||
// for this routine will be invalidated.
|
||||
//
|
||||
// (This is conditional because ruff uses this code too and doesn't set roots)
|
||||
if let Some(root) = db.files().root(db, base_path) {
|
||||
let _ = root.revision(db);
|
||||
}
|
||||
|
||||
// Only allow searching up to the first-party path's root
|
||||
for rel_dir in rel_path.ancestors() {
|
||||
let candidate_path = base_path.join(rel_dir);
|
||||
if system.path_exists(&candidate_path.join("pyproject.toml"))
|
||||
|| system.path_exists(&candidate_path.join("ty.toml"))
|
||||
{
|
||||
let search_path = SearchPath::first_party(system, candidate_path).ok()?;
|
||||
return Some(search_path);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)]
|
||||
pub struct SearchPaths {
|
||||
/// Search paths that have been statically determined purely from reading
|
||||
|
|
@ -756,6 +939,30 @@ struct ModuleNameIngredient<'db> {
|
|||
/// Given a module name and a list of search paths in which to lookup modules,
|
||||
/// attempt to resolve the module name
|
||||
fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Option<ResolvedName> {
|
||||
let search_paths = search_paths(db, mode);
|
||||
resolve_name_impl(db, name, mode, search_paths)
|
||||
}
|
||||
|
||||
/// Like `resolve_name` but for cases where it failed to resolve the module
|
||||
/// and we are now Getting Desperate and willing to try the ancestor directories of
|
||||
/// the `importing_file` as potential temporary search paths that are private
|
||||
/// to this import.
|
||||
fn desperately_resolve_name(
|
||||
db: &dyn Db,
|
||||
importing_file: File,
|
||||
name: &ModuleName,
|
||||
mode: ModuleResolveMode,
|
||||
) -> Option<ResolvedName> {
|
||||
let search_paths = desperate_search_paths(db, importing_file);
|
||||
resolve_name_impl(db, name, mode, search_paths.iter())
|
||||
}
|
||||
|
||||
fn resolve_name_impl<'a>(
|
||||
db: &dyn Db,
|
||||
name: &ModuleName,
|
||||
mode: ModuleResolveMode,
|
||||
search_paths: impl Iterator<Item = &'a SearchPath>,
|
||||
) -> Option<ResolvedName> {
|
||||
let program = Program::get(db);
|
||||
let python_version = program.python_version(db);
|
||||
let resolver_state = ResolverContext::new(db, python_version, mode);
|
||||
|
|
@ -765,7 +972,7 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti
|
|||
let stub_name = name.to_stub_package();
|
||||
let mut is_namespace_package = false;
|
||||
|
||||
for search_path in search_paths(db, mode) {
|
||||
for search_path in search_paths {
|
||||
// When a builtin module is imported, standard module resolution is bypassed:
|
||||
// the module name always resolves to the stdlib module,
|
||||
// even if there's a module of the same name in the first-party root
|
||||
|
|
@ -1409,11 +1616,11 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(&foo_module),
|
||||
resolve_module(&db, &foo_module_name).as_ref()
|
||||
resolve_module_confident(&db, &foo_module_name).as_ref()
|
||||
);
|
||||
|
||||
assert_eq!("foo", foo_module.name(&db));
|
||||
|
|
@ -1435,11 +1642,11 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(&foo_module),
|
||||
resolve_module(&db, &foo_module_name).as_ref()
|
||||
resolve_module_confident(&db, &foo_module_name).as_ref()
|
||||
);
|
||||
|
||||
assert_eq!("foo", foo_module.name(&db));
|
||||
|
|
@ -1467,11 +1674,11 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(&foo_module),
|
||||
resolve_module(&db, &foo_module_name).as_ref()
|
||||
resolve_module_confident(&db, &foo_module_name).as_ref()
|
||||
);
|
||||
|
||||
assert_eq!("foo", foo_module.name(&db));
|
||||
|
|
@ -1494,7 +1701,8 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
let builtins = resolve_module(&db, &builtins_module_name).expect("builtins to resolve");
|
||||
let builtins =
|
||||
resolve_module_confident(&db, &builtins_module_name).expect("builtins to resolve");
|
||||
|
||||
assert_eq!(
|
||||
builtins.file(&db).unwrap().path(&db),
|
||||
|
|
@ -1518,7 +1726,8 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
let builtins = resolve_module(&db, &builtins_module_name).expect("builtins to resolve");
|
||||
let builtins =
|
||||
resolve_module_confident(&db, &builtins_module_name).expect("builtins to resolve");
|
||||
|
||||
assert_eq!(
|
||||
builtins.file(&db).unwrap().path(&db),
|
||||
|
|
@ -1539,11 +1748,11 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(&functools_module),
|
||||
resolve_module(&db, &functools_module_name).as_ref()
|
||||
resolve_module_confident(&db, &functools_module_name).as_ref()
|
||||
);
|
||||
|
||||
assert_eq!(&stdlib, functools_module.search_path(&db).unwrap());
|
||||
|
|
@ -1596,9 +1805,10 @@ mod tests {
|
|||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
for module_name in existing_modules {
|
||||
let resolved_module = resolve_module(&db, &module_name).unwrap_or_else(|| {
|
||||
panic!("Expected module {module_name} to exist in the mock stdlib")
|
||||
});
|
||||
let resolved_module =
|
||||
resolve_module_confident(&db, &module_name).unwrap_or_else(|| {
|
||||
panic!("Expected module {module_name} to exist in the mock stdlib")
|
||||
});
|
||||
let search_path = resolved_module.search_path(&db).unwrap();
|
||||
assert_eq!(
|
||||
&stdlib, search_path,
|
||||
|
|
@ -1649,7 +1859,7 @@ mod tests {
|
|||
|
||||
for module_name in nonexisting_modules {
|
||||
assert!(
|
||||
resolve_module(&db, &module_name).is_none(),
|
||||
resolve_module_confident(&db, &module_name).is_none(),
|
||||
"Unexpectedly resolved a module for {module_name}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1692,9 +1902,10 @@ mod tests {
|
|||
]);
|
||||
|
||||
for module_name in existing_modules {
|
||||
let resolved_module = resolve_module(&db, &module_name).unwrap_or_else(|| {
|
||||
panic!("Expected module {module_name} to exist in the mock stdlib")
|
||||
});
|
||||
let resolved_module =
|
||||
resolve_module_confident(&db, &module_name).unwrap_or_else(|| {
|
||||
panic!("Expected module {module_name} to exist in the mock stdlib")
|
||||
});
|
||||
let search_path = resolved_module.search_path(&db).unwrap();
|
||||
assert_eq!(
|
||||
&stdlib, search_path,
|
||||
|
|
@ -1728,7 +1939,7 @@ mod tests {
|
|||
let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
|
||||
for module_name in nonexisting_modules {
|
||||
assert!(
|
||||
resolve_module(&db, &module_name).is_none(),
|
||||
resolve_module_confident(&db, &module_name).is_none(),
|
||||
"Unexpectedly resolved a module for {module_name}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1750,11 +1961,11 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(&functools_module),
|
||||
resolve_module(&db, &functools_module_name).as_ref()
|
||||
resolve_module_confident(&db, &functools_module_name).as_ref()
|
||||
);
|
||||
assert_eq!(&src, functools_module.search_path(&db).unwrap());
|
||||
assert_eq!(ModuleKind::Module, functools_module.kind(&db));
|
||||
|
|
@ -1777,7 +1988,7 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
|
||||
let pydoc_data_topics = resolve_module(&db, &pydoc_data_topics_name).unwrap();
|
||||
let pydoc_data_topics = resolve_module_confident(&db, &pydoc_data_topics_name).unwrap();
|
||||
|
||||
assert_eq!("pydoc_data.topics", pydoc_data_topics.name(&db));
|
||||
assert_eq!(pydoc_data_topics.search_path(&db).unwrap(), &stdlib);
|
||||
|
|
@ -1794,7 +2005,8 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let foo_path = src.join("foo/__init__.py");
|
||||
let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_module =
|
||||
resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
|
||||
assert_eq!("foo", foo_module.name(&db));
|
||||
assert_eq!(&src, foo_module.search_path(&db).unwrap());
|
||||
|
|
@ -1821,7 +2033,8 @@ mod tests {
|
|||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_module =
|
||||
resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_init_path = src.join("foo/__init__.py");
|
||||
|
||||
assert_eq!(&src, foo_module.search_path(&db).unwrap());
|
||||
|
|
@ -1844,8 +2057,9 @@ mod tests {
|
|||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let foo = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_real = resolve_real_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo = resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_real =
|
||||
resolve_real_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_stub = src.join("foo.pyi");
|
||||
|
||||
assert_eq!(&src, foo.search_path(&db).unwrap());
|
||||
|
|
@ -1870,7 +2084,7 @@ mod tests {
|
|||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let baz_module =
|
||||
resolve_module(&db, &ModuleName::new_static("foo.bar.baz").unwrap()).unwrap();
|
||||
resolve_module_confident(&db, &ModuleName::new_static("foo.bar.baz").unwrap()).unwrap();
|
||||
let baz_path = src.join("foo/bar/baz.py");
|
||||
|
||||
assert_eq!(&src, baz_module.search_path(&db).unwrap());
|
||||
|
|
@ -1894,7 +2108,8 @@ mod tests {
|
|||
.with_site_packages_files(&[("foo.py", "")])
|
||||
.build();
|
||||
|
||||
let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_module =
|
||||
resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_src_path = src.join("foo.py");
|
||||
|
||||
assert_eq!(&src, foo_module.search_path(&db).unwrap());
|
||||
|
|
@ -1965,8 +2180,10 @@ mod tests {
|
|||
},
|
||||
);
|
||||
|
||||
let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let bar_module = resolve_module(&db, &ModuleName::new_static("bar").unwrap()).unwrap();
|
||||
let foo_module =
|
||||
resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let bar_module =
|
||||
resolve_module_confident(&db, &ModuleName::new_static("bar").unwrap()).unwrap();
|
||||
|
||||
assert_ne!(foo_module, bar_module);
|
||||
|
||||
|
|
@ -2001,7 +2218,7 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
let foo_pieces = (
|
||||
foo_module.name(&db).clone(),
|
||||
foo_module.file(&db),
|
||||
|
|
@ -2022,7 +2239,7 @@ mod tests {
|
|||
// Re-query the foo module. The foo module should still be cached
|
||||
// because `bar.py` isn't relevant for resolving `foo`.
|
||||
|
||||
let foo_module2 = resolve_module(&db, &foo_module_name);
|
||||
let foo_module2 = resolve_module_confident(&db, &foo_module_name);
|
||||
let foo_pieces2 = foo_module2.map(|foo_module2| {
|
||||
(
|
||||
foo_module2.name(&db).clone(),
|
||||
|
|
@ -2049,14 +2266,15 @@ mod tests {
|
|||
let foo_path = src.join("foo.py");
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
assert_eq!(resolve_module(&db, &foo_module_name), None);
|
||||
assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
|
||||
|
||||
// Now write the foo file
|
||||
db.write_file(&foo_path, "x = 1")?;
|
||||
|
||||
let foo_file = system_path_to_file(&db, &foo_path).expect("foo.py to exist");
|
||||
|
||||
let foo_module = resolve_module(&db, &foo_module_name).expect("Foo module to resolve");
|
||||
let foo_module =
|
||||
resolve_module_confident(&db, &foo_module_name).expect("Foo module to resolve");
|
||||
assert_eq!(foo_file, foo_module.file(&db).unwrap());
|
||||
|
||||
Ok(())
|
||||
|
|
@ -2070,7 +2288,8 @@ mod tests {
|
|||
let TestCase { mut db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).expect("foo module to exist");
|
||||
let foo_module =
|
||||
resolve_module_confident(&db, &foo_module_name).expect("foo module to exist");
|
||||
let foo_init_path = src.join("foo/__init__.py");
|
||||
|
||||
assert_eq!(&foo_init_path, foo_module.file(&db).unwrap().path(&db));
|
||||
|
|
@ -2082,7 +2301,8 @@ mod tests {
|
|||
File::sync_path(&mut db, &foo_init_path);
|
||||
File::sync_path(&mut db, foo_init_path.parent().unwrap());
|
||||
|
||||
let foo_module = resolve_module(&db, &foo_module_name).expect("Foo module to resolve");
|
||||
let foo_module =
|
||||
resolve_module_confident(&db, &foo_module_name).expect("Foo module to resolve");
|
||||
assert_eq!(&src.join("foo.py"), foo_module.file(&db).unwrap().path(&db));
|
||||
|
||||
Ok(())
|
||||
|
|
@ -2108,7 +2328,7 @@ mod tests {
|
|||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let stdlib_functools_path = stdlib.join("functools.pyi");
|
||||
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file(&db).unwrap()),
|
||||
|
|
@ -2121,7 +2341,7 @@ mod tests {
|
|||
let site_packages_functools_path = site_packages.join("functools.py");
|
||||
db.write_file(&site_packages_functools_path, "f: int")
|
||||
.unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
let functools_file = functools_module.file(&db).unwrap();
|
||||
let functools_search_path = functools_module.search_path(&db).unwrap().clone();
|
||||
let events = db.take_salsa_events();
|
||||
|
|
@ -2156,7 +2376,7 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file(&db).unwrap()),
|
||||
|
|
@ -2167,7 +2387,7 @@ mod tests {
|
|||
// since first-party files take higher priority in module resolution:
|
||||
let src_functools_path = src.join("functools.py");
|
||||
db.write_file(&src_functools_path, "FOO: int").unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(&db).unwrap(), &src);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file(&db).unwrap()),
|
||||
|
|
@ -2198,7 +2418,7 @@ mod tests {
|
|||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let src_functools_path = src.join("functools.py");
|
||||
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(&db).unwrap(), &src);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file(&db).unwrap()),
|
||||
|
|
@ -2211,7 +2431,7 @@ mod tests {
|
|||
.remove_file(&src_functools_path)
|
||||
.unwrap();
|
||||
File::sync_path(&mut db, &src_functools_path);
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file(&db).unwrap()),
|
||||
|
|
@ -2233,8 +2453,8 @@ mod tests {
|
|||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap();
|
||||
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_bar_module = resolve_module(&db, &foo_bar_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
let foo_bar_module = resolve_module_confident(&db, &foo_bar_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
foo_module.file(&db).unwrap().path(&db),
|
||||
|
|
@ -2262,11 +2482,11 @@ mod tests {
|
|||
|
||||
// Lines with leading whitespace in `.pth` files do not parse:
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
assert_eq!(resolve_module(&db, &foo_module_name), None);
|
||||
assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
|
||||
|
||||
// Lines with trailing whitespace in `.pth` files do:
|
||||
let bar_module_name = ModuleName::new_static("bar").unwrap();
|
||||
let bar_module = resolve_module(&db, &bar_module_name).unwrap();
|
||||
let bar_module = resolve_module_confident(&db, &bar_module_name).unwrap();
|
||||
assert_eq!(
|
||||
bar_module.file(&db).unwrap().path(&db),
|
||||
&FilePath::system("/y/src/bar.py")
|
||||
|
|
@ -2285,7 +2505,7 @@ mod tests {
|
|||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
foo_module.file(&db).unwrap().path(&db),
|
||||
|
|
@ -2333,10 +2553,10 @@ not_a_directory
|
|||
let b_module_name = ModuleName::new_static("b").unwrap();
|
||||
let spam_module_name = ModuleName::new_static("spam").unwrap();
|
||||
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let a_module = resolve_module(&db, &a_module_name).unwrap();
|
||||
let b_module = resolve_module(&db, &b_module_name).unwrap();
|
||||
let spam_module = resolve_module(&db, &spam_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
let a_module = resolve_module_confident(&db, &a_module_name).unwrap();
|
||||
let b_module = resolve_module_confident(&db, &b_module_name).unwrap();
|
||||
let spam_module = resolve_module_confident(&db, &spam_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
foo_module.file(&db).unwrap().path(&db),
|
||||
|
|
@ -2370,14 +2590,14 @@ not_a_directory
|
|||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let bar_module_name = ModuleName::new_static("bar").unwrap();
|
||||
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
assert_eq!(
|
||||
foo_module.file(&db).unwrap().path(&db),
|
||||
&FilePath::system("/x/src/foo.py")
|
||||
);
|
||||
|
||||
db.clear_salsa_events();
|
||||
let bar_module = resolve_module(&db, &bar_module_name).unwrap();
|
||||
let bar_module = resolve_module_confident(&db, &bar_module_name).unwrap();
|
||||
assert_eq!(
|
||||
bar_module.file(&db).unwrap().path(&db),
|
||||
&FilePath::system("/y/src/bar.py")
|
||||
|
|
@ -2407,7 +2627,7 @@ not_a_directory
|
|||
db.write_files(x_directory).unwrap();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
assert_eq!(
|
||||
foo_module.file(&db).unwrap().path(&db),
|
||||
&FilePath::system("/x/src/foo.py")
|
||||
|
|
@ -2419,7 +2639,7 @@ not_a_directory
|
|||
|
||||
File::sync_path(&mut db, &site_packages.join("_foo.pth"));
|
||||
|
||||
assert_eq!(resolve_module(&db, &foo_module_name), None);
|
||||
assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2434,7 +2654,7 @@ not_a_directory
|
|||
db.write_files(x_directory).unwrap();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
|
||||
let src_path = SystemPathBuf::from("/x/src");
|
||||
assert_eq!(
|
||||
foo_module.file(&db).unwrap().path(&db),
|
||||
|
|
@ -2447,7 +2667,7 @@ not_a_directory
|
|||
db.memory_file_system().remove_directory(&src_path).unwrap();
|
||||
File::sync_path(&mut db, &src_path.join("foo.py"));
|
||||
File::sync_path(&mut db, &src_path);
|
||||
assert_eq!(resolve_module(&db, &foo_module_name), None);
|
||||
assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2507,7 +2727,7 @@ not_a_directory
|
|||
// The editable installs discovered from the `.pth` file in the first `site-packages` directory
|
||||
// take precedence over the second `site-packages` directory...
|
||||
let a_module_name = ModuleName::new_static("a").unwrap();
|
||||
let a_module = resolve_module(&db, &a_module_name).unwrap();
|
||||
let a_module = resolve_module_confident(&db, &a_module_name).unwrap();
|
||||
assert_eq!(
|
||||
a_module.file(&db).unwrap().path(&db),
|
||||
&editable_install_location
|
||||
|
|
@ -2521,7 +2741,7 @@ not_a_directory
|
|||
// ...But now that the `.pth` file in the first `site-packages` directory has been deleted,
|
||||
// the editable install no longer exists, so the module now resolves to the file in the
|
||||
// second `site-packages` directory
|
||||
let a_module = resolve_module(&db, &a_module_name).unwrap();
|
||||
let a_module = resolve_module_confident(&db, &a_module_name).unwrap();
|
||||
assert_eq!(
|
||||
a_module.file(&db).unwrap().path(&db),
|
||||
&system_site_packages_location
|
||||
|
|
@ -2579,12 +2799,12 @@ not_a_directory
|
|||
|
||||
// Now try to resolve the module `A` (note the capital `A` instead of `a`).
|
||||
let a_module_name = ModuleName::new_static("A").unwrap();
|
||||
assert_eq!(resolve_module(&db, &a_module_name), None);
|
||||
assert_eq!(resolve_module_confident(&db, &a_module_name), None);
|
||||
|
||||
// Now lookup the same module using the lowercase `a` and it should
|
||||
// resolve to the file in the system site-packages
|
||||
let a_module_name = ModuleName::new_static("a").unwrap();
|
||||
let a_module = resolve_module(&db, &a_module_name).expect("a.py to resolve");
|
||||
let a_module = resolve_module_confident(&db, &a_module_name).expect("a.py to resolve");
|
||||
assert!(
|
||||
a_module
|
||||
.file(&db)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use ruff_db::files::File;
|
||||
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::module_resolver::{KnownModule, file_to_module};
|
||||
use crate::module_resolver::{KnownModule, file_to_module, resolve_module_confident};
|
||||
use crate::semantic_index::definition::{Definition, DefinitionState};
|
||||
use crate::semantic_index::place::{PlaceExprRef, ScopedPlaceId};
|
||||
use crate::semantic_index::scope::ScopeId;
|
||||
|
|
@ -14,7 +14,7 @@ use crate::types::{
|
|||
Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, binding_type,
|
||||
declaration_type, todo_type,
|
||||
};
|
||||
use crate::{Db, FxOrderSet, Program, resolve_module};
|
||||
use crate::{Db, FxOrderSet, Program};
|
||||
|
||||
pub(crate) use implicit_globals::{
|
||||
module_type_implicit_global_declaration, module_type_implicit_global_symbol,
|
||||
|
|
@ -379,7 +379,7 @@ pub(crate) fn imported_symbol<'db>(
|
|||
/// and should not be used when a symbol is being explicitly imported from the `builtins` module
|
||||
/// (e.g. `from builtins import int`).
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> {
|
||||
resolve_module(db, &KnownModule::Builtins.name())
|
||||
resolve_module_confident(db, &KnownModule::Builtins.name())
|
||||
.and_then(|module| {
|
||||
let file = module.file(db)?;
|
||||
Some(
|
||||
|
|
@ -409,7 +409,7 @@ pub(crate) fn known_module_symbol<'db>(
|
|||
known_module: KnownModule,
|
||||
symbol: &str,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
resolve_module(db, &known_module.name())
|
||||
resolve_module_confident(db, &known_module.name())
|
||||
.and_then(|module| {
|
||||
let file = module.file(db)?;
|
||||
Some(imported_symbol(db, file, symbol, None))
|
||||
|
|
@ -448,7 +448,7 @@ pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
|||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
|
||||
fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option<ScopeId<'_>> {
|
||||
let module = resolve_module(db, &core_module.name())?;
|
||||
let module = resolve_module_confident(db, &core_module.name())?;
|
||||
Some(global_scope(db, module.file(db)?))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1582,7 +1582,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||
continue;
|
||||
};
|
||||
|
||||
let Some(module) = resolve_module(self.db, &module_name) else {
|
||||
let Some(module) = resolve_module(self.db, self.file, &module_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
|
@ -1616,9 +1616,12 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||
|
||||
let star_import_predicate = self.add_predicate(star_import.into());
|
||||
|
||||
let associated_member_ids = self.place_tables[self.current_scope()]
|
||||
.associated_place_ids(ScopedPlaceId::Symbol(symbol_id));
|
||||
let pre_definition = self
|
||||
.current_use_def_map()
|
||||
.single_symbol_place_snapshot(symbol_id);
|
||||
.single_symbol_snapshot(symbol_id, associated_member_ids);
|
||||
|
||||
let pre_definition_reachability =
|
||||
self.current_use_def_map().reachability;
|
||||
|
||||
|
|
|
|||
|
|
@ -250,7 +250,9 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||
for export in
|
||||
ModuleName::from_import_statement(self.db, self.file, node)
|
||||
.ok()
|
||||
.and_then(|module_name| resolve_module(self.db, &module_name))
|
||||
.and_then(|module_name| {
|
||||
resolve_module(self.db, self.file, &module_name)
|
||||
})
|
||||
.iter()
|
||||
.flat_map(|module| {
|
||||
module
|
||||
|
|
|
|||
|
|
@ -801,6 +801,13 @@ pub(super) struct FlowSnapshot {
|
|||
reachability: ScopedReachabilityConstraintId,
|
||||
}
|
||||
|
||||
/// A snapshot of the state of a single symbol (e.g. `obj`) and all of its associated members
|
||||
/// (e.g. `obj.attr`, `obj["key"]`).
|
||||
pub(super) struct SingleSymbolSnapshot {
|
||||
symbol_state: PlaceState,
|
||||
associated_member_states: FxHashMap<ScopedMemberId, PlaceState>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Append-only array of [`DefinitionState`].
|
||||
|
|
@ -991,13 +998,26 @@ impl<'db> UseDefMapBuilder<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Snapshot the state of a single place at the current point in control flow.
|
||||
/// Snapshot the state of a single symbol and all of its associated members, at the current
|
||||
/// point in control flow.
|
||||
///
|
||||
/// This is only used for `*`-import reachability constraints, which are handled differently
|
||||
/// to most other reachability constraints. See the doc-comment for
|
||||
/// [`Self::record_and_negate_star_import_reachability_constraint`] for more details.
|
||||
pub(super) fn single_symbol_place_snapshot(&self, symbol: ScopedSymbolId) -> PlaceState {
|
||||
self.symbol_states[symbol].clone()
|
||||
pub(super) fn single_symbol_snapshot(
|
||||
&self,
|
||||
symbol: ScopedSymbolId,
|
||||
associated_member_ids: &[ScopedMemberId],
|
||||
) -> SingleSymbolSnapshot {
|
||||
let symbol_state = self.symbol_states[symbol].clone();
|
||||
let mut associated_member_states = FxHashMap::default();
|
||||
for &member_id in associated_member_ids {
|
||||
associated_member_states.insert(member_id, self.member_states[member_id].clone());
|
||||
}
|
||||
SingleSymbolSnapshot {
|
||||
symbol_state,
|
||||
associated_member_states,
|
||||
}
|
||||
}
|
||||
|
||||
/// This method exists solely for handling `*`-import reachability constraints.
|
||||
|
|
@ -1033,14 +1053,14 @@ impl<'db> UseDefMapBuilder<'db> {
|
|||
&mut self,
|
||||
reachability_id: ScopedReachabilityConstraintId,
|
||||
symbol: ScopedSymbolId,
|
||||
pre_definition_state: PlaceState,
|
||||
pre_definition: SingleSymbolSnapshot,
|
||||
) {
|
||||
let negated_reachability_id = self
|
||||
.reachability_constraints
|
||||
.add_not_constraint(reachability_id);
|
||||
|
||||
let mut post_definition_state =
|
||||
std::mem::replace(&mut self.symbol_states[symbol], pre_definition_state);
|
||||
std::mem::replace(&mut self.symbol_states[symbol], pre_definition.symbol_state);
|
||||
|
||||
post_definition_state
|
||||
.record_reachability_constraint(&mut self.reachability_constraints, reachability_id);
|
||||
|
|
@ -1055,6 +1075,30 @@ impl<'db> UseDefMapBuilder<'db> {
|
|||
&mut self.narrowing_constraints,
|
||||
&mut self.reachability_constraints,
|
||||
);
|
||||
|
||||
// And similarly for all associated members:
|
||||
for (member_id, pre_definition_member_state) in pre_definition.associated_member_states {
|
||||
let mut post_definition_state = std::mem::replace(
|
||||
&mut self.member_states[member_id],
|
||||
pre_definition_member_state,
|
||||
);
|
||||
|
||||
post_definition_state.record_reachability_constraint(
|
||||
&mut self.reachability_constraints,
|
||||
reachability_id,
|
||||
);
|
||||
|
||||
self.member_states[member_id].record_reachability_constraint(
|
||||
&mut self.reachability_constraints,
|
||||
negated_reachability_id,
|
||||
);
|
||||
|
||||
self.member_states[member_id].merge(
|
||||
post_definition_state,
|
||||
&mut self.narrowing_constraints,
|
||||
&mut self.reachability_constraints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn record_reachability_constraint(
|
||||
|
|
|
|||
|
|
@ -100,14 +100,14 @@ impl<'db> SemanticModel<'db> {
|
|||
pub fn resolve_module(&self, module: Option<&str>, level: u32) -> Option<Module<'db>> {
|
||||
let module_name =
|
||||
ModuleName::from_identifier_parts(self.db, self.file, module, level).ok()?;
|
||||
resolve_module(self.db, &module_name)
|
||||
resolve_module(self.db, self.file, &module_name)
|
||||
}
|
||||
|
||||
/// Returns completions for symbols available in a `import <CURSOR>` context.
|
||||
pub fn import_completions(&self) -> Vec<Completion<'db>> {
|
||||
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
|
||||
let is_typing_extensions_available = self.file.is_stub(self.db)
|
||||
|| resolve_real_shadowable_module(self.db, &typing_extensions).is_some();
|
||||
|| resolve_real_shadowable_module(self.db, self.file, &typing_extensions).is_some();
|
||||
list_modules(self.db)
|
||||
.into_iter()
|
||||
.filter(|module| {
|
||||
|
|
@ -146,7 +146,7 @@ impl<'db> SemanticModel<'db> {
|
|||
&self,
|
||||
module_name: &ModuleName,
|
||||
) -> Vec<Completion<'db>> {
|
||||
let Some(module) = resolve_module(self.db, module_name) else {
|
||||
let Some(module) = resolve_module(self.db, self.file, module_name) else {
|
||||
tracing::debug!("Could not resolve module from `{module_name:?}`");
|
||||
return vec![];
|
||||
};
|
||||
|
|
@ -156,7 +156,7 @@ impl<'db> SemanticModel<'db> {
|
|||
/// Returns completions for symbols available in the given module as if
|
||||
/// it were imported by this model's `File`.
|
||||
fn module_completions(&self, module_name: &ModuleName) -> Vec<Completion<'db>> {
|
||||
let Some(module) = resolve_module(self.db, module_name) else {
|
||||
let Some(module) = resolve_module(self.db, self.file, module_name) else {
|
||||
tracing::debug!("Could not resolve module from `{module_name:?}`");
|
||||
return vec![];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -413,9 +413,15 @@ pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRa
|
|||
}
|
||||
|
||||
// Always insert a new suppression at the end of the range to avoid having to deal with multiline strings
|
||||
// etc.
|
||||
// etc. Also make sure to not pass a sub-token range to `Tokens::after`.
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let tokens_after = parsed.tokens().after(range.end());
|
||||
let tokens = parsed.tokens().at_offset(range.end());
|
||||
let token_range = match tokens {
|
||||
ruff_python_ast::token::TokenAt::None => range,
|
||||
ruff_python_ast::token::TokenAt::Single(token) => token.range(),
|
||||
ruff_python_ast::token::TokenAt::Between(..) => range,
|
||||
};
|
||||
let tokens_after = parsed.tokens().after(token_range.end());
|
||||
|
||||
// Same as for `line_end` when building up the `suppressions`: Ignore newlines
|
||||
// in multiline-strings, inside f-strings, or after a line continuation because we can't
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ use crate::semantic_index::scope::ScopeId;
|
|||
use crate::semantic_index::{imported_modules, place_table, semantic_index};
|
||||
use crate::suppression::check_suppressions;
|
||||
use crate::types::bound_super::BoundSuperType;
|
||||
use crate::types::builder::RecursivelyDefined;
|
||||
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
|
||||
pub(crate) use crate::types::class_base::ClassBase;
|
||||
use crate::types::constraints::{
|
||||
|
|
@ -67,7 +68,7 @@ pub(crate) use crate::types::narrow::infer_narrowing_constraint;
|
|||
use crate::types::newtype::NewType;
|
||||
pub(crate) use crate::types::signatures::{Parameter, Parameters};
|
||||
use crate::types::signatures::{ParameterForm, walk_signature};
|
||||
use crate::types::tuple::{TupleSpec, TupleSpecBuilder};
|
||||
use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder};
|
||||
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
|
||||
pub use crate::types::variance::TypeVarVariance;
|
||||
use crate::types::variance::VarianceInferable;
|
||||
|
|
@ -5472,9 +5473,9 @@ impl<'db> Type<'db> {
|
|||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
bound.try_bool_impl(db, allow_short_circuit, visitor)?
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
try_union(constraints)?
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.as_type(db)
|
||||
.try_bool_impl(db, allow_short_circuit, visitor)?,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6524,7 +6525,7 @@ impl<'db> Type<'db> {
|
|||
TypeVarBoundOrConstraints::UpperBound(bound) => {
|
||||
non_async_special_case(db, bound)
|
||||
}
|
||||
TypeVarBoundOrConstraints::Constraints(union) => non_async_special_case(db, Type::Union(union)),
|
||||
TypeVarBoundOrConstraints::Constraints(constraints) => non_async_special_case(db, constraints.as_type(db)),
|
||||
},
|
||||
Type::Union(union) => {
|
||||
let elements = union.elements(db);
|
||||
|
|
@ -7403,7 +7404,7 @@ impl<'db> Type<'db> {
|
|||
| SpecialFormType::Union
|
||||
| SpecialFormType::Intersection => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec_inline![
|
||||
InvalidTypeExpression::RequiresArguments(*self)
|
||||
InvalidTypeExpression::RequiresArguments(*special_form)
|
||||
],
|
||||
fallback_type: Type::unknown(),
|
||||
}),
|
||||
|
|
@ -7429,7 +7430,7 @@ impl<'db> Type<'db> {
|
|||
| SpecialFormType::Unpack
|
||||
| SpecialFormType::CallableTypeOf => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec_inline![
|
||||
InvalidTypeExpression::RequiresOneArgument(*self)
|
||||
InvalidTypeExpression::RequiresOneArgument(*special_form)
|
||||
],
|
||||
fallback_type: Type::unknown(),
|
||||
}),
|
||||
|
|
@ -7437,7 +7438,7 @@ impl<'db> Type<'db> {
|
|||
SpecialFormType::Annotated | SpecialFormType::Concatenate => {
|
||||
Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec_inline![
|
||||
InvalidTypeExpression::RequiresTwoArguments(*self)
|
||||
InvalidTypeExpression::RequiresTwoArguments(*special_form)
|
||||
],
|
||||
fallback_type: Type::unknown(),
|
||||
})
|
||||
|
|
@ -8606,12 +8607,9 @@ impl<'db> TypeMapping<'_, 'db> {
|
|||
| TypeMapping::Materialize(_)
|
||||
| TypeMapping::ReplaceParameterDefaults
|
||||
| TypeMapping::EagerExpansion => context,
|
||||
TypeMapping::BindSelf { .. } => GenericContext::from_typevar_instances(
|
||||
db,
|
||||
context
|
||||
.variables(db)
|
||||
.filter(|var| !var.typevar(db).is_self(db)),
|
||||
),
|
||||
TypeMapping::BindSelf {
|
||||
binding_context, ..
|
||||
} => context.remove_self(db, *binding_context),
|
||||
TypeMapping::ReplaceSelf { new_upper_bound } => GenericContext::from_typevar_instances(
|
||||
db,
|
||||
context.variables(db).map(|typevar| {
|
||||
|
|
@ -9159,11 +9157,11 @@ impl<'db> InvalidTypeExpressionError<'db> {
|
|||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
enum InvalidTypeExpression<'db> {
|
||||
/// Some types always require exactly one argument when used in a type expression
|
||||
RequiresOneArgument(Type<'db>),
|
||||
RequiresOneArgument(SpecialFormType),
|
||||
/// Some types always require at least one argument when used in a type expression
|
||||
RequiresArguments(Type<'db>),
|
||||
RequiresArguments(SpecialFormType),
|
||||
/// Some types always require at least two arguments when used in a type expression
|
||||
RequiresTwoArguments(Type<'db>),
|
||||
RequiresTwoArguments(SpecialFormType),
|
||||
/// The `Protocol` class is invalid in type expressions
|
||||
Protocol,
|
||||
/// Same for `Generic`
|
||||
|
|
@ -9203,20 +9201,17 @@ impl<'db> InvalidTypeExpression<'db> {
|
|||
impl std::fmt::Display for Display<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.error {
|
||||
InvalidTypeExpression::RequiresOneArgument(ty) => write!(
|
||||
InvalidTypeExpression::RequiresOneArgument(special_form) => write!(
|
||||
f,
|
||||
"`{ty}` requires exactly one argument when used in a type expression",
|
||||
ty = ty.display(self.db)
|
||||
"`{special_form}` requires exactly one argument when used in a type expression",
|
||||
),
|
||||
InvalidTypeExpression::RequiresArguments(ty) => write!(
|
||||
InvalidTypeExpression::RequiresArguments(special_form) => write!(
|
||||
f,
|
||||
"`{ty}` requires at least one argument when used in a type expression",
|
||||
ty = ty.display(self.db)
|
||||
"`{special_form}` requires at least one argument when used in a type expression",
|
||||
),
|
||||
InvalidTypeExpression::RequiresTwoArguments(ty) => write!(
|
||||
InvalidTypeExpression::RequiresTwoArguments(special_form) => write!(
|
||||
f,
|
||||
"`{ty}` requires at least two arguments when used in a type expression",
|
||||
ty = ty.display(self.db)
|
||||
"`{special_form}` requires at least two arguments when used in a type expression",
|
||||
),
|
||||
InvalidTypeExpression::Protocol => {
|
||||
f.write_str("`typing.Protocol` is not allowed in type expressions")
|
||||
|
|
@ -9668,7 +9663,7 @@ impl<'db> TypeVarInstance<'db> {
|
|||
TypeVarBoundOrConstraints::UpperBound(upper_bound.to_instance(db)?)
|
||||
}
|
||||
TypeVarBoundOrConstraints::Constraints(constraints) => {
|
||||
TypeVarBoundOrConstraints::Constraints(constraints.to_instance(db)?.as_union()?)
|
||||
TypeVarBoundOrConstraints::Constraints(constraints.to_instance(db)?)
|
||||
}
|
||||
};
|
||||
let identity = TypeVarIdentity::new(
|
||||
|
|
@ -9703,6 +9698,7 @@ impl<'db> TypeVarInstance<'db> {
|
|||
}
|
||||
|
||||
#[salsa::tracked(
|
||||
cycle_fn=lazy_bound_or_constraints_cycle_recover,
|
||||
cycle_initial=lazy_bound_or_constraints_cycle_initial,
|
||||
heap_size=ruff_memory_usage::heap_size
|
||||
)]
|
||||
|
|
@ -9732,28 +9728,37 @@ impl<'db> TypeVarInstance<'db> {
|
|||
}
|
||||
|
||||
#[salsa::tracked(
|
||||
cycle_fn=lazy_bound_or_constraints_cycle_recover,
|
||||
cycle_initial=lazy_bound_or_constraints_cycle_initial,
|
||||
heap_size=ruff_memory_usage::heap_size
|
||||
)]
|
||||
fn lazy_constraints(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
|
||||
let definition = self.definition(db)?;
|
||||
let module = parsed_module(db, definition.file(db)).load(db);
|
||||
let ty = match definition.kind(db) {
|
||||
let constraints = match definition.kind(db) {
|
||||
// PEP 695 typevar
|
||||
DefinitionKind::TypeVar(typevar) => {
|
||||
let typevar_node = typevar.node(&module);
|
||||
definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
|
||||
.as_union()?
|
||||
let bound =
|
||||
definition_expression_type(db, definition, typevar_node.bound.as_ref()?);
|
||||
let constraints = if let Some(tuple) = bound
|
||||
.as_nominal_instance()
|
||||
.and_then(|instance| instance.tuple_spec(db))
|
||||
{
|
||||
if let Tuple::Fixed(tuple) = tuple.into_owned() {
|
||||
tuple.owned_elements()
|
||||
} else {
|
||||
vec![Type::unknown()].into_boxed_slice()
|
||||
}
|
||||
} else {
|
||||
vec![Type::unknown()].into_boxed_slice()
|
||||
};
|
||||
TypeVarConstraints::new(db, constraints)
|
||||
}
|
||||
// legacy typevar
|
||||
DefinitionKind::Assignment(assignment) => {
|
||||
let call_expr = assignment.value(&module).as_call_expr()?;
|
||||
// We don't use `UnionType::from_elements` or `UnionBuilder` here,
|
||||
// because we don't want to simplify the list of constraints as we would with
|
||||
// an actual union type.
|
||||
// TODO: We probably shouldn't use `UnionType` to store these at all? TypeVar
|
||||
// constraints are not a union.
|
||||
UnionType::new(
|
||||
TypeVarConstraints::new(
|
||||
db,
|
||||
call_expr
|
||||
.arguments
|
||||
|
|
@ -9767,7 +9772,7 @@ impl<'db> TypeVarInstance<'db> {
|
|||
_ => return None,
|
||||
};
|
||||
|
||||
if ty
|
||||
if constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|ty| self.type_is_self_referential(db, *ty))
|
||||
|
|
@ -9775,7 +9780,7 @@ impl<'db> TypeVarInstance<'db> {
|
|||
return None;
|
||||
}
|
||||
|
||||
Some(TypeVarBoundOrConstraints::Constraints(ty))
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints))
|
||||
}
|
||||
|
||||
#[salsa::tracked(cycle_fn=lazy_default_cycle_recover, cycle_initial=lazy_default_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
|
|
@ -9833,7 +9838,23 @@ fn lazy_bound_or_constraints_cycle_initial<'db>(
|
|||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
#[expect(clippy::ref_option)]
|
||||
fn lazy_bound_or_constraints_cycle_recover<'db>(
|
||||
db: &'db dyn Db,
|
||||
cycle: &salsa::Cycle,
|
||||
previous: &Option<TypeVarBoundOrConstraints<'db>>,
|
||||
current: Option<TypeVarBoundOrConstraints<'db>>,
|
||||
_typevar: TypeVarInstance<'db>,
|
||||
) -> Option<TypeVarBoundOrConstraints<'db>> {
|
||||
// Normalize the bounds/constraints to ensure cycle convergence.
|
||||
match (previous, current) {
|
||||
(Some(prev), Some(current)) => Some(current.cycle_normalized(db, *prev, cycle)),
|
||||
(None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)),
|
||||
(_, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::ref_option)]
|
||||
fn lazy_default_cycle_recover<'db>(
|
||||
db: &'db dyn Db,
|
||||
cycle: &salsa::Cycle,
|
||||
|
|
@ -9841,6 +9862,7 @@ fn lazy_default_cycle_recover<'db>(
|
|||
default: Option<Type<'db>>,
|
||||
_typevar: TypeVarInstance<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
// Normalize the default to ensure cycle convergence.
|
||||
match (previous_default, default) {
|
||||
(Some(prev), Some(default)) => Some(default.cycle_normalized(db, *prev, cycle)),
|
||||
(None, Some(default)) => Some(default.recursive_type_normalized(db, cycle)),
|
||||
|
|
@ -10168,10 +10190,133 @@ impl<'db> From<TypeVarBoundOrConstraints<'db>> for TypeVarBoundOrConstraintsEval
|
|||
}
|
||||
}
|
||||
|
||||
/// Type variable constraints (e.g. `T: (int, str)`).
|
||||
/// This is structurally identical to [`UnionType`], except that it does not perform simplification and preserves the element types.
|
||||
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
pub struct TypeVarConstraints<'db> {
|
||||
#[returns(ref)]
|
||||
elements: Box<[Type<'db>]>,
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for TypeVarConstraints<'_> {}
|
||||
|
||||
fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
db: &'db dyn Db,
|
||||
constraints: TypeVarConstraints<'db>,
|
||||
visitor: &V,
|
||||
) {
|
||||
for ty in constraints.elements(db) {
|
||||
visitor.visit_type(db, *ty);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> TypeVarConstraints<'db> {
|
||||
fn as_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for ty in self.elements(db) {
|
||||
builder = builder.add(*ty);
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
fn to_instance(self, db: &'db dyn Db) -> Option<TypeVarConstraints<'db>> {
|
||||
let mut instance_elements = Vec::new();
|
||||
for ty in self.elements(db) {
|
||||
instance_elements.push(ty.to_instance(db)?);
|
||||
}
|
||||
Some(TypeVarConstraints::new(
|
||||
db,
|
||||
instance_elements.into_boxed_slice(),
|
||||
))
|
||||
}
|
||||
|
||||
fn map(self, db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Type<'db>) -> Self {
|
||||
let mapped = self
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(transform_fn)
|
||||
.collect::<Box<_>>();
|
||||
TypeVarConstraints::new(db, mapped)
|
||||
}
|
||||
|
||||
pub(crate) fn map_with_boundness_and_qualifiers(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
let mut qualifiers = TypeQualifiers::empty();
|
||||
|
||||
let mut all_unbound = true;
|
||||
let mut possibly_unbound = false;
|
||||
let mut origin = TypeOrigin::Declared;
|
||||
for ty in self.elements(db) {
|
||||
let PlaceAndQualifiers {
|
||||
place: ty_member,
|
||||
qualifiers: new_qualifiers,
|
||||
} = transform_fn(ty);
|
||||
qualifiers |= new_qualifiers;
|
||||
match ty_member {
|
||||
Place::Undefined => {
|
||||
possibly_unbound = true;
|
||||
}
|
||||
Place::Defined(ty_member, member_origin, member_boundness) => {
|
||||
origin = origin.merge(member_origin);
|
||||
if member_boundness == Definedness::PossiblyUndefined {
|
||||
possibly_unbound = true;
|
||||
}
|
||||
|
||||
all_unbound = false;
|
||||
builder = builder.add(ty_member);
|
||||
}
|
||||
}
|
||||
}
|
||||
PlaceAndQualifiers {
|
||||
place: if all_unbound {
|
||||
Place::Undefined
|
||||
} else {
|
||||
Place::Defined(
|
||||
builder.build(),
|
||||
origin,
|
||||
if possibly_unbound {
|
||||
Definedness::PossiblyUndefined
|
||||
} else {
|
||||
Definedness::AlwaysDefined
|
||||
},
|
||||
)
|
||||
},
|
||||
qualifiers,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
|
||||
let normalized = self
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| ty.normalized_impl(db, visitor))
|
||||
.collect::<Box<_>>();
|
||||
TypeVarConstraints::new(db, normalized)
|
||||
}
|
||||
|
||||
fn materialize_impl(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
materialization_kind: MaterializationKind,
|
||||
visitor: &ApplyTypeMappingVisitor<'db>,
|
||||
) -> Self {
|
||||
let materialized = self
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| ty.materialize(db, materialization_kind, visitor))
|
||||
.collect::<Box<_>>();
|
||||
TypeVarConstraints::new(db, materialized)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub enum TypeVarBoundOrConstraints<'db> {
|
||||
UpperBound(Type<'db>),
|
||||
Constraints(UnionType<'db>),
|
||||
Constraints(TypeVarConstraints<'db>),
|
||||
}
|
||||
|
||||
fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
|
|
@ -10182,7 +10327,7 @@ fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
|||
match bounds {
|
||||
TypeVarBoundOrConstraints::UpperBound(bound) => visitor.visit_type(db, bound),
|
||||
TypeVarBoundOrConstraints::Constraints(constraints) => {
|
||||
visitor.visit_union_type(db, constraints);
|
||||
walk_type_var_constraints(db, constraints, visitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10194,18 +10339,61 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
|
|||
TypeVarBoundOrConstraints::UpperBound(bound.normalized_impl(db, visitor))
|
||||
}
|
||||
TypeVarBoundOrConstraints::Constraints(constraints) => {
|
||||
// Constraints are a non-normalized union by design (it's not really a union at
|
||||
// all, we are just using a union to store the types). Normalize the types but not
|
||||
// the containing union.
|
||||
TypeVarBoundOrConstraints::Constraints(UnionType::new(
|
||||
TypeVarBoundOrConstraints::Constraints(constraints.normalized_impl(db, visitor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize for cycle recovery by combining with the previous value and
|
||||
/// removing divergent types introduced by the cycle.
|
||||
///
|
||||
/// See [`Type::cycle_normalized`] for more details on how this works.
|
||||
fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self {
|
||||
match (self, previous) {
|
||||
(
|
||||
TypeVarBoundOrConstraints::UpperBound(bound),
|
||||
TypeVarBoundOrConstraints::UpperBound(prev_bound),
|
||||
) => {
|
||||
TypeVarBoundOrConstraints::UpperBound(bound.cycle_normalized(db, prev_bound, cycle))
|
||||
}
|
||||
(
|
||||
TypeVarBoundOrConstraints::Constraints(constraints),
|
||||
TypeVarBoundOrConstraints::Constraints(prev_constraints),
|
||||
) => {
|
||||
// Normalize each constraint with its corresponding previous constraint
|
||||
let current_elements = constraints.elements(db);
|
||||
let prev_elements = prev_constraints.elements(db);
|
||||
TypeVarBoundOrConstraints::Constraints(TypeVarConstraints::new(
|
||||
db,
|
||||
constraints
|
||||
.elements(db)
|
||||
current_elements
|
||||
.iter()
|
||||
.map(|ty| ty.normalized_impl(db, visitor))
|
||||
.zip(prev_elements.iter())
|
||||
.map(|(ty, prev_ty)| ty.cycle_normalized(db, *prev_ty, cycle))
|
||||
.collect::<Box<_>>(),
|
||||
))
|
||||
}
|
||||
// The choice of whether it's an upper bound or constraints is purely syntactic and
|
||||
// thus can never change in a cycle: `parsed_module` does not participate in cycles,
|
||||
// the AST will never change from one iteration to the next.
|
||||
_ => unreachable!(
|
||||
"TypeVar switched from bound to constraints (or vice versa) in fixpoint iteration"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize recursive types for cycle recovery when there's no previous value.
|
||||
///
|
||||
/// See [`Type::recursive_type_normalized`] for more details.
|
||||
fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self {
|
||||
match self {
|
||||
TypeVarBoundOrConstraints::UpperBound(bound) => {
|
||||
TypeVarBoundOrConstraints::UpperBound(bound.recursive_type_normalized(db, cycle))
|
||||
}
|
||||
TypeVarBoundOrConstraints::Constraints(constraints) => {
|
||||
TypeVarBoundOrConstraints::Constraints(
|
||||
constraints.map(db, |ty| ty.recursive_type_normalized(db, cycle)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10220,13 +10408,10 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
|
|||
bound.materialize(db, materialization_kind, visitor),
|
||||
),
|
||||
TypeVarBoundOrConstraints::Constraints(constraints) => {
|
||||
TypeVarBoundOrConstraints::Constraints(UnionType::new(
|
||||
TypeVarBoundOrConstraints::Constraints(constraints.materialize_impl(
|
||||
db,
|
||||
constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| ty.materialize(db, materialization_kind, visitor))
|
||||
.collect::<Box<_>>(),
|
||||
materialization_kind,
|
||||
visitor,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -12797,7 +12982,7 @@ impl<'db> ModuleLiteralType<'db> {
|
|||
let relative_submodule_name = ModuleName::new(name)?;
|
||||
let mut absolute_submodule_name = self.module(db).name(db).clone();
|
||||
absolute_submodule_name.extend(&relative_submodule_name);
|
||||
let submodule = resolve_module(db, &absolute_submodule_name)?;
|
||||
let submodule = resolve_module(db, importing_file, &absolute_submodule_name)?;
|
||||
Some(Type::module_literal(db, importing_file, submodule))
|
||||
}
|
||||
|
||||
|
|
@ -13181,6 +13366,9 @@ pub struct UnionType<'db> {
|
|||
/// The union type includes values in any of these types.
|
||||
#[returns(deref)]
|
||||
pub elements: Box<[Type<'db>]>,
|
||||
/// Whether the value pointed to by this type is recursively defined.
|
||||
/// If `Yes`, union literal widening is performed early.
|
||||
recursively_defined: RecursivelyDefined,
|
||||
}
|
||||
|
||||
pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
|
|
@ -13265,7 +13453,14 @@ impl<'db> UnionType<'db> {
|
|||
db: &'db dyn Db,
|
||||
transform_fn: impl FnMut(&Type<'db>) -> Type<'db>,
|
||||
) -> Type<'db> {
|
||||
Self::from_elements(db, self.elements(db).iter().map(transform_fn))
|
||||
self.elements(db)
|
||||
.iter()
|
||||
.map(transform_fn)
|
||||
.fold(UnionBuilder::new(db), |builder, element| {
|
||||
builder.add(element)
|
||||
})
|
||||
.recursively_defined(self.recursively_defined(db))
|
||||
.build()
|
||||
}
|
||||
|
||||
/// A fallible version of [`UnionType::map`].
|
||||
|
|
@ -13280,7 +13475,12 @@ impl<'db> UnionType<'db> {
|
|||
db: &'db dyn Db,
|
||||
transform_fn: impl FnMut(&Type<'db>) -> Option<Type<'db>>,
|
||||
) -> Option<Type<'db>> {
|
||||
Self::try_from_elements(db, self.elements(db).iter().map(transform_fn))
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in self.elements(db).iter().map(transform_fn) {
|
||||
builder = builder.add(element?);
|
||||
}
|
||||
builder = builder.recursively_defined(self.recursively_defined(db));
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
|
|
@ -13292,7 +13492,14 @@ impl<'db> UnionType<'db> {
|
|||
db: &'db dyn Db,
|
||||
mut f: impl FnMut(&Type<'db>) -> bool,
|
||||
) -> Type<'db> {
|
||||
Self::from_elements(db, self.elements(db).iter().filter(|ty| f(ty)))
|
||||
self.elements(db)
|
||||
.iter()
|
||||
.filter(|ty| f(ty))
|
||||
.fold(UnionBuilder::new(db), |builder, element| {
|
||||
builder.add(*element)
|
||||
})
|
||||
.recursively_defined(self.recursively_defined(db))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub(crate) fn map_with_boundness(
|
||||
|
|
@ -13327,7 +13534,9 @@ impl<'db> UnionType<'db> {
|
|||
Place::Undefined
|
||||
} else {
|
||||
Place::Defined(
|
||||
builder.build(),
|
||||
builder
|
||||
.recursively_defined(self.recursively_defined(db))
|
||||
.build(),
|
||||
origin,
|
||||
if possibly_unbound {
|
||||
Definedness::PossiblyUndefined
|
||||
|
|
@ -13375,7 +13584,9 @@ impl<'db> UnionType<'db> {
|
|||
Place::Undefined
|
||||
} else {
|
||||
Place::Defined(
|
||||
builder.build(),
|
||||
builder
|
||||
.recursively_defined(self.recursively_defined(db))
|
||||
.build(),
|
||||
origin,
|
||||
if possibly_unbound {
|
||||
Definedness::PossiblyUndefined
|
||||
|
|
@ -13410,6 +13621,7 @@ impl<'db> UnionType<'db> {
|
|||
.unpack_aliases(true),
|
||||
UnionBuilder::add,
|
||||
)
|
||||
.recursively_defined(self.recursively_defined(db))
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
@ -13422,7 +13634,8 @@ impl<'db> UnionType<'db> {
|
|||
let mut builder = UnionBuilder::new(db)
|
||||
.order_elements(false)
|
||||
.unpack_aliases(false)
|
||||
.cycle_recovery(true);
|
||||
.cycle_recovery(true)
|
||||
.recursively_defined(self.recursively_defined(db));
|
||||
let mut empty = true;
|
||||
for ty in self.elements(db) {
|
||||
if nested {
|
||||
|
|
@ -13437,6 +13650,7 @@ impl<'db> UnionType<'db> {
|
|||
// `Divergent` in a union type does not mean true divergence, so we skip it if not nested.
|
||||
// e.g. T | Divergent == T | (T | (T | (T | ...))) == T
|
||||
if ty == &div {
|
||||
builder = builder.recursively_defined(RecursivelyDefined::Yes);
|
||||
continue;
|
||||
}
|
||||
builder = builder.add(
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ impl<'db> BoundSuperError<'db> {
|
|||
.map(|c| c.display(db))
|
||||
.join(", ")
|
||||
));
|
||||
Type::Union(constraints)
|
||||
constraints.as_type(db)
|
||||
}
|
||||
None => {
|
||||
diagnostic.info(format_args!(
|
||||
|
|
@ -374,7 +374,7 @@ impl<'db> BoundSuperType<'db> {
|
|||
delegate_with_error_mapped(bound, Some(type_var))
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
delegate_with_error_mapped(Type::Union(constraints), Some(type_var))
|
||||
delegate_with_error_mapped(constraints.as_type(db), Some(type_var))
|
||||
}
|
||||
None => delegate_with_error_mapped(Type::object(), Some(type_var)),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -202,12 +202,30 @@ enum ReduceResult<'db> {
|
|||
Type(Type<'db>),
|
||||
}
|
||||
|
||||
// TODO increase this once we extend `UnionElement` throughout all union/intersection
|
||||
// representations, so that we can make large unions of literals fast in all operations.
|
||||
//
|
||||
// For now (until we solve https://github.com/astral-sh/ty/issues/957), keep this number
|
||||
// below 200, which is the salsa fixpoint iteration limit.
|
||||
const MAX_UNION_LITERALS: usize = 190;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub enum RecursivelyDefined {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
impl RecursivelyDefined {
|
||||
const fn is_yes(self) -> bool {
|
||||
matches!(self, RecursivelyDefined::Yes)
|
||||
}
|
||||
|
||||
const fn or(self, other: RecursivelyDefined) -> RecursivelyDefined {
|
||||
match (self, other) {
|
||||
(RecursivelyDefined::Yes, _) | (_, RecursivelyDefined::Yes) => RecursivelyDefined::Yes,
|
||||
_ => RecursivelyDefined::No,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the value is defined recursively, widening is performed from fewer literal elements, resulting in faster convergence of the fixed-point iteration.
|
||||
const MAX_RECURSIVE_UNION_LITERALS: usize = 10;
|
||||
/// If the value is defined non-recursively, the fixed-point iteration will converge in one go,
|
||||
/// so in principle we can have as many literal elements as we want, but to avoid unintended huge computational loads, we limit it to 256.
|
||||
const MAX_NON_RECURSIVE_UNION_LITERALS: usize = 256;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<UnionElement<'db>>,
|
||||
|
|
@ -217,6 +235,7 @@ pub(crate) struct UnionBuilder<'db> {
|
|||
// This is enabled when joining types in a `cycle_recovery` function.
|
||||
// Since a cycle cannot be created within a `cycle_recovery` function, execution of `is_redundant_with` is skipped.
|
||||
cycle_recovery: bool,
|
||||
recursively_defined: RecursivelyDefined,
|
||||
}
|
||||
|
||||
impl<'db> UnionBuilder<'db> {
|
||||
|
|
@ -227,6 +246,7 @@ impl<'db> UnionBuilder<'db> {
|
|||
unpack_aliases: true,
|
||||
order_elements: false,
|
||||
cycle_recovery: false,
|
||||
recursively_defined: RecursivelyDefined::No,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,6 +268,11 @@ impl<'db> UnionBuilder<'db> {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn recursively_defined(mut self, val: RecursivelyDefined) -> Self {
|
||||
self.recursively_defined = val;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.elements.is_empty()
|
||||
}
|
||||
|
|
@ -258,6 +283,27 @@ impl<'db> UnionBuilder<'db> {
|
|||
self.elements.push(UnionElement::Type(Type::object()));
|
||||
}
|
||||
|
||||
fn widen_literal_types(&mut self, seen_aliases: &mut Vec<Type<'db>>) {
|
||||
let mut replace_with = vec![];
|
||||
for elem in &self.elements {
|
||||
match elem {
|
||||
UnionElement::IntLiterals(_) => {
|
||||
replace_with.push(KnownClass::Int.to_instance(self.db));
|
||||
}
|
||||
UnionElement::StringLiterals(_) => {
|
||||
replace_with.push(KnownClass::Str.to_instance(self.db));
|
||||
}
|
||||
UnionElement::BytesLiterals(_) => {
|
||||
replace_with.push(KnownClass::Bytes.to_instance(self.db));
|
||||
}
|
||||
UnionElement::Type(_) => {}
|
||||
}
|
||||
}
|
||||
for ty in replace_with {
|
||||
self.add_in_place_impl(ty, seen_aliases);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a type to this union.
|
||||
pub(crate) fn add(mut self, ty: Type<'db>) -> Self {
|
||||
self.add_in_place(ty);
|
||||
|
|
@ -270,6 +316,15 @@ impl<'db> UnionBuilder<'db> {
|
|||
}
|
||||
|
||||
pub(crate) fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec<Type<'db>>) {
|
||||
let cycle_recovery = self.cycle_recovery;
|
||||
let should_widen = |literals, recursively_defined: RecursivelyDefined| {
|
||||
if recursively_defined.is_yes() && cycle_recovery {
|
||||
literals >= MAX_RECURSIVE_UNION_LITERALS
|
||||
} else {
|
||||
literals >= MAX_NON_RECURSIVE_UNION_LITERALS
|
||||
}
|
||||
};
|
||||
|
||||
match ty {
|
||||
Type::Union(union) => {
|
||||
let new_elements = union.elements(self.db);
|
||||
|
|
@ -277,6 +332,20 @@ impl<'db> UnionBuilder<'db> {
|
|||
for element in new_elements {
|
||||
self.add_in_place_impl(*element, seen_aliases);
|
||||
}
|
||||
self.recursively_defined = self
|
||||
.recursively_defined
|
||||
.or(union.recursively_defined(self.db));
|
||||
if self.cycle_recovery && self.recursively_defined.is_yes() {
|
||||
let literals = self.elements.iter().fold(0, |acc, elem| match elem {
|
||||
UnionElement::IntLiterals(literals) => acc + literals.len(),
|
||||
UnionElement::StringLiterals(literals) => acc + literals.len(),
|
||||
UnionElement::BytesLiterals(literals) => acc + literals.len(),
|
||||
UnionElement::Type(_) => acc,
|
||||
});
|
||||
if should_widen(literals, self.recursively_defined) {
|
||||
self.widen_literal_types(seen_aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Adding `Never` to a union is a no-op.
|
||||
Type::Never => {}
|
||||
|
|
@ -300,7 +369,7 @@ impl<'db> UnionBuilder<'db> {
|
|||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::StringLiterals(literals) => {
|
||||
if literals.len() >= MAX_UNION_LITERALS {
|
||||
if should_widen(literals.len(), self.recursively_defined) {
|
||||
let replace_with = KnownClass::Str.to_instance(self.db);
|
||||
self.add_in_place_impl(replace_with, seen_aliases);
|
||||
return;
|
||||
|
|
@ -345,7 +414,7 @@ impl<'db> UnionBuilder<'db> {
|
|||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::BytesLiterals(literals) => {
|
||||
if literals.len() >= MAX_UNION_LITERALS {
|
||||
if should_widen(literals.len(), self.recursively_defined) {
|
||||
let replace_with = KnownClass::Bytes.to_instance(self.db);
|
||||
self.add_in_place_impl(replace_with, seen_aliases);
|
||||
return;
|
||||
|
|
@ -390,7 +459,7 @@ impl<'db> UnionBuilder<'db> {
|
|||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::IntLiterals(literals) => {
|
||||
if literals.len() >= MAX_UNION_LITERALS {
|
||||
if should_widen(literals.len(), self.recursively_defined) {
|
||||
let replace_with = KnownClass::Int.to_instance(self.db);
|
||||
self.add_in_place_impl(replace_with, seen_aliases);
|
||||
return;
|
||||
|
|
@ -585,6 +654,7 @@ impl<'db> UnionBuilder<'db> {
|
|||
_ => Some(Type::Union(UnionType::new(
|
||||
self.db,
|
||||
types.into_boxed_slice(),
|
||||
self.recursively_defined,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
|
@ -696,6 +766,7 @@ impl<'db> IntersectionBuilder<'db> {
|
|||
enum_member_literals(db, instance.class_literal(db), None)
|
||||
.expect("Calling `enum_member_literals` on an enum class")
|
||||
.collect::<Box<[_]>>(),
|
||||
RecursivelyDefined::No,
|
||||
)),
|
||||
seen_aliases,
|
||||
)
|
||||
|
|
@ -1184,7 +1255,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
speculative = speculative.add_positive(bound);
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
speculative = speculative.add_positive(Type::Union(constraints));
|
||||
speculative = speculative.add_positive(constraints.as_type(db));
|
||||
}
|
||||
// TypeVars without a bound or constraint implicitly have `object` as their
|
||||
// upper bound, and it is always a no-op to add `object` to an intersection.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ use crate::types::function::{
|
|||
use crate::types::generics::{
|
||||
InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError,
|
||||
};
|
||||
use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters};
|
||||
use crate::types::signatures::{
|
||||
CallableSignature, Parameter, ParameterForm, ParameterKind, Parameters,
|
||||
};
|
||||
use crate::types::tuple::{TupleLength, TupleType};
|
||||
use crate::types::{
|
||||
BoundMethodType, BoundTypeVarIdentity, ClassLiteral, DATACLASS_FLAGS, DataclassFlags,
|
||||
|
|
@ -788,51 +790,67 @@ impl<'db> Bindings<'db> {
|
|||
))
|
||||
};
|
||||
|
||||
let function_generic_context = |function: FunctionType<'db>| {
|
||||
let union = UnionType::from_elements(
|
||||
db,
|
||||
function
|
||||
.signature(db)
|
||||
.overloads
|
||||
.iter()
|
||||
.filter_map(|signature| signature.generic_context)
|
||||
.map(wrap_generic_context),
|
||||
);
|
||||
if union.is_never() {
|
||||
Type::none(db)
|
||||
} else {
|
||||
union
|
||||
}
|
||||
};
|
||||
let signature_generic_context =
|
||||
|signature: &CallableSignature<'db>| {
|
||||
UnionType::try_from_elements(
|
||||
db,
|
||||
signature.overloads.iter().map(|signature| {
|
||||
signature.generic_context.map(wrap_generic_context)
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
// TODO: Handle generic functions, and unions/intersections of
|
||||
// generic types
|
||||
overload.set_return_type(match ty {
|
||||
Type::ClassLiteral(class) => class
|
||||
.generic_context(db)
|
||||
.map(wrap_generic_context)
|
||||
.unwrap_or_else(|| Type::none(db)),
|
||||
let generic_context_for_simple_type = |ty: Type<'db>| match ty {
|
||||
Type::ClassLiteral(class) => {
|
||||
class.generic_context(db).map(wrap_generic_context)
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(function) => {
|
||||
function_generic_context(*function)
|
||||
signature_generic_context(function.signature(db))
|
||||
}
|
||||
|
||||
Type::BoundMethod(bound_method) => {
|
||||
function_generic_context(bound_method.function(db))
|
||||
Type::BoundMethod(bound_method) => signature_generic_context(
|
||||
bound_method.function(db).signature(db),
|
||||
),
|
||||
|
||||
Type::Callable(callable) => {
|
||||
signature_generic_context(callable.signatures(db))
|
||||
}
|
||||
|
||||
Type::KnownInstance(KnownInstanceType::TypeAliasType(
|
||||
TypeAliasType::PEP695(alias),
|
||||
)) => alias
|
||||
.generic_context(db)
|
||||
.map(wrap_generic_context)
|
||||
.unwrap_or_else(|| Type::none(db)),
|
||||
)) => alias.generic_context(db).map(wrap_generic_context),
|
||||
|
||||
_ => Type::none(db),
|
||||
});
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let generic_context = match ty {
|
||||
Type::Union(union_type) => UnionType::try_from_elements(
|
||||
db,
|
||||
union_type
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| generic_context_for_simple_type(*ty)),
|
||||
),
|
||||
_ => generic_context_for_simple_type(*ty),
|
||||
};
|
||||
|
||||
overload.set_return_type(
|
||||
generic_context.unwrap_or_else(|| Type::none(db)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IntoCallable) => {
|
||||
let [Some(ty)] = overload.parameter_types() else {
|
||||
continue;
|
||||
};
|
||||
let Some(callables) = ty.try_upcast_to_callable(db) else {
|
||||
continue;
|
||||
};
|
||||
overload.set_return_type(callables.into_type(db));
|
||||
}
|
||||
|
||||
Some(KnownFunction::DunderAllNames) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(match ty {
|
||||
|
|
|
|||
|
|
@ -340,9 +340,18 @@ impl<'db> From<GenericAlias<'db>> for Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
fn variance_of_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_id: salsa::Id,
|
||||
_self: GenericAlias<'db>,
|
||||
_typevar: BoundTypeVarInstance<'db>,
|
||||
) -> TypeVarVariance {
|
||||
TypeVarVariance::Bivariant
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> VarianceInferable<'db> for GenericAlias<'db> {
|
||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size, cycle_initial=variance_of_cycle_initial)]
|
||||
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
|
||||
let origin = self.origin(db);
|
||||
|
||||
|
|
@ -1126,8 +1135,12 @@ impl<'db> ClassType<'db> {
|
|||
/// constructor signature of this class.
|
||||
#[salsa::tracked(cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> {
|
||||
// TODO: This mimics a lot of the logic in Type::try_call_from_constructor. Can we
|
||||
// consolidate the two? Can we invoke a class by upcasting the class into a Callable, and
|
||||
// then relying on the call binding machinery to Just Work™?
|
||||
|
||||
let (class_literal, _) = self.class_literal(db);
|
||||
let generic_context = class_literal.generic_context(db);
|
||||
let class_generic_context = class_literal.generic_context(db);
|
||||
|
||||
let self_ty = Type::from(self);
|
||||
let metaclass_dunder_call_function_symbol = self_ty
|
||||
|
|
@ -1225,6 +1238,11 @@ impl<'db> ClassType<'db> {
|
|||
});
|
||||
let return_type = self_annotation.unwrap_or(correct_return_type);
|
||||
let instance_ty = self_annotation.unwrap_or_else(|| Type::instance(db, self));
|
||||
let generic_context = GenericContext::merge_optional(
|
||||
db,
|
||||
class_generic_context,
|
||||
signature.generic_context,
|
||||
);
|
||||
Signature::new_generic(
|
||||
generic_context,
|
||||
signature.parameters().clone(),
|
||||
|
|
@ -1271,9 +1289,13 @@ impl<'db> ClassType<'db> {
|
|||
)
|
||||
.place;
|
||||
|
||||
if let Place::Defined(Type::FunctionLiteral(new_function), _, _) =
|
||||
if let Place::Defined(Type::FunctionLiteral(mut new_function), _, _) =
|
||||
new_function_symbol
|
||||
{
|
||||
if let Some(class_generic_context) = class_generic_context {
|
||||
new_function =
|
||||
new_function.with_inherited_generic_context(db, class_generic_context);
|
||||
}
|
||||
CallableTypes::one(
|
||||
new_function
|
||||
.into_bound_method_type(db, correct_return_type)
|
||||
|
|
@ -1283,7 +1305,11 @@ impl<'db> ClassType<'db> {
|
|||
// Fallback if no `object.__new__` is found.
|
||||
CallableTypes::one(CallableType::single(
|
||||
db,
|
||||
Signature::new(Parameters::empty(), Some(correct_return_type)),
|
||||
Signature::new_generic(
|
||||
class_generic_context,
|
||||
Parameters::empty(),
|
||||
Some(correct_return_type),
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -5860,7 +5886,7 @@ impl SlotsKind {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::module_resolver::resolve_module_confident;
|
||||
use crate::{PythonVersionSource, PythonVersionWithSource};
|
||||
use salsa::Setter;
|
||||
use strum::IntoEnumIterator;
|
||||
|
|
@ -5876,7 +5902,8 @@ mod tests {
|
|||
});
|
||||
for class in KnownClass::iter() {
|
||||
let class_name = class.name(&db);
|
||||
let class_module = resolve_module(&db, &class.canonical_module(&db).name()).unwrap();
|
||||
let class_module =
|
||||
resolve_module_confident(&db, &class.canonical_module(&db).name()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
KnownClass::try_from_file_and_name(
|
||||
|
|
|
|||
|
|
@ -2998,11 +2998,10 @@ pub(crate) fn report_invalid_arguments_to_annotated(
|
|||
let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, subscript) else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Special form `{}` expected at least 2 arguments \
|
||||
builder.into_diagnostic(
|
||||
"Special form `typing.Annotated` expected at least 2 arguments \
|
||||
(one type and at least one metadata element)",
|
||||
SpecialFormType::Annotated
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_argument_number_to_special_form(
|
||||
|
|
@ -3103,8 +3102,7 @@ pub(crate) fn report_invalid_arguments_to_callable(
|
|||
return;
|
||||
};
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Special form `{}` expected exactly two arguments (parameter types and return type)",
|
||||
SpecialFormType::Callable
|
||||
"Special form `typing.Callable` expected exactly two arguments (parameter types and return type)",
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -696,7 +696,8 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
|
|||
),
|
||||
},
|
||||
Type::SpecialForm(special_form) => {
|
||||
write!(f.with_type(self.ty), "{special_form}")
|
||||
f.set_invalid_syntax();
|
||||
write!(f.with_type(self.ty), "<special form '{special_form}'>")
|
||||
}
|
||||
Type::KnownInstance(known_instance) => known_instance
|
||||
.display_with(self.db, self.settings.clone())
|
||||
|
|
@ -2173,16 +2174,24 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> {
|
|||
let ty = Type::KnownInstance(self.known_instance);
|
||||
match self.known_instance {
|
||||
KnownInstanceType::SubscriptedProtocol(generic_context) => {
|
||||
f.set_invalid_syntax();
|
||||
f.write_str("<special form '")?;
|
||||
f.with_type(ty).write_str("typing.Protocol")?;
|
||||
f.write_str(&generic_context.display(self.db).to_string())
|
||||
f.write_str(&generic_context.display(self.db).to_string())?;
|
||||
f.write_str("'>")
|
||||
}
|
||||
KnownInstanceType::SubscriptedGeneric(generic_context) => {
|
||||
f.set_invalid_syntax();
|
||||
f.write_str("<special form '")?;
|
||||
f.with_type(ty).write_str("typing.Generic")?;
|
||||
f.write_str(&generic_context.display(self.db).to_string())
|
||||
f.write_str(&generic_context.display(self.db).to_string())?;
|
||||
f.write_str("'>")
|
||||
}
|
||||
KnownInstanceType::TypeAliasType(alias) => {
|
||||
if let Some(specialization) = alias.specialization(self.db) {
|
||||
f.write_str(alias.name(self.db))?;
|
||||
f.set_invalid_syntax();
|
||||
f.write_str("<type alias '")?;
|
||||
f.with_type(ty).write_str(alias.name(self.db))?;
|
||||
f.write_str(
|
||||
&specialization
|
||||
.display_short(
|
||||
|
|
@ -2191,7 +2200,8 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> {
|
|||
DisplaySettings::default(),
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
)?;
|
||||
f.write_str("'>")
|
||||
} else {
|
||||
f.with_type(ty).write_str("typing.TypeAliasType")
|
||||
}
|
||||
|
|
@ -2201,9 +2211,9 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> {
|
|||
// have a `Type::TypeVar(_)`, which is rendered as the typevar's name.
|
||||
KnownInstanceType::TypeVar(typevar_instance) => {
|
||||
if typevar_instance.kind(self.db).is_paramspec() {
|
||||
f.write_str("typing.ParamSpec")
|
||||
f.with_type(ty).write_str("typing.ParamSpec")
|
||||
} else {
|
||||
f.write_str("typing.TypeVar")
|
||||
f.with_type(ty).write_str("typing.TypeVar")
|
||||
}
|
||||
}
|
||||
KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"),
|
||||
|
|
@ -2226,22 +2236,56 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> {
|
|||
f.with_type(ty).write_str("ty_extensions.Specialization")?;
|
||||
write!(f, "{}", specialization.display_full(self.db))
|
||||
}
|
||||
KnownInstanceType::UnionType(_) => f.with_type(ty).write_str("types.UnionType"),
|
||||
KnownInstanceType::Literal(_) => {
|
||||
KnownInstanceType::UnionType(union) => {
|
||||
f.set_invalid_syntax();
|
||||
f.write_str("<typing.Literal special form>")
|
||||
f.write_char('<')?;
|
||||
f.with_type(ty).write_str("types.UnionType")?;
|
||||
f.write_str(" special form")?;
|
||||
if let Ok(ty) = union.union_type(self.db) {
|
||||
write!(f, " '{}'", ty.display(self.db))?;
|
||||
}
|
||||
f.write_char('>')
|
||||
}
|
||||
KnownInstanceType::Annotated(_) => {
|
||||
KnownInstanceType::Literal(inner) => {
|
||||
f.set_invalid_syntax();
|
||||
f.write_str("<typing.Annotated special form>")
|
||||
write!(
|
||||
f,
|
||||
"<special form '{}'>",
|
||||
inner.inner(self.db).display(self.db)
|
||||
)
|
||||
}
|
||||
KnownInstanceType::TypeGenericAlias(_) | KnownInstanceType::Callable(_) => {
|
||||
f.with_type(ty).write_str("GenericAlias")
|
||||
KnownInstanceType::Annotated(inner) => {
|
||||
f.set_invalid_syntax();
|
||||
f.write_str("<special form '")?;
|
||||
f.with_type(ty).write_str("typing.Annotated")?;
|
||||
write!(
|
||||
f,
|
||||
"[{}, <metadata>]'>",
|
||||
inner.inner(self.db).display(self.db)
|
||||
)
|
||||
}
|
||||
KnownInstanceType::Callable(callable) => {
|
||||
f.set_invalid_syntax();
|
||||
f.write_char('<')?;
|
||||
f.with_type(ty).write_str("typing.Callable")?;
|
||||
write!(f, " special form '{}'>", callable.display(self.db))
|
||||
}
|
||||
KnownInstanceType::TypeGenericAlias(inner) => {
|
||||
f.set_invalid_syntax();
|
||||
f.write_str("<special form '")?;
|
||||
write!(
|
||||
f.with_type(ty),
|
||||
"type[{}]",
|
||||
inner.inner(self.db).display(self.db)
|
||||
)?;
|
||||
f.write_str("'>")
|
||||
}
|
||||
KnownInstanceType::LiteralStringAlias(_) => f.write_str("str"),
|
||||
KnownInstanceType::NewType(declaration) => {
|
||||
f.set_invalid_syntax();
|
||||
write!(f, "<NewType pseudo-class '{}'>", declaration.name(self.db))
|
||||
f.write_str("<NewType pseudo-class '")?;
|
||||
f.with_type(ty).write_str(declaration.name(self.db))?;
|
||||
f.write_str("'>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1339,6 +1339,8 @@ pub enum KnownFunction {
|
|||
IsSingleValued,
|
||||
/// `ty_extensions.generic_context`
|
||||
GenericContext,
|
||||
/// `ty_extensions.into_callable`
|
||||
IntoCallable,
|
||||
/// `ty_extensions.dunder_all_names`
|
||||
DunderAllNames,
|
||||
/// `ty_extensions.enum_members`
|
||||
|
|
@ -1411,6 +1413,7 @@ impl KnownFunction {
|
|||
| Self::IsSingleton
|
||||
| Self::IsSubtypeOf
|
||||
| Self::GenericContext
|
||||
| Self::IntoCallable
|
||||
| Self::DunderAllNames
|
||||
| Self::EnumMembers
|
||||
| Self::StaticAssert
|
||||
|
|
@ -1483,17 +1486,22 @@ impl KnownFunction {
|
|||
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(context.span(&call_expression.arguments.args[0]))
|
||||
.message(format_args!(
|
||||
"Inferred type of argument is `{}`",
|
||||
actual_ty.display(db),
|
||||
)),
|
||||
.message(format_args!("Inferred type is `{}`", actual_ty.display(db),)),
|
||||
);
|
||||
|
||||
diagnostic.info(format_args!(
|
||||
"`{asserted_type}` and `{inferred_type}` are not equivalent types",
|
||||
asserted_type = asserted_ty.display(db),
|
||||
inferred_type = actual_ty.display(db),
|
||||
));
|
||||
if actual_ty.is_subtype_of(db, *asserted_ty) {
|
||||
diagnostic.info(format_args!(
|
||||
"`{inferred_type}` is a subtype of `{asserted_type}`, but they are not equivalent",
|
||||
asserted_type = asserted_ty.display(db),
|
||||
inferred_type = actual_ty.display(db),
|
||||
));
|
||||
} else {
|
||||
diagnostic.info(format_args!(
|
||||
"`{asserted_type}` and `{inferred_type}` are not equivalent types",
|
||||
asserted_type = asserted_ty.display(db),
|
||||
inferred_type = actual_ty.display(db),
|
||||
));
|
||||
}
|
||||
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"Type `{}` does not match asserted type `{}`",
|
||||
|
|
@ -1882,7 +1890,7 @@ impl KnownFunction {
|
|||
let Some(module_name) = ModuleName::new(module_name) else {
|
||||
return;
|
||||
};
|
||||
let Some(module) = resolve_module(db, &module_name) else {
|
||||
let Some(module) = resolve_module(db, file, &module_name) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
@ -1941,6 +1949,7 @@ pub(crate) mod tests {
|
|||
KnownFunction::IsSingleton
|
||||
| KnownFunction::IsSubtypeOf
|
||||
| KnownFunction::GenericContext
|
||||
| KnownFunction::IntoCallable
|
||||
| KnownFunction::DunderAllNames
|
||||
| KnownFunction::EnumMembers
|
||||
| KnownFunction::StaticAssert
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@ use crate::types::visitor::{
|
|||
TypeCollector, TypeVisitor, any_over_type, walk_type_with_recursion_guard,
|
||||
};
|
||||
use crate::types::{
|
||||
ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, ClassLiteral,
|
||||
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor,
|
||||
KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext,
|
||||
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance,
|
||||
TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_bound_type_var_type,
|
||||
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance,
|
||||
ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor,
|
||||
IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor,
|
||||
Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity,
|
||||
TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type,
|
||||
walk_bound_type_var_type,
|
||||
};
|
||||
use crate::{Db, FxOrderMap, FxOrderSet};
|
||||
|
||||
|
|
@ -263,6 +264,34 @@ impl<'db> GenericContext<'db> {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn merge_optional(
|
||||
db: &'db dyn Db,
|
||||
left: Option<Self>,
|
||||
right: Option<Self>,
|
||||
) -> Option<Self> {
|
||||
match (left, right) {
|
||||
(None, None) => None,
|
||||
(Some(one), None) | (None, Some(one)) => Some(one),
|
||||
(Some(left), Some(right)) => Some(left.merge(db, right)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_self(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
binding_context: Option<BindingContext<'db>>,
|
||||
) -> Self {
|
||||
Self::from_typevar_instances(
|
||||
db,
|
||||
self.variables(db).filter(|bound_typevar| {
|
||||
!(bound_typevar.typevar(db).is_self(db)
|
||||
&& binding_context.is_none_or(|binding_context| {
|
||||
bound_typevar.binding_context(db) == binding_context
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> {
|
||||
#[derive(Default)]
|
||||
struct CollectTypeVars<'db> {
|
||||
|
|
|
|||
|
|
@ -938,7 +938,7 @@ mod resolve_definition {
|
|||
};
|
||||
|
||||
// Resolve the module to its file
|
||||
let Some(resolved_module) = resolve_module(db, &module_name) else {
|
||||
let Some(resolved_module) = resolve_module(db, file, &module_name) else {
|
||||
return Vec::new(); // Module not found, return empty list
|
||||
};
|
||||
|
||||
|
|
@ -1025,7 +1025,7 @@ mod resolve_definition {
|
|||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(resolved_module) = resolve_module(db, &module_name) else {
|
||||
let Some(resolved_module) = resolve_module(db, file, &module_name) else {
|
||||
return Vec::new();
|
||||
};
|
||||
resolved_module.file(db)
|
||||
|
|
@ -1134,7 +1134,12 @@ mod resolve_definition {
|
|||
// It's definitely a stub, so now rerun module resolution but with stubs disabled.
|
||||
let stub_module = file_to_module(db, stub_file_for_module_lookup)?;
|
||||
trace!("Found stub module: {}", stub_module.name(db));
|
||||
let real_module = resolve_real_module(db, stub_module.name(db))?;
|
||||
// We need to pass an importing file to `resolve_real_module` which is a bit odd
|
||||
// here because there isn't really an importing file. However this `resolve_real_module`
|
||||
// can be understood as essentially `import .`, which is also what `file_to_module` is,
|
||||
// so this is in fact exactly the file we want to consider the importer.
|
||||
let real_module =
|
||||
resolve_real_module(db, stub_file_for_module_lookup, stub_module.name(db))?;
|
||||
trace!("Found real module: {}", real_module.name(db));
|
||||
let real_file = real_module.file(db)?;
|
||||
trace!("Found real file: {}", real_file.path(db));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use itertools::{Either, EitherOrBoth, Itertools};
|
|||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_python_ast::visitor::{Visitor, walk_expr};
|
||||
use ruff_python_ast::{
|
||||
self as ast, AnyNodeRef, ExprContext, HasNodeIndex, NodeIndex, PythonVersion,
|
||||
|
|
@ -3272,18 +3273,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred);
|
||||
match bound.as_deref() {
|
||||
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
|
||||
// We don't use UnionType::from_elements or UnionBuilder here, because we don't
|
||||
// want to simplify the list of constraints like we do with the elements of an
|
||||
// actual union type.
|
||||
// TODO: Consider using a new `OneOfType` connective here instead, since that
|
||||
// more accurately represents the actual semantics of typevar constraints.
|
||||
let ty = Type::Union(UnionType::new(
|
||||
// Here, we interpret `bound` as a heterogeneous tuple and convert it to `TypeVarConstraints` in `TypeVarInstance::lazy_constraints`.
|
||||
let tuple_ty = Type::heterogeneous_tuple(
|
||||
self.db(),
|
||||
elts.iter()
|
||||
.map(|expr| self.infer_type_expression(expr))
|
||||
.collect::<Box<[_]>>(),
|
||||
));
|
||||
self.store_expression_type(expr, ty);
|
||||
);
|
||||
self.store_expression_type(expr, tuple_ty);
|
||||
}
|
||||
Some(expr) => {
|
||||
self.infer_type_expression(expr);
|
||||
|
|
@ -5935,7 +5932,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
) else {
|
||||
return false;
|
||||
};
|
||||
resolve_module(self.db(), &module_name).is_some()
|
||||
resolve_module(self.db(), self.file(), &module_name).is_some()
|
||||
}) {
|
||||
diagnostic
|
||||
.help("The module can be resolved if the number of leading dots is reduced");
|
||||
|
|
@ -6172,7 +6169,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
};
|
||||
|
||||
if resolve_module(self.db(), &module_name).is_none() {
|
||||
if resolve_module(self.db(), self.file(), &module_name).is_none() {
|
||||
self.report_unresolved_import(import_from.into(), module_ref.range(), *level, module);
|
||||
}
|
||||
}
|
||||
|
|
@ -6190,7 +6187,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(module) = resolve_module(self.db(), &module_name) else {
|
||||
let Some(module) = resolve_module(self.db(), self.file(), &module_name) else {
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
return;
|
||||
};
|
||||
|
|
@ -6375,7 +6372,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
self.add_binding(import_from.into(), definition, |_, _| Type::unknown());
|
||||
return;
|
||||
};
|
||||
let Some(module) = resolve_module(self.db(), &thispackage_name) else {
|
||||
let Some(module) = resolve_module(self.db(), self.file(), &thispackage_name) else {
|
||||
self.add_binding(import_from.into(), definition, |_, _| Type::unknown());
|
||||
return;
|
||||
};
|
||||
|
|
@ -6606,7 +6603,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
|
||||
fn module_type_from_name(&self, module_name: &ModuleName) -> Option<Type<'db>> {
|
||||
resolve_module(self.db(), module_name)
|
||||
resolve_module(self.db(), self.file(), module_name)
|
||||
.map(|module| Type::module_literal(self.db(), self.file(), module))
|
||||
}
|
||||
|
||||
|
|
@ -7105,10 +7102,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
|
||||
#[track_caller]
|
||||
fn store_expression_type(&mut self, expression: &ast::Expr, ty: Type<'db>) {
|
||||
if self.deferred_state.in_string_annotation() {
|
||||
if self.deferred_state.in_string_annotation()
|
||||
|| self.inner_expression_inference_state.is_get()
|
||||
{
|
||||
// Avoid storing the type of expressions that are part of a string annotation because
|
||||
// the expression ids don't exists in the semantic index. Instead, we'll store the type
|
||||
// on the string expression itself that represents the annotation.
|
||||
// Also, if `inner_expression_inference_state` is `Get`, the expression type has already been stored.
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -9111,6 +9111,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
|
||||
/// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context.
|
||||
fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
|
||||
fn is_dotted_name(attribute: &ast::Expr) -> bool {
|
||||
match attribute {
|
||||
ast::Expr::Name(_) => true,
|
||||
ast::Expr::Attribute(ast::ExprAttribute { value, .. }) => is_dotted_name(value),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
let ast::ExprAttribute { value, attr, .. } = attribute;
|
||||
|
||||
let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default());
|
||||
|
|
@ -9186,7 +9194,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
{
|
||||
let mut maybe_submodule_name = module_name.clone();
|
||||
maybe_submodule_name.extend(&relative_submodule);
|
||||
if resolve_module(db, &maybe_submodule_name).is_some() {
|
||||
if resolve_module(db, self.file(), &maybe_submodule_name).is_some() {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&POSSIBLY_MISSING_ATTRIBUTE, attribute)
|
||||
|
|
@ -9204,6 +9212,42 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
|
||||
if let Type::SpecialForm(special_form) = value_type {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute)
|
||||
{
|
||||
let mut diag = builder.into_diagnostic(format_args!(
|
||||
"Special form `{special_form}` has no attribute `{attr_name}`",
|
||||
));
|
||||
if let Ok(defined_type) = value_type.in_type_expression(
|
||||
db,
|
||||
self.scope(),
|
||||
self.typevar_binding_context,
|
||||
) && !defined_type.member(db, attr_name).place.is_undefined()
|
||||
{
|
||||
diag.help(format_args!(
|
||||
"Objects with type `{ty}` have a{maybe_n} `{attr_name}` attribute, but the symbol \
|
||||
`{special_form}` does not itself inhabit the type `{ty}`",
|
||||
maybe_n = if attr_name.starts_with(['a', 'e', 'i', 'o', 'u']) {
|
||||
"n"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
ty = defined_type.display(self.db())
|
||||
));
|
||||
if is_dotted_name(value) {
|
||||
let source = &source_text(self.db(), self.file())[value.range()];
|
||||
diag.help(format_args!(
|
||||
"This error may indicate that `{source}` was defined as \
|
||||
`{source} = {special_form}` when `{source}: {special_form}` \
|
||||
was intended"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback();
|
||||
}
|
||||
|
||||
let Some(builder) = self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute)
|
||||
else {
|
||||
return fallback();
|
||||
|
|
@ -11436,6 +11480,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
if typevar.default_type(db).is_some() {
|
||||
typevar_with_defaults += 1;
|
||||
}
|
||||
// TODO consider just accepting the given specialization without checking
|
||||
// against bounds/constraints, but recording the expression for deferred
|
||||
// checking at end of scope. This would avoid a lot of cycles caused by eagerly
|
||||
// doing assignment checks here.
|
||||
match typevar.typevar(db).bound_or_constraints(db) {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
if provided_type
|
||||
|
|
@ -11460,10 +11508,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
// TODO: this is wrong, the given specialization needs to be assignable
|
||||
// to _at least one_ of the individual constraints, not to the union of
|
||||
// all of them. `int | str` is not a valid specialization of a typevar
|
||||
// constrained to `(int, str)`.
|
||||
if provided_type
|
||||
.when_assignable_to(
|
||||
db,
|
||||
Type::Union(constraints),
|
||||
constraints.as_type(db),
|
||||
InferableTypeVars::None,
|
||||
)
|
||||
.is_never_satisfied(db)
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ impl ClassInfoConstraintFunction {
|
|||
self.generate_constraint(db, bound)
|
||||
}
|
||||
TypeVarBoundOrConstraints::Constraints(constraints) => {
|
||||
self.generate_constraint(db, Type::Union(constraints))
|
||||
self.generate_constraint(db, constraints.as_type(db))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -667,10 +667,11 @@ impl<'db> Signature<'db> {
|
|||
|
||||
let mut parameters = Parameters::new(db, parameters);
|
||||
let mut return_ty = self.return_ty;
|
||||
let binding_context = self.definition.map(BindingContext::Definition);
|
||||
if let Some(self_type) = self_type {
|
||||
let self_mapping = TypeMapping::BindSelf {
|
||||
self_type,
|
||||
binding_context: self.definition.map(BindingContext::Definition),
|
||||
binding_context,
|
||||
};
|
||||
parameters = parameters.apply_type_mapping_impl(
|
||||
db,
|
||||
|
|
@ -682,7 +683,9 @@ impl<'db> Signature<'db> {
|
|||
.map(|ty| ty.apply_type_mapping(db, &self_mapping, TypeContext::default()));
|
||||
}
|
||||
Self {
|
||||
generic_context: self.generic_context,
|
||||
generic_context: self
|
||||
.generic_context
|
||||
.map(|generic_context| generic_context.remove_self(db, binding_context)),
|
||||
definition: self.definition,
|
||||
parameters,
|
||||
return_ty,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
use super::{ClassType, Type, class::KnownClass};
|
||||
use crate::db::Db;
|
||||
use crate::module_resolver::{KnownModule, file_to_module};
|
||||
use crate::resolve_module;
|
||||
use crate::module_resolver::{KnownModule, file_to_module, resolve_module_confident};
|
||||
use crate::semantic_index::place::ScopedPlaceId;
|
||||
use crate::semantic_index::{FileScopeId, place_table, use_def_map};
|
||||
use crate::types::TypeDefinition;
|
||||
|
|
@ -544,7 +543,7 @@ impl SpecialFormType {
|
|||
self.definition_modules()
|
||||
.iter()
|
||||
.find_map(|module| {
|
||||
let file = resolve_module(db, &module.name())?.file(db)?;
|
||||
let file = resolve_module_confident(db, &module.name())?.file(db)?;
|
||||
let scope = FileScopeId::global().to_scope_id(db, file);
|
||||
let symbol_id = place_table(db, scope).symbol_id(self.name())?;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::types::{
|
|||
ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType,
|
||||
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, KnownClass,
|
||||
MaterializationKind, MemberLookupPolicy, NormalizedVisitor, SpecialFormType, Type, TypeContext,
|
||||
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, UnionType, todo_type,
|
||||
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, todo_type,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
|
|
@ -190,7 +190,9 @@ impl<'db> SubclassOfType<'db> {
|
|||
match bound_typevar.typevar(db).bound_or_constraints(db) {
|
||||
None => unreachable!(),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound,
|
||||
Some(TypeVarBoundOrConstraints::Constraints(union)) => Type::Union(union),
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
constraints.as_type(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -351,7 +353,7 @@ impl<'db> SubclassOfInner<'db> {
|
|||
.and_then(|subclass_of| subclass_of.into_class(db))
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
match constraints.elements(db) {
|
||||
match &**constraints.elements(db) {
|
||||
[bound] => Self::try_from_instance(db, *bound)
|
||||
.and_then(|subclass_of| subclass_of.into_class(db)),
|
||||
_ => Some(ClassType::object(db)),
|
||||
|
|
@ -416,16 +418,10 @@ impl<'db> SubclassOfInner<'db> {
|
|||
)
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
let constraints = constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|constraint| {
|
||||
SubclassOfType::try_from_instance(db, *constraint)
|
||||
.unwrap_or(SubclassOfType::subclass_of_unknown())
|
||||
})
|
||||
.collect::<Box<_>>();
|
||||
|
||||
TypeVarBoundOrConstraints::Constraints(UnionType::new(db, constraints))
|
||||
TypeVarBoundOrConstraints::Constraints(constraints.map(db, |constraint| {
|
||||
SubclassOfType::try_from_instance(db, *constraint)
|
||||
.unwrap_or(SubclassOfType::subclass_of_unknown())
|
||||
}))
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use itertools::{Either, EitherOrBoth, Itertools};
|
|||
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError};
|
||||
use crate::types::builder::RecursivelyDefined;
|
||||
use crate::types::class::{ClassType, KnownClass};
|
||||
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
|
||||
use crate::types::generics::InferableTypeVars;
|
||||
|
|
@ -348,6 +349,10 @@ impl<T> FixedLengthTuple<T> {
|
|||
&self.0
|
||||
}
|
||||
|
||||
pub(crate) fn owned_elements(self) -> Box<[T]> {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub(crate) fn elements(&self) -> impl DoubleEndedIterator<Item = &T> + ExactSizeIterator + '_ {
|
||||
self.0.iter()
|
||||
}
|
||||
|
|
@ -1458,7 +1463,7 @@ impl<'db> Tuple<Type<'db>> {
|
|||
// those techniques ensure that union elements are deduplicated and unions are eagerly simplified
|
||||
// into other types where necessary. Here, however, we know that there are no duplicates
|
||||
// in this union, so it's probably more efficient to use `UnionType::new()` directly.
|
||||
Type::Union(UnionType::new(db, elements))
|
||||
Type::Union(UnionType::new(db, elements, RecursivelyDefined::No))
|
||||
};
|
||||
|
||||
TupleSpec::heterogeneous([
|
||||
|
|
|
|||
|
|
@ -79,8 +79,7 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> {
|
|||
let root = SystemPathBuf::from("/src");
|
||||
|
||||
let mut db = CorpusDb::new();
|
||||
db.memory_file_system()
|
||||
.create_directory_all(root.as_ref())?;
|
||||
db.memory_file_system().create_directory_all(&root)?;
|
||||
|
||||
let workspace_root = get_cargo_workspace_root()?;
|
||||
let workspace_root = workspace_root.to_string();
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue