mirror of
https://github.com/astral-sh/ruff.git
synced 2025-12-23 09:19:39 +00:00
Merge branch 'main' into infer-return-type
This commit is contained in:
commit
eb4b078a30
80 changed files with 3261 additions and 527 deletions
3
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
3
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
|
|
@ -64,11 +64,12 @@ jobs:
|
|||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@1f560d07d672effae250e3d271da53d96c5260ff"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
diff \
|
||||
--profile=release \
|
||||
--projects-old ruff/projects_old.txt \
|
||||
--projects-new ruff/projects_new.txt \
|
||||
--old old_commit \
|
||||
|
|
|
|||
88
Cargo.lock
generated
88
Cargo.lock
generated
|
|
@ -295,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
@ -1231,8 +1231,8 @@ dependencies = [
|
|||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1459,7 +1459,7 @@ dependencies = [
|
|||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
|
|
@ -1913,11 +1913,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2074,12 +2074,11 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
|||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
version = "0.50.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2154,12 +2153,6 @@ 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"
|
||||
|
|
@ -2688,17 +2681,8 @@ checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"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",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2709,7 +2693,7 @@ checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2718,12 +2702,6 @@ 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"
|
||||
|
|
@ -4161,15 +4139,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.19"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
|
|
@ -4241,6 +4219,7 @@ name = "ty_ide"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"camino",
|
||||
"get-size2",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
|
|
@ -4278,7 +4257,7 @@ dependencies = [
|
|||
"pep440_rs",
|
||||
"rayon",
|
||||
"regex",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-automata",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_macros",
|
||||
|
|
@ -4892,22 +4871,6 @@ 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"
|
||||
|
|
@ -4917,12 +4880,6 @@ 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"
|
||||
|
|
@ -4982,6 +4939,15 @@ 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"
|
||||
|
|
|
|||
|
|
@ -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/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
/// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
|
||||
#[cfg(feature = "serde")]
|
||||
Gitlab,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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
|
||||
|
|
@ -52,6 +53,8 @@ DatasetAliasEvent()
|
|||
# airflow.operators.subdag.*
|
||||
SubDagOperator()
|
||||
|
||||
# airflow.operators.postgres_operator
|
||||
Mapping()
|
||||
|
||||
# airflow.secrets
|
||||
# get_connection
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ from airflow.timetables.datasets import DatasetOrTimeSchedule
|
|||
from airflow.utils.dag_parsing_context import get_parsing_context
|
||||
|
||||
# airflow.timetables.datasets
|
||||
DatasetOrTimeSchedule()
|
||||
DatasetOrTimeSchedule(datasets=[])
|
||||
|
||||
# airflow.utils.dag_parsing_context
|
||||
get_parsing_context()
|
||||
|
|
|
|||
1
crates/ruff_linter/resources/test/fixtures/syntax_errors/yield_from_in_async_function.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/syntax_errors/yield_from_in_async_function.py
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
async def f(): yield from x # error
|
||||
|
|
@ -1319,13 +1319,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
pylint::rules::yield_in_init(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::YieldFrom(yield_from) => {
|
||||
Expr::YieldFrom(_) => {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ use crate::registry::Rule;
|
|||
use crate::rules::pyflakes::rules::{
|
||||
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
|
||||
};
|
||||
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
|
||||
use crate::rules::pylint::rules::{
|
||||
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
|
||||
};
|
||||
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
|
||||
use crate::settings::rule_table::RuleTable;
|
||||
use crate::settings::{LinterSettings, TargetVersion, flags};
|
||||
|
|
@ -668,6 +670,12 @@ 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(_)
|
||||
|
|
|
|||
|
|
@ -1231,6 +1231,10 @@ 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);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ pub(crate) enum Replacement {
|
|||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ProviderReplacement {
|
||||
None,
|
||||
AutoImport {
|
||||
module: &'static str,
|
||||
name: &'static str,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ pub(crate) struct AirflowDagNoScheduleArgument;
|
|||
impl Violation for AirflowDagNoScheduleArgument {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"DAG should have an explicit `schedule` argument".to_string()
|
||||
"`DAG` or `@dag` should have an explicit `schedule` argument".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,6 @@ impl Violation for Airflow3MovedToProvider<'_> {
|
|||
replacement,
|
||||
} = self;
|
||||
match replacement {
|
||||
ProviderReplacement::None => {
|
||||
format!("`{deprecated}` is removed in Airflow 3.0")
|
||||
}
|
||||
ProviderReplacement::AutoImport {
|
||||
name: _,
|
||||
module: _,
|
||||
|
|
@ -85,7 +82,6 @@ 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."
|
||||
|
|
@ -1020,7 +1016,6 @@ 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 {
|
||||
|
|
@ -1209,16 +1204,6 @@ 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()) {
|
||||
|
|
|
|||
|
|
@ -704,6 +704,7 @@ 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",
|
||||
|
|
|
|||
|
|
@ -65,9 +65,6 @@ impl Violation for Airflow3SuggestedToMoveToProvider<'_> {
|
|||
replacement,
|
||||
} = self;
|
||||
match replacement {
|
||||
ProviderReplacement::None => {
|
||||
format!("`{deprecated}` is removed in Airflow 3.0")
|
||||
}
|
||||
ProviderReplacement::AutoImport {
|
||||
name: _,
|
||||
module: _,
|
||||
|
|
@ -91,7 +88,6 @@ impl Violation for Airflow3SuggestedToMoveToProvider<'_> {
|
|||
fn fix_title(&self) -> Option<String> {
|
||||
let Airflow3SuggestedToMoveToProvider { replacement, .. } = self;
|
||||
match replacement {
|
||||
ProviderReplacement::None => None,
|
||||
ProviderReplacement::AutoImport {
|
||||
module,
|
||||
name,
|
||||
|
|
@ -319,16 +315,6 @@ 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()) {
|
||||
|
|
|
|||
|
|
@ -157,6 +157,9 @@ 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);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
AIR002 DAG should have an explicit `schedule` argument
|
||||
AIR002 `DAG` or `@dag` should have an explicit `schedule` argument
|
||||
--> AIR002.py:4:1
|
||||
|
|
||||
2 | from airflow.timetables.simple import NullTimetable
|
||||
|
|
@ -12,7 +12,7 @@ AIR002 DAG should have an explicit `schedule` argument
|
|||
6 | DAG(dag_id="class_schedule", schedule="@hourly")
|
||||
|
|
||||
|
||||
AIR002 DAG should have an explicit `schedule` argument
|
||||
AIR002 `DAG` or `@dag` should have an explicit `schedule` argument
|
||||
--> AIR002.py:13:2
|
||||
|
|
||||
13 | @dag()
|
||||
|
|
|
|||
|
|
@ -2,350 +2,362 @@
|
|||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
AIR301 `airflow.PY36` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:39:1
|
||||
--> AIR301_names.py:40:1
|
||||
|
|
||||
38 | # airflow root
|
||||
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
39 | # airflow root
|
||||
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^
|
||||
40 |
|
||||
41 | # airflow.api_connexion.security
|
||||
41 |
|
||||
42 | # airflow.api_connexion.security
|
||||
|
|
||||
help: Use `sys.version_info` instead
|
||||
|
||||
AIR301 `airflow.PY37` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:39:7
|
||||
--> AIR301_names.py:40:7
|
||||
|
|
||||
38 | # airflow root
|
||||
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
39 | # airflow root
|
||||
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^
|
||||
40 |
|
||||
41 | # airflow.api_connexion.security
|
||||
41 |
|
||||
42 | # airflow.api_connexion.security
|
||||
|
|
||||
help: Use `sys.version_info` instead
|
||||
|
||||
AIR301 `airflow.PY38` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:39:13
|
||||
--> AIR301_names.py:40:13
|
||||
|
|
||||
38 | # airflow root
|
||||
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
39 | # airflow root
|
||||
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^
|
||||
40 |
|
||||
41 | # airflow.api_connexion.security
|
||||
41 |
|
||||
42 | # airflow.api_connexion.security
|
||||
|
|
||||
help: Use `sys.version_info` instead
|
||||
|
||||
AIR301 `airflow.PY39` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:39:19
|
||||
--> AIR301_names.py:40:19
|
||||
|
|
||||
38 | # airflow root
|
||||
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
39 | # airflow root
|
||||
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^
|
||||
40 |
|
||||
41 | # airflow.api_connexion.security
|
||||
41 |
|
||||
42 | # airflow.api_connexion.security
|
||||
|
|
||||
help: Use `sys.version_info` instead
|
||||
|
||||
AIR301 `airflow.PY310` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:39:25
|
||||
--> AIR301_names.py:40:25
|
||||
|
|
||||
38 | # airflow root
|
||||
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
39 | # airflow root
|
||||
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^^
|
||||
40 |
|
||||
41 | # airflow.api_connexion.security
|
||||
41 |
|
||||
42 | # airflow.api_connexion.security
|
||||
|
|
||||
help: Use `sys.version_info` instead
|
||||
|
||||
AIR301 `airflow.PY311` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:39:32
|
||||
--> AIR301_names.py:40:32
|
||||
|
|
||||
38 | # airflow root
|
||||
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
39 | # airflow root
|
||||
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^^
|
||||
40 |
|
||||
41 | # airflow.api_connexion.security
|
||||
41 |
|
||||
42 | # airflow.api_connexion.security
|
||||
|
|
||||
help: Use `sys.version_info` instead
|
||||
|
||||
AIR301 `airflow.PY312` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:39:39
|
||||
--> AIR301_names.py:40:39
|
||||
|
|
||||
38 | # airflow root
|
||||
39 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
39 | # airflow root
|
||||
40 | PY36, PY37, PY38, PY39, PY310, PY311, PY312
|
||||
| ^^^^^
|
||||
40 |
|
||||
41 | # airflow.api_connexion.security
|
||||
41 |
|
||||
42 | # 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:42:1
|
||||
--> AIR301_names.py:43:1
|
||||
|
|
||||
41 | # airflow.api_connexion.security
|
||||
42 | requires_access
|
||||
42 | # airflow.api_connexion.security
|
||||
43 | requires_access
|
||||
| ^^^^^^^^^^^^^^^
|
||||
43 |
|
||||
44 | # airflow.contrib.*
|
||||
44 |
|
||||
45 | # 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:45:1
|
||||
--> AIR301_names.py:46:1
|
||||
|
|
||||
44 | # airflow.contrib.*
|
||||
45 | AWSAthenaHook()
|
||||
45 | # airflow.contrib.*
|
||||
46 | AWSAthenaHook()
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
help: The whole `airflow.contrib` module has been removed.
|
||||
|
||||
AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:49:1
|
||||
--> AIR301_names.py:50:1
|
||||
|
|
||||
48 | # airflow.datasets
|
||||
49 | DatasetAliasEvent()
|
||||
49 | # airflow.datasets
|
||||
50 | DatasetAliasEvent()
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:53:1
|
||||
--> AIR301_names.py:54:1
|
||||
|
|
||||
52 | # airflow.operators.subdag.*
|
||||
53 | SubDagOperator()
|
||||
53 | # airflow.operators.subdag.*
|
||||
54 | SubDagOperator()
|
||||
| ^^^^^^^^^^^^^^
|
||||
55 |
|
||||
56 | # airflow.operators.postgres_operator
|
||||
|
|
||||
help: The whole `airflow.subdag` module has been removed.
|
||||
|
||||
AIR301 [*] `airflow.secrets.cache.SecretCache` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:61:1
|
||||
AIR301 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:57:1
|
||||
|
|
||||
60 | # airflow.secrets.cache
|
||||
61 | SecretCache()
|
||||
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
|
||||
|
|
||||
63 | # airflow.secrets.cache
|
||||
64 | 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.subdag import SubDagOperator
|
||||
15 | from airflow.operators.postgres_operator import Mapping
|
||||
16 | from airflow.operators.subdag import SubDagOperator
|
||||
- from airflow.secrets.cache import SecretCache
|
||||
16 | from airflow.secrets.local_filesystem import LocalFilesystemBackend
|
||||
17 | from airflow.triggers.external_task import TaskStateTrigger
|
||||
18 | from airflow.utils import dates
|
||||
17 | from airflow.secrets.local_filesystem import LocalFilesystemBackend
|
||||
18 | from airflow.triggers.external_task import TaskStateTrigger
|
||||
19 | from airflow.utils import dates
|
||||
--------------------------------------------------------------------------------
|
||||
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
|
||||
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
|
||||
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.utils.date
|
||||
68 | dates.date_range
|
||||
67 | # airflow.triggers.external_task
|
||||
68 | TaskStateTrigger()
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
69 | dates.days_ago
|
||||
69 |
|
||||
70 | # airflow.utils.date
|
||||
|
|
||||
|
||||
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
|
||||
|
|
||||
69 | dates.days_ago
|
||||
70 |
|
||||
71 | date_range
|
||||
| ^^^^^^^^^^
|
||||
72 | days_ago
|
||||
73 | infer_time_unit
|
||||
70 | # airflow.utils.date
|
||||
71 | dates.date_range
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
72 | dates.days_ago
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:72:1
|
||||
|
|
||||
71 | date_range
|
||||
72 | days_ago
|
||||
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
|
||||
| ^^^^^^^^
|
||||
73 | infer_time_unit
|
||||
74 | parse_execution_date
|
||||
76 | infer_time_unit
|
||||
77 | 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:73:1
|
||||
--> AIR301_names.py:76:1
|
||||
|
|
||||
71 | date_range
|
||||
72 | days_ago
|
||||
73 | infer_time_unit
|
||||
74 | date_range
|
||||
75 | days_ago
|
||||
76 | infer_time_unit
|
||||
| ^^^^^^^^^^^^^^^
|
||||
74 | parse_execution_date
|
||||
75 | round_time
|
||||
77 | parse_execution_date
|
||||
78 | round_time
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:74:1
|
||||
--> AIR301_names.py:77:1
|
||||
|
|
||||
72 | days_ago
|
||||
73 | infer_time_unit
|
||||
74 | parse_execution_date
|
||||
75 | days_ago
|
||||
76 | infer_time_unit
|
||||
77 | parse_execution_date
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
75 | round_time
|
||||
76 | scale_time_units
|
||||
78 | round_time
|
||||
79 | scale_time_units
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:75:1
|
||||
--> AIR301_names.py:78:1
|
||||
|
|
||||
73 | infer_time_unit
|
||||
74 | parse_execution_date
|
||||
75 | round_time
|
||||
76 | infer_time_unit
|
||||
77 | parse_execution_date
|
||||
78 | round_time
|
||||
| ^^^^^^^^^^
|
||||
76 | scale_time_units
|
||||
79 | scale_time_units
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:76:1
|
||||
--> AIR301_names.py:79:1
|
||||
|
|
||||
74 | parse_execution_date
|
||||
75 | round_time
|
||||
76 | scale_time_units
|
||||
77 | parse_execution_date
|
||||
78 | round_time
|
||||
79 | scale_time_units
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
77 |
|
||||
78 | # This one was not deprecated.
|
||||
80 |
|
||||
81 | # This one was not deprecated.
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:83:1
|
||||
--> AIR301_names.py:86:1
|
||||
|
|
||||
82 | # airflow.utils.dag_cycle_tester
|
||||
83 | test_cycle
|
||||
85 | # airflow.utils.dag_cycle_tester
|
||||
86 | test_cycle
|
||||
| ^^^^^^^^^^
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:87:1
|
||||
--> AIR301_names.py:90:1
|
||||
|
|
||||
86 | # airflow.utils.db
|
||||
87 | create_session
|
||||
89 | # airflow.utils.db
|
||||
90 | create_session
|
||||
| ^^^^^^^^^^^^^^
|
||||
88 |
|
||||
89 | # airflow.utils.decorators
|
||||
91 |
|
||||
92 | # airflow.utils.decorators
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:90:1
|
||||
--> AIR301_names.py:93:1
|
||||
|
|
||||
89 | # airflow.utils.decorators
|
||||
90 | apply_defaults
|
||||
92 | # airflow.utils.decorators
|
||||
93 | apply_defaults
|
||||
| ^^^^^^^^^^^^^^
|
||||
91 |
|
||||
92 | # airflow.utils.file
|
||||
94 |
|
||||
95 | # 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:93:1
|
||||
--> AIR301_names.py:96:1
|
||||
|
|
||||
92 | # airflow.utils.file
|
||||
93 | mkdirs
|
||||
95 | # airflow.utils.file
|
||||
96 | mkdirs
|
||||
| ^^^^^^
|
||||
|
|
||||
help: Use `pathlib.Path({path}).mkdir` instead
|
||||
|
||||
AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:97:1
|
||||
|
|
||||
96 | # airflow.utils.state
|
||||
97 | SHUTDOWN
|
||||
| ^^^^^^^^
|
||||
98 | terminating_states
|
||||
|
|
||||
--> AIR301_names.py:100:1
|
||||
|
|
||||
99 | # airflow.utils.state
|
||||
100 | SHUTDOWN
|
||||
| ^^^^^^^^
|
||||
101 | terminating_states
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:98:1
|
||||
--> AIR301_names.py:101:1
|
||||
|
|
||||
96 | # airflow.utils.state
|
||||
97 | SHUTDOWN
|
||||
98 | terminating_states
|
||||
99 | # airflow.utils.state
|
||||
100 | SHUTDOWN
|
||||
101 | terminating_states
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
99 |
|
||||
100 | # airflow.utils.trigger_rule
|
||||
102 |
|
||||
103 | # airflow.utils.trigger_rule
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:101:1
|
||||
--> AIR301_names.py:104:1
|
||||
|
|
||||
100 | # airflow.utils.trigger_rule
|
||||
101 | TriggerRule.DUMMY
|
||||
103 | # airflow.utils.trigger_rule
|
||||
104 | TriggerRule.DUMMY
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
102 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
||||
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
||||
|
|
||||
|
||||
AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:102:1
|
||||
--> AIR301_names.py:105:1
|
||||
|
|
||||
100 | # airflow.utils.trigger_rule
|
||||
101 | TriggerRule.DUMMY
|
||||
102 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
||||
103 | # airflow.utils.trigger_rule
|
||||
104 | TriggerRule.DUMMY
|
||||
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:106:1
|
||||
--> AIR301_names.py:109:1
|
||||
|
|
||||
105 | # airflow.www.auth
|
||||
106 | has_access
|
||||
108 | # airflow.www.auth
|
||||
109 | has_access
|
||||
| ^^^^^^^^^^
|
||||
107 | has_access_dataset
|
||||
110 | has_access_dataset
|
||||
|
|
||||
|
||||
AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:107:1
|
||||
--> AIR301_names.py:110:1
|
||||
|
|
||||
105 | # airflow.www.auth
|
||||
106 | has_access
|
||||
107 | has_access_dataset
|
||||
108 | # airflow.www.auth
|
||||
109 | has_access
|
||||
110 | has_access_dataset
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
108 |
|
||||
109 | # airflow.www.utils
|
||||
111 |
|
||||
112 | # airflow.www.utils
|
||||
|
|
||||
|
||||
AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:110:1
|
||||
--> AIR301_names.py:113:1
|
||||
|
|
||||
109 | # airflow.www.utils
|
||||
110 | get_sensitive_variables_fields
|
||||
112 | # airflow.www.utils
|
||||
113 | get_sensitive_variables_fields
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
111 | should_hide_value_for_key
|
||||
114 | should_hide_value_for_key
|
||||
|
|
||||
|
||||
AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
|
||||
--> AIR301_names.py:111:1
|
||||
--> AIR301_names.py:114:1
|
||||
|
|
||||
109 | # airflow.www.utils
|
||||
110 | get_sensitive_variables_fields
|
||||
111 | should_hide_value_for_key
|
||||
112 | # airflow.www.utils
|
||||
113 | get_sensitive_variables_fields
|
||||
114 | should_hide_value_for_key
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
|
|
|||
|
|
@ -20,11 +20,3 @@ 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()
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
|
|
|||
|
|
@ -558,7 +558,7 @@ AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Air
|
|||
--> AIR311_names.py:73:1
|
||||
|
|
||||
72 | # airflow.timetables.datasets
|
||||
73 | DatasetOrTimeSchedule()
|
||||
73 | DatasetOrTimeSchedule(datasets=[])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
74 |
|
||||
75 | # airflow.utils.dag_parsing_context
|
||||
|
|
@ -570,12 +570,31 @@ help: Use `AssetOrTimeSchedule` from `airflow.timetables.assets` instead.
|
|||
71 + from airflow.timetables.assets import AssetOrTimeSchedule
|
||||
72 |
|
||||
73 | # airflow.timetables.datasets
|
||||
- DatasetOrTimeSchedule()
|
||||
74 + AssetOrTimeSchedule()
|
||||
- DatasetOrTimeSchedule(datasets=[])
|
||||
74 + AssetOrTimeSchedule(datasets=[])
|
||||
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
|
||||
|
|
||||
|
|
@ -593,7 +612,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()
|
||||
73 | DatasetOrTimeSchedule(datasets=[])
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ 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;
|
||||
|
|
@ -74,33 +75,39 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
|
|||
|
||||
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
|
||||
|
||||
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,
|
||||
))
|
||||
});
|
||||
if !fix_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
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`.
|
||||
|
|
@ -34,7 +36,16 @@ use ruff_python_ast::ExprCall;
|
|||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
/// 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.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
|
||||
|
|
@ -62,6 +73,7 @@ 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,
|
||||
|
|
@ -69,5 +81,6 @@ pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&
|
|||
"p",
|
||||
is_fix_os_path_basename_enabled(checker.settings()),
|
||||
OsPathBasename,
|
||||
Some(Applicability::Unsafe),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
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`.
|
||||
|
|
@ -29,7 +31,16 @@ use ruff_python_ast::ExprCall;
|
|||
/// ```
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
/// 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.
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
|
|
@ -62,6 +73,7 @@ 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,
|
||||
|
|
@ -69,5 +81,6 @@ 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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -62,6 +63,7 @@ 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,
|
||||
|
|
@ -69,5 +71,6 @@ pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&st
|
|||
"path",
|
||||
is_fix_os_path_exists_enabled(checker.settings()),
|
||||
OsPathExists,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -62,6 +63,7 @@ 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,
|
||||
|
|
@ -69,5 +71,6 @@ pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &
|
|||
"path",
|
||||
is_fix_os_path_expanduser_enabled(checker.settings()),
|
||||
OsPathExpanduser,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -65,6 +66,7 @@ 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,
|
||||
|
|
@ -72,5 +74,6 @@ pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&
|
|||
"filename",
|
||||
is_fix_os_path_getatime_enabled(checker.settings()),
|
||||
OsPathGetatime,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -66,6 +67,7 @@ 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,
|
||||
|
|
@ -73,5 +75,6 @@ pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&
|
|||
"filename",
|
||||
is_fix_os_path_getctime_enabled(checker.settings()),
|
||||
OsPathGetctime,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -66,6 +67,7 @@ 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,
|
||||
|
|
@ -73,5 +75,6 @@ pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&
|
|||
"filename",
|
||||
is_fix_os_path_getmtime_enabled(checker.settings()),
|
||||
OsPathGetmtime,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -66,6 +67,7 @@ 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,
|
||||
|
|
@ -73,5 +75,6 @@ pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&s
|
|||
"filename",
|
||||
is_fix_os_path_getsize_enabled(checker.settings()),
|
||||
OsPathGetsize,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -61,6 +62,7 @@ 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,
|
||||
|
|
@ -68,5 +70,6 @@ pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str
|
|||
"s",
|
||||
is_fix_os_path_isabs_enabled(checker.settings()),
|
||||
OsPathIsabs,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -71,5 +72,6 @@ pub(crate) fn os_path_isdir(checker: &Checker, call: &ExprCall, segments: &[&str
|
|||
"s",
|
||||
is_fix_os_path_isdir_enabled(checker.settings()),
|
||||
OsPathIsdir,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -71,5 +72,6 @@ pub(crate) fn os_path_isfile(checker: &Checker, call: &ExprCall, segments: &[&st
|
|||
"path",
|
||||
is_fix_os_path_isfile_enabled(checker.settings()),
|
||||
OsPathIsfile,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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`.
|
||||
|
|
@ -71,5 +72,6 @@ pub(crate) fn os_path_islink(checker: &Checker, call: &ExprCall, segments: &[&st
|
|||
"path",
|
||||
is_fix_os_path_islink_enabled(checker.settings()),
|
||||
OsPathIslink,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
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`.
|
||||
|
|
@ -87,5 +88,6 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
|
|||
"path",
|
||||
is_fix_os_readlink_enabled(checker.settings()),
|
||||
OsReadlink,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
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`.
|
||||
|
|
@ -82,5 +83,6 @@ pub(crate) fn os_remove(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
|||
"path",
|
||||
is_fix_os_remove_enabled(checker.settings()),
|
||||
OsRemove,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
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`.
|
||||
|
|
@ -82,5 +83,6 @@ pub(crate) fn os_rmdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
|||
"path",
|
||||
is_fix_os_rmdir_enabled(checker.settings()),
|
||||
OsRmdir,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
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`.
|
||||
|
|
@ -82,5 +83,6 @@ pub(crate) fn os_unlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
|||
"path",
|
||||
is_fix_os_unlink_enabled(checker.settings()),
|
||||
OsUnlink,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -466,6 +466,7 @@ 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
|
||||
|
|
@ -493,6 +494,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -466,6 +466,7 @@ 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
|
||||
|
|
@ -492,6 +493,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -480,6 +480,7 @@ 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
|
||||
|
|
@ -508,6 +509,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -480,6 +480,7 @@ 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
|
||||
|
|
@ -507,6 +508,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
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.
|
||||
|
|
@ -36,13 +32,3 @@ 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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
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
|
||||
| ^^^^^^^^^^^^
|
||||
|
|
||||
|
|
@ -0,0 +1 @@
|
|||
async def f(): yield from x
|
||||
|
|
@ -709,6 +709,16 @@ 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);
|
||||
|
|
@ -989,6 +999,9 @@ 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1346,6 +1359,9 @@ 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)]
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
|
|||
|
||||
enum Scope {
|
||||
Module,
|
||||
Function,
|
||||
Function { is_async: bool },
|
||||
Comprehension { is_async: bool },
|
||||
Class,
|
||||
}
|
||||
|
|
@ -528,7 +528,15 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
|
|||
}
|
||||
|
||||
fn in_async_context(&self) -> bool {
|
||||
true
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fn in_sync_comprehension(&self) -> bool {
|
||||
|
|
@ -589,8 +597,10 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> {
|
|||
self.visit_body(body);
|
||||
self.scopes.pop().unwrap();
|
||||
}
|
||||
ast::Stmt::FunctionDef(ast::StmtFunctionDef { .. }) => {
|
||||
self.scopes.push(Scope::Function);
|
||||
ast::Stmt::FunctionDef(ast::StmtFunctionDef { is_async, .. }) => {
|
||||
self.scopes.push(Scope::Function {
|
||||
is_async: *is_async,
|
||||
});
|
||||
ast::visitor::walk_stmt(self, stmt);
|
||||
self.scopes.pop().unwrap();
|
||||
}
|
||||
|
|
@ -604,7 +614,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);
|
||||
self.scopes.push(Scope::Function { is_async: false });
|
||||
ast::visitor::walk_expr(self, expr);
|
||||
self.scopes.pop().unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
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
|
||||
|
|
||||
3
crates/ty/docs/cli.md
generated
3
crates/ty/docs/cli.md
generated
|
|
@ -60,8 +60,9 @@ 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>
|
||||
|
|
|
|||
|
|
@ -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,6 +321,9 @@ 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 {
|
||||
|
|
@ -328,6 +331,7 @@ impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
|
|||
match format {
|
||||
OutputFormat::Full => Self::Full,
|
||||
OutputFormat::Concise => Self::Concise,
|
||||
OutputFormat::Gitlab => Self::Gitlab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, Severity};
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, DisplayDiagnostics, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
|
|
@ -319,37 +319,48 @@ impl MainLoop {
|
|||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
let is_human_readable = terminal_settings.output_format.is_human_readable();
|
||||
|
||||
if result.is_empty() {
|
||||
writeln!(
|
||||
self.printer.stream_for_success_summary(),
|
||||
"{}",
|
||||
"All checks passed!".green().bold()
|
||||
)?;
|
||||
if is_human_readable {
|
||||
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();
|
||||
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))?;
|
||||
}
|
||||
let max_severity = result
|
||||
.iter()
|
||||
.map(Diagnostic::severity)
|
||||
.max()
|
||||
.unwrap_or(Severity::Info);
|
||||
|
||||
max_severity = max_severity.max(diagnostic.severity());
|
||||
// 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)
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
self.printer.stream_for_failure_summary(),
|
||||
"Found {} diagnostic{}",
|
||||
diagnostics_count,
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
if is_human_readable {
|
||||
writeln!(
|
||||
self.printer.stream_for_failure_summary(),
|
||||
"Found {} diagnostic{}",
|
||||
diagnostics_count,
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
}
|
||||
|
||||
if max_severity.is_fatal() {
|
||||
tracing::warn!(
|
||||
|
|
|
|||
|
|
@ -618,6 +618,71 @@ 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
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ impl TestCase {
|
|||
.module(parent_module_name)
|
||||
.all_submodules(self.db())
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.map(|submodule| submodule.name(self.db()).to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ smallvec = { workspace = true }
|
|||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
camino = { workspace = true }
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
|
||||
[lints]
|
||||
|
|
|
|||
182
crates/ty_ide/src/all_symbols.rs
Normal file
182
crates/ty_ide/src/all_symbols.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,22 @@ 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};
|
||||
|
||||
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<DetailedCompletion<'_>> {
|
||||
#[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>> {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
|
||||
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
|
||||
|
|
@ -37,14 +47,31 @@ pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<DetailedComp
|
|||
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
|
||||
model.import_completions()
|
||||
}
|
||||
CompletionTargetAst::Scoped { node } => model.scoped_completions(node),
|
||||
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
|
||||
}
|
||||
};
|
||||
completions.sort_by(compare_suggestions);
|
||||
completions.dedup_by(|c1, c2| c1.name == c2.name);
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let definition = DefinitionsOrTargets::from_ty(db, completion.ty);
|
||||
let definition = completion
|
||||
.ty
|
||||
.and_then(|ty| DefinitionsOrTargets::from_ty(db, ty));
|
||||
let documentation = definition.and_then(|def| def.docstring(db));
|
||||
DetailedCompletion {
|
||||
inner: completion,
|
||||
|
|
@ -54,10 +81,12 @@ pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<DetailedComp
|
|||
.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 {
|
||||
|
|
@ -258,16 +287,22 @@ impl<'t> CompletionTargetTokens<'t> {
|
|||
}
|
||||
}
|
||||
CompletionTargetTokens::Generic { token } => {
|
||||
let covering_node = covering_node(parsed.syntax().into(), token.range());
|
||||
Some(CompletionTargetAst::Scoped {
|
||||
node: covering_node.node(),
|
||||
})
|
||||
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 })
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -321,7 +356,16 @@ 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 { node: ast::AnyNodeRef<'t> },
|
||||
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>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Returns a suffix of `tokens` corresponding to the `kinds` given.
|
||||
|
|
@ -505,7 +549,7 @@ mod tests {
|
|||
use crate::completion::{DetailedCompletion, completion};
|
||||
use crate::tests::{CursorTest, cursor_test};
|
||||
|
||||
use super::token_suffix_by_kinds;
|
||||
use super::{CompletionSettings, token_suffix_by_kinds};
|
||||
|
||||
#[test]
|
||||
fn token_suffixes_match() {
|
||||
|
|
@ -3013,6 +3057,24 @@ 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
|
||||
|
|
@ -3031,6 +3093,10 @@ 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 {
|
||||
|
|
@ -3040,7 +3106,14 @@ from os.<CURSOR>
|
|||
fn completions_without_builtins_with_types(&self) -> String {
|
||||
self.completions_if_snapshot(
|
||||
|c| !c.builtin,
|
||||
|c| format!("{} :: {}", c.name, c.ty.display(&self.db)),
|
||||
|c| {
|
||||
format!(
|
||||
"{} :: {}",
|
||||
c.name,
|
||||
c.ty.map(|ty| ty.display(&self.db).to_string())
|
||||
.unwrap_or_else(|| "Unavailable".to_string())
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3053,7 +3126,8 @@ from os.<CURSOR>
|
|||
predicate: impl Fn(&DetailedCompletion) -> bool,
|
||||
snapshot: impl Fn(&DetailedCompletion) -> String,
|
||||
) -> String {
|
||||
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
|
||||
let settings = CompletionSettings::default();
|
||||
let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset);
|
||||
if completions.is_empty() {
|
||||
return "<No completions found>".to_string();
|
||||
}
|
||||
|
|
@ -3076,7 +3150,8 @@ from os.<CURSOR>
|
|||
|
||||
#[track_caller]
|
||||
fn assert_completions_include(&self, expected: &str) {
|
||||
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
|
||||
let settings = CompletionSettings::default();
|
||||
let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset);
|
||||
|
||||
assert!(
|
||||
completions
|
||||
|
|
@ -3088,7 +3163,8 @@ from os.<CURSOR>
|
|||
|
||||
#[track_caller]
|
||||
fn assert_completions_do_not_include(&self, unexpected: &str) {
|
||||
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
|
||||
let settings = CompletionSettings::default();
|
||||
let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset);
|
||||
|
||||
assert!(
|
||||
completions
|
||||
|
|
|
|||
|
|
@ -8,14 +8,15 @@ 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,
|
||||
};
|
||||
|
|
@ -145,6 +146,26 @@ 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`
|
||||
|
|
@ -258,6 +279,9 @@ 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(_)
|
||||
|
|
@ -293,18 +317,10 @@ impl GotoTarget<'_> {
|
|||
alias_resolution: ImportAliasResolution,
|
||||
) -> Option<DefinitionsOrTargets<'db>> {
|
||||
use crate::NavigationTarget;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
match self {
|
||||
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,
|
||||
},
|
||||
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression)
|
||||
.map(DefinitionsOrTargets::Definitions),
|
||||
|
||||
// For already-defined symbols, they are their own definitions
|
||||
GotoTarget::FunctionDef(function) => {
|
||||
|
|
@ -417,6 +433,22 @@ 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -427,7 +459,11 @@ impl GotoTarget<'_> {
|
|||
/// to this goto target.
|
||||
pub(crate) fn to_string(&self) -> Option<Cow<'_, str>> {
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => match expression {
|
||||
GotoTarget::Call {
|
||||
callable: 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,
|
||||
|
|
@ -627,7 +663,18 @@ impl GotoTarget<'_> {
|
|||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||
}
|
||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
||||
Some(GotoTarget::Expression(attribute.into()))
|
||||
// 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(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||
|
|
@ -641,7 +688,19 @@ impl GotoTarget<'_> {
|
|||
}
|
||||
},
|
||||
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -649,7 +708,11 @@ impl GotoTarget<'_> {
|
|||
impl Ranged for GotoTarget<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => match expression {
|
||||
GotoTarget::Call {
|
||||
callable: expression,
|
||||
..
|
||||
}
|
||||
| GotoTarget::Expression(expression) => match expression {
|
||||
ast::ExprRef::Attribute(attribute) => attribute.attr.range,
|
||||
_ => expression.range(),
|
||||
},
|
||||
|
|
@ -711,6 +774,35 @@ 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,
|
||||
|
|
|
|||
|
|
@ -123,6 +123,23 @@ 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()
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
|
@ -1355,6 +1372,462 @@ 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)
|
||||
|
|
|
|||
|
|
@ -466,6 +466,22 @@ 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)
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
|
@ -802,6 +818,318 @@ 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,
|
||||
|
|
|
|||
|
|
@ -465,6 +465,123 @@ mod tests {
|
|||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<class 'MyClass'>
|
||||
---------------------------------------------
|
||||
initializes MyClass (perfectly)
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---
|
||||
```text
|
||||
initializes MyClass (perfectly)
|
||||
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:24:5
|
||||
|
|
||||
22 | return 0
|
||||
23 |
|
||||
24 | x = MyClass(0)
|
||||
| ^^^^^-^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[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'>
|
||||
---------------------------------------------
|
||||
|
|
@ -489,11 +606,11 @@ mod tests {
|
|||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:24:5
|
||||
--> main.py:23:5
|
||||
|
|
||||
22 | return 0
|
||||
23 |
|
||||
24 | x = MyClass(0)
|
||||
21 | return 0
|
||||
22 |
|
||||
23 | x = MyClass(0)
|
||||
| ^^^^^-^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
|
|
@ -791,7 +908,453 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn hover_overload() {
|
||||
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() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import overload
|
||||
|
|
@ -858,7 +1421,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn hover_overload_compact() {
|
||||
fn hover_overload_ambiguous_compact() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import overload
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
clippy::disallowed_methods,
|
||||
reason = "Prefer System trait methods over std methods in ty crates"
|
||||
)]
|
||||
mod all_symbols;
|
||||
mod completion;
|
||||
mod doc_highlights;
|
||||
mod docstring;
|
||||
|
|
@ -24,7 +25,8 @@ mod stub_mapping;
|
|||
mod symbols;
|
||||
mod workspace_symbols;
|
||||
|
||||
pub use completion::completion;
|
||||
pub use all_symbols::{AllSymbolInfo, all_symbols};
|
||||
pub use completion::{CompletionSettings, completion};
|
||||
pub use doc_highlights::document_highlights;
|
||||
pub use document_symbols::document_symbols;
|
||||
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
|
||||
|
|
@ -286,10 +288,12 @@ 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, system_path_to_file};
|
||||
use ruff_db::files::{File, FileRootKind, system_path_to_file};
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_text_size::TextSize;
|
||||
|
|
@ -378,6 +382,19 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -258,6 +258,9 @@ 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};
|
||||
|
|
@ -470,6 +473,354 @@ 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(
|
||||
|
|
@ -826,5 +1177,94 @@ mod tests {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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.
|
||||
///
|
||||
|
|
@ -282,6 +283,27 @@ 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.
|
||||
|
|
|
|||
|
|
@ -1055,6 +1055,21 @@ 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 {
|
||||
|
|
@ -1062,6 +1077,7 @@ impl From<OutputFormat> for DiagnosticFormat {
|
|||
match value {
|
||||
OutputFormat::Full => Self::Full,
|
||||
OutputFormat::Concise => Self::Concise,
|
||||
OutputFormat::Gitlab => Self::Gitlab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,10 +74,15 @@ def _(
|
|||
def bar() -> None:
|
||||
return None
|
||||
|
||||
async def baz() -> int:
|
||||
return 42
|
||||
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 outer(): # avoid unrelated syntax errors on yield, yield from, and await
|
||||
async def baz():
|
||||
yield
|
||||
|
||||
async def outer_async(): # avoid unrelated syntax errors on `yield` 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"
|
||||
|
|
@ -92,11 +97,10 @@ async def outer(): # avoid unrelated syntax errors on yield, yield from, and aw
|
|||
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: (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"
|
||||
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"
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
|
@ -109,9 +113,12 @@ async def outer(): # avoid unrelated syntax errors on yield, yield from, and aw
|
|||
reveal_type(i) # revealed: Unknown
|
||||
reveal_type(j) # revealed: Unknown
|
||||
reveal_type(k) # revealed: Unknown
|
||||
reveal_type(p) # revealed: Unknown
|
||||
reveal_type(q) # revealed: int | Unknown
|
||||
reveal_type(r) # revealed: @Todo(unknown type subscript)
|
||||
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)
|
||||
|
||||
class Mat:
|
||||
def __init__(self, value: int):
|
||||
|
|
|
|||
|
|
@ -92,3 +92,106 @@ 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 "b", 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ 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
|
||||
|
|
|
|||
|
|
@ -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, list_modules, resolve_module,
|
||||
resolve_real_module, system_module_search_paths,
|
||||
Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules, list_modules,
|
||||
resolve_module, resolve_real_module, system_module_search_paths,
|
||||
};
|
||||
pub use program::{
|
||||
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,20 @@ 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<'_>> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::iter::FusedIterator;
|
||||
|
||||
pub use list::list_modules;
|
||||
pub use list::{all_modules, list_modules};
|
||||
pub(crate) use module::KnownModule;
|
||||
pub use module::Module;
|
||||
pub use path::{SearchPath, SearchPathValidationError};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
use ruff_db::files::{File, system_path_to_file, vendored_path_to_file};
|
||||
use ruff_db::system::SystemPath;
|
||||
use ruff_db::vendored::VendoredPath;
|
||||
use salsa::Database;
|
||||
use salsa::plumbing::AsId;
|
||||
|
||||
|
|
@ -97,23 +97,10 @@ 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 [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()
|
||||
pub fn all_submodules(self, db: &'db dyn Db) -> &'db [Module<'db>] {
|
||||
all_submodule_names_for_package(db, self)
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +121,10 @@ impl std::fmt::Debug for Module<'_> {
|
|||
|
||||
#[allow(clippy::ref_option)]
|
||||
#[salsa::tracked(returns(ref))]
|
||||
fn all_submodule_names_for_package(db: &dyn Db, file: File) -> Option<Vec<Name>> {
|
||||
fn all_submodule_names_for_package<'db>(
|
||||
db: &'db dyn Db,
|
||||
module: Module<'db>,
|
||||
) -> Option<Vec<Module<'db>>> {
|
||||
fn is_submodule(
|
||||
is_dir: bool,
|
||||
is_file: bool,
|
||||
|
|
@ -147,7 +137,31 @@ fn all_submodule_names_for_package(db: &dyn Db, file: File) -> Option<Vec<Name>>
|
|||
&& !matches!(basename, Some("__init__.py" | "__init__.pyi")))
|
||||
}
|
||||
|
||||
let path = SystemOrVendoredPathRef::try_from_file(db, file)?;
|
||||
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))?;
|
||||
debug_assert!(
|
||||
matches!(path.file_name(), Some("__init__.py" | "__init__.pyi")),
|
||||
"expected package file `{:?}` to be `__init__.py` or `__init__.pyi`",
|
||||
|
|
@ -161,9 +175,11 @@ fn all_submodule_names_for_package(db: &dyn Db, file: File) -> Option<Vec<Name>>
|
|||
// tree. When the revision gets bumped, the cache
|
||||
// that Salsa creates does for this routine will be
|
||||
// invalidated.
|
||||
if let Some(root) = db.files().root(db, parent_directory) {
|
||||
let _ = root.revision(db);
|
||||
}
|
||||
let root = db
|
||||
.files()
|
||||
.root(db, parent_directory)
|
||||
.expect("System search path should have a registered root");
|
||||
let _ = root.revision(db);
|
||||
|
||||
db.system()
|
||||
.read_directory(parent_directory)
|
||||
|
|
@ -187,7 +203,23 @@ fn all_submodule_names_for_package(db: &dyn Db, file: File) -> Option<Vec<Name>>
|
|||
})
|
||||
.filter_map(|entry| {
|
||||
let stem = entry.path().file_stem()?;
|
||||
is_identifier(stem).then(|| Name::from(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,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -207,7 +239,23 @@ fn all_submodule_names_for_package(db: &dyn Db, file: File) -> Option<Vec<Name>>
|
|||
})
|
||||
.filter_map(|entry| {
|
||||
let stem = entry.path().file_stem()?;
|
||||
is_identifier(stem).then(|| Name::from(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,
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ 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,
|
||||
ty: Some(ty),
|
||||
kind: None,
|
||||
builtin,
|
||||
}
|
||||
})
|
||||
|
|
@ -162,7 +163,12 @@ 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, builtin });
|
||||
completions.push(Completion {
|
||||
name,
|
||||
ty: Some(ty),
|
||||
kind: None,
|
||||
builtin,
|
||||
});
|
||||
}
|
||||
completions.extend(self.submodule_completions(&module));
|
||||
completions
|
||||
|
|
@ -173,20 +179,12 @@ impl<'db> SemanticModel<'db> {
|
|||
let builtin = module.is_known(self.db, KnownModule::Builtins);
|
||||
|
||||
let mut completions = vec![];
|
||||
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);
|
||||
for submodule in module.all_submodules(self.db) {
|
||||
let ty = Type::module_literal(self.db, self.file, *submodule);
|
||||
completions.push(Completion {
|
||||
name: submodule_basename.clone(),
|
||||
ty,
|
||||
name: Name::new(submodule.name(self.db).as_str()),
|
||||
ty: Some(ty),
|
||||
kind: None,
|
||||
builtin,
|
||||
});
|
||||
}
|
||||
|
|
@ -200,7 +198,8 @@ impl<'db> SemanticModel<'db> {
|
|||
.into_iter()
|
||||
.map(|member| Completion {
|
||||
name: member.name,
|
||||
ty: member.ty,
|
||||
ty: Some(member.ty),
|
||||
kind: None,
|
||||
builtin: false,
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -236,7 +235,8 @@ 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: member.ty,
|
||||
ty: Some(member.ty),
|
||||
kind: None,
|
||||
builtin: false,
|
||||
}),
|
||||
);
|
||||
|
|
@ -286,8 +286,22 @@ impl NameKind {
|
|||
pub struct Completion<'db> {
|
||||
/// The label shown to the user for this suggestion.
|
||||
pub name: Name,
|
||||
/// The type of this completion.
|
||||
pub ty: Type<'db>,
|
||||
/// 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>,
|
||||
/// Whether this suggestion came from builtins or not.
|
||||
///
|
||||
/// At time of writing (2025-06-26), this information
|
||||
|
|
@ -345,7 +359,7 @@ impl<'db> Completion<'db> {
|
|||
Type::TypeAlias(alias) => imp(db, alias.value_type(db))?,
|
||||
})
|
||||
}
|
||||
imp(db, self.ty)
|
||||
self.kind.or_else(|| self.ty.and_then(|ty| imp(db, ty)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1082,6 +1082,16 @@ 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),
|
||||
|
|
@ -10178,14 +10188,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -739,7 +739,10 @@ impl<'db> FunctionType<'db> {
|
|||
/// classmethod.
|
||||
pub(crate) fn is_classmethod(self, db: &'db dyn Db) -> bool {
|
||||
self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD)
|
||||
|| self.name(db) == "__init_subclass__"
|
||||
|| matches!(
|
||||
self.name(db).as_str(),
|
||||
"__init_subclass__" | "__class_getitem__"
|
||||
)
|
||||
}
|
||||
|
||||
/// If the implementation of this function is deprecated, returns the `@warnings.deprecated`.
|
||||
|
|
|
|||
|
|
@ -9321,7 +9321,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
|
||||
match ty.try_call(db, &CallArguments::positional([value_ty, slice_ty])) {
|
||||
match ty.try_call(db, &CallArguments::positional([slice_ty])) {
|
||||
Ok(bindings) => return bindings.return_type(db),
|
||||
Err(CallError(_, bindings)) => {
|
||||
if let Some(builder) =
|
||||
|
|
|
|||
|
|
@ -615,24 +615,88 @@ 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) {
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -660,9 +724,7 @@ 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_in(lhs_ty, rhs_ty)
|
||||
.map(|ty| ty.negate(self.db)),
|
||||
ast::CmpOp::NotIn => self.evaluate_expr_not_in(lhs_ty, rhs_ty),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use lsp_types::{
|
|||
};
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ty_ide::completion;
|
||||
use ty_ide::{CompletionSettings, completion};
|
||||
use ty_project::ProjectDatabase;
|
||||
use ty_python_semantic::CompletionKind;
|
||||
|
||||
|
|
@ -55,7 +55,10 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
|||
&line_index,
|
||||
snapshot.encoding(),
|
||||
);
|
||||
let completions = completion(db, file, offset);
|
||||
let settings = CompletionSettings {
|
||||
auto_import: snapshot.global_settings().is_auto_import_enabled(),
|
||||
};
|
||||
let completions = completion(db, &settings, file, offset);
|
||||
if completions.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
|
@ -71,7 +74,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
|||
label: comp.inner.name.into(),
|
||||
kind,
|
||||
sort_text: Some(format!("{i:-max_index_len$}")),
|
||||
detail: comp.inner.ty.display(db).to_string().into(),
|
||||
detail: comp.inner.ty.map(|ty| ty.display(db).to_string()),
|
||||
documentation: comp
|
||||
.documentation
|
||||
.map(|docstring| Documentation::String(docstring.render_plaintext())),
|
||||
|
|
|
|||
|
|
@ -826,6 +826,7 @@ 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()
|
||||
|
|
@ -1000,6 +1001,7 @@ 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>,
|
||||
|
|
@ -1016,6 +1018,11 @@ 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
|
||||
|
|
|
|||
|
|
@ -122,6 +122,12 @@ 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;
|
||||
|
|
@ -148,7 +154,8 @@ impl GlobalOptions {
|
|||
let experimental = self
|
||||
.experimental
|
||||
.map(|experimental| ExperimentalSettings {
|
||||
rename: experimental.rename.unwrap_or(true),
|
||||
rename: experimental.rename.unwrap_or(false),
|
||||
auto_import: experimental.auto_import.unwrap_or(false),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
|
@ -293,6 +300,12 @@ 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)]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ 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 {
|
||||
|
|
@ -25,6 +29,7 @@ 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.
|
||||
|
|
|
|||
|
|
@ -415,7 +415,11 @@ impl Workspace {
|
|||
|
||||
let offset = position.to_text_size(&source, &index, self.position_encoding)?;
|
||||
|
||||
let completions = ty_ide::completion(&self.db, file_id.file, offset);
|
||||
// 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);
|
||||
|
||||
Ok(completions
|
||||
.into_iter()
|
||||
|
|
@ -425,7 +429,10 @@ impl Workspace {
|
|||
documentation: completion
|
||||
.documentation
|
||||
.map(|documentation| documentation.render_plaintext()),
|
||||
detail: completion.inner.ty.display(&self.db).to_string().into(),
|
||||
detail: completion
|
||||
.inner
|
||||
.ty
|
||||
.map(|ty| ty.display(&self.db).to_string()),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
|
|
|||
7
ty.schema.json
generated
7
ty.schema.json
generated
|
|
@ -164,6 +164,13 @@
|
|||
"enum": [
|
||||
"concise"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Print diagnostics in the JSON format expected by GitLab [Code Quality] reports.\n\n[Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"gitlab"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue