Compare commits

..

3 Commits

Author SHA1 Message Date
David Peter
fee980a1ba Make infer_local_place_load a query 2025-09-02 09:21:56 +02:00
David Peter
f25b3e2254 Remove projects 2025-09-01 17:38:24 +02:00
David Peter
c6b661f374 [ty] Experiment: make infer_expression_types/infer_definition_types non-queries 2025-09-01 16:47:53 +02:00
103 changed files with 1906 additions and 3833 deletions

View File

@@ -64,12 +64,11 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@1f560d07d672effae250e3d271da53d96c5260ff"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
ecosystem-analyzer \
--repository ruff \
diff \
--profile=release \
--projects-old ruff/projects_old.txt \
--projects-new ruff/projects_new.txt \
--old old_commit \

View File

@@ -1,33 +1,5 @@
# Changelog
## 0.12.12
### Preview features
- Show fixes by default ([#19919](https://github.com/astral-sh/ruff/pull/19919))
- \[`airflow`\] Convert `DatasetOrTimeSchedule(datasets=...)` to `AssetOrTimeSchedule(assets=...)` (`AIR311`) ([#20202](https://github.com/astral-sh/ruff/pull/20202))
- \[`airflow`\] Improve the `AIR002` error message ([#20173](https://github.com/astral-sh/ruff/pull/20173))
- \[`airflow`\] Move `airflow.operators.postgres_operator.Mapping` from `AIR302` to `AIR301` ([#20172](https://github.com/astral-sh/ruff/pull/20172))
- \[`flake8-async`\] Implement `blocking-input` rule (`ASYNC250`) ([#20122](https://github.com/astral-sh/ruff/pull/20122))
- \[`flake8-use-pathlib`\] Make `PTH119` and `PTH120` fixes unsafe because they can change behavior ([#20118](https://github.com/astral-sh/ruff/pull/20118))
- \[`pylint`\] Add U+061C to `PLE2502` ([#20106](https://github.com/astral-sh/ruff/pull/20106))
- \[`ruff`\] Fix false negative for empty f-strings in `deque` calls (`RUF037`) ([#20109](https://github.com/astral-sh/ruff/pull/20109))
### Bug fixes
- Less confidently mark f-strings as empty when inferring truthiness ([#20152](https://github.com/astral-sh/ruff/pull/20152))
- \[`fastapi`\] Fix false positive for paths with spaces around parameters (`FAST003`) ([#20077](https://github.com/astral-sh/ruff/pull/20077))
- \[`flake8-comprehensions`\] Skip `C417` when lambda contains `yield`/`yield from` ([#20201](https://github.com/astral-sh/ruff/pull/20201))
- \[`perflint`\] Handle tuples in dictionary comprehensions (`PERF403`) ([#19934](https://github.com/astral-sh/ruff/pull/19934))
### Rule changes
- \[`pycodestyle`\] Preserve return type annotation for `ParamSpec` (`E731`) ([#20108](https://github.com/astral-sh/ruff/pull/20108))
### Documentation
- Add fix safety sections to docs ([#17490](https://github.com/astral-sh/ruff/pull/17490),[#17499](https://github.com/astral-sh/ruff/pull/17499))
## 0.12.11
### Preview features

94
Cargo.lock generated
View File

@@ -295,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata",
"regex-automata 0.4.10",
"serde",
]
@@ -1231,8 +1231,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.10",
"regex-syntax 0.8.5",
]
[[package]]
@@ -1459,7 +1459,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata",
"regex-automata 0.4.10",
"same-file",
"walkdir",
"winapi-util",
@@ -1913,11 +1913,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.2.0"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
"regex-automata 0.1.10",
]
[[package]]
@@ -2074,11 +2074,12 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"windows-sys 0.52.0",
"overload",
"winapi",
]
[[package]]
@@ -2153,6 +2154,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking_lot"
version = "0.12.4"
@@ -2681,8 +2688,17 @@ checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.10",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
@@ -2693,7 +2709,7 @@ checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.8.5",
]
[[package]]
@@ -2702,6 +2718,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -2721,7 +2743,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.12"
version = "0.12.11"
dependencies = [
"anyhow",
"argfile",
@@ -2977,7 +2999,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.12"
version = "0.12.11"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3315,7 +3337,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.12"
version = "0.12.11"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -4139,15 +4161,15 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"chrono",
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
@@ -4219,7 +4241,6 @@ name = "ty_ide"
version = "0.0.0"
dependencies = [
"bitflags 2.9.3",
"camino",
"get-size2",
"insta",
"itertools 0.14.0",
@@ -4257,7 +4278,7 @@ dependencies = [
"pep440_rs",
"rayon",
"regex",
"regex-automata",
"regex-automata 0.4.10",
"ruff_cache",
"ruff_db",
"ruff_macros",
@@ -4871,6 +4892,22 @@ dependencies = [
"glob",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
@@ -4880,6 +4917,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -4939,15 +4982,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"

1160
LICENSE

File diff suppressed because it is too large Load Diff

View File

@@ -148,8 +148,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.12.12/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.12/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.11/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.11/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,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.12.12
rev: v0.12.11
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -1444,7 +1444,7 @@ pub enum DiagnosticFormat {
Junit,
/// Print diagnostics in the JSON format used by GitLab [Code Quality] reports.
///
/// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
/// [Code Quality]: https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
#[cfg(feature = "serde")]
Gitlab,
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.12.12"
version = "0.12.11"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -12,7 +12,6 @@ from airflow import (
from airflow.api_connexion.security import requires_access
from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.datasets import DatasetAliasEvent
from airflow.operators.postgres_operator import Mapping
from airflow.operators.subdag import SubDagOperator
from airflow.secrets.cache import SecretCache
from airflow.secrets.local_filesystem import LocalFilesystemBackend
@@ -53,8 +52,6 @@ DatasetAliasEvent()
# airflow.operators.subdag.*
SubDagOperator()
# airflow.operators.postgres_operator
Mapping()
# airflow.secrets
# get_connection

View File

@@ -70,7 +70,7 @@ from airflow.timetables.datasets import DatasetOrTimeSchedule
from airflow.utils.dag_parsing_context import get_parsing_context
# airflow.timetables.datasets
DatasetOrTimeSchedule(datasets=[])
DatasetOrTimeSchedule()
# airflow.utils.dag_parsing_context
get_parsing_context()

View File

@@ -75,7 +75,3 @@ list(map(lambda x, y: x, [(1, 2), (3, 4)]))
_ = t"{set(map(lambda x: x % 2 == 0, nums))}"
_ = t"{dict(map(lambda v: (v, v**2), nums))}"
# See https://github.com/astral-sh/ruff/issues/20198
# No error: lambda contains `yield`, so map() should not be rewritten
map(lambda x: (yield x), [1, 2, 3])

View File

@@ -1 +0,0 @@
async def f(): yield from x # error

View File

@@ -1319,10 +1319,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
pylint::rules::yield_in_init(checker, expr);
}
}
Expr::YieldFrom(_) => {
Expr::YieldFrom(yield_from) => {
if checker.is_rule_enabled(Rule::YieldInInit) {
pylint::rules::yield_in_init(checker, expr);
}
if checker.is_rule_enabled(Rule::YieldFromInAsyncFunction) {
pylint::rules::yield_from_in_async_function(checker, yield_from);
}
}
Expr::FString(f_string_expr @ ast::ExprFString { value, .. }) => {
if checker.is_rule_enabled(Rule::FStringMissingPlaceholders) {

View File

@@ -71,9 +71,7 @@ use crate::registry::Rule;
use crate::rules::pyflakes::rules::{
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
};
use crate::rules::pylint::rules::{
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
};
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
use crate::settings::rule_table::RuleTable;
use crate::settings::{LinterSettings, TargetVersion, flags};
@@ -670,12 +668,6 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(AwaitOutsideAsync, error.range);
}
}
SemanticSyntaxErrorKind::YieldFromInAsyncFunction => {
// PLE1700
if self.is_rule_enabled(Rule::YieldFromInAsyncFunction) {
self.report_diagnostic(YieldFromInAsyncFunction, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)

View File

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

View File

@@ -37,6 +37,7 @@ pub(crate) enum Replacement {
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ProviderReplacement {
None,
AutoImport {
module: &'static str,
name: &'static str,

View File

@@ -46,7 +46,7 @@ pub(crate) struct AirflowDagNoScheduleArgument;
impl Violation for AirflowDagNoScheduleArgument {
#[derive_message_formats]
fn message(&self) -> String {
"`DAG` or `@dag` should have an explicit `schedule` argument".to_string()
"DAG should have an explicit `schedule` argument".to_string()
}
}

View File

@@ -50,6 +50,9 @@ impl Violation for Airflow3MovedToProvider<'_> {
replacement,
} = self;
match replacement {
ProviderReplacement::None => {
format!("`{deprecated}` is removed in Airflow 3.0")
}
ProviderReplacement::AutoImport {
name: _,
module: _,
@@ -82,6 +85,7 @@ impl Violation for Airflow3MovedToProvider<'_> {
provider,
version,
} => Some((module, name.as_str(), provider, version)),
ProviderReplacement::None => None,
} {
Some(format!(
"Install `apache-airflow-providers-{provider}>={version}` and use `{name}` from `{module}` instead."
@@ -1016,6 +1020,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
provider: "postgres",
version: "1.0.0",
},
["airflow", "operators", "postgres_operator", "Mapping"] => ProviderReplacement::None,
// apache-airflow-providers-presto
["airflow", "hooks", "presto_hook", "PrestoHook"] => ProviderReplacement::AutoImport {
@@ -1204,6 +1209,16 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => {
(module, name.as_str())
}
ProviderReplacement::None => {
checker.report_diagnostic(
Airflow3MovedToProvider {
deprecated: qualified_name,
replacement,
},
ranged,
);
return;
}
};
if is_guarded_by_try_except(expr, module, name, checker.semantic()) {

View File

@@ -704,7 +704,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
["airflow", "operators", "subdag", ..] => {
Replacement::Message("The whole `airflow.subdag` module has been removed.")
}
["airflow", "operators", "postgres_operator", "Mapping"] => Replacement::None,
["airflow", "operators", "python", "get_current_context"] => Replacement::AutoImport {
module: "airflow.sdk",
name: "get_current_context",

View File

@@ -65,6 +65,9 @@ impl Violation for Airflow3SuggestedToMoveToProvider<'_> {
replacement,
} = self;
match replacement {
ProviderReplacement::None => {
format!("`{deprecated}` is removed in Airflow 3.0")
}
ProviderReplacement::AutoImport {
name: _,
module: _,
@@ -88,6 +91,7 @@ impl Violation for Airflow3SuggestedToMoveToProvider<'_> {
fn fix_title(&self) -> Option<String> {
let Airflow3SuggestedToMoveToProvider { replacement, .. } = self;
match replacement {
ProviderReplacement::None => None,
ProviderReplacement::AutoImport {
module,
name,
@@ -315,6 +319,16 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => {
(module, name.as_str())
}
ProviderReplacement::None => {
checker.report_diagnostic(
Airflow3SuggestedToMoveToProvider {
deprecated: qualified_name,
replacement: replacement.clone(),
},
ranged.range(),
);
return;
}
};
if is_guarded_by_try_except(expr, module, name, checker.semantic()) {

View File

@@ -157,9 +157,6 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
["airflow", .., "DAG" | "dag"] => {
diagnostic_for_argument(checker, arguments, "sla_miss_callback", None);
}
["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => {
diagnostic_for_argument(checker, arguments, "datasets", Some("assets"));
}
segments => {
if is_airflow_builtin_or_provider(segments, "operators", "Operator") {
diagnostic_for_argument(checker, arguments, "sla", None);

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR002 `DAG` or `@dag` should have an explicit `schedule` argument
AIR002 DAG should have an explicit `schedule` argument
--> AIR002.py:4:1
|
2 | from airflow.timetables.simple import NullTimetable
@@ -12,7 +12,7 @@ AIR002 `DAG` or `@dag` should have an explicit `schedule` argument
6 | DAG(dag_id="class_schedule", schedule="@hourly")
|
AIR002 `DAG` or `@dag` should have an explicit `schedule` argument
AIR002 DAG should have an explicit `schedule` argument
--> AIR002.py:13:2
|
13 | @dag()

View File

@@ -2,362 +2,350 @@
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR301 `airflow.PY36` is removed in Airflow 3.0
--> AIR301_names.py:40:1
--> AIR301_names.py:39:1
|
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^
41 |
42 | # airflow.api_connexion.security
40 |
41 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY37` is removed in Airflow 3.0
--> AIR301_names.py:40:7
--> AIR301_names.py:39:7
|
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^
41 |
42 | # airflow.api_connexion.security
40 |
41 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY38` is removed in Airflow 3.0
--> AIR301_names.py:40:13
--> AIR301_names.py:39:13
|
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^
41 |
42 | # airflow.api_connexion.security
40 |
41 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY39` is removed in Airflow 3.0
--> AIR301_names.py:40:19
--> AIR301_names.py:39:19
|
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^
41 |
42 | # airflow.api_connexion.security
40 |
41 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY310` is removed in Airflow 3.0
--> AIR301_names.py:40:25
--> AIR301_names.py:39:25
|
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^
41 |
42 | # airflow.api_connexion.security
40 |
41 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY311` is removed in Airflow 3.0
--> AIR301_names.py:40:32
--> AIR301_names.py:39:32
|
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^
41 |
42 | # airflow.api_connexion.security
40 |
41 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.PY312` is removed in Airflow 3.0
--> AIR301_names.py:40:39
--> AIR301_names.py:39:39
|
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
| ^^^^^
41 |
42 | # airflow.api_connexion.security
40 |
41 | # airflow.api_connexion.security
|
help: Use `sys.version_info` instead
AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0
--> AIR301_names.py:43:1
--> AIR301_names.py:42:1
|
42 | # airflow.api_connexion.security
43 | requires_access
41 | # airflow.api_connexion.security
42 | requires_access
| ^^^^^^^^^^^^^^^
44 |
45 | # airflow.contrib.*
43 |
44 | # airflow.contrib.*
|
help: Use `airflow.api_fastapi.core_api.security.requires_access_*` instead
AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0
--> AIR301_names.py:46:1
--> AIR301_names.py:45:1
|
45 | # airflow.contrib.*
46 | AWSAthenaHook()
44 | # airflow.contrib.*
45 | AWSAthenaHook()
| ^^^^^^^^^^^^^
|
help: The whole `airflow.contrib` module has been removed.
AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
--> AIR301_names.py:50:1
--> AIR301_names.py:49:1
|
49 | # airflow.datasets
50 | DatasetAliasEvent()
48 | # airflow.datasets
49 | DatasetAliasEvent()
| ^^^^^^^^^^^^^^^^^
|
AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
--> AIR301_names.py:54:1
--> AIR301_names.py:53:1
|
53 | # airflow.operators.subdag.*
54 | SubDagOperator()
52 | # airflow.operators.subdag.*
53 | SubDagOperator()
| ^^^^^^^^^^^^^^
55 |
56 | # airflow.operators.postgres_operator
|
help: The whole `airflow.subdag` module has been removed.
AIR301 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0
--> AIR301_names.py:57:1
|
56 | # airflow.operators.postgres_operator
57 | Mapping()
| ^^^^^^^
58 |
59 | # airflow.secrets
|
AIR301 [*] `airflow.secrets.cache.SecretCache` is removed in Airflow 3.0
--> AIR301_names.py:64:1
--> AIR301_names.py:61:1
|
63 | # airflow.secrets.cache
64 | SecretCache()
60 | # airflow.secrets.cache
61 | SecretCache()
| ^^^^^^^^^^^
|
help: Use `SecretCache` from `airflow.sdk` instead.
13 | from airflow.contrib.aws_athena_hook import AWSAthenaHook
14 | from airflow.datasets import DatasetAliasEvent
15 | from airflow.operators.postgres_operator import Mapping
16 | from airflow.operators.subdag import SubDagOperator
15 | from airflow.operators.subdag import SubDagOperator
- from airflow.secrets.cache import SecretCache
17 | from airflow.secrets.local_filesystem import LocalFilesystemBackend
18 | from airflow.triggers.external_task import TaskStateTrigger
19 | from airflow.utils import dates
16 | from airflow.secrets.local_filesystem import LocalFilesystemBackend
17 | from airflow.triggers.external_task import TaskStateTrigger
18 | from airflow.utils import dates
--------------------------------------------------------------------------------
34 | from airflow.utils.trigger_rule import TriggerRule
35 | from airflow.www.auth import has_access, has_access_dataset
36 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
37 + from airflow.sdk import SecretCache
38 |
39 | # airflow root
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
33 | from airflow.utils.trigger_rule import TriggerRule
34 | from airflow.www.auth import has_access, has_access_dataset
35 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
36 + from airflow.sdk import SecretCache
37 |
38 | # airflow root
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
note: This is an unsafe fix and may change runtime behavior
AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
--> AIR301_names.py:65:1
|
64 | # airflow.triggers.external_task
65 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^
66 |
67 | # airflow.utils.date
|
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:68:1
|
67 | # airflow.triggers.external_task
68 | TaskStateTrigger()
67 | # airflow.utils.date
68 | dates.date_range
| ^^^^^^^^^^^^^^^^
69 |
70 | # airflow.utils.date
69 | dates.days_ago
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:69:1
|
67 | # airflow.utils.date
68 | dates.date_range
69 | dates.days_ago
| ^^^^^^^^^^^^^^
70 |
71 | date_range
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:71:1
|
70 | # airflow.utils.date
71 | dates.date_range
| ^^^^^^^^^^^^^^^^
72 | dates.days_ago
69 | dates.days_ago
70 |
71 | date_range
| ^^^^^^^^^^
72 | days_ago
73 | infer_time_unit
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:72:1
|
70 | # airflow.utils.date
71 | dates.date_range
72 | dates.days_ago
| ^^^^^^^^^^^^^^
73 |
74 | date_range
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:74:1
|
72 | dates.days_ago
73 |
74 | date_range
| ^^^^^^^^^^
75 | days_ago
76 | infer_time_unit
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:75:1
|
74 | date_range
75 | days_ago
71 | date_range
72 | days_ago
| ^^^^^^^^
76 | infer_time_unit
77 | parse_execution_date
73 | infer_time_unit
74 | parse_execution_date
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
--> AIR301_names.py:76:1
--> AIR301_names.py:73:1
|
74 | date_range
75 | days_ago
76 | infer_time_unit
71 | date_range
72 | days_ago
73 | infer_time_unit
| ^^^^^^^^^^^^^^^
77 | parse_execution_date
78 | round_time
74 | parse_execution_date
75 | round_time
|
AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
--> AIR301_names.py:77:1
--> AIR301_names.py:74:1
|
75 | days_ago
76 | infer_time_unit
77 | parse_execution_date
72 | days_ago
73 | infer_time_unit
74 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^
78 | round_time
79 | scale_time_units
75 | round_time
76 | scale_time_units
|
AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
--> AIR301_names.py:78:1
--> AIR301_names.py:75:1
|
76 | infer_time_unit
77 | parse_execution_date
78 | round_time
73 | infer_time_unit
74 | parse_execution_date
75 | round_time
| ^^^^^^^^^^
79 | scale_time_units
76 | scale_time_units
|
AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
--> AIR301_names.py:79:1
--> AIR301_names.py:76:1
|
77 | parse_execution_date
78 | round_time
79 | scale_time_units
74 | parse_execution_date
75 | round_time
76 | scale_time_units
| ^^^^^^^^^^^^^^^^
80 |
81 | # This one was not deprecated.
77 |
78 | # This one was not deprecated.
|
AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
--> AIR301_names.py:86:1
--> AIR301_names.py:83:1
|
85 | # airflow.utils.dag_cycle_tester
86 | test_cycle
82 | # airflow.utils.dag_cycle_tester
83 | test_cycle
| ^^^^^^^^^^
|
AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
--> AIR301_names.py:90:1
--> AIR301_names.py:87:1
|
89 | # airflow.utils.db
90 | create_session
86 | # airflow.utils.db
87 | create_session
| ^^^^^^^^^^^^^^
91 |
92 | # airflow.utils.decorators
88 |
89 | # airflow.utils.decorators
|
AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0
--> AIR301_names.py:93:1
--> AIR301_names.py:90:1
|
92 | # airflow.utils.decorators
93 | apply_defaults
89 | # airflow.utils.decorators
90 | apply_defaults
| ^^^^^^^^^^^^^^
94 |
95 | # airflow.utils.file
91 |
92 | # airflow.utils.file
|
help: `apply_defaults` is now unconditionally done and can be safely removed.
AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
--> AIR301_names.py:96:1
--> AIR301_names.py:93:1
|
95 | # airflow.utils.file
96 | mkdirs
92 | # airflow.utils.file
93 | mkdirs
| ^^^^^^
|
help: Use `pathlib.Path({path}).mkdir` instead
AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
--> AIR301_names.py:100:1
|
99 | # airflow.utils.state
100 | SHUTDOWN
| ^^^^^^^^
101 | terminating_states
|
--> AIR301_names.py:97:1
|
96 | # airflow.utils.state
97 | SHUTDOWN
| ^^^^^^^^
98 | terminating_states
|
AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
--> AIR301_names.py:101:1
--> AIR301_names.py:98:1
|
99 | # airflow.utils.state
100 | SHUTDOWN
101 | terminating_states
96 | # airflow.utils.state
97 | SHUTDOWN
98 | terminating_states
| ^^^^^^^^^^^^^^^^^^
102 |
103 | # airflow.utils.trigger_rule
99 |
100 | # airflow.utils.trigger_rule
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
--> AIR301_names.py:104:1
--> AIR301_names.py:101:1
|
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
100 | # airflow.utils.trigger_rule
101 | TriggerRule.DUMMY
| ^^^^^^^^^^^^^^^^^
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
102 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
--> AIR301_names.py:105:1
--> AIR301_names.py:102:1
|
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
100 | # airflow.utils.trigger_rule
101 | TriggerRule.DUMMY
102 | TriggerRule.NONE_FAILED_OR_SKIPPED
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
--> AIR301_names.py:109:1
--> AIR301_names.py:106:1
|
108 | # airflow.www.auth
109 | has_access
105 | # airflow.www.auth
106 | has_access
| ^^^^^^^^^^
110 | has_access_dataset
107 | has_access_dataset
|
AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0
--> AIR301_names.py:110:1
--> AIR301_names.py:107:1
|
108 | # airflow.www.auth
109 | has_access
110 | has_access_dataset
105 | # airflow.www.auth
106 | has_access
107 | has_access_dataset
| ^^^^^^^^^^^^^^^^^^
111 |
112 | # airflow.www.utils
108 |
109 | # airflow.www.utils
|
AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
--> AIR301_names.py:113:1
--> AIR301_names.py:110:1
|
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
109 | # airflow.www.utils
110 | get_sensitive_variables_fields
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
114 | should_hide_value_for_key
111 | should_hide_value_for_key
|
AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
--> AIR301_names.py:114:1
--> AIR301_names.py:111:1
|
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
114 | should_hide_value_for_key
109 | # airflow.www.utils
110 | get_sensitive_variables_fields
111 | should_hide_value_for_key
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -20,3 +20,11 @@ help: Install `apache-airflow-providers-postgres>=1.0.0` and use `PostgresHook`
6 | PostgresHook()
7 | Mapping()
note: This is an unsafe fix and may change runtime behavior
AIR302 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0
--> AIR302_postgres.py:7:1
|
6 | PostgresHook()
7 | Mapping()
| ^^^^^^^
|

View File

@@ -558,7 +558,7 @@ AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Air
--> AIR311_names.py:73:1
|
72 | # airflow.timetables.datasets
73 | DatasetOrTimeSchedule(datasets=[])
73 | DatasetOrTimeSchedule()
| ^^^^^^^^^^^^^^^^^^^^^
74 |
75 | # airflow.utils.dag_parsing_context
@@ -570,31 +570,12 @@ help: Use `AssetOrTimeSchedule` from `airflow.timetables.assets` instead.
71 + from airflow.timetables.assets import AssetOrTimeSchedule
72 |
73 | # airflow.timetables.datasets
- DatasetOrTimeSchedule(datasets=[])
74 + AssetOrTimeSchedule(datasets=[])
- DatasetOrTimeSchedule()
74 + AssetOrTimeSchedule()
75 |
76 | # airflow.utils.dag_parsing_context
77 | get_parsing_context()
AIR311 [*] `datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:73:23
|
72 | # airflow.timetables.datasets
73 | DatasetOrTimeSchedule(datasets=[])
| ^^^^^^^^
74 |
75 | # airflow.utils.dag_parsing_context
|
help: Use `assets` instead
70 | from airflow.utils.dag_parsing_context import get_parsing_context
71 |
72 | # airflow.timetables.datasets
- DatasetOrTimeSchedule(datasets=[])
73 + DatasetOrTimeSchedule(assets=[])
74 |
75 | # airflow.utils.dag_parsing_context
76 | get_parsing_context()
AIR311 [*] `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:76:1
|
@@ -612,7 +593,7 @@ help: Use `get_parsing_context` from `airflow.sdk` instead.
70 + from airflow.sdk import get_parsing_context
71 |
72 | # airflow.timetables.datasets
73 | DatasetOrTimeSchedule(datasets=[])
73 | DatasetOrTimeSchedule()
note: This is an unsafe fix and may change runtime behavior
AIR311 [*] `airflow.decorators.base.DecoratedMappedOperator` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.

View File

@@ -122,13 +122,6 @@ pub(crate) fn unnecessary_map(checker: &Checker, call: &ast::ExprCall) {
}
};
// If the lambda body contains a `yield` or `yield from`, rewriting `map(lambda ...)` to a
// generator expression or any comprehension is invalid Python syntax
// (e.g., `yield` is not allowed inside generator or comprehension expressions). In such cases, skip.
if lambda_contains_yield(&lambda.body) {
return;
}
for iterable in iterables {
// For example, (x+1 for x in (c:=a)) is invalid syntax
// so we can't suggest it.
@@ -190,13 +183,6 @@ fn map_lambda_and_iterables<'a>(
Some((lambda, iterables))
}
/// Returns true if the expression tree contains a `yield` or `yield from` expression.
fn lambda_contains_yield(expr: &Expr) -> bool {
any_over_expr(expr, &|expr| {
matches!(expr, Expr::Yield(_) | Expr::YieldFrom(_))
})
}
/// A lambda as the first argument to `map()` has the "expected" arity when:
///
/// * It has exactly one parameter

View File

@@ -342,7 +342,6 @@ help: Replace `map()` with a set comprehension
75 + _ = t"{ {x % 2 == 0 for x in nums} }"
76 | _ = t"{dict(map(lambda v: (v, v**2), nums))}"
77 |
78 |
note: This is an unsafe fix and may change runtime behavior
C417 [*] Unnecessary `map()` usage (rewrite using a dict comprehension)
@@ -360,6 +359,4 @@ help: Replace `map()` with a dict comprehension
- _ = t"{dict(map(lambda v: (v, v**2), nums))}"
76 + _ = t"{ {v: v**2 for v in nums} }"
77 |
78 |
79 | # See https://github.com/astral-sh/ruff/issues/20198
note: This is an unsafe fix and may change runtime behavior

View File

@@ -60,7 +60,6 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
fn_argument: &str,
fix_enabled: bool,
violation: impl Violation,
applicability: Option<Applicability>,
) {
if call.arguments.len() != 1 {
return;
@@ -75,39 +74,33 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
if !fix_enabled {
return;
if fix_enabled {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.{attr}")
} else {
format!("{binding}({arg_code}).{attr}")
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.{attr}")
} else {
format!("{binding}({arg_code}).{attr}")
};
let edit = Edit::range_replacement(replacement, range);
let fix = match applicability {
Some(Applicability::Unsafe) => Fix::unsafe_edits(edit, [import_edit]),
_ => {
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
Fix::applicable_edits(edit, [import_edit], applicability)
}
};
Ok(fix)
});
}
pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {

View File

@@ -1,11 +1,9 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_basename_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.basename`.
@@ -36,16 +34,7 @@ use crate::{FixAvailability, Violation};
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is always marked as unsafe because the replacement is not always semantically
/// equivalent to the original code. In particular, `pathlib` performs path normalization,
/// which can alter the result compared to `os.path.basename`. For example, this normalization:
///
/// - Collapses consecutive slashes (e.g., `"a//b"` → `"a/b"`).
/// - Removes trailing slashes (e.g., `"a/b/"` → `"a/b"`).
/// - Eliminates `"."` (e.g., `"a/./b"` → `"a/b"`).
///
/// As a result, code relying on the exact string returned by `os.path.basename`
/// may behave differently after the fix.
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
@@ -73,7 +62,6 @@ pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&
if segments != ["os", "path", "basename"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -81,6 +69,5 @@ pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&
"p",
is_fix_os_path_basename_enabled(checker.settings()),
OsPathBasename,
Some(Applicability::Unsafe),
);
}

View File

@@ -1,11 +1,9 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_dirname_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.dirname`.
@@ -31,16 +29,7 @@ use crate::{FixAvailability, Violation};
/// ```
///
/// ## Fix Safety
/// This rule's fix is always marked as unsafe because the replacement is not always semantically
/// equivalent to the original code. In particular, `pathlib` performs path normalization,
/// which can alter the result compared to `os.path.dirname`. For example, this normalization:
///
/// - Collapses consecutive slashes (e.g., `"a//b"` → `"a/b"`).
/// - Removes trailing slashes (e.g., `"a/b/"` → `"a/b"`).
/// - Eliminates `"."` (e.g., `"a/./b"` → `"a/b"`).
///
/// As a result, code relying on the exact string returned by `os.path.dirname`
/// may behave differently after the fix.
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
@@ -73,7 +62,6 @@ pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&s
if segments != ["os", "path", "dirname"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -81,6 +69,5 @@ pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&s
"p",
is_fix_os_path_dirname_enabled(checker.settings()),
OsPathDirname,
Some(Applicability::Unsafe),
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_exists_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.exists`.
@@ -63,7 +62,6 @@ pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&st
if segments != ["os", "path", "exists"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -71,6 +69,5 @@ pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_exists_enabled(checker.settings()),
OsPathExists,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_expanduser_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.expanduser`.
@@ -63,7 +62,6 @@ pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &
if segments != ["os", "path", "expanduser"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -71,6 +69,5 @@ pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &
"path",
is_fix_os_path_expanduser_enabled(checker.settings()),
OsPathExpanduser,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getatime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getatime`.
@@ -66,7 +65,6 @@ pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&
if segments != ["os", "path", "getatime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -74,6 +72,5 @@ pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getatime_enabled(checker.settings()),
OsPathGetatime,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getctime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getctime`.
@@ -67,7 +66,6 @@ pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&
if segments != ["os", "path", "getctime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -75,6 +73,5 @@ pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getctime_enabled(checker.settings()),
OsPathGetctime,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getmtime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getmtime`.
@@ -67,7 +66,6 @@ pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&
if segments != ["os", "path", "getmtime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -75,6 +73,5 @@ pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getmtime_enabled(checker.settings()),
OsPathGetmtime,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getsize_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getsize`.
@@ -67,7 +66,6 @@ pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&s
if segments != ["os", "path", "getsize"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -75,6 +73,5 @@ pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&s
"filename",
is_fix_os_path_getsize_enabled(checker.settings()),
OsPathGetsize,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isabs_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isabs`.
@@ -62,7 +61,6 @@ pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str
if segments != ["os", "path", "isabs"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -70,6 +68,5 @@ pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str
"s",
is_fix_os_path_isabs_enabled(checker.settings()),
OsPathIsabs,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isdir`.
@@ -72,6 +71,5 @@ pub(crate) fn os_path_isdir(checker: &Checker, call: &ExprCall, segments: &[&str
"s",
is_fix_os_path_isdir_enabled(checker.settings()),
OsPathIsdir,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isfile_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isfile`.
@@ -72,6 +71,5 @@ pub(crate) fn os_path_isfile(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_isfile_enabled(checker.settings()),
OsPathIsfile,
None,
);
}

View File

@@ -1,10 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_islink_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.islink`.
@@ -72,6 +71,5 @@ pub(crate) fn os_path_islink(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_islink_enabled(checker.settings()),
OsPathIslink,
None,
);
}

View File

@@ -1,12 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ExprCall, PythonVersion};
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_readlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ExprCall, PythonVersion};
/// ## What it does
/// Checks for uses of `os.readlink`.
@@ -88,6 +87,5 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
"path",
is_fix_os_readlink_enabled(checker.settings()),
OsReadlink,
None,
);
}

View File

@@ -1,12 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_remove_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.remove`.
@@ -83,6 +82,5 @@ pub(crate) fn os_remove(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_remove_enabled(checker.settings()),
OsRemove,
None,
);
}

View File

@@ -1,12 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rmdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.rmdir`.
@@ -83,6 +82,5 @@ pub(crate) fn os_rmdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_rmdir_enabled(checker.settings()),
OsRmdir,
None,
);
}

View File

@@ -1,12 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_unlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.unlink`.
@@ -83,6 +82,5 @@ pub(crate) fn os_unlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_unlink_enabled(checker.settings()),
OsUnlink,
None,
);
}

View File

@@ -466,7 +466,6 @@ help: Replace with `Path(...).name`
30 | os.path.dirname(p)
31 | os.path.samefile(p)
32 | os.path.splitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
--> full_name.py:29:1
@@ -494,7 +493,6 @@ help: Replace with `Path(...).parent`
31 | os.path.samefile(p)
32 | os.path.splitext(p)
33 | with open(p) as fp:
note: This is an unsafe fix and may change runtime behavior
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> full_name.py:30:1

View File

@@ -466,7 +466,6 @@ help: Replace with `Path(...).name`
30 | foo_p.dirname(p)
31 | foo_p.samefile(p)
32 | foo_p.splitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
--> import_as.py:29:1
@@ -493,7 +492,6 @@ help: Replace with `Path(...).parent`
30 + pathlib.Path(p).parent
31 | foo_p.samefile(p)
32 | foo_p.splitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> import_as.py:30:1

View File

@@ -480,7 +480,6 @@ help: Replace with `Path(...).name`
32 | dirname(p)
33 | samefile(p)
34 | splitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
--> import_from.py:31:1
@@ -509,7 +508,6 @@ help: Replace with `Path(...).parent`
33 | samefile(p)
34 | splitext(p)
35 | with open(p) as fp:
note: This is an unsafe fix and may change runtime behavior
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> import_from.py:32:1

View File

@@ -480,7 +480,6 @@ help: Replace with `Path(...).name`
37 | xdirname(p)
38 | xsamefile(p)
39 | xsplitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
--> import_from_as.py:36:1
@@ -508,7 +507,6 @@ help: Replace with `Path(...).parent`
37 + pathlib.Path(p).parent
38 | xsamefile(p)
39 | xsplitext(p)
note: This is an unsafe fix and may change runtime behavior
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> import_from_as.py:37:1

View File

@@ -1,6 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast};
use ruff_python_semantic::ScopeKind;
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `yield from` in async functions.
@@ -32,3 +36,13 @@ impl Violation for YieldFromInAsyncFunction {
"`yield from` statement in async function; use `async for` instead".to_string()
}
}
/// PLE1700
pub(crate) fn yield_from_in_async_function(checker: &Checker, expr: &ast::ExprYieldFrom) {
if matches!(
checker.semantic().current_scope().kind,
ScopeKind::Function(ast::StmtFunctionDef { is_async: true, .. })
) {
checker.report_diagnostic(YieldFromInAsyncFunction, expr.range());
}
}

View File

@@ -1,9 +0,0 @@
---
source: crates/ruff_linter/src/linter.rs
---
PLE1700 `yield from` statement in async function; use `async for` instead
--> resources/test/fixtures/syntax_errors/yield_from_in_async_function.py:1:16
|
1 | async def f(): yield from x # error
| ^^^^^^^^^^^^
|

View File

@@ -1,6 +1,6 @@
pub use generator::Generator;
use ruff_python_parser::{ParseError, parse_module};
pub use stylist::{Indentation, Stylist};
pub use stylist::Stylist;
mod generator;
mod stylist;

View File

@@ -1 +0,0 @@
async def f(): yield from x

View File

@@ -709,16 +709,6 @@ impl SemanticSyntaxChecker {
}
Expr::YieldFrom(_) => {
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::YieldFrom);
if ctx.in_function_scope() && ctx.in_async_context() {
// test_err yield_from_in_async_function
// async def f(): yield from x
Self::add_error(
ctx,
SemanticSyntaxErrorKind::YieldFromInAsyncFunction,
expr.range(),
);
}
}
Expr::Await(_) => {
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
@@ -999,9 +989,6 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
write!(f, "annotated name `{name}` can't be nonlocal")
}
SemanticSyntaxErrorKind::YieldFromInAsyncFunction => {
f.write_str("`yield from` statement in async function; use `async for` instead")
}
}
}
}
@@ -1359,9 +1346,6 @@ pub enum SemanticSyntaxErrorKind {
/// Represents a type annotation on a variable that's been declared nonlocal
AnnotatedNonlocal(String),
/// Represents the use of `yield from` inside an asynchronous function.
YieldFromInAsyncFunction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]

View File

@@ -465,7 +465,7 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
enum Scope {
Module,
Function { is_async: bool },
Function,
Comprehension { is_async: bool },
Class,
}
@@ -528,15 +528,7 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
}
fn in_async_context(&self) -> bool {
if let Some(scope) = self.scopes.iter().next_back() {
match scope {
Scope::Class | Scope::Module => false,
Scope::Comprehension { is_async } => *is_async,
Scope::Function { is_async } => *is_async,
}
} else {
false
}
true
}
fn in_sync_comprehension(&self) -> bool {
@@ -597,10 +589,8 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> {
self.visit_body(body);
self.scopes.pop().unwrap();
}
ast::Stmt::FunctionDef(ast::StmtFunctionDef { is_async, .. }) => {
self.scopes.push(Scope::Function {
is_async: *is_async,
});
ast::Stmt::FunctionDef(ast::StmtFunctionDef { .. }) => {
self.scopes.push(Scope::Function);
ast::visitor::walk_stmt(self, stmt);
self.scopes.pop().unwrap();
}
@@ -614,7 +604,7 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> {
self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context));
match expr {
ast::Expr::Lambda(_) => {
self.scopes.push(Scope::Function { is_async: false });
self.scopes.push(Scope::Function);
ast::visitor::walk_expr(self, expr);
self.scopes.pop().unwrap();
}

View File

@@ -1,68 +0,0 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/yield_from_in_async_function.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..28,
body: [
FunctionDef(
StmtFunctionDef {
node_index: NodeIndex(None),
range: 0..27,
is_async: true,
decorator_list: [],
name: Identifier {
id: Name("f"),
range: 10..11,
node_index: NodeIndex(None),
},
type_params: None,
parameters: Parameters {
range: 11..13,
node_index: NodeIndex(None),
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 15..27,
value: YieldFrom(
ExprYieldFrom {
node_index: NodeIndex(None),
range: 15..27,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 26..27,
id: Name("x"),
ctx: Load,
},
),
},
),
},
),
],
},
),
],
},
)
```
## Semantic Syntax Errors
|
1 | async def f(): yield from x
| ^^^^^^^^^^^^ Syntax Error: `yield from` statement in async function; use `async for` instead
|

View File

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

3
crates/ty/docs/cli.md generated
View File

@@ -60,9 +60,8 @@ over all configuration files.</p>
</dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p>
<p>Possible values:</p>
<ul>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints (default)</li>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints [default]</li>
<li><code>concise</code>: Print diagnostics concisely, one per line</li>
<li><code>gitlab</code>: Print diagnostics in the JSON format expected by GitLab Code Quality reports</li>
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>

View File

@@ -306,7 +306,7 @@ impl clap::Args for RulesArg {
/// The diagnostic output format.
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum OutputFormat {
/// Print diagnostics verbosely, with context and helpful hints (default).
/// Print diagnostics verbosely, with context and helpful hints \[default\].
///
/// Diagnostic messages may include additional context and
/// annotations on the input to help understand the message.
@@ -321,9 +321,6 @@ pub enum OutputFormat {
/// dropped.
#[value(name = "concise")]
Concise,
/// Print diagnostics in the JSON format expected by GitLab Code Quality reports.
#[value(name = "gitlab")]
Gitlab,
}
impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
@@ -331,7 +328,6 @@ impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
match format {
OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise,
OutputFormat::Gitlab => Self::Gitlab,
}
}
}

View File

@@ -21,7 +21,7 @@ use clap::{CommandFactory, Parser};
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, DisplayDiagnostics, Severity};
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
use ruff_db::files::File;
use ruff_db::max_parallelism;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
@@ -319,48 +319,37 @@ impl MainLoop {
return Ok(ExitStatus::Success);
}
let is_human_readable = terminal_settings.output_format.is_human_readable();
if result.is_empty() {
if is_human_readable {
writeln!(
self.printer.stream_for_success_summary(),
"{}",
"All checks passed!".green().bold()
)?;
}
writeln!(
self.printer.stream_for_success_summary(),
"{}",
"All checks passed!".green().bold()
)?;
if self.watcher.is_none() {
return Ok(ExitStatus::Success);
}
} else {
let mut max_severity = Severity::Info;
let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock();
let max_severity = result
.iter()
.map(Diagnostic::severity)
.max()
.unwrap_or(Severity::Info);
for diagnostic in result {
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
if stdout.is_enabled() {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
}
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
if stdout.is_enabled() {
write!(
stdout,
"{}",
DisplayDiagnostics::new(db, &display_config, &result)
)?;
max_severity = max_severity.max(diagnostic.severity());
}
if is_human_readable {
writeln!(
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;
}
writeln!(
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;
if max_severity.is_fatal() {
tracing::warn!(

View File

@@ -618,71 +618,6 @@ fn concise_diagnostics() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn gitlab_diagnostics() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"#,
)?;
let mut settings = insta::Settings::clone_current();
settings.add_filter(r#"("fingerprint": ")[a-z0-9]+(",)"#, "$1[FINGERPRINT]$2");
let _s = settings.bind_to_scope();
assert_cmd_snapshot!(case.command().arg("--output-format=gitlab").arg("--warn").arg("unresolved-reference"), @r#"
success: false
exit_code: 1
----- stdout -----
[
{
"check_name": "unresolved-reference",
"description": "unresolved-reference: Name `x` used when not defined",
"severity": "minor",
"fingerprint": "[FINGERPRINT]",
"location": {
"path": "test.py",
"positions": {
"begin": {
"line": 2,
"column": 7
},
"end": {
"line": 2,
"column": 8
}
}
}
},
{
"check_name": "non-subscriptable",
"description": "non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method",
"severity": "major",
"fingerprint": "[FINGERPRINT]",
"location": {
"path": "test.py",
"positions": {
"begin": {
"line": 3,
"column": 7
},
"end": {
"line": 3,
"column": 8
}
}
}
}
]
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
Ok(())
}
/// This tests the diagnostic format for revealed type.
///
/// This test was introduced because changes were made to

View File

@@ -240,7 +240,7 @@ impl TestCase {
.module(parent_module_name)
.all_submodules(self.db())
.iter()
.map(|submodule| submodule.name(self.db()).to_string())
.map(|name| name.as_str().to_string())
.collect::<Vec<String>>();
names.sort();
names

View File

@@ -33,7 +33,7 @@ smallvec = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
camino = { workspace = true }
insta = { workspace = true, features = ["filters"] }
[lints]

View File

@@ -1,182 +0,0 @@
use ruff_db::files::File;
use ty_project::Db;
use ty_python_semantic::all_modules;
use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
/// Get all symbols matching the query string.
///
/// Returns symbols from all files in the workspace and dependencies, filtered
/// by the query.
pub fn all_symbols(db: &dyn Db, query: &str) -> Vec<AllSymbolInfo> {
// If the query is empty, return immediately to avoid expensive file scanning
if query.is_empty() {
return Vec::new();
}
let query = QueryPattern::new(query);
let results = std::sync::Mutex::new(Vec::new());
{
let modules = all_modules(db);
let db = db.dyn_clone();
let results = &results;
let query = &query;
rayon::scope(move |s| {
// For each file, extract symbols and add them to results
for module in modules {
let db = db.dyn_clone();
let Some(file) = module.file(&*db) else {
continue;
};
s.spawn(move |_| {
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(),
file,
});
}
});
}
});
}
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());
key1.cmp(&key2)
});
results
}
/// A symbol found in the workspace and dependencies, including the
/// file it was found in.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllSymbolInfo {
/// The symbol information
pub symbol: SymbolInfo<'static>,
/// The file containing the symbol
pub file: File,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::CursorTest;
use crate::tests::IntoDiagnostic;
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
SubDiagnosticSeverity,
};
#[test]
fn test_all_symbols_multi_file() {
// We use odd symbol names here so that we can
// write queries that target them specifically
// and (hopefully) nothing else.
let test = CursorTest::builder()
.source(
"utils.py",
"
def abcdefghijklmnop():
'''A helpful utility function'''
pass
",
)
.source(
"models.py",
"
class Abcdefghijklmnop:
'''A data model class'''
def __init__(self):
pass
",
)
.source(
"constants.py",
"
ABCDEFGHIJKLMNOP = 'https://api.example.com'
<CURSOR>",
)
.build();
assert_snapshot!(test.all_symbols("acegikmo"), @r"
info[all-symbols]: AllSymbolInfo
--> constants.py:2:1
|
2 | ABCDEFGHIJKLMNOP = 'https://api.example.com'
| ^^^^^^^^^^^^^^^^
|
info: Constant ABCDEFGHIJKLMNOP
info[all-symbols]: AllSymbolInfo
--> models.py:2:7
|
2 | class Abcdefghijklmnop:
| ^^^^^^^^^^^^^^^^
3 | '''A data model class'''
4 | def __init__(self):
|
info: Class Abcdefghijklmnop
info[all-symbols]: AllSymbolInfo
--> utils.py:2:5
|
2 | def abcdefghijklmnop():
| ^^^^^^^^^^^^^^^^
3 | '''A helpful utility function'''
4 | pass
|
info: Function abcdefghijklmnop
");
}
impl CursorTest {
fn all_symbols(&self, query: &str) -> String {
let symbols = all_symbols(&self.db, query);
if symbols.is_empty() {
return "No symbols found".to_string();
}
self.render_diagnostics(symbols.into_iter().map(AllSymbolDiagnostic::new))
}
}
struct AllSymbolDiagnostic {
symbol_info: AllSymbolInfo,
}
impl AllSymbolDiagnostic {
fn new(symbol_info: AllSymbolInfo) -> 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 info_text = format!("{} {}", symbol_kind_str, self.symbol_info.symbol.name);
let sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, info_text);
let mut main = Diagnostic::new(
DiagnosticId::Lint(LintName::of("all-symbols")),
Severity::Info,
"AllSymbolInfo".to_string(),
);
main.annotate(Annotation::primary(
Span::from(self.symbol_info.file).with_range(self.symbol_info.symbol.name_range),
));
main.sub(sub);
main
}
}
}

View File

@@ -7,22 +7,12 @@ use ruff_python_parser::{Token, TokenAt, TokenKind};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::{Completion, NameKind, SemanticModel};
use crate::Db;
use crate::docstring::Docstring;
use crate::find_node::covering_node;
use crate::goto::DefinitionsOrTargets;
use crate::{Db, all_symbols};
#[derive(Clone, Debug, Default)]
pub struct CompletionSettings {
pub auto_import: bool,
}
pub fn completion<'db>(
db: &'db dyn Db,
settings: &CompletionSettings,
file: File,
offset: TextSize,
) -> Vec<DetailedCompletion<'db>> {
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<DetailedCompletion<'_>> {
let parsed = parsed_module(db, file).load(db);
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
@@ -47,31 +37,14 @@ pub fn completion<'db>(
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
model.import_completions()
}
CompletionTargetAst::Scoped { node, typed } => {
let mut completions = model.scoped_completions(node);
if settings.auto_import {
if let Some(typed) = typed {
for symbol in all_symbols(db, typed) {
completions.push(Completion {
name: ast::name::Name::new(&symbol.symbol.name),
ty: None,
kind: symbol.symbol.kind.to_completion_kind(),
builtin: false,
});
}
}
}
completions
}
CompletionTargetAst::Scoped { node } => model.scoped_completions(node),
};
completions.sort_by(compare_suggestions);
completions.dedup_by(|c1, c2| c1.name == c2.name);
completions
.into_iter()
.map(|completion| {
let definition = completion
.ty
.and_then(|ty| DefinitionsOrTargets::from_ty(db, ty));
let definition = DefinitionsOrTargets::from_ty(db, completion.ty);
let documentation = definition.and_then(|def| def.docstring(db));
DetailedCompletion {
inner: completion,
@@ -81,12 +54,10 @@ pub fn completion<'db>(
.collect()
}
#[derive(Clone, Debug)]
pub struct DetailedCompletion<'db> {
pub inner: Completion<'db>,
pub documentation: Option<Docstring>,
}
impl<'db> std::ops::Deref for DetailedCompletion<'db> {
type Target = Completion<'db>;
fn deref(&self) -> &Self::Target {
@@ -287,22 +258,16 @@ impl<'t> CompletionTargetTokens<'t> {
}
}
CompletionTargetTokens::Generic { token } => {
let node = covering_node(parsed.syntax().into(), token.range()).node();
let typed = match node {
ast::AnyNodeRef::ExprName(ast::ExprName { id, .. }) => {
let name = id.as_str();
if name.is_empty() { None } else { Some(name) }
}
_ => None,
};
Some(CompletionTargetAst::Scoped { node, typed })
let covering_node = covering_node(parsed.syntax().into(), token.range());
Some(CompletionTargetAst::Scoped {
node: covering_node.node(),
})
}
CompletionTargetTokens::Unknown => {
let range = TextRange::empty(offset);
let covering_node = covering_node(parsed.syntax().into(), range);
Some(CompletionTargetAst::Scoped {
node: covering_node.node(),
typed: None,
})
}
}
@@ -356,16 +321,7 @@ enum CompletionTargetAst<'t> {
},
/// A scoped scenario, where we want to list all items available in
/// the most narrow scope containing the giving AST node.
Scoped {
/// The node with the smallest range that fully covers
/// the token under the cursor.
node: ast::AnyNodeRef<'t>,
/// The text that has been typed so far, if available.
///
/// When not `None`, the typed text is guaranteed to be
/// non-empty.
typed: Option<&'t str>,
},
Scoped { node: ast::AnyNodeRef<'t> },
}
/// Returns a suffix of `tokens` corresponding to the `kinds` given.
@@ -549,7 +505,7 @@ mod tests {
use crate::completion::{DetailedCompletion, completion};
use crate::tests::{CursorTest, cursor_test};
use super::{CompletionSettings, token_suffix_by_kinds};
use super::token_suffix_by_kinds;
#[test]
fn token_suffixes_match() {
@@ -3057,24 +3013,6 @@ from os.<CURSOR>
test.assert_completions_do_not_include("abspath");
}
#[test]
fn auto_import_with_submodule() {
let test = CursorTest::builder()
.source("main.py", "Abra<CURSOR>")
.source("package/__init__.py", "AbraKadabra = 1")
.build();
let settings = CompletionSettings { auto_import: true };
let expected = "AbraKadabra";
let completions = completion(&test.db, &settings, test.cursor.file, test.cursor.offset);
assert!(
completions
.iter()
.any(|completion| completion.name == expected),
"Expected completions to include `{expected}`"
);
}
#[test]
fn regression_test_issue_642() {
// Regression test for https://github.com/astral-sh/ty/issues/642
@@ -3093,10 +3031,6 @@ from os.<CURSOR>
);
}
// NOTE: The methods below are getting somewhat ridiculous.
// We should refactor this by converting to using a builder
// to set different modes. ---AG
impl CursorTest {
/// Returns all completions except for builtins.
fn completions_without_builtins(&self) -> String {
@@ -3106,14 +3040,7 @@ from os.<CURSOR>
fn completions_without_builtins_with_types(&self) -> String {
self.completions_if_snapshot(
|c| !c.builtin,
|c| {
format!(
"{} :: {}",
c.name,
c.ty.map(|ty| ty.display(&self.db).to_string())
.unwrap_or_else(|| "Unavailable".to_string())
)
},
|c| format!("{} :: {}", c.name, c.ty.display(&self.db)),
)
}
@@ -3126,8 +3053,7 @@ from os.<CURSOR>
predicate: impl Fn(&DetailedCompletion) -> bool,
snapshot: impl Fn(&DetailedCompletion) -> String,
) -> String {
let settings = CompletionSettings::default();
let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset);
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
if completions.is_empty() {
return "<No completions found>".to_string();
}
@@ -3150,8 +3076,7 @@ from os.<CURSOR>
#[track_caller]
fn assert_completions_include(&self, expected: &str) {
let settings = CompletionSettings::default();
let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset);
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
assert!(
completions
@@ -3163,8 +3088,7 @@ from os.<CURSOR>
#[track_caller]
fn assert_completions_do_not_include(&self, unexpected: &str) {
let settings = CompletionSettings::default();
let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset);
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
assert!(
completions

View File

@@ -8,15 +8,14 @@ use std::borrow::Cow;
use crate::find_node::covering_node;
use crate::stub_mapping::StubMapper;
use ruff_db::parsed::ParsedModuleRef;
use ruff_python_ast::ExprCall;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::HasDefinition;
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::Type;
use ty_python_semantic::types::definitions_for_keyword_argument;
use ty_python_semantic::types::{Type, call_signature_details};
use ty_python_semantic::{
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
};
@@ -146,26 +145,6 @@ pub(crate) enum GotoTarget<'a> {
Globals {
identifier: &'a ast::Identifier,
},
/// Go to on the invocation of a callable
///
/// ```py
/// x = mymodule.MyClass(1, 2)
/// ^^^^^^^
/// ```
///
/// This is equivalent to `GotoTarget::Expression(callable)` but enriched
/// with information about the actual callable implementation.
///
/// That is, if you click on `MyClass` in `MyClass()` it is *both* a
/// reference to the class and to the initializer of the class. Therefore
/// it would be ideal for goto-* and docstrings to be some intelligent
/// merging of both the class and the initializer.
Call {
/// The callable that can actually be selected by a cursor
callable: ast::ExprRef<'a>,
/// The call of the callable
call: &'a ExprCall,
},
}
/// The resolved definitions for a `GotoTarget`
@@ -279,9 +258,6 @@ impl GotoTarget<'_> {
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
GotoTarget::KeywordArgument { keyword, .. } => keyword.value.inferred_type(model),
// When asking the type of a callable, usually you want the callable itself?
// (i.e. the type of `MyClass` in `MyClass()` is `<class MyClass>` and not `() -> MyClass`)
GotoTarget::Call { callable, .. } => callable.inferred_type(model),
// TODO: Support identifier targets
GotoTarget::PatternMatchRest(_)
| GotoTarget::PatternKeywordArgument(_)
@@ -317,10 +293,18 @@ impl GotoTarget<'_> {
alias_resolution: ImportAliasResolution,
) -> Option<DefinitionsOrTargets<'db>> {
use crate::NavigationTarget;
use ruff_python_ast as ast;
match self {
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression)
.map(DefinitionsOrTargets::Definitions),
GotoTarget::Expression(expression) => match expression {
ast::ExprRef::Name(name) => Some(DefinitionsOrTargets::Definitions(
definitions_for_name(db, file, name),
)),
ast::ExprRef::Attribute(attribute) => Some(DefinitionsOrTargets::Definitions(
ty_python_semantic::definitions_for_attribute(db, file, attribute),
)),
_ => None,
},
// For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => {
@@ -433,22 +417,6 @@ impl GotoTarget<'_> {
}
}
// For callables, both the definition of the callable and the actual function impl are relevant.
//
// Prefer the function impl over the callable so that its docstrings win if defined.
GotoTarget::Call { callable, call } => {
let mut definitions = definitions_for_callable(db, file, call);
let expr_definitions =
definitions_for_expression(db, file, callable).unwrap_or_default();
definitions.extend(expr_definitions);
if definitions.is_empty() {
None
} else {
Some(DefinitionsOrTargets::Definitions(definitions))
}
}
_ => None,
}
}
@@ -459,11 +427,7 @@ impl GotoTarget<'_> {
/// to this goto target.
pub(crate) fn to_string(&self) -> Option<Cow<'_, str>> {
match self {
GotoTarget::Call {
callable: expression,
..
}
| GotoTarget::Expression(expression) => match expression {
GotoTarget::Expression(expression) => match expression {
ast::ExprRef::Name(name) => Some(Cow::Borrowed(name.id.as_str())),
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
_ => None,
@@ -663,18 +627,7 @@ impl GotoTarget<'_> {
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
}
Some(AnyNodeRef::ExprAttribute(attribute)) => {
// Check if this is seemingly a callable being invoked (the `y` in `x.y(...)`)
let grandparent_expr = covering_node.ancestors().nth(2);
let attribute_expr = attribute.into();
if let Some(AnyNodeRef::ExprCall(call)) = grandparent_expr {
if ruff_python_ast::ExprRef::from(&call.func) == attribute_expr {
return Some(GotoTarget::Call {
call,
callable: attribute_expr,
});
}
}
Some(GotoTarget::Expression(attribute_expr))
Some(GotoTarget::Expression(attribute.into()))
}
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
@@ -688,19 +641,7 @@ impl GotoTarget<'_> {
}
},
node => {
// Check if this is seemingly a callable being invoked (the `x` in `x(...)`)
let parent = covering_node.parent();
if let (Some(AnyNodeRef::ExprCall(call)), AnyNodeRef::ExprName(name)) =
(parent, node)
{
return Some(GotoTarget::Call {
call,
callable: name.into(),
});
}
node.as_expr_ref().map(GotoTarget::Expression)
}
node => node.as_expr_ref().map(GotoTarget::Expression),
}
}
}
@@ -708,11 +649,7 @@ impl GotoTarget<'_> {
impl Ranged for GotoTarget<'_> {
fn range(&self) -> TextRange {
match self {
GotoTarget::Call {
callable: expression,
..
}
| GotoTarget::Expression(expression) => match expression {
GotoTarget::Expression(expression) => match expression {
ast::ExprRef::Attribute(attribute) => attribute.attr.range,
_ => expression.range(),
},
@@ -774,35 +711,6 @@ fn convert_resolved_definitions_to_targets(
.collect()
}
/// Shared helper to get definitions for an expr (that is presumably a name/attr)
fn definitions_for_expression<'db>(
db: &'db dyn crate::Db,
file: ruff_db::files::File,
expression: &ruff_python_ast::ExprRef<'_>,
) -> Option<Vec<ResolvedDefinition<'db>>> {
match expression {
ast::ExprRef::Name(name) => Some(definitions_for_name(db, file, name)),
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
db, file, attribute,
)),
_ => None,
}
}
fn definitions_for_callable<'db>(
db: &'db dyn crate::Db,
file: ruff_db::files::File,
call: &ExprCall,
) -> Vec<ResolvedDefinition<'db>> {
let model = SemanticModel::new(db, file);
// Attempt to refine to a specific call
let signature_info = call_signature_details(db, &model, call);
signature_info
.into_iter()
.filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition))
.collect()
}
/// Shared helper to map and convert resolved definitions into navigation targets.
fn definitions_to_navigation_targets<'db>(
db: &dyn crate::Db,

View File

@@ -123,23 +123,6 @@ mod tests {
|
4 | pass
5 |
6 | instance = MyClass()
| ^^^^^^^
|
info[goto-declaration]: Declaration
--> main.py:3:9
|
2 | class MyClass:
3 | def __init__(self):
| ^^^^^^^^
4 | pass
|
info: Source
--> main.py:6:12
|
4 | pass
5 |
6 | instance = MyClass()
| ^^^^^^^
|
@@ -1372,462 +1355,6 @@ class MyClass:
"#);
}
#[test]
fn goto_declaration_overload_type_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1)
",
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: str): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: str): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
");
}
#[test]
fn goto_declaration_overload_type_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mymodule import ab
a<CURSOR>b("hello")
"#,
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: str): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab("hello")
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: str): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab("hello")
| ^^
|
"#);
}
#[test]
fn goto_declaration_overload_arity_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, 2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int): ...
@overload
def ab(a: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int, b: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, 2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: int): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, 2)
| ^^
|
");
}
#[test]
fn goto_declaration_overload_arity_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int): ...
@overload
def ab(a: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int, b: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: int): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
");
}
#[test]
fn goto_declaration_overload_keyword_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, b=2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: int, *, b: int): ...
@overload
def ab(a: int, *, c: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, b=2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: int, *, b: int): ...
| ^^
9 |
10 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, b=2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:11:5
|
10 | @overload
11 | def ab(a: int, *, c: int): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, b=2)
| ^^
|
");
}
#[test]
fn goto_declaration_overload_keyword_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, c=2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: int, *, b: int): ...
@overload
def ab(a: int, *, c: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, c=2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: int, *, b: int): ...
| ^^
9 |
10 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, c=2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:11:5
|
10 | @overload
11 | def ab(a: int, *, c: int): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, c=2)
| ^^
|
");
}
impl CursorTest {
fn goto_declaration(&self) -> String {
let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset)

View File

@@ -466,22 +466,6 @@ class MyOtherClass:
--> main.py:3:5
|
2 | from mymodule import MyClass
3 | x = MyClass(0)
| ^^^^^^^
|
info[goto-definition]: Definition
--> mymodule.py:3:9
|
2 | class MyClass:
3 | def __init__(self, val):
| ^^^^^^^^
4 | self.val = val
|
info: Source
--> main.py:3:5
|
2 | from mymodule import MyClass
3 | x = MyClass(0)
| ^^^^^^^
|
@@ -818,318 +802,6 @@ my_func(my_other_func(a<CURSOR>b=5, y=2), 0)
}
}
#[test]
fn goto_definition_overload_type_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1)
",
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: str): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:2:5
|
2 | def ab(a):
| ^^
3 | """the real implementation!"""
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
"#);
}
#[test]
fn goto_definition_overload_type_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mymodule import ab
a<CURSOR>b("hello")
"#,
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: str): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:2:5
|
2 | def ab(a):
| ^^
3 | """the real implementation!"""
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab("hello")
| ^^
|
"#);
}
#[test]
fn goto_definition_overload_arity_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, 2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int): ...
@overload
def ab(a: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:2:5
|
2 | def ab(a, b = None):
| ^^
3 | """the real implementation!"""
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, 2)
| ^^
|
"#);
}
#[test]
fn goto_definition_overload_arity_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int): ...
@overload
def ab(a: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:2:5
|
2 | def ab(a, b = None):
| ^^
3 | """the real implementation!"""
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
"#);
}
#[test]
fn goto_definition_overload_keyword_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, b=2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: int, *, b: int): ...
@overload
def ab(a: int, *, c: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:2:5
|
2 | def ab(a, *, b = None, c = None):
| ^^
3 | """the real implementation!"""
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, b=2)
| ^^
|
"#);
}
#[test]
fn goto_definition_overload_keyword_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, c=2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: int, *, b: int): ...
@overload
def ab(a: int, *, c: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:2:5
|
2 | def ab(a, *, b = None, c = None):
| ^^
3 | """the real implementation!"""
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, c=2)
| ^^
|
"#);
}
struct GotoDefinitionDiagnostic {
source: FileRange,
target: FileRange,

View File

@@ -468,7 +468,11 @@ mod tests {
assert_snapshot!(test.hover(), @r"
<class 'MyClass'>
---------------------------------------------
initializes MyClass (perfectly)
This is such a great class!!
Don't you know?
Everyone loves my class!!
---------------------------------------------
```python
@@ -476,7 +480,11 @@ mod tests {
```
---
```text
initializes MyClass (perfectly)
This is such a great class!!
Don't you know?
Everyone loves my class!!
```
---------------------------------------------
@@ -494,131 +502,6 @@ mod tests {
");
}
#[test]
fn hover_class_init_attr() {
let test = CursorTest::builder()
.source(
"mymod.py",
r#"
class MyClass:
'''
This is such a great class!!
Don't you know?
Everyone loves my class!!
'''
def __init__(self, val):
"""initializes MyClass (perfectly)"""
self.val = val
"#,
)
.source(
"main.py",
r#"
import mymod
x = mymod.MyCla<CURSOR>ss(0)
"#,
)
.build();
assert_snapshot!(test.hover(), @r"
<class 'MyClass'>
---------------------------------------------
initializes MyClass (perfectly)
---------------------------------------------
```python
<class 'MyClass'>
```
---
```text
initializes MyClass (perfectly)
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:11
|
2 | import mymod
3 |
4 | x = mymod.MyClass(0)
| ^^^^^-^
| | |
| | Cursor offset
| source
|
");
}
#[test]
fn hover_class_init_no_init_docs() {
let test = cursor_test(
r#"
class MyClass:
'''
This is such a great class!!
Don't you know?
Everyone loves my class!!
'''
def __init__(self, val):
self.val = val
def my_method(self, a, b):
'''This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
'''
return 0
x = MyCla<CURSOR>ss(0)
"#,
);
assert_snapshot!(test.hover(), @r"
<class 'MyClass'>
---------------------------------------------
This is such a great class!!
Don't you know?
Everyone loves my class!!
---------------------------------------------
```python
<class 'MyClass'>
```
---
```text
This is such a great class!!
Don't you know?
Everyone loves my class!!
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:23:5
|
21 | return 0
22 |
23 | x = MyClass(0)
| ^^^^^-^
| | |
| | Cursor offset
| source
|
");
}
#[test]
fn hover_class_method() {
let test = cursor_test(
@@ -908,453 +791,7 @@ mod tests {
}
#[test]
fn hover_overload_type_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1)
",
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int):
"""the int overload"""
@overload
def ab(a: str): ...
"""the str overload"""
"#,
)
.build();
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
the int overload
---------------------------------------------
```python
(a: int) -> Unknown
(a: str) -> Unknown
```
---
```text
the int overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_overload_type_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mymodule import ab
a<CURSOR>b("hello")
"#,
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int):
"""the int overload"""
@overload
def ab(a: str):
"""the str overload"""
"#,
)
.build();
assert_snapshot!(test.hover(), @r#"
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
the int overload
---------------------------------------------
```python
(a: int) -> Unknown
(a: str) -> Unknown
```
---
```text
the int overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab("hello")
| ^-
| ||
| |Cursor offset
| source
|
"#);
}
#[test]
fn hover_overload_arity_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, 2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int):
"""the two arg overload"""
@overload
def ab(a: int):
"""the one arg overload"""
"#,
)
.build();
assert_snapshot!(test.hover(), @r"
(
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the two arg overload
---------------------------------------------
```python
(
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
```
---
```text
the two arg overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, 2)
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_overload_arity_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int):
"""the two arg overload"""
@overload
def ab(a: int):
"""the one arg overload"""
"#,
)
.build();
assert_snapshot!(test.hover(), @r"
(
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the two arg overload
---------------------------------------------
```python
(
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
```
---
```text
the two arg overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_overload_keyword_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, b=2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int):
"""keywordless overload"""
@overload
def ab(a: int, *, b: int):
"""b overload"""
@overload
def ab(a: int, *, c: int):
"""c overload"""
"#,
)
.build();
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
---------------------------------------------
keywordless overload
---------------------------------------------
```python
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
```
---
```text
keywordless overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, b=2)
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_overload_keyword_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, c=2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int):
"""keywordless overload"""
@overload
def ab(a: int, *, b: int):
"""b overload"""
@overload
def ab(a: int, *, c: int):
"""c overload"""
"#,
)
.build();
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
---------------------------------------------
keywordless overload
---------------------------------------------
```python
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
```
---
```text
keywordless overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, c=2)
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_overload_ambiguous() {
fn hover_overload() {
let test = cursor_test(
r#"
from typing import overload
@@ -1421,7 +858,7 @@ def ab(a: int, *, c: int):
}
#[test]
fn hover_overload_ambiguous_compact() {
fn hover_overload_compact() {
let test = cursor_test(
r#"
from typing import overload

View File

@@ -2,7 +2,6 @@
clippy::disallowed_methods,
reason = "Prefer System trait methods over std methods in ty crates"
)]
mod all_symbols;
mod completion;
mod doc_highlights;
mod docstring;
@@ -25,8 +24,7 @@ mod stub_mapping;
mod symbols;
mod workspace_symbols;
pub use all_symbols::{AllSymbolInfo, all_symbols};
pub use completion::{CompletionSettings, completion};
pub use completion::completion;
pub use doc_highlights::document_highlights;
pub use document_symbols::document_symbols;
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
@@ -288,12 +286,10 @@ impl HasNavigationTargets for TypeDefinition<'_> {
#[cfg(test)]
mod tests {
use camino::Utf8Component;
use insta::internals::SettingsBindDropGuard;
use ruff_db::Db;
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
use ruff_db::files::{File, FileRootKind, system_path_to_file};
use ruff_db::files::{File, system_path_to_file};
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_python_trivia::textwrap::dedent;
use ruff_text_size::TextSize;
@@ -382,19 +378,6 @@ mod tests {
db.write_file(path, contents)
.expect("write to memory file system to be successful");
// Add a root for the top-most component.
let top = path.components().find_map(|c| match c {
Utf8Component::Normal(c) => Some(c),
_ => None,
});
if let Some(top) = top {
let top = SystemPath::new(top);
if db.system().is_directory(top) {
db.files()
.try_add_root(&db, top, FileRootKind::LibrarySearchPath);
}
}
let file = system_path_to_file(&db, path).expect("newly written file to existing");
if let Some(offset) = cursor_offset {

View File

@@ -258,9 +258,6 @@ fn create_parameters_from_offsets(
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::MarkupKind;
use crate::docstring::Docstring;
use crate::signature_help::SignatureHelpInfo;
use crate::tests::{CursorTest, cursor_test};
@@ -473,354 +470,6 @@ mod tests {
assert_eq!(param2.name, "value");
}
#[test]
fn signature_help_overload_type_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
ab(1<CURSOR>)
",
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int):
"""the int overload"""
@overload
def ab(a: str): ...
"""the str overload"""
"#,
)
.build();
assert_snapshot!(test.signature_help_render(), @r"
============== active signature =============
(a: int) -> Unknown
---------------------------------------------
the int overload
-------------- active parameter -------------
a: int
---------------------------------------------
=============== other signature =============
(a: str) -> Unknown
---------------------------------------------
the real implementation!
-------------- active parameter -------------
a: str
---------------------------------------------
");
}
#[test]
fn signature_help_overload_type_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mymodule import ab
ab("hello"<CURSOR>)
"#,
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int):
"""the int overload"""
@overload
def ab(a: str):
"""the str overload"""
"#,
)
.build();
assert_snapshot!(test.signature_help_render(), @r"
============== active signature =============
(a: int) -> Unknown
---------------------------------------------
the int overload
-------------- active parameter -------------
a: int
---------------------------------------------
=============== other signature =============
(a: str) -> Unknown
---------------------------------------------
the str overload
-------------- active parameter -------------
a: str
---------------------------------------------
");
}
#[test]
fn signature_help_overload_arity_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
ab(1, 2<CURSOR>)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int):
"""the two arg overload"""
@overload
def ab(a: int):
"""the one arg overload"""
"#,
)
.build();
assert_snapshot!(test.signature_help_render(), @r"
============== active signature =============
(a: int, b: int) -> Unknown
---------------------------------------------
the two arg overload
-------------- active parameter -------------
b: int
---------------------------------------------
=============== other signature =============
(a: int) -> Unknown
---------------------------------------------
the one arg overload
(no active parameter specified)
");
}
#[test]
fn signature_help_overload_arity_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
ab(1<CURSOR>)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int):
"""the two arg overload"""
@overload
def ab(a: int):
"""the one arg overload"""
"#,
)
.build();
assert_snapshot!(test.signature_help_render(), @r"
============== active signature =============
(a: int, b: int) -> Unknown
---------------------------------------------
the two arg overload
-------------- active parameter -------------
a: int
---------------------------------------------
=============== other signature =============
(a: int) -> Unknown
---------------------------------------------
the one arg overload
-------------- active parameter -------------
a: int
---------------------------------------------
");
}
#[test]
fn signature_help_overload_keyword_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
ab(1, b=2<CURSOR>)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int):
"""keywordless overload"""
@overload
def ab(a: int, *, b: int):
"""b overload"""
@overload
def ab(a: int, *, c: int):
"""c overload"""
"#,
)
.build();
assert_snapshot!(test.signature_help_render(), @r"
============== active signature =============
(a: int, *, b: int) -> Unknown
---------------------------------------------
b overload
-------------- active parameter -------------
b: int
---------------------------------------------
=============== other signature =============
(a: int) -> Unknown
---------------------------------------------
keywordless overload
(no active parameter specified)
=============== other signature =============
(a: int, *, c: int) -> Unknown
---------------------------------------------
c overload
-------------- active parameter -------------
c: int
---------------------------------------------
");
}
#[test]
fn signature_help_overload_keyword_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
ab(1, c=2<CURSOR>)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int):
"""keywordless overload"""
@overload
def ab(a: int, *, b: int):
"""b overload"""
@overload
def ab(a: int, *, c: int):
"""c overload"""
"#,
)
.build();
assert_snapshot!(test.signature_help_render(), @r"
============== active signature =============
(a: int, *, c: int) -> Unknown
---------------------------------------------
c overload
-------------- active parameter -------------
c: int
---------------------------------------------
=============== other signature =============
(a: int) -> Unknown
---------------------------------------------
keywordless overload
(no active parameter specified)
=============== other signature =============
(a: int, *, b: int) -> Unknown
---------------------------------------------
b overload
-------------- active parameter -------------
b: int
---------------------------------------------
");
}
#[test]
fn signature_help_class_constructor() {
let test = cursor_test(
@@ -1177,94 +826,5 @@ def ab(a: int, *, c: int):
fn signature_help(&self) -> Option<SignatureHelpInfo> {
crate::signature_help::signature_help(&self.db, self.cursor.file, self.cursor.offset)
}
fn signature_help_render(&self) -> String {
use std::fmt::Write;
let Some(signature_help) = self.signature_help() else {
return "Signature help found no signatures".to_string();
};
let active_sig_heading = "\n============== active signature =============\n";
let second_sig_heading = "\n=============== other signature =============\n";
let active_arg_heading = "\n-------------- active parameter -------------\n";
let mut buf = String::new();
if let Some(active_signature) = signature_help.active_signature {
let signature = signature_help
.signatures
.get(active_signature)
.expect("failed to find active signature!");
write!(
&mut buf,
"{heading}{label}{line}{docs}",
heading = active_sig_heading,
label = signature.label,
line = MarkupKind::PlainText.horizontal_line(),
docs = signature
.documentation
.as_ref()
.map(Docstring::render_plaintext)
.unwrap_or_default(),
)
.unwrap();
if let Some(active_parameter) = signature.active_parameter {
let parameter = signature
.parameters
.get(active_parameter)
.expect("failed to find active parameter!");
write!(
&mut buf,
"{heading}{label}{line}{docs}",
heading = active_arg_heading,
label = parameter.label,
line = MarkupKind::PlainText.horizontal_line(),
docs = parameter.documentation.as_deref().unwrap_or_default(),
)
.unwrap();
} else {
writeln!(&mut buf, "\n(no active parameter specified)").unwrap();
}
} else {
writeln!(&mut buf, "\n(no active signature specified)").unwrap();
}
for (idx, signature) in signature_help.signatures.iter().enumerate() {
if Some(idx) == signature_help.active_signature {
continue;
}
write!(
&mut buf,
"{heading}{label}{line}{docs}",
heading = second_sig_heading,
label = signature.label,
line = MarkupKind::PlainText.horizontal_line(),
docs = signature
.documentation
.as_ref()
.map(Docstring::render_plaintext)
.unwrap_or_default(),
)
.unwrap();
if let Some(active_parameter) = signature.active_parameter {
let parameter = signature
.parameters
.get(active_parameter)
.expect("failed to find active parameter!");
write!(
&mut buf,
"{heading}{label}{line}{docs}",
heading = active_arg_heading,
label = parameter.label,
line = MarkupKind::PlainText.horizontal_line(),
docs = parameter.documentation.as_deref().unwrap_or_default(),
)
.unwrap();
} else {
write!(&mut buf, "\n(no active parameter specified)").unwrap();
}
}
buf
}
}
}

View File

@@ -13,7 +13,6 @@ use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor};
use ruff_python_ast::{Expr, Stmt};
use ruff_text_size::{Ranged, TextRange};
use ty_project::Db;
use ty_python_semantic::CompletionKind;
/// A compiled query pattern used for searching symbols.
///
@@ -283,27 +282,6 @@ impl SymbolKind {
SymbolKind::Import => "Import",
}
}
/// Maps this to a "completion" kind if a sensible mapping exists.
pub fn to_completion_kind(self) -> Option<CompletionKind> {
Some(match self {
SymbolKind::Module => CompletionKind::Module,
SymbolKind::Class => CompletionKind::Class,
SymbolKind::Method => CompletionKind::Method,
SymbolKind::Function => CompletionKind::Function,
SymbolKind::Variable => CompletionKind::Variable,
SymbolKind::Constant => CompletionKind::Constant,
SymbolKind::Property => CompletionKind::Property,
SymbolKind::Field => CompletionKind::Field,
SymbolKind::Constructor => CompletionKind::Constructor,
SymbolKind::Parameter => CompletionKind::Variable,
SymbolKind::TypeParameter => CompletionKind::TypeParameter,
// Not quite sure what to do with this one. I guess
// in theory the import should be "resolved" to its
// underlying kind, but that seems expensive.
SymbolKind::Import => return None,
})
}
}
/// Returns a flat list of symbols in the file given.

View File

@@ -1055,21 +1055,6 @@ pub enum OutputFormat {
///
/// This may use color when printing to a `tty`.
Concise,
/// Print diagnostics in the JSON format expected by GitLab [Code Quality] reports.
///
/// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
Gitlab,
}
impl OutputFormat {
/// Returns `true` if this format is intended for users to read directly, in contrast to
/// machine-readable or structured formats.
///
/// This can be used to check whether information beyond the diagnostics, such as a header or
/// `Found N diagnostics` footer, should be included.
pub const fn is_human_readable(&self) -> bool {
matches!(self, OutputFormat::Full | OutputFormat::Concise)
}
}
impl From<OutputFormat> for DiagnosticFormat {
@@ -1077,7 +1062,6 @@ impl From<OutputFormat> for DiagnosticFormat {
match value {
OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise,
OutputFormat::Gitlab => Self::Gitlab,
}
}
}

View File

@@ -74,13 +74,8 @@ def _(
def bar() -> None:
return None
def outer_sync(): # `yield` from is only valid syntax inside a synchronous function
def _(
a: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
): ...
async def baz(): ...
async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
async def outer(): # avoid unrelated syntax errors on yield, yield from, and await
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
@@ -95,10 +90,11 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await baz(), # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
o: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
p: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
q: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -111,12 +107,9 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(l) # revealed: Unknown
reveal_type(m) # revealed: Unknown
reveal_type(n) # revealed: Unknown
reveal_type(o) # revealed: Unknown
reveal_type(p) # revealed: int | Unknown
reveal_type(q) # revealed: @Todo(unknown type subscript)
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(unknown type subscript)
class Mat:
def __init__(self, value: int):

View File

@@ -129,7 +129,7 @@ specialization. Thus, the typevar is a subtype of itself and of `object`, but no
(including other typevars).
```py
from ty_extensions import reveal_when_assignable_to, reveal_when_subtype_of
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
class Super: ...
class Base(Super): ...
@@ -137,23 +137,23 @@ class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T, U](t: T, u: U) -> None:
reveal_when_assignable_to(T, T) # revealed: always
reveal_when_assignable_to(T, object) # revealed: always
reveal_when_assignable_to(T, Super) # revealed: never
reveal_when_assignable_to(U, U) # revealed: always
reveal_when_assignable_to(U, object) # revealed: always
reveal_when_assignable_to(U, Super) # revealed: never
reveal_when_assignable_to(T, U) # revealed: never
reveal_when_assignable_to(U, T) # revealed: never
static_assert(is_assignable_to(T, T))
static_assert(is_assignable_to(T, object))
static_assert(not is_assignable_to(T, Super))
static_assert(is_assignable_to(U, U))
static_assert(is_assignable_to(U, object))
static_assert(not is_assignable_to(U, Super))
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
reveal_when_subtype_of(T, T) # revealed: always
reveal_when_subtype_of(T, object) # revealed: always
reveal_when_subtype_of(T, Super) # revealed: never
reveal_when_subtype_of(U, U) # revealed: always
reveal_when_subtype_of(U, object) # revealed: always
reveal_when_subtype_of(U, Super) # revealed: never
reveal_when_subtype_of(T, U) # revealed: never
reveal_when_subtype_of(U, T) # revealed: never
static_assert(is_subtype_of(T, T))
static_assert(is_subtype_of(T, object))
static_assert(not is_subtype_of(T, Super))
static_assert(is_subtype_of(U, U))
static_assert(is_subtype_of(U, object))
static_assert(not is_subtype_of(U, Super))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of
@@ -167,40 +167,40 @@ from typing import Any
from typing_extensions import final
def bounded[T: Super](t: T) -> None:
reveal_when_assignable_to(T, Super) # revealed: always
reveal_when_assignable_to(T, Sub) # revealed: never
reveal_when_assignable_to(Super, T) # revealed: never
reveal_when_assignable_to(Sub, T) # revealed: never
static_assert(is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(Super, T))
static_assert(not is_assignable_to(Sub, T))
reveal_when_subtype_of(T, Super) # revealed: always
reveal_when_subtype_of(T, Sub) # revealed: never
reveal_when_subtype_of(Super, T) # revealed: never
reveal_when_subtype_of(Sub, T) # revealed: never
static_assert(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Sub, T))
def bounded_by_gradual[T: Any](t: T) -> None:
reveal_when_assignable_to(T, Any) # revealed: always
reveal_when_assignable_to(Any, T) # revealed: always
reveal_when_assignable_to(T, Super) # revealed: always
reveal_when_assignable_to(Super, T) # revealed: never
reveal_when_assignable_to(T, Sub) # revealed: always
reveal_when_assignable_to(Sub, T) # revealed: never
static_assert(is_assignable_to(T, Any))
static_assert(is_assignable_to(Any, T))
static_assert(is_assignable_to(T, Super))
static_assert(not is_assignable_to(Super, T))
static_assert(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(Sub, T))
reveal_when_subtype_of(T, Any) # revealed: never
reveal_when_subtype_of(Any, T) # revealed: never
reveal_when_subtype_of(T, Super) # revealed: never
reveal_when_subtype_of(Super, T) # revealed: never
reveal_when_subtype_of(T, Sub) # revealed: never
reveal_when_subtype_of(Sub, T) # revealed: never
static_assert(not is_subtype_of(T, Any))
static_assert(not is_subtype_of(Any, T))
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(Sub, T))
@final
class FinalClass: ...
def bounded_final[T: FinalClass](t: T) -> None:
reveal_when_assignable_to(T, FinalClass) # revealed: always
reveal_when_assignable_to(FinalClass, T) # revealed: never
static_assert(is_assignable_to(T, FinalClass))
static_assert(not is_assignable_to(FinalClass, T))
reveal_when_subtype_of(T, FinalClass) # revealed: always
reveal_when_subtype_of(FinalClass, T) # revealed: never
static_assert(is_subtype_of(T, FinalClass))
static_assert(not is_subtype_of(FinalClass, T))
```
Two distinct fully static typevars are not subtypes of each other, even if they have the same
@@ -210,18 +210,18 @@ typevars to `Never` in addition to that final class.
```py
def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
reveal_when_assignable_to(T, U) # revealed: never
reveal_when_assignable_to(U, T) # revealed: never
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
reveal_when_subtype_of(T, U) # revealed: never
reveal_when_subtype_of(U, T) # revealed: never
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None:
reveal_when_assignable_to(T, U) # revealed: never
reveal_when_assignable_to(U, T) # revealed: never
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
reveal_when_subtype_of(T, U) # revealed: never
reveal_when_subtype_of(U, T) # revealed: never
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
A constrained fully static typevar is assignable to the union of its constraints, but not to any of
@@ -232,64 +232,64 @@ intersection of all of its constraints is a subtype of the typevar.
from ty_extensions import Intersection
def constrained[T: (Base, Unrelated)](t: T) -> None:
reveal_when_assignable_to(T, Super) # revealed: never
reveal_when_assignable_to(T, Base) # revealed: never
reveal_when_assignable_to(T, Sub) # revealed: never
reveal_when_assignable_to(T, Unrelated) # revealed: never
reveal_when_assignable_to(T, Super | Unrelated) # revealed: always
reveal_when_assignable_to(T, Base | Unrelated) # revealed: always
reveal_when_assignable_to(T, Sub | Unrelated) # revealed: never
reveal_when_assignable_to(Super, T) # revealed: never
reveal_when_assignable_to(Unrelated, T) # revealed: never
reveal_when_assignable_to(Super | Unrelated, T) # revealed: never
reveal_when_assignable_to(Intersection[Base, Unrelated], T) # revealed: always
static_assert(not is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Unrelated))
static_assert(is_assignable_to(T, Super | Unrelated))
static_assert(is_assignable_to(T, Base | Unrelated))
static_assert(not is_assignable_to(T, Sub | Unrelated))
static_assert(not is_assignable_to(Super, T))
static_assert(not is_assignable_to(Unrelated, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
reveal_when_subtype_of(T, Super) # revealed: never
reveal_when_subtype_of(T, Base) # revealed: never
reveal_when_subtype_of(T, Sub) # revealed: never
reveal_when_subtype_of(T, Unrelated) # revealed: never
reveal_when_subtype_of(T, Super | Unrelated) # revealed: always
reveal_when_subtype_of(T, Base | Unrelated) # revealed: always
reveal_when_subtype_of(T, Sub | Unrelated) # revealed: never
reveal_when_subtype_of(Super, T) # revealed: never
reveal_when_subtype_of(Unrelated, T) # revealed: never
reveal_when_subtype_of(Super | Unrelated, T) # revealed: never
reveal_when_subtype_of(Intersection[Base, Unrelated], T) # revealed: always
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Unrelated))
static_assert(is_subtype_of(T, Super | Unrelated))
static_assert(is_subtype_of(T, Base | Unrelated))
static_assert(not is_subtype_of(T, Sub | Unrelated))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
reveal_when_assignable_to(T, Super) # revealed: always
reveal_when_assignable_to(T, Base) # revealed: always
reveal_when_assignable_to(T, Sub) # revealed: never
reveal_when_assignable_to(T, Unrelated) # revealed: never
reveal_when_assignable_to(T, Any) # revealed: always
reveal_when_assignable_to(T, Super | Any) # revealed: always
reveal_when_assignable_to(T, Super | Unrelated) # revealed: always
reveal_when_assignable_to(Super, T) # revealed: never
reveal_when_assignable_to(Base, T) # revealed: always
reveal_when_assignable_to(Unrelated, T) # revealed: never
reveal_when_assignable_to(Any, T) # revealed: always
reveal_when_assignable_to(Super | Any, T) # revealed: never
reveal_when_assignable_to(Base | Any, T) # revealed: always
reveal_when_assignable_to(Super | Unrelated, T) # revealed: never
reveal_when_assignable_to(Intersection[Base, Unrelated], T) # revealed: always
reveal_when_assignable_to(Intersection[Base, Any], T) # revealed: always
static_assert(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Unrelated))
static_assert(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Super | Any))
static_assert(is_assignable_to(T, Super | Unrelated))
static_assert(not is_assignable_to(Super, T))
static_assert(is_assignable_to(Base, T))
static_assert(not is_assignable_to(Unrelated, T))
static_assert(is_assignable_to(Any, T))
static_assert(not is_assignable_to(Super | Any, T))
static_assert(is_assignable_to(Base | Any, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(is_assignable_to(Intersection[Base, Any], T))
reveal_when_subtype_of(T, Super) # revealed: never
reveal_when_subtype_of(T, Base) # revealed: never
reveal_when_subtype_of(T, Sub) # revealed: never
reveal_when_subtype_of(T, Unrelated) # revealed: never
reveal_when_subtype_of(T, Any) # revealed: never
reveal_when_subtype_of(T, Super | Any) # revealed: never
reveal_when_subtype_of(T, Super | Unrelated) # revealed: never
reveal_when_subtype_of(Super, T) # revealed: never
reveal_when_subtype_of(Base, T) # revealed: never
reveal_when_subtype_of(Unrelated, T) # revealed: never
reveal_when_subtype_of(Any, T) # revealed: never
reveal_when_subtype_of(Super | Any, T) # revealed: never
reveal_when_subtype_of(Base | Any, T) # revealed: never
reveal_when_subtype_of(Super | Unrelated, T) # revealed: never
reveal_when_subtype_of(Intersection[Base, Unrelated], T) # revealed: never
reveal_when_subtype_of(Intersection[Base, Any], T) # revealed: never
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Unrelated))
static_assert(not is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Super | Any))
static_assert(not is_subtype_of(T, Super | Unrelated))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Base, T))
static_assert(not is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Any, T))
static_assert(not is_subtype_of(Super | Any, T))
static_assert(not is_subtype_of(Base | Any, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T))
static_assert(not is_subtype_of(Intersection[Base, Any], T))
```
Two distinct fully static typevars are not subtypes of each other, even if they have the same
@@ -299,58 +299,58 @@ the same type.
```py
def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
reveal_when_assignable_to(T, U) # revealed: never
reveal_when_assignable_to(U, T) # revealed: never
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
reveal_when_subtype_of(T, U) # revealed: never
reveal_when_subtype_of(U, T) # revealed: never
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
@final
class AnotherFinalClass: ...
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None:
reveal_when_assignable_to(T, U) # revealed: never
reveal_when_assignable_to(U, T) # revealed: never
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
reveal_when_subtype_of(T, U) # revealed: never
reveal_when_subtype_of(U, T) # revealed: never
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
A bound or constrained typevar is a subtype of itself in a union:
```py
def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
reveal_when_assignable_to(T, T | None) # revealed: always
reveal_when_assignable_to(U, U | None) # revealed: always
static_assert(is_assignable_to(T, T | None))
static_assert(is_assignable_to(U, U | None))
reveal_when_subtype_of(T, T | None) # revealed: always
reveal_when_subtype_of(U, U | None) # revealed: always
static_assert(is_subtype_of(T, T | None))
static_assert(is_subtype_of(U, U | None))
```
A bound or constrained typevar in a union with a dynamic type is assignable to the typevar:
```py
def union_with_dynamic[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
reveal_when_assignable_to(T | Any, T) # revealed: always
reveal_when_assignable_to(U | Any, U) # revealed: always
static_assert(is_assignable_to(T | Any, T))
static_assert(is_assignable_to(U | Any, U))
reveal_when_subtype_of(T | Any, T) # revealed: never
reveal_when_subtype_of(U | Any, U) # revealed: never
static_assert(not is_subtype_of(T | Any, T))
static_assert(not is_subtype_of(U | Any, U))
```
And an intersection of a typevar with another type is always a subtype of the TypeVar:
```py
from ty_extensions import Intersection, Not, is_disjoint_from, static_assert
from ty_extensions import Intersection, Not, is_disjoint_from
class A: ...
def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
reveal_when_assignable_to(Intersection[T, Unrelated], T) # revealed: always
reveal_when_subtype_of(Intersection[T, Unrelated], T) # revealed: always
static_assert(is_assignable_to(Intersection[T, Unrelated], T))
static_assert(is_subtype_of(Intersection[T, Unrelated], T))
reveal_when_assignable_to(Intersection[U, A], U) # revealed: always
reveal_when_subtype_of(Intersection[U, A], U) # revealed: always
static_assert(is_assignable_to(Intersection[U, A], U))
static_assert(is_subtype_of(Intersection[U, A], U))
static_assert(is_disjoint_from(Not[T], T))
static_assert(is_disjoint_from(T, Not[T]))
@@ -647,14 +647,14 @@ The intersection of a typevar with any other type is assignable to (and if fully
of) itself.
```py
from ty_extensions import reveal_when_assignable_to, reveal_when_subtype_of, Not
from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Not
def intersection_is_assignable[T](t: T) -> None:
reveal_when_assignable_to(Intersection[T, None], T) # revealed: always
reveal_when_assignable_to(Intersection[T, Not[None]], T) # revealed: always
static_assert(is_assignable_to(Intersection[T, None], T))
static_assert(is_assignable_to(Intersection[T, Not[None]], T))
reveal_when_subtype_of(Intersection[T, None], T) # revealed: always
reveal_when_subtype_of(Intersection[T, Not[None]], T) # revealed: always
static_assert(is_subtype_of(Intersection[T, None], T))
static_assert(is_subtype_of(Intersection[T, Not[None]], T))
```
## Narrowing

View File

@@ -92,106 +92,3 @@ if (x := f()) in (1,):
else:
reveal_type(x) # revealed: Literal[2, 3]
```
## Union with `Literal`, `None` and `int`
```py
from typing import Literal
def test(x: Literal["a", "b", "c"] | None | int = None):
if x in ("a", "b"):
# int is included because custom __eq__ methods could make
# an int equal to "a" or "b", so we can't eliminate it
reveal_type(x) # revealed: Literal["a", "b"] | int
else:
reveal_type(x) # revealed: Literal["c"] | None | int
```
## Direct `not in` conditional
```py
from typing import Literal
def test(x: Literal["a", "b", "c"] | None | int = None):
if x not in ("a", "c"):
# int is included because custom __eq__ methods could make
# an int equal to "a" or "c", so we can't eliminate it
reveal_type(x) # revealed: Literal["b"] | None | int
else:
reveal_type(x) # revealed: Literal["a", "c"] | int
```
## bool
```py
def _(x: bool):
if x in (True,):
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
def _(x: bool | str):
if x in (False,):
# `str` remains due to possible custom __eq__ methods on a subclass
reveal_type(x) # revealed: Literal[False] | str
else:
reveal_type(x) # revealed: Literal[True] | str
```
## LiteralString
```py
from typing_extensions import LiteralString
def _(x: LiteralString):
if x in ("a", "b", "c"):
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: LiteralString & ~Literal["a"] & ~Literal["b"] & ~Literal["c"]
def _(x: LiteralString | int):
if x in ("a", "b", "c"):
reveal_type(x) # revealed: Literal["a", "b", "c"] | int
else:
reveal_type(x) # revealed: (LiteralString & ~Literal["a"] & ~Literal["b"] & ~Literal["c"]) | int
```
## enums
```py
from enum import Enum
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
def _(x: Color):
if x in (Color.RED, Color.GREEN):
# TODO should be `Literal[Color.RED, Color.GREEN]`
reveal_type(x) # revealed: Color
else:
# TODO should be `Literal[Color.BLUE]`
reveal_type(x) # revealed: Color
```
## Union with enum and `int`
```py
from enum import Enum
class Status(Enum):
PENDING = 1
APPROVED = 2
REJECTED = 3
def test(x: Status | int):
if x in (Status.PENDING, Status.APPROVED):
# TODO should be `Literal[Status.PENDING, Status.APPROVED] | int`
# int is included because custom __eq__ methods could make
# an int equal to Status.PENDING or Status.APPROVED, so we can't eliminate it
reveal_type(x) # revealed: Status | int
else:
# TODO should be `Literal[Status.REJECTED] | int`
reveal_type(x) # revealed: Status | int
```

View File

@@ -413,13 +413,13 @@ To see the kinds and types of the protocol members, you can use the debugging ai
from ty_extensions import reveal_protocol_interface
from typing import SupportsIndex, SupportsAbs, ClassVar, Iterator
# revealed: {"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}
# error: [revealed-type] "Revealed protocol interface: `{"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}`"
reveal_protocol_interface(Foo)
# revealed: {"__index__": MethodMember(`(self) -> int`)}
# error: [revealed-type] "Revealed protocol interface: `{"__index__": MethodMember(`(self) -> int`)}`"
reveal_protocol_interface(SupportsIndex)
# revealed: {"__abs__": MethodMember(`(self) -> Unknown`)}
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> Unknown`)}`"
reveal_protocol_interface(SupportsAbs)
# revealed: {"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)}
# error: [revealed-type] "Revealed protocol interface: `{"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)}`"
reveal_protocol_interface(Iterator)
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
@@ -439,9 +439,9 @@ do not implement any special handling for generic aliases passed to the function
reveal_type(get_protocol_members(SupportsAbs[int])) # revealed: frozenset[str]
reveal_type(get_protocol_members(Iterator[int])) # revealed: frozenset[str]
# revealed: {"__abs__": MethodMember(`(self) -> int`)}
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> int`)}`"
reveal_protocol_interface(SupportsAbs[int])
# revealed: {"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)}
# error: [revealed-type] "Revealed protocol interface: `{"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)}`"
reveal_protocol_interface(Iterator[int])
class BaseProto(Protocol):
@@ -450,16 +450,16 @@ class BaseProto(Protocol):
class SubProto(BaseProto, Protocol):
def member(self) -> bool: ...
# revealed: {"member": MethodMember(`(self) -> int`)}
# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> int`)}`"
reveal_protocol_interface(BaseProto)
# revealed: {"member": MethodMember(`(self) -> bool`)}
# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> bool`)}`"
reveal_protocol_interface(SubProto)
class ProtoWithClassVar(Protocol):
x: ClassVar[int]
# revealed: {"x": AttributeMember(`int`; ClassVar)}
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`"
reveal_protocol_interface(ProtoWithClassVar)
class ProtocolWithDefault(Protocol):
@@ -468,7 +468,7 @@ class ProtocolWithDefault(Protocol):
# We used to incorrectly report this as having an `x: Literal[0]` member;
# declared types should take priority over inferred types for protocol interfaces!
#
# revealed: {"x": AttributeMember(`int`)}
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`)}`"
reveal_protocol_interface(ProtocolWithDefault)
```
@@ -2474,7 +2474,7 @@ class Foo(Protocol):
from stub import Foo
from ty_extensions import reveal_protocol_interface
# revealed: {"x": AttributeMember(`int`; ClassVar)}
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`"
reveal_protocol_interface(Foo)
```

View File

@@ -19,12 +19,6 @@ class Identity:
reveal_type(Identity[0]) # revealed: str
```
`__class_getitem__` is implicitly a classmethod, so it can be called like this:
```py
reveal_type(Identity.__class_getitem__(0)) # revealed: str
```
## Class getitem union
```py

View File

@@ -128,9 +128,6 @@ Person({"name": "Alice"})
accepts_person({"name": "Alice"})
# TODO: this should be an error, similar to the above
house.owner = {"name": "Alice"}
a_person: Person
# TODO: this should be an error, similar to the above
a_person = {"name": "Alice"}
```
All of these have an invalid type for the `name` field:
@@ -147,9 +144,6 @@ Person({"name": None, "age": 30})
accepts_person({"name": None, "age": 30})
# TODO: this should be an error, similar to the above
house.owner = {"name": None, "age": 30}
a_person: Person
# TODO: this should be an error, similar to the above
a_person = {"name": None, "age": 30}
```
All of these have an extra field that is not defined in the `TypedDict`:
@@ -166,9 +160,6 @@ Person({"name": "Alice", "age": 30, "extra": True})
accepts_person({"name": "Alice", "age": 30, "extra": True})
# TODO: this should be an error
house.owner = {"name": "Alice", "age": 30, "extra": True}
# TODO: this should be an error
a_person: Person
a_person = {"name": "Alice", "age": 30, "extra": True}
```
## Type ignore compatibility issues
@@ -251,9 +242,8 @@ invalid_extra = OptionalPerson(name="George", extra=True)
## `Required` and `NotRequired`
You can have fine-grained control over keys using `Required` and `NotRequired` qualifiers. These
qualifiers override the class-level `total` setting, which sets the default (`total=True` means that
all keys are required by default, `total=False` means that all keys are non-required by default):
You can have fine-grained control over field requirements using `Required` and `NotRequired`
qualifiers, which override the class-level `total=` setting:
```py
from typing_extensions import TypedDict, Required, NotRequired
@@ -454,12 +444,6 @@ class Person(TypedDict, total=False):
id: ReadOnly[Required[int]]
name: str
age: int | None
alice: Person = {"id": 1, "name": "Alice", "age": 30}
alice["age"] = 31 # okay
# TODO: this should be an error
alice["id"] = 2
```
## Methods on `TypedDict`
@@ -780,38 +764,6 @@ from typing import TypedDict
x: TypedDict = {"name": "Alice"}
```
### `dict`-subclass inhabitants
Values that inhabit a `TypedDict` type must be instances of `dict` itself, not a subclass:
```py
from typing import TypedDict
class MyDict(dict):
pass
class Person(TypedDict):
name: str
age: int | None
# TODO: this should be an error
x: Person = MyDict({"name": "Alice", "age": 30})
```
### Cannot be used in `isinstance` tests
```py
from typing import TypedDict
class Person(TypedDict):
name: str
age: int | None
def _(obj: object) -> bool:
# TODO: this should be an error
return isinstance(obj, Person)
```
## Diagnostics
<!-- snapshot-diagnostics -->

View File

@@ -11,8 +11,8 @@ use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COM
pub use db::Db;
pub use module_name::ModuleName;
pub use module_resolver::{
Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules, list_modules,
resolve_module, resolve_real_module, system_module_search_paths,
Module, SearchPath, SearchPathValidationError, SearchPaths, list_modules, resolve_module,
resolve_real_module, system_module_search_paths,
};
pub use program::{
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,

View File

@@ -12,20 +12,6 @@ use super::resolver::{
ModuleResolveMode, ResolverContext, is_non_shadowable, resolve_file_module, search_paths,
};
/// List all available modules, including all sub-modules, sorted in lexicographic order.
pub fn all_modules(db: &dyn Db) -> Vec<Module<'_>> {
let mut modules = list_modules(db);
let mut stack = modules.clone();
while let Some(module) = stack.pop() {
for &submodule in module.all_submodules(db) {
modules.push(submodule);
stack.push(submodule);
}
}
modules.sort_by_key(|module| module.name(db));
modules
}
/// List all available top-level modules.
#[salsa::tracked]
pub fn list_modules(db: &dyn Db) -> Vec<Module<'_>> {

View File

@@ -1,6 +1,6 @@
use std::iter::FusedIterator;
pub use list::{all_modules, list_modules};
pub use list::list_modules;
pub(crate) use module::KnownModule;
pub use module::Module;
pub use path::{SearchPath, SearchPathValidationError};

View File

@@ -1,9 +1,9 @@
use std::fmt::Formatter;
use std::str::FromStr;
use ruff_db::files::{File, system_path_to_file, vendored_path_to_file};
use ruff_db::system::SystemPath;
use ruff_db::vendored::VendoredPath;
use ruff_db::files::File;
use ruff_python_ast::name::Name;
use ruff_python_stdlib::identifiers::is_identifier;
use salsa::Database;
use salsa::plumbing::AsId;
@@ -97,10 +97,23 @@ impl<'db> Module<'db> {
///
/// The names returned correspond to the "base" name of the module.
/// That is, `{self.name}.{basename}` should give the full module name.
pub fn all_submodules(self, db: &'db dyn Db) -> &'db [Module<'db>] {
all_submodule_names_for_package(db, self)
.as_deref()
.unwrap_or_default()
pub fn all_submodules(self, db: &'db dyn Db) -> &'db [Name] {
self.all_submodules_inner(db).unwrap_or_default()
}
fn all_submodules_inner(self, db: &'db dyn Db) -> Option<&'db [Name]> {
// It would be complex and expensive to compute all submodules for
// namespace packages, since a namespace package doesn't correspond
// to a single file; it can span multiple directories across multiple
// search paths. For now, we only compute submodules for traditional
// packages that exist in a single directory on a single search path.
let Module::File(module) = self else {
return None;
};
if !matches!(module.kind(db), ModuleKind::Package) {
return None;
}
all_submodule_names_for_package(db, module.file(db)).as_deref()
}
}
@@ -121,10 +134,7 @@ impl std::fmt::Debug for Module<'_> {
#[allow(clippy::ref_option)]
#[salsa::tracked(returns(ref))]
fn all_submodule_names_for_package<'db>(
db: &'db dyn Db,
module: Module<'db>,
) -> Option<Vec<Module<'db>>> {
fn all_submodule_names_for_package(db: &dyn Db, file: File) -> Option<Vec<Name>> {
fn is_submodule(
is_dir: bool,
is_file: bool,
@@ -137,31 +147,7 @@ fn all_submodule_names_for_package<'db>(
&& !matches!(basename, Some("__init__.py" | "__init__.pyi")))
}
fn find_package_init_system(db: &dyn Db, dir: &SystemPath) -> Option<File> {
system_path_to_file(db, dir.join("__init__.pyi"))
.or_else(|_| system_path_to_file(db, dir.join("__init__.py")))
.ok()
}
fn find_package_init_vendored(db: &dyn Db, dir: &VendoredPath) -> Option<File> {
vendored_path_to_file(db, dir.join("__init__.pyi"))
.or_else(|_| vendored_path_to_file(db, dir.join("__init__.py")))
.ok()
}
// It would be complex and expensive to compute all submodules for
// namespace packages, since a namespace package doesn't correspond
// to a single file; it can span multiple directories across multiple
// search paths. For now, we only compute submodules for traditional
// packages that exist in a single directory on a single search path.
let Module::File(module) = module else {
return None;
};
if !matches!(module.kind(db), ModuleKind::Package) {
return None;
}
let path = SystemOrVendoredPathRef::try_from_file(db, module.file(db))?;
let path = SystemOrVendoredPathRef::try_from_file(db, file)?;
debug_assert!(
matches!(path.file_name(), Some("__init__.py" | "__init__.pyi")),
"expected package file `{:?}` to be `__init__.py` or `__init__.pyi`",
@@ -175,11 +161,9 @@ fn all_submodule_names_for_package<'db>(
// tree. When the revision gets bumped, the cache
// that Salsa creates does for this routine will be
// invalidated.
let root = db
.files()
.root(db, parent_directory)
.expect("System search path should have a registered root");
let _ = root.revision(db);
if let Some(root) = db.files().root(db, parent_directory) {
let _ = root.revision(db);
}
db.system()
.read_directory(parent_directory)
@@ -203,23 +187,7 @@ fn all_submodule_names_for_package<'db>(
})
.filter_map(|entry| {
let stem = entry.path().file_stem()?;
let name = ModuleName::new(stem)?;
let (kind, file) = if entry.file_type().is_directory() {
(
ModuleKind::Package,
find_package_init_system(db, entry.path())?,
)
} else {
let file = system_path_to_file(db, entry.path()).ok()?;
(ModuleKind::Module, file)
};
Some(Module::file_module(
db,
name,
kind,
module.search_path(db).clone(),
file,
))
is_identifier(stem).then(|| Name::from(stem))
})
.collect()
}
@@ -239,23 +207,7 @@ fn all_submodule_names_for_package<'db>(
})
.filter_map(|entry| {
let stem = entry.path().file_stem()?;
let name = ModuleName::new(stem)?;
let (kind, file) = if entry.file_type().is_directory() {
(
ModuleKind::Package,
find_package_init_vendored(db, entry.path())?,
)
} else {
let file = vendored_path_to_file(db, entry.path()).ok()?;
(ModuleKind::Module, file)
};
Some(Module::file_module(
db,
name,
kind,
module.search_path(db).clone(),
file,
))
is_identifier(stem).then(|| Name::from(stem))
})
.collect(),
})

View File

@@ -42,7 +42,7 @@ fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds {
/// Uniquely identifies a use of a name in a [`crate::semantic_index::FileScopeId`].
#[newtype_index]
#[derive(get_size2::GetSize)]
#[derive(get_size2::GetSize, salsa::Update)]
pub struct ScopedUseId;
pub trait HasScopedUseId {

View File

@@ -50,8 +50,7 @@ impl<'db> SemanticModel<'db> {
let ty = Type::module_literal(self.db, self.file, module);
Completion {
name: Name::new(module.name(self.db).as_str()),
ty: Some(ty),
kind: None,
ty,
builtin,
}
})
@@ -163,12 +162,7 @@ impl<'db> SemanticModel<'db> {
let mut completions = vec![];
for crate::types::Member { name, ty } in crate::types::all_members(self.db, ty) {
completions.push(Completion {
name,
ty: Some(ty),
kind: None,
builtin,
});
completions.push(Completion { name, ty, builtin });
}
completions.extend(self.submodule_completions(&module));
completions
@@ -179,12 +173,20 @@ impl<'db> SemanticModel<'db> {
let builtin = module.is_known(self.db, KnownModule::Builtins);
let mut completions = vec![];
for submodule in module.all_submodules(self.db) {
let ty = Type::module_literal(self.db, self.file, *submodule);
for submodule_basename in module.all_submodules(self.db) {
let Some(basename) = ModuleName::new(submodule_basename.as_str()) else {
continue;
};
let mut submodule_name = module.name(self.db).clone();
submodule_name.extend(&basename);
let Some(submodule) = resolve_module(self.db, &submodule_name) else {
continue;
};
let ty = Type::module_literal(self.db, self.file, submodule);
completions.push(Completion {
name: Name::new(submodule.name(self.db).as_str()),
ty: Some(ty),
kind: None,
name: submodule_basename.clone(),
ty,
builtin,
});
}
@@ -198,8 +200,7 @@ impl<'db> SemanticModel<'db> {
.into_iter()
.map(|member| Completion {
name: member.name,
ty: Some(member.ty),
kind: None,
ty: member.ty,
builtin: false,
})
.collect()
@@ -235,8 +236,7 @@ impl<'db> SemanticModel<'db> {
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
.map(|member| Completion {
name: member.name,
ty: Some(member.ty),
kind: None,
ty: member.ty,
builtin: false,
}),
);
@@ -286,22 +286,8 @@ impl NameKind {
pub struct Completion<'db> {
/// The label shown to the user for this suggestion.
pub name: Name,
/// The type of this completion, if available.
///
/// Generally speaking, this is always available
/// *unless* this was a completion corresponding to
/// an unimported symbol. In that case, computing the
/// type of all such symbols could be quite expensive.
pub ty: Option<Type<'db>>,
/// The "kind" of this completion.
///
/// When this is set, it takes priority over any kind
/// inferred from `ty`.
///
/// Usually this is set when `ty` is `None`, since it
/// may be cheaper to compute at scale. (e.g., For
/// unimported symbol completions.)
pub kind: Option<CompletionKind>,
/// The type of this completion.
pub ty: Type<'db>,
/// Whether this suggestion came from builtins or not.
///
/// At time of writing (2025-06-26), this information
@@ -359,7 +345,7 @@ impl<'db> Completion<'db> {
Type::TypeAlias(alias) => imp(db, alias.value_type(db))?,
})
}
self.kind.or_else(|| self.ty.and_then(|ty| imp(db, ty)))
imp(db, self.ty)
}
}

View File

@@ -1054,16 +1054,6 @@ impl<'db> Type<'db> {
|| self.is_literal_string()
}
pub(crate) fn is_union_with_single_valued(&self, db: &'db dyn Db) -> bool {
self.into_union().is_some_and(|union| {
union
.elements(db)
.iter()
.any(|ty| ty.is_single_valued(db) || ty.is_bool(db) || ty.is_literal_string())
}) || self.is_bool(db)
|| self.is_literal_string()
}
pub(crate) fn into_string_literal(self) -> Option<StringLiteralType<'db>> {
match self {
Type::StringLiteral(string_literal) => Some(string_literal),
@@ -3347,7 +3337,7 @@ impl<'db> Type<'db> {
name: Name,
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
tracing::trace!("member_lookup_with_policy: {}.{}", self.display(db), name);
let _span = tracing::trace_span!("member_lookup_with_policy: {}", ?name).entered();
if name == "__class__" {
return Place::bound(self.dunder_class(db)).into();
}
@@ -4168,24 +4158,6 @@ impl<'db> Type<'db> {
)
.into(),
Some(
KnownFunction::RevealWhenAssignableTo | KnownFunction::RevealWhenSubtypeOf,
) => Binding::single(
self,
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("a")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("b")))
.type_form()
.with_annotated_type(Type::any()),
]),
Some(KnownClass::NoneType.to_instance(db)),
),
)
.into(),
Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => {
Binding::single(
self,
@@ -9981,6 +9953,14 @@ impl<'db> StringLiteralType<'db> {
pub(crate) fn python_len(self, db: &'db dyn Db) -> usize {
self.value(db).chars().count()
}
/// Return an iterator over each character in the string literal.
/// as would be returned by Python's `iter()`.
pub(crate) fn iter_each_char(self, db: &'db dyn Db) -> impl Iterator<Item = Self> {
self.value(db)
.chars()
.map(|c| StringLiteralType::new(db, c.to_string().into_boxed_str()))
}
}
/// # Ordering

View File

@@ -290,7 +290,8 @@ impl<'db> Bindings<'db> {
}
_ => {}
}
} else if function.is_staticmethod(db) {
} else if function.has_known_decorator(db, FunctionDecorators::STATICMETHOD)
{
overload.set_return_type(Type::FunctionLiteral(function));
} else if let [Some(first), _] = overload.parameter_types() {
if first.is_none(db) {

View File

@@ -3,8 +3,10 @@ use std::sync::{LazyLock, Mutex};
use super::TypeVarVariance;
use super::{
BoundTypeVarInstance, IntersectionBuilder, MemberLookupPolicy, Mro, MroError, MroIterator,
SpecialFormType, SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase,
function::FunctionType, infer_expression_type, infer_unpack_types,
SpecialFormType, SubclassOfType, Truthiness, Type, TypeQualifiers,
class_base::ClassBase,
function::{FunctionDecorators, FunctionType},
infer_expression_type, infer_unpack_types,
};
use crate::FxOrderMap;
use crate::module_resolver::KnownModule;
@@ -1255,7 +1257,10 @@ pub(super) enum MethodDecorator {
impl MethodDecorator {
fn try_from_fn_type(db: &dyn Db, fn_type: FunctionType) -> Result<Self, ()> {
match (fn_type.is_classmethod(db), fn_type.is_staticmethod(db)) {
match (
fn_type.is_classmethod(db),
fn_type.has_known_decorator(db, FunctionDecorators::STATICMETHOD),
) {
(true, true) => Err(()), // A method can't be static and class method at the same time.
(true, false) => Ok(Self::ClassMethod),
(false, true) => Ok(Self::StaticMethod),
@@ -2811,6 +2816,8 @@ impl<'db> ClassLiteral<'db> {
name: String,
target_method_decorator: MethodDecorator,
) -> PlaceAndQualifiers<'db> {
let _span =
tracing::trace_span!("implicit_attribute_inner", ?class_body_scope, ?name).entered();
// If we do not see any declarations of an attribute, neither in the class body nor in
// any method, we build a union of `Unknown` with the inferred types of all bindings of
// that attribute. We include `Unknown` in that union to account for the fact that the

View File

@@ -133,6 +133,9 @@ pub(crate) trait Constraints<'db>: Clone + Sized {
}
// This is here so that we can easily print constraint sets when debugging.
// TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead
// code, and so that we verify the contents of our rendering.
#[expect(dead_code)]
fn display(&self, db: &'db dyn Db) -> impl Display;
}
@@ -342,6 +345,34 @@ impl<'db> ConstraintSet<'db> {
}
}
}
// This is here so that we can easily print constraint sets when debugging.
// TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead
// code, and so that we verify the contents of our rendering.
#[expect(dead_code)]
pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display {
struct DisplayConstraintSet<'a, 'db> {
set: &'a ConstraintSet<'db>,
db: &'db dyn Db,
}
impl Display for DisplayConstraintSet<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.set.clauses.is_empty() {
return f.write_str("0");
}
for (i, clause) in self.set.clauses.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
clause.display(self.db).fmt(f)?;
}
Ok(())
}
}
DisplayConstraintSet { set: self, db }
}
}
impl<'db> Constraints<'db> for ConstraintSet<'db> {
@@ -380,27 +411,7 @@ impl<'db> Constraints<'db> for ConstraintSet<'db> {
}
fn display(&self, db: &'db dyn Db) -> impl Display {
struct DisplayConstraintSet<'a, 'db> {
set: &'a ConstraintSet<'db>,
db: &'db dyn Db,
}
impl Display for DisplayConstraintSet<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.set.clauses.is_empty() {
return f.write_str("0");
}
for (i, clause) in self.set.clauses.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
clause.display(self.db).fmt(f)?;
}
Ok(())
}
}
DisplayConstraintSet { set: self, db }
self.display(db)
}
}

View File

@@ -65,7 +65,7 @@ use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::semantic_index;
use crate::types::call::{Binding, CallArguments};
use crate::types::constraints::{ConstraintSet, Constraints};
use crate::types::constraints::Constraints;
use crate::types::context::InferContext;
use crate::types::diagnostic::{
INVALID_ARGUMENT_TYPE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE,
@@ -725,16 +725,7 @@ impl<'db> FunctionType<'db> {
/// classmethod.
pub(crate) fn is_classmethod(self, db: &'db dyn Db) -> bool {
self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD)
|| matches!(
self.name(db).as_str(),
"__init_subclass__" | "__class_getitem__"
)
}
/// Returns true if this method is decorated with `@staticmethod`, or if it is implicitly a
/// static method.
pub(crate) fn is_staticmethod(self, db: &'db dyn Db) -> bool {
self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || self.name(db) == "__new__"
|| self.name(db) == "__init_subclass__"
}
/// If the implementation of this function is deprecated, returns the `@warnings.deprecated`.
@@ -1188,10 +1179,6 @@ pub enum KnownFunction {
HasMember,
/// `ty_extensions.reveal_protocol_interface`
RevealProtocolInterface,
/// `ty_extensions.reveal_when_assignable_to`
RevealWhenAssignableTo,
/// `ty_extensions.reveal_when_subtype_of`
RevealWhenSubtypeOf,
}
impl KnownFunction {
@@ -1257,8 +1244,6 @@ impl KnownFunction {
| Self::StaticAssert
| Self::HasMember
| Self::RevealProtocolInterface
| Self::RevealWhenAssignableTo
| Self::RevealWhenSubtypeOf
| Self::AllMembers => module.is_ty_extensions(),
Self::ImportModule => module.is_importlib(),
}
@@ -1554,54 +1539,6 @@ impl KnownFunction {
overload.set_return_type(Type::module_literal(db, file, module));
}
KnownFunction::RevealWhenAssignableTo => {
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
return;
};
let constraints = ty_a.when_assignable_to::<ConstraintSet>(db, *ty_b);
let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
else {
return;
};
let mut diag = builder.into_diagnostic("Assignability holds");
let span = context.span(call_expression);
if constraints.is_always_satisfied(db) {
diag.annotate(Annotation::primary(span).message("always"));
} else if constraints.is_never_satisfied(db) {
diag.annotate(Annotation::primary(span).message("never"));
} else {
diag.annotate(
Annotation::primary(span)
.message(format_args!("when {}", constraints.display(db))),
);
}
}
KnownFunction::RevealWhenSubtypeOf => {
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
return;
};
let constraints = ty_a.when_subtype_of::<ConstraintSet>(db, *ty_b);
let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
else {
return;
};
let mut diag = builder.into_diagnostic("Subtyping holds");
let span = context.span(call_expression);
if constraints.is_always_satisfied(db) {
diag.annotate(Annotation::primary(span).message("always"));
} else if constraints.is_never_satisfied(db) {
diag.annotate(Annotation::primary(span).message("never"));
} else {
diag.annotate(
Annotation::primary(span)
.message(format_args!("when {}", constraints.display(db))),
);
}
}
_ => {}
}
}
@@ -1662,8 +1599,6 @@ pub(crate) mod tests {
| KnownFunction::IsEquivalentTo
| KnownFunction::HasMember
| KnownFunction::RevealProtocolInterface
| KnownFunction::RevealWhenAssignableTo
| KnownFunction::RevealWhenSubtypeOf
| KnownFunction::AllMembers => KnownModule::TyExtensions,
KnownFunction::ImportModule => KnownModule::ImportLib,

View File

@@ -83,7 +83,7 @@ use crate::semantic_index::definition::{
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::narrowing_constraints::ConstraintKey;
use crate::semantic_index::place::{PlaceExpr, PlaceExprRef};
use crate::semantic_index::place::{PlaceExpr, PlaceExprRef, ScopedPlaceId};
use crate::semantic_index::scope::{
FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind,
};
@@ -137,7 +137,7 @@ use crate::types::{
use crate::unpack::{EvaluationMode, Unpack, UnpackPosition};
use crate::util::diagnostics::format_enumeration;
use crate::util::subscript::{PyIndex, PySlice};
use crate::{Db, FxOrderSet, Program};
use crate::{Db, FxOrderSet, Program, db};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@@ -171,7 +171,6 @@ fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> ScopeInfer
/// Infer all types for a [`Definition`] (including sub-expressions).
/// Use when resolving a place use or public type of a place.
#[salsa::tracked(returns(ref), cycle_fn=definition_cycle_recover, cycle_initial=definition_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn infer_definition_types<'db>(
db: &'db dyn Db,
definition: Definition<'db>,
@@ -252,7 +251,6 @@ fn deferred_cycle_initial<'db>(
/// Use rarely; only for cases where we'd otherwise risk double-inferring an expression: RHS of an
/// assignment, which might be unpacking/multi-target and thus part of multiple definitions, or a
/// type narrowing guard expression (e.g. if statement test node).
#[salsa::tracked(returns(ref), cycle_fn=expression_cycle_recover, cycle_initial=expression_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn infer_expression_types<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
@@ -2676,7 +2674,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_definition(&mut self, node: impl Into<DefinitionNodeKey> + std::fmt::Debug + Copy) {
let definition = self.index.expect_single_definition(node);
let result = infer_definition_types(self.db(), definition);
self.extend_definition(result);
self.extend_definition(&result);
}
fn infer_function_definition_statement(&mut self, function: &ast::StmtFunctionDef) {
@@ -5142,7 +5140,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.check_deprecated(alias, ty.inner);
}
}
self.extend_definition(inferred);
self.extend_definition(&inferred);
}
}
}
@@ -5636,7 +5634,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
standalone_expression: Expression<'db>,
) -> Type<'db> {
let types = infer_expression_types(self.db(), standalone_expression);
self.extend_expression(types);
self.extend_expression(&types);
// Instead of calling `self.expression_type(expr)` after extending here, we get
// the result from `types` directly because we might be in cycle recovery where
@@ -6102,7 +6100,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if comprehension.is_first() && target.is_name_expr() {
result.expression_type(iterable)
} else {
self.extend_expression_unchecked(result);
self.extend_expression_unchecked(&result);
result.expression_type(iterable)
}
};
@@ -6141,7 +6139,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if named.target.is_name_expr() {
let definition = self.index.expect_single_definition(named);
let result = infer_definition_types(self.db(), definition);
self.extend_definition(result);
self.extend_definition(&result);
result.binding_type(definition)
} else {
// For syntactically invalid targets, we still need to run type inference:
@@ -6797,37 +6795,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
expr: PlaceExprRef,
expr_ref: ast::ExprRef,
) -> (Place<'db>, Option<ScopedUseId>) {
if expr_ref
.as_name_expr()
.is_some_and(|name| name.is_invalid())
{
return (Place::Unbound, None);
}
let db = self.db();
let scope = self.scope();
let file_scope_id = scope.file_scope_id(db);
let place_table = self.index.place_table(file_scope_id);
let use_def = self.index.use_def_map(file_scope_id);
// If we're inferring types of deferred expressions, look them up from end-of-scope.
if self.is_deferred() {
let place = if let Some(place_id) = place_table.place_id(expr) {
place_from_bindings(db, use_def.all_reachable_bindings(place_id))
} else {
assert!(
self.deferred_state.in_string_annotation(),
"Expected the place table to create a place for every valid PlaceExpr node"
);
Place::Unbound
};
(place, None)
let is_deferred = self.is_deferred();
let deferred_state = self.deferred_state;
let use_id = if is_deferred {
None
} else {
if expr_ref
.as_name_expr()
.is_some_and(|name| name.is_invalid())
{
return (Place::Unbound, None);
}
Some(expr_ref.scoped_use_id(db, scope))
};
let use_id = expr_ref.scoped_use_id(db, scope);
let place = place_from_bindings(db, use_def.bindings_at_use(use_id));
let place_table = self.index.place_table(scope.file_scope_id(db));
let place_id = place_table.place_id(expr);
(place, Some(use_id))
}
infer_local_place_load(db, scope, deferred_state, place_id, use_id)
}
/// Infer the type of a place expression from definitions, assuming a load context.
@@ -7341,6 +7329,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
let _span = tracing::debug_span!("infer_attribute_expression", ?attribute).entered();
let ast::ExprAttribute {
value,
attr: _,
@@ -9203,7 +9193,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
match ty.try_call(db, &CallArguments::positional([slice_ty])) {
match ty.try_call(db, &CallArguments::positional([value_ty, slice_ty])) {
Ok(bindings) => return bindings.return_type(db),
Err(CallError(_, bindings)) => {
if let Some(builder) =
@@ -11296,6 +11286,60 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
}
fn infer_local_place_load_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &(Place<'db>, Option<ScopedUseId>),
_count: u32,
_scope: ScopeId<'db>,
_deferred_state: DeferredExpressionState,
_place_id: Option<ScopedPlaceId>,
_use_id: Option<ScopedUseId>,
) -> salsa::CycleRecoveryAction<(Place<'db>, Option<ScopedUseId>)> {
salsa::CycleRecoveryAction::Iterate
}
fn infer_local_place_load_cycle_initial<'db>(
_db: &'db dyn Db,
_scope: ScopeId<'db>,
_deferred_state: DeferredExpressionState,
_place_id: Option<ScopedPlaceId>,
_use_id: Option<ScopedUseId>,
) -> (Place<'db>, Option<ScopedUseId>) {
(Place::Unbound, None)
}
#[salsa::tracked(cycle_fn=infer_local_place_load_cycle_recover, cycle_initial=infer_local_place_load_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn infer_local_place_load<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
deferred_state: DeferredExpressionState,
place_id: Option<ScopedPlaceId>,
use_id: Option<ScopedUseId>,
) -> (Place<'db>, Option<ScopedUseId>) {
let file_scope_id = scope.file_scope_id(db);
let index = semantic_index(db, scope.file(db));
let use_def = index.use_def_map(file_scope_id);
if let Some(use_id) = use_id {
// Non-deferred load
let place = place_from_bindings(db, use_def.bindings_at_use(use_id));
(place, Some(use_id))
} else {
// If we're inferring types of deferred expressions, look them up from end-of-scope.
let place = if let Some(place_id) = place_id {
place_from_bindings(db, use_def.all_reachable_bindings(place_id))
} else {
assert!(
deferred_state.in_string_annotation(),
"Expected the place table to create a place for every valid PlaceExpr node"
);
Place::Unbound
};
(place, None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GenericContextError {
/// It's invalid to subscript `Generic` or `Protocol` with this type
@@ -11320,7 +11364,7 @@ impl GenericContextError {
}
/// The deferred state of a specific expression in an inference region.
#[derive(Default, Debug, Clone, Copy)]
#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq)]
enum DeferredExpressionState {
/// The expression is not deferred.
#[default]

View File

@@ -615,88 +615,24 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
}
}
// TODO `expr_in` and `expr_not_in` should perhaps be unified with `expr_eq` and `expr_ne`,
// since `eq` and `ne` are equivalent to `in` and `not in` with only one element in the RHS.
fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option<Type<'db>> {
if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) {
rhs_ty
.try_iterate(self.db)
.ok()
.map(|iterable| iterable.homogeneous_element_type(self.db))
} else if lhs_ty.is_union_with_single_valued(self.db) {
let rhs_values = rhs_ty
.try_iterate(self.db)
.ok()?
.homogeneous_element_type(self.db);
let mut builder = UnionBuilder::new(self.db);
// Add the narrowed values from the RHS first, to keep literals before broader types.
builder = builder.add(rhs_values);
if let Some(lhs_union) = lhs_ty.into_union() {
for element in lhs_union.elements(self.db) {
// Keep only the non-single-valued portion of the original type.
if !element.is_single_valued(self.db)
&& !element.is_literal_string()
&& !element.is_bool(self.db)
{
builder = builder.add(*element);
}
}
if let Type::StringLiteral(string_literal) = rhs_ty {
Some(UnionType::from_elements(
self.db,
string_literal
.iter_each_char(self.db)
.map(Type::StringLiteral),
))
} else if let Some(tuple_spec) = rhs_ty.tuple_instance_spec(self.db) {
// N.B. Strictly speaking this is unsound, since a tuple subclass might override `__contains__`
// but we'd still apply the narrowing here. This seems unlikely, however, and narrowing is
// generally unsound in numerous ways anyway (attribute narrowing, subscript, narrowing,
// narrowing of globals, etc.). So this doesn't seem worth worrying about too much.
Some(UnionType::from_elements(self.db, tuple_spec.all_elements()))
} else {
None
}
Some(builder.build())
} else {
None
}
}
fn evaluate_expr_not_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option<Type<'db>> {
let rhs_values = rhs_ty
.try_iterate(self.db)
.ok()?
.homogeneous_element_type(self.db);
if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) {
// Exclude the RHS values from the entire (single-valued) LHS domain.
let complement = IntersectionBuilder::new(self.db)
.add_positive(lhs_ty)
.add_negative(rhs_values)
.build();
Some(complement)
} else if lhs_ty.is_union_with_single_valued(self.db) {
// Split LHS into single-valued portion and the rest. Exclude RHS values from the
// single-valued portion, keep the rest intact.
let mut single_builder = UnionBuilder::new(self.db);
let mut rest_builder = UnionBuilder::new(self.db);
if let Some(lhs_union) = lhs_ty.into_union() {
for element in lhs_union.elements(self.db) {
if element.is_single_valued(self.db)
|| element.is_literal_string()
|| element.is_bool(self.db)
{
single_builder = single_builder.add(*element);
} else {
rest_builder = rest_builder.add(*element);
}
}
}
let single_union = single_builder.build();
let rest_union = rest_builder.build();
let narrowed_single = IntersectionBuilder::new(self.db)
.add_positive(single_union)
.add_negative(rhs_values)
.build();
// Keep order: first literal complement, then broader arms.
let result = UnionBuilder::new(self.db)
.add(narrowed_single)
.add(rest_union)
.build();
Some(result)
} else {
None
}
@@ -724,7 +660,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
ast::CmpOp::Eq => self.evaluate_expr_eq(lhs_ty, rhs_ty),
ast::CmpOp::NotEq => self.evaluate_expr_ne(lhs_ty, rhs_ty),
ast::CmpOp::In => self.evaluate_expr_in(lhs_ty, rhs_ty),
ast::CmpOp::NotIn => self.evaluate_expr_not_in(lhs_ty, rhs_ty),
ast::CmpOp::NotIn => self
.evaluate_expr_in(lhs_ty, rhs_ty)
.map(|ty| ty.negate(self.db)),
_ => None,
}
}

View File

@@ -7,7 +7,7 @@ use lsp_types::{
};
use ruff_db::source::{line_index, source_text};
use ruff_source_file::OneIndexed;
use ty_ide::{CompletionSettings, completion};
use ty_ide::completion;
use ty_project::ProjectDatabase;
use ty_python_semantic::CompletionKind;
@@ -55,10 +55,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
&line_index,
snapshot.encoding(),
);
let settings = CompletionSettings {
auto_import: snapshot.global_settings().is_auto_import_enabled(),
};
let completions = completion(db, &settings, file, offset);
let completions = completion(db, file, offset);
if completions.is_empty() {
return Ok(None);
}
@@ -74,7 +71,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
label: comp.inner.name.into(),
kind,
sort_text: Some(format!("{i:-max_index_len$}")),
detail: comp.inner.ty.map(|ty| ty.display(db).to_string()),
detail: comp.inner.ty.display(db).to_string().into(),
documentation: comp
.documentation
.map(|docstring| Documentation::String(docstring.render_plaintext())),

View File

@@ -826,7 +826,6 @@ impl Session {
.map_err(DocumentQueryError::InvalidUrl);
DocumentSnapshot {
resolved_client_capabilities: self.resolved_client_capabilities,
global_settings: self.global_settings.clone(),
workspace_settings: key
.as_ref()
.ok()
@@ -1001,7 +1000,6 @@ impl Drop for MutIndexGuard<'_> {
#[derive(Debug)]
pub(crate) struct DocumentSnapshot {
resolved_client_capabilities: ResolvedClientCapabilities,
global_settings: Arc<GlobalSettings>,
workspace_settings: Arc<WorkspaceSettings>,
position_encoding: PositionEncoding,
document_query_result: Result<DocumentQuery, DocumentQueryError>,
@@ -1018,11 +1016,6 @@ impl DocumentSnapshot {
self.position_encoding
}
/// Returns the client settings for all workspaces.
pub(crate) fn global_settings(&self) -> &GlobalSettings {
&self.global_settings
}
/// Returns the client settings for the workspace that this document belongs to.
pub(crate) fn workspace_settings(&self) -> &WorkspaceSettings {
&self.workspace_settings

View File

@@ -122,12 +122,6 @@ impl ClientOptions {
self
}
#[must_use]
pub fn with_experimental_auto_import(mut self, enabled: bool) -> Self {
self.global.experimental.get_or_insert_default().auto_import = Some(enabled);
self
}
#[must_use]
pub fn with_unknown(mut self, unknown: HashMap<String, Value>) -> Self {
self.unknown = unknown;
@@ -154,8 +148,7 @@ impl GlobalOptions {
let experimental = self
.experimental
.map(|experimental| ExperimentalSettings {
rename: experimental.rename.unwrap_or(false),
auto_import: experimental.auto_import.unwrap_or(false),
rename: experimental.rename.unwrap_or(true),
})
.unwrap_or_default();
@@ -300,12 +293,6 @@ impl Combine for DiagnosticMode {
pub(crate) struct Experimental {
/// Whether to enable the experimental symbol rename feature.
pub(crate) rename: Option<bool>,
/// Whether to enable the experimental "auto-import" feature.
///
/// At time of writing (2025-08-29), this feature is still
/// under active development. It may not work right or may be
/// incomplete.
pub(crate) auto_import: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]

View File

@@ -14,10 +14,6 @@ impl GlobalSettings {
pub(crate) fn is_rename_enabled(&self) -> bool {
self.experimental.rename
}
pub(crate) fn is_auto_import_enabled(&self) -> bool {
self.experimental.auto_import
}
}
impl GlobalSettings {
@@ -29,7 +25,6 @@ impl GlobalSettings {
#[derive(Clone, Default, Debug, PartialEq)]
pub(crate) struct ExperimentalSettings {
pub(super) rename: bool,
pub(super) auto_import: bool,
}
/// Resolved client settings for a specific workspace.

View File

@@ -1,19 +1,15 @@
//! Match [`Diagnostic`]s against assertions and produce test failure
//! messages for any mismatches.
use std::borrow::Cow;
use std::cmp::Ordering;
use std::ops::Range;
use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion};
use crate::db::Db;
use crate::diagnostic::SortedDiagnostics;
use colored::Colorize;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
use ruff_db::files::File;
use ruff_db::source::{SourceText, line_index, source_text};
use ruff_source_file::{LineIndex, OneIndexed};
use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion};
use crate::db::Db;
use crate::diagnostic::SortedDiagnostics;
use std::cmp::Ordering;
use std::ops::Range;
#[derive(Debug, Default)]
pub(super) struct FailuresByLine {
@@ -198,17 +194,12 @@ impl UnmatchedWithColumn for &Diagnostic {
/// Discard `@Todo`-type metadata from expected types, which is not available
/// when running in release mode.
fn discard_todo_metadata(ty: &str) -> Cow<'_, str> {
#[cfg(not(debug_assertions))]
{
static TODO_METADATA_REGEX: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());
#[cfg(not(debug_assertions))]
fn discard_todo_metadata(ty: &str) -> std::borrow::Cow<'_, str> {
static TODO_METADATA_REGEX: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());
TODO_METADATA_REGEX.replace_all(ty, "@Todo")
}
#[cfg(debug_assertions)]
Cow::Borrowed(ty)
TODO_METADATA_REGEX.replace_all(ty, "@Todo")
}
struct Matcher {
@@ -306,53 +297,21 @@ impl Matcher {
}
}
ParsedAssertion::Revealed(expected_type) => {
let expected_type = discard_todo_metadata(expected_type);
let expected_reveal_type_message = format!("`{expected_type}`");
let diagnostic_matches_reveal = |diagnostic: &Diagnostic| {
if diagnostic.id() != DiagnosticId::RevealedType {
return false;
}
let primary_message = diagnostic.primary_message();
let Some(primary_annotation) =
(diagnostic.primary_annotation()).and_then(|a| a.get_message())
else {
return false;
};
// reveal_type
if primary_message == "Revealed type"
&& primary_annotation == expected_reveal_type_message
{
return true;
}
// reveal_protocol_interface
if primary_message == "Revealed protocol interface"
&& primary_annotation == expected_reveal_type_message
{
return true;
}
// reveal_when_assignable_to
if primary_message == "Assignability holds"
&& primary_annotation == expected_type
{
return true;
}
// reveal_when_subtype_of
if primary_message == "Subtyping holds" && primary_annotation == expected_type {
return true;
}
false
};
#[cfg(not(debug_assertions))]
let expected_type = discard_todo_metadata(&expected_type);
let mut matched_revealed_type = None;
let mut matched_undefined_reveal = None;
let expected_reveal_type_message = format!("`{expected_type}`");
for (index, diagnostic) in unmatched.iter().enumerate() {
if matched_revealed_type.is_none() && diagnostic_matches_reveal(diagnostic) {
if matched_revealed_type.is_none()
&& diagnostic.id() == DiagnosticId::RevealedType
&& diagnostic
.primary_annotation()
.and_then(|a| a.get_message())
.unwrap_or_default()
== expected_reveal_type_message
{
matched_revealed_type = Some(index);
} else if matched_undefined_reveal.is_none()
&& diagnostic.id().is_lint_named("undefined-reveal")

View File

@@ -45,21 +45,15 @@ type JustComplex = TypeOf[1.0j]
# Ideally, these would be annotated using `TypeForm`, but that has not been
# standardized yet (https://peps.python.org/pep-0747).
def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
def is_subtype_of(type_a: Any, type_b: Any) -> bool: ...
def is_assignable_to(type_a: Any, type_b: Any) -> bool: ...
def is_subtype_of(type_derived: Any, type_base: Any) -> bool: ...
def is_assignable_to(type_target: Any, type_source: Any) -> bool: ...
def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ...
def is_singleton(ty: Any) -> bool: ...
def is_single_valued(ty: Any) -> bool: ...
# These are the same as above, but instead of returning _whether_ the property
# holds, it shows a diagnostic that describes under what constraints the
# property holds.
def reveal_when_assignable_to(type_a: Any, type_b: Any) -> None: ...
def reveal_when_subtype_of(type_a: Any, type_b: Any) -> None: ...
def is_singleton(type: Any) -> bool: ...
def is_single_valued(type: Any) -> bool: ...
# Returns the generic context of a type as a tuple of typevars, or `None` if the
# type is not generic.
def generic_context(ty: Any) -> Any: ...
def generic_context(type: Any) -> Any: ...
# Returns the `__all__` names of a module as a tuple of sorted strings, or `None` if
# either the module does not have `__all__` or it has invalid elements.

View File

@@ -415,11 +415,7 @@ impl Workspace {
let offset = position.to_text_size(&source, &index, self.position_encoding)?;
// NOTE: At time of writing, 2025-08-29, auto-import isn't
// ready to be enabled by default yet. Once it is, we should
// either just enable it or provide a way to configure it.
let settings = ty_ide::CompletionSettings { auto_import: false };
let completions = ty_ide::completion(&self.db, &settings, file_id.file, offset);
let completions = ty_ide::completion(&self.db, file_id.file, offset);
Ok(completions
.into_iter()
@@ -429,10 +425,7 @@ impl Workspace {
documentation: completion
.documentation
.map(|documentation| documentation.render_plaintext()),
detail: completion
.inner
.ty
.map(|ty| ty.display(&self.db).to_string()),
detail: completion.inner.ty.display(&self.db).to_string().into(),
})
.collect())
}

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.12.12-alpine
name: ghcr.io/astral-sh/ruff:0.12.11-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.12
rev: v0.12.11
hooks:
# Run the linter.
- id: ruff-check
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.12
rev: v0.12.11
hooks:
# Run the linter.
- id: ruff-check
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.12
rev: v0.12.11
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.12
rev: v0.12.11
hooks:
# Run the linter.
- id: ruff

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