Compare commits
26 Commits
brent/lamb
...
brent/unar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0b0b54b25 | ||
|
|
9bee558c3a | ||
|
|
f49fda317e | ||
|
|
1cf5eff9f4 | ||
|
|
ecccfd2a56 | ||
|
|
a303d1aa49 | ||
|
|
6eebc35eec | ||
|
|
923b4dd01e | ||
|
|
a1d9cb5830 | ||
|
|
8a85a2961e | ||
|
|
43427abb61 | ||
|
|
84c3cecad6 | ||
|
|
e8e8180888 | ||
|
|
f5cf672ed4 | ||
|
|
6322f37015 | ||
|
|
d272a623d3 | ||
|
|
19c7994e90 | ||
|
|
725ae69773 | ||
|
|
d2c3996f4e | ||
|
|
988c38c013 | ||
|
|
164c2a6cc6 | ||
|
|
1bbe4f0d5e | ||
|
|
cd7354a5c6 | ||
|
|
ec48a47a88 | ||
|
|
43297d3455 | ||
|
|
4373974dd9 |
@@ -491,6 +491,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [PyTorch](https://github.com/pytorch/pytorch)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
- [PyScripter](https://github.com/pyscripter/pyscripter)
|
||||
- [PyVista](https://github.com/pyvista/pyvista)
|
||||
- [Reflex](https://github.com/reflex-dev/reflex)
|
||||
- [River](https://github.com/online-ml/river)
|
||||
|
||||
@@ -71,16 +71,13 @@ impl Display for Benchmark<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_project(db: &ProjectDatabase, max_diagnostics: usize) {
|
||||
fn check_project(db: &ProjectDatabase, project_name: &str, max_diagnostics: usize) {
|
||||
let result = db.check();
|
||||
let diagnostics = result.len();
|
||||
|
||||
assert!(
|
||||
diagnostics > 1 && diagnostics <= max_diagnostics,
|
||||
"Expected between {} and {} diagnostics but got {}",
|
||||
1,
|
||||
max_diagnostics,
|
||||
diagnostics
|
||||
"Expected between 1 and {max_diagnostics} diagnostics on project '{project_name}' but got {diagnostics}",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY39,
|
||||
},
|
||||
1000,
|
||||
3000,
|
||||
);
|
||||
|
||||
static SYMPY: Benchmark = Benchmark::new(
|
||||
@@ -226,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
800,
|
||||
900,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
@@ -234,11 +231,11 @@ fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
bencher
|
||||
.with_inputs(|| benchmark.setup_iteration())
|
||||
.bench_local_refs(|db| {
|
||||
check_project(db, benchmark.max_diagnostics);
|
||||
check_project(db, benchmark.project.name, benchmark.max_diagnostics);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench(args=[&ALTAIR, &FREQTRADE, &PYDANTIC, &TANJUN], sample_size=2, sample_count=3)]
|
||||
#[bench(args=[&ALTAIR, &FREQTRADE, &TANJUN], sample_size=2, sample_count=3)]
|
||||
fn small(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
@@ -248,12 +245,12 @@ fn medium(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
|
||||
#[bench(args=[&SYMPY], sample_size=1, sample_count=2)]
|
||||
#[bench(args=[&SYMPY, &PYDANTIC], sample_size=1, sample_count=2)]
|
||||
fn large(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
|
||||
#[bench(args=[&PYDANTIC], sample_size=3, sample_count=8)]
|
||||
#[bench(args=[&ALTAIR], sample_size=3, sample_count=8)]
|
||||
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
|
||||
|
||||
@@ -261,7 +258,7 @@ fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
.with_inputs(|| benchmark.setup_iteration())
|
||||
.bench_local_values(|db| {
|
||||
thread_pool.install(|| {
|
||||
check_project(&db, benchmark.max_diagnostics);
|
||||
check_project(&db, benchmark.project.name, benchmark.max_diagnostics);
|
||||
db
|
||||
})
|
||||
});
|
||||
@@ -285,7 +282,7 @@ fn main() {
|
||||
// branch when looking up the ingredient index.
|
||||
{
|
||||
let db = TANJUN.setup_iteration();
|
||||
check_project(&db, TANJUN.max_diagnostics);
|
||||
check_project(&db, TANJUN.project.name, TANJUN.max_diagnostics);
|
||||
}
|
||||
|
||||
divan::main();
|
||||
|
||||
@@ -46,7 +46,8 @@ def func():
|
||||
|
||||
|
||||
def func():
|
||||
# OK (index doesn't start at 0
|
||||
# SIM113
|
||||
# https://github.com/astral-sh/ruff/pull/21395
|
||||
idx = 10
|
||||
for x in range(5):
|
||||
g(x, idx)
|
||||
|
||||
@@ -371,6 +371,61 @@ class Foo:
|
||||
"""
|
||||
return
|
||||
|
||||
# DOC102 - Test case from issue #20959: comma-separated parameters
|
||||
def leq(x: object, y: object) -> bool:
|
||||
"""Compare two objects for loose equality.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x1, x2 : object
|
||||
Objects.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the objects are identical or equal.
|
||||
"""
|
||||
return x is y or x == y
|
||||
|
||||
|
||||
# OK - comma-separated parameters that match function signature
|
||||
def compare_values(x1: int, x2: int) -> bool:
|
||||
"""Compare two integer values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x1, x2 : int
|
||||
Values to compare.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if values are equal.
|
||||
"""
|
||||
return x1 == x2
|
||||
|
||||
|
||||
# DOC102 - mixed comma-separated and regular parameters
|
||||
def process_data(data, x1: str, x2: str) -> str:
|
||||
"""Process data with multiple string parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : list
|
||||
Input data to process.
|
||||
x1, x2 : str
|
||||
String parameters for processing.
|
||||
extra_param : str
|
||||
Extra parameter not in signature.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Processed result.
|
||||
"""
|
||||
return f"{x1}{x2}{len(data)}"
|
||||
|
||||
|
||||
# OK
|
||||
def baz(x: int) -> int:
|
||||
"""
|
||||
@@ -389,3 +444,21 @@ def baz(x: int) -> int:
|
||||
int
|
||||
"""
|
||||
return x
|
||||
|
||||
|
||||
# OK - comma-separated parameters without type annotations
|
||||
def add_numbers(a, b):
|
||||
"""
|
||||
Adds two numbers and returns the result.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
a, b
|
||||
The numbers to add.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The sum of the two numbers.
|
||||
"""
|
||||
return a + b
|
||||
|
||||
@@ -269,3 +269,8 @@ pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSett
|
||||
pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/21395
|
||||
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
|
||||
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
|
||||
#[test_case(Rule::EnumerateForLoop, Path::new("SIM113.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::preview::is_enumerate_for_loop_int_index_enabled;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt};
|
||||
use ruff_python_ast::{self as ast, Expr, Int, Number, Operator, Stmt};
|
||||
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -11,6 +13,9 @@ use crate::checkers::ast::Checker;
|
||||
/// Checks for `for` loops with explicit loop-index variables that can be replaced
|
||||
/// with `enumerate()`.
|
||||
///
|
||||
/// In [preview], this rule checks for index variables initialized with any integer rather than only
|
||||
/// a literal zero.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When iterating over a sequence, it's often desirable to keep track of the
|
||||
/// index of each element alongside the element itself. Prefer the `enumerate`
|
||||
@@ -35,6 +40,8 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `enumerate`](https://docs.python.org/3/library/functions.html#enumerate)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.2.0")]
|
||||
pub(crate) struct EnumerateForLoop {
|
||||
@@ -82,17 +89,21 @@ pub(crate) fn enumerate_for_loop(checker: &Checker, for_stmt: &ast::StmtFor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure that the index variable was initialized to 0.
|
||||
// Ensure that the index variable was initialized to 0 (or instance of `int` if preview is enabled).
|
||||
let Some(value) = typing::find_binding_value(binding, checker.semantic()) else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(
|
||||
if !(matches!(
|
||||
value,
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: Number::Int(Int::ZERO),
|
||||
..
|
||||
})
|
||||
) {
|
||||
) || matches!(
|
||||
ResolvedPythonType::from(value),
|
||||
ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer))
|
||||
) && is_enumerate_for_loop_int_index_enabled(checker.settings()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:6:9
|
||||
|
|
||||
4 | for x in range(5):
|
||||
5 | g(x, idx)
|
||||
6 | idx += 1
|
||||
| ^^^^^^^^
|
||||
7 | h(x)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:17:9
|
||||
|
|
||||
15 | if g(x):
|
||||
16 | break
|
||||
17 | idx += 1
|
||||
| ^^^^^^^^
|
||||
18 | sum += h(x, idx)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:27:9
|
||||
|
|
||||
25 | g(x)
|
||||
26 | h(x, y)
|
||||
27 | idx += 1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:36:9
|
||||
|
|
||||
34 | for x in range(5):
|
||||
35 | sum += h(x, idx)
|
||||
36 | idx += 1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:44:9
|
||||
|
|
||||
42 | for x in range(5):
|
||||
43 | g(x, idx)
|
||||
44 | idx += 1
|
||||
| ^^^^^^^^
|
||||
45 | h(x)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:54:9
|
||||
|
|
||||
52 | for x in range(5):
|
||||
53 | g(x, idx)
|
||||
54 | idx += 1
|
||||
| ^^^^^^^^
|
||||
55 | h(x)
|
||||
|
|
||||
@@ -661,19 +661,31 @@ fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec<Paramet
|
||||
.is_some_and(|first_char| !first_char.is_whitespace())
|
||||
{
|
||||
if let Some(before_colon) = entry.split(':').next() {
|
||||
let param = before_colon.trim_end();
|
||||
let param_name = param.trim_start_matches('*');
|
||||
if is_identifier(param_name) {
|
||||
let param_start = line_start + indentation.text_len();
|
||||
let param_end = param_start + param.text_len();
|
||||
let param_line = before_colon.trim_end();
|
||||
|
||||
entries.push(ParameterEntry {
|
||||
name: param_name,
|
||||
range: TextRange::new(
|
||||
content_start + param_start,
|
||||
content_start + param_end,
|
||||
),
|
||||
});
|
||||
// Split on commas to handle comma-separated parameters
|
||||
let mut current_offset = TextSize::from(0);
|
||||
for param_part in param_line.split(',') {
|
||||
let param_part_trimmed = param_part.trim();
|
||||
let param_name = param_part_trimmed.trim_start_matches('*');
|
||||
if is_identifier(param_name) {
|
||||
// Calculate the position of this specific parameter part within the line
|
||||
// Account for leading whitespace that gets trimmed
|
||||
let param_start_in_line = current_offset
|
||||
+ (param_part.text_len() - param_part_trimmed.text_len());
|
||||
let param_start =
|
||||
line_start + indentation.text_len() + param_start_in_line;
|
||||
|
||||
entries.push(ParameterEntry {
|
||||
name: param_name,
|
||||
range: TextRange::at(
|
||||
content_start + param_start,
|
||||
param_part_trimmed.text_len(),
|
||||
),
|
||||
});
|
||||
}
|
||||
// Update offset for next iteration: add the part length plus comma length
|
||||
current_offset = current_offset + param_part.text_len() + ','.text_len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,3 +187,36 @@ DOC102 Documented parameter `a` is not in the function's signature
|
||||
302 | b
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `x1` is not in the function's signature
|
||||
--> DOC102_numpy.py:380:5
|
||||
|
|
||||
378 | Parameters
|
||||
379 | ----------
|
||||
380 | x1, x2 : object
|
||||
| ^^
|
||||
381 | Objects.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `x2` is not in the function's signature
|
||||
--> DOC102_numpy.py:380:9
|
||||
|
|
||||
378 | Parameters
|
||||
379 | ----------
|
||||
380 | x1, x2 : object
|
||||
| ^^
|
||||
381 | Objects.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `extra_param` is not in the function's signature
|
||||
--> DOC102_numpy.py:418:5
|
||||
|
|
||||
416 | x1, x2 : str
|
||||
417 | String parameters for processing.
|
||||
418 | extra_param : str
|
||||
| ^^^^^^^^^^^
|
||||
419 | Extra parameter not in signature.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
@@ -766,11 +766,12 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport
|
||||
}
|
||||
|
||||
for operation in fixer.with_renames() {
|
||||
checker.report_diagnostic(
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
DeprecatedImport {
|
||||
deprecation: Deprecation::WithRename(operation),
|
||||
},
|
||||
import_from_stmt.range(),
|
||||
);
|
||||
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[
|
||||
{
|
||||
"preview": "disabled"
|
||||
},
|
||||
{
|
||||
"preview": "enabled"
|
||||
}
|
||||
]
|
||||
@@ -125,13 +125,6 @@ lambda a, /, c: a
|
||||
*x: x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment
|
||||
*x,
|
||||
**y: x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment 1
|
||||
@@ -142,17 +135,6 @@ lambda a, /, c: a
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment 1
|
||||
*
|
||||
# comment 2
|
||||
x,
|
||||
**y:
|
||||
# comment 3
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # comment 1
|
||||
* # comment 2
|
||||
@@ -160,14 +142,6 @@ lambda a, /, c: a
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # comment 1
|
||||
* # comment 2
|
||||
x,
|
||||
y: # comment 3
|
||||
x
|
||||
)
|
||||
|
||||
lambda *x\
|
||||
:x
|
||||
|
||||
@@ -222,17 +196,6 @@ lambda: ( # comment
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # 1
|
||||
# 2
|
||||
x, # 3
|
||||
# 4
|
||||
y
|
||||
: # 5
|
||||
# 6
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
x,
|
||||
@@ -241,71 +204,6 @@ lambda: ( # comment
|
||||
z
|
||||
)
|
||||
|
||||
|
||||
# Leading
|
||||
lambda x: (
|
||||
lambda y: lambda z: x
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ z # Trailing
|
||||
) # Trailing
|
||||
|
||||
|
||||
# Leading
|
||||
lambda x: lambda y: lambda z: [
|
||||
x,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
z
|
||||
] # Trailing
|
||||
# Trailing
|
||||
|
||||
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/8179
|
||||
|
||||
@@ -193,3 +193,19 @@ def foo():
|
||||
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
|
||||
if '' and (not #
|
||||
0):
|
||||
pass
|
||||
|
||||
if '' and (not #
|
||||
(0)
|
||||
):
|
||||
pass
|
||||
|
||||
if '' and (not
|
||||
( #
|
||||
0
|
||||
)):
|
||||
pass
|
||||
|
||||
@@ -836,7 +836,8 @@ impl<'a> Operand<'a> {
|
||||
let leading = comments.leading(*expression);
|
||||
if is_expression_parenthesized((*expression).into(), comments.ranges(), source) {
|
||||
leading.iter().any(|comment| {
|
||||
!comment.is_formatted()
|
||||
comment.end() <= expression.start()
|
||||
&& !comment.is_formatted()
|
||||
&& matches!(
|
||||
SimpleTokenizer::new(
|
||||
source,
|
||||
@@ -922,7 +923,8 @@ impl Format<PyFormatContext<'_>> for Operand<'_> {
|
||||
let leading_before_parentheses_end = leading
|
||||
.iter()
|
||||
.rposition(|comment| {
|
||||
comment.is_unformatted()
|
||||
comment.end() <= expression.start()
|
||||
&& comment.is_unformatted()
|
||||
&& matches!(
|
||||
SimpleTokenizer::new(
|
||||
f.context().source(),
|
||||
|
||||
@@ -4,7 +4,6 @@ use ruff_python_ast::ExprLambda;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::dangling_comments;
|
||||
use crate::comments::leading_comments;
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||
use crate::other::parameters::ParametersParentheses;
|
||||
use crate::prelude::*;
|
||||
@@ -34,45 +33,24 @@ impl FormatNodeRule<ExprLambda> for FormatExprLambda {
|
||||
|
||||
if dangling_before_parameters.is_empty() {
|
||||
write!(f, [space()])?;
|
||||
} else {
|
||||
write!(f, [dangling_comments(dangling_before_parameters)])?;
|
||||
}
|
||||
|
||||
group(&format_with(|f: &mut PyFormatter| {
|
||||
if f.context().node_level().is_parenthesized()
|
||||
&& (parameters.len() > 1 || !dangling_before_parameters.is_empty())
|
||||
{
|
||||
let end_of_line_start = dangling_before_parameters
|
||||
.partition_point(|comment| comment.line_position().is_end_of_line());
|
||||
let (same_line_comments, own_line_comments) =
|
||||
dangling_before_parameters.split_at(end_of_line_start);
|
||||
write!(
|
||||
f,
|
||||
[parameters
|
||||
.format()
|
||||
.with_options(ParametersParentheses::Never)]
|
||||
)?;
|
||||
|
||||
dangling_comments(same_line_comments).fmt(f)?;
|
||||
write!(f, [token(":")])?;
|
||||
|
||||
write![
|
||||
f,
|
||||
[
|
||||
soft_line_break(),
|
||||
leading_comments(own_line_comments),
|
||||
parameters
|
||||
.format()
|
||||
.with_options(ParametersParentheses::Never),
|
||||
]
|
||||
]
|
||||
} else {
|
||||
parameters
|
||||
.format()
|
||||
.with_options(ParametersParentheses::Never)
|
||||
.fmt(f)
|
||||
}?;
|
||||
|
||||
write!(f, [token(":")])?;
|
||||
|
||||
if dangling_after_parameters.is_empty() {
|
||||
write!(f, [space()])
|
||||
} else {
|
||||
write!(f, [dangling_comments(dangling_after_parameters)])
|
||||
}
|
||||
}))
|
||||
.fmt(f)?;
|
||||
if dangling_after_parameters.is_empty() {
|
||||
write!(f, [space()])?;
|
||||
} else {
|
||||
write!(f, [dangling_comments(dangling_after_parameters)])?;
|
||||
}
|
||||
} else {
|
||||
write!(f, [token(":")])?;
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ impl FormatNodeRule<Parameters> for FormatParameters {
|
||||
let num_parameters = item.len();
|
||||
|
||||
if self.parentheses == ParametersParentheses::Never {
|
||||
write!(f, [format_inner, dangling_comments(dangling)])
|
||||
write!(f, [group(&format_inner), dangling_comments(dangling)])
|
||||
} else if num_parameters == 0 {
|
||||
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
|
||||
// No parameters, format any dangling comments between `()`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
@@ -131,13 +132,6 @@ lambda a, /, c: a
|
||||
*x: x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment
|
||||
*x,
|
||||
**y: x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment 1
|
||||
@@ -148,17 +142,6 @@ lambda a, /, c: a
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment 1
|
||||
*
|
||||
# comment 2
|
||||
x,
|
||||
**y:
|
||||
# comment 3
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # comment 1
|
||||
* # comment 2
|
||||
@@ -166,14 +149,6 @@ lambda a, /, c: a
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # comment 1
|
||||
* # comment 2
|
||||
x,
|
||||
y: # comment 3
|
||||
x
|
||||
)
|
||||
|
||||
lambda *x\
|
||||
:x
|
||||
|
||||
@@ -228,17 +203,6 @@ lambda: ( # comment
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # 1
|
||||
# 2
|
||||
x, # 3
|
||||
# 4
|
||||
y
|
||||
: # 5
|
||||
# 6
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
x,
|
||||
@@ -247,71 +211,6 @@ lambda: ( # comment
|
||||
z
|
||||
)
|
||||
|
||||
|
||||
# Leading
|
||||
lambda x: (
|
||||
lambda y: lambda z: x
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ z # Trailing
|
||||
) # Trailing
|
||||
|
||||
|
||||
# Leading
|
||||
lambda x: lambda y: lambda z: [
|
||||
x,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
z
|
||||
] # Trailing
|
||||
# Trailing
|
||||
|
||||
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/8179
|
||||
@@ -338,22 +237,7 @@ def a():
|
||||
|
||||
```
|
||||
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = 3.10
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
# Leading
|
||||
lambda x: x # Trailing
|
||||
@@ -417,8 +301,7 @@ a = (
|
||||
)
|
||||
|
||||
a = (
|
||||
lambda
|
||||
x, # Dangling
|
||||
lambda x, # Dangling
|
||||
y: 1
|
||||
)
|
||||
|
||||
@@ -484,13 +367,6 @@ lambda a, /, c: a
|
||||
*x: x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment
|
||||
*x,
|
||||
**y: x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment 1
|
||||
@@ -500,16 +376,6 @@ lambda a, /, c: a
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment 1
|
||||
# comment 2
|
||||
*x,
|
||||
**y:
|
||||
# comment 3
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # comment 1
|
||||
# comment 2
|
||||
@@ -517,14 +383,6 @@ lambda a, /, c: a
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # comment 1
|
||||
# comment 2
|
||||
*x,
|
||||
y: # comment 3
|
||||
x
|
||||
)
|
||||
|
||||
lambda *x: x
|
||||
|
||||
(
|
||||
@@ -577,87 +435,11 @@ lambda: ( # comment
|
||||
)
|
||||
|
||||
(
|
||||
lambda # 1
|
||||
# 2
|
||||
x, # 3
|
||||
# 4
|
||||
y: # 5
|
||||
# 6
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
x,
|
||||
lambda x,
|
||||
# comment
|
||||
y: z
|
||||
)
|
||||
|
||||
|
||||
# Leading
|
||||
lambda x: (
|
||||
lambda y: lambda z: x
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ z # Trailing
|
||||
) # Trailing
|
||||
|
||||
|
||||
# Leading
|
||||
lambda x: lambda y: lambda z: [
|
||||
x,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
z,
|
||||
] # Trailing
|
||||
# Trailing
|
||||
|
||||
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
|
||||
*args, **kwargs
|
||||
), e=1, f=2, g=2: d
|
||||
@@ -669,8 +451,7 @@ def a():
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f=lambda
|
||||
self,
|
||||
f=lambda self,
|
||||
*args,
|
||||
**kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs),
|
||||
)
|
||||
@@ -681,365 +462,7 @@ def a():
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f=lambda
|
||||
self,
|
||||
araa,
|
||||
kkkwargs,
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
|
||||
args,
|
||||
kwargs,
|
||||
e=1,
|
||||
f=2,
|
||||
g=2: d,
|
||||
g=10,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Enabled
|
||||
target_version = 3.10
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
# Leading
|
||||
lambda x: x # Trailing
|
||||
# Trailing
|
||||
|
||||
# Leading
|
||||
lambda x, y: x # Trailing
|
||||
# Trailing
|
||||
|
||||
# Leading
|
||||
lambda x, y: x, y # Trailing
|
||||
# Trailing
|
||||
|
||||
# Leading
|
||||
lambda x, /, y: x # Trailing
|
||||
# Trailing
|
||||
|
||||
# Leading
|
||||
lambda x: lambda y: lambda z: x # Trailing
|
||||
# Trailing
|
||||
|
||||
# Leading
|
||||
lambda x: lambda y: lambda z: (x, y, z) # Trailing
|
||||
# Trailing
|
||||
|
||||
# Leading
|
||||
lambda x: lambda y: lambda z: (x, y, z) # Trailing
|
||||
# Trailing
|
||||
|
||||
# Leading
|
||||
lambda x: lambda y: lambda z: (
|
||||
x,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
z,
|
||||
) # Trailing
|
||||
# Trailing
|
||||
|
||||
a = (
|
||||
lambda: # Dangling
|
||||
1
|
||||
)
|
||||
|
||||
a = (
|
||||
lambda
|
||||
x, # Dangling
|
||||
y: 1
|
||||
)
|
||||
|
||||
# Regression test: lambda empty arguments ranges were too long, leading to unstable
|
||||
# formatting
|
||||
(
|
||||
lambda: ( #
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# lambda arguments don't have parentheses, so we never add a magic trailing comma ...
|
||||
def f(
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = lambda x: y,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
# ...but we do preserve a trailing comma after the arguments
|
||||
a = lambda b,: 0
|
||||
|
||||
lambda a,: 0
|
||||
lambda *args,: 0
|
||||
lambda **kwds,: 0
|
||||
lambda a, *args,: 0
|
||||
lambda a, **kwds,: 0
|
||||
lambda *args, b,: 0
|
||||
lambda *, b,: 0
|
||||
lambda *args, **kwds,: 0
|
||||
lambda a, *args, b,: 0
|
||||
lambda a, *, b,: 0
|
||||
lambda a, *args, **kwds,: 0
|
||||
lambda *args, b, **kwds,: 0
|
||||
lambda *, b, **kwds,: 0
|
||||
lambda a, *args, b, **kwds,: 0
|
||||
lambda a, *, b, **kwds,: 0
|
||||
lambda a, /: a
|
||||
lambda a, /, c: a
|
||||
|
||||
# Dangling comments without parameters.
|
||||
(
|
||||
lambda: # 3
|
||||
None
|
||||
)
|
||||
|
||||
(
|
||||
lambda:
|
||||
# 3
|
||||
None
|
||||
)
|
||||
|
||||
(
|
||||
lambda: # 1
|
||||
# 2
|
||||
# 3
|
||||
# 4
|
||||
None # 5
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment
|
||||
*x: x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment
|
||||
*x,
|
||||
**y: x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment 1
|
||||
# comment 2
|
||||
*x:
|
||||
# comment 3
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment 1
|
||||
# comment 2
|
||||
*x,
|
||||
**y:
|
||||
# comment 3
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # comment 1
|
||||
# comment 2
|
||||
*x: # comment 3
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # comment 1
|
||||
# comment 2
|
||||
*x,
|
||||
y: # comment 3
|
||||
x
|
||||
)
|
||||
|
||||
lambda *x: x
|
||||
|
||||
(
|
||||
lambda
|
||||
# comment
|
||||
*x: x
|
||||
)
|
||||
|
||||
lambda: ( # comment
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda: # comment
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda:
|
||||
# comment
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda: # comment
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda:
|
||||
# comment
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda: # comment
|
||||
( # comment
|
||||
x
|
||||
)
|
||||
)
|
||||
|
||||
(
|
||||
lambda # 1
|
||||
# 2
|
||||
x: # 3
|
||||
# 4
|
||||
# 5
|
||||
# 6
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda # 1
|
||||
# 2
|
||||
x, # 3
|
||||
# 4
|
||||
y: # 5
|
||||
# 6
|
||||
x
|
||||
)
|
||||
|
||||
(
|
||||
lambda
|
||||
x,
|
||||
# comment
|
||||
y: z
|
||||
)
|
||||
|
||||
|
||||
# Leading
|
||||
lambda x: (
|
||||
lambda y: lambda z: x
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ y
|
||||
+ z # Trailing
|
||||
) # Trailing
|
||||
|
||||
|
||||
# Leading
|
||||
lambda x: lambda y: lambda z: [
|
||||
x,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
y,
|
||||
z,
|
||||
] # Trailing
|
||||
# Trailing
|
||||
|
||||
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
|
||||
*args, **kwargs
|
||||
), e=1, f=2, g=2: d
|
||||
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/8179
|
||||
def a():
|
||||
return b(
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f=lambda
|
||||
self,
|
||||
*args,
|
||||
**kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs),
|
||||
)
|
||||
|
||||
|
||||
def a():
|
||||
return b(
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f=lambda
|
||||
self,
|
||||
f=lambda self,
|
||||
araa,
|
||||
kkkwargs,
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
@@ -200,6 +199,22 @@ def foo():
|
||||
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
|
||||
if '' and (not #
|
||||
0):
|
||||
pass
|
||||
|
||||
if '' and (not #
|
||||
(0)
|
||||
):
|
||||
pass
|
||||
|
||||
if '' and (not
|
||||
( #
|
||||
0
|
||||
)):
|
||||
pass
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -415,4 +430,23 @@ def foo():
|
||||
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
|
||||
if "" and ( #
|
||||
not 0
|
||||
):
|
||||
pass
|
||||
|
||||
if "" and ( #
|
||||
not (0)
|
||||
):
|
||||
pass
|
||||
|
||||
if "" and (
|
||||
not ( #
|
||||
0
|
||||
)
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -10,12 +10,14 @@ import-deprioritizes-type_check_only,main.py,1,1
|
||||
import-deprioritizes-type_check_only,main.py,2,1
|
||||
import-deprioritizes-type_check_only,main.py,3,2
|
||||
import-deprioritizes-type_check_only,main.py,4,3
|
||||
import-keyword-completion,main.py,0,1
|
||||
internal-typeshed-hidden,main.py,0,5
|
||||
none-completion,main.py,0,11
|
||||
none-completion,main.py,0,2
|
||||
numpy-array,main.py,0,
|
||||
numpy-array,main.py,1,1
|
||||
object-attr-instance-methods,main.py,0,1
|
||||
object-attr-instance-methods,main.py,1,1
|
||||
pass-keyword-completion,main.py,0,1
|
||||
raise-uses-base-exception,main.py,0,2
|
||||
scope-existing-over-new-import,main.py,0,1
|
||||
scope-prioritize-closer,main.py,0,2
|
||||
@@ -23,4 +25,4 @@ scope-simple-long-identifier,main.py,0,1
|
||||
tstring-completions,main.py,0,1
|
||||
ty-extensions-lower-stdlib,main.py,0,8
|
||||
type-var-typing-over-ast,main.py,0,3
|
||||
type-var-typing-over-ast,main.py,1,277
|
||||
type-var-typing-over-ast,main.py,1,279
|
||||
|
||||
|
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = false
|
||||
@@ -0,0 +1 @@
|
||||
from collections im<CURSOR: import>
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
8
crates/ty_completion_eval/truth/import-keyword-completion/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/import-keyword-completion/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = false
|
||||
@@ -0,0 +1,3 @@
|
||||
match x:
|
||||
case int():
|
||||
pa<CURSOR: pass>
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
8
crates/ty_completion_eval/truth/pass-keyword-completion/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/pass-keyword-completion/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1592,6 +1592,111 @@ a = Test()
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_annotation() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
a: float<CURSOR> = 3.14
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:346:7
|
||||
|
|
||||
345 | @disjoint_base
|
||||
346 | class int:
|
||||
| ^^^
|
||||
347 | """int([x]) -> integer
|
||||
348 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: float = 3.14
|
||||
| ^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:659:7
|
||||
|
|
||||
658 | @disjoint_base
|
||||
659 | class float:
|
||||
| ^^^^^
|
||||
660 | """Convert a string or number to a floating-point number, if possible."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: float = 3.14
|
||||
| ^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_annotation() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
a: complex<CURSOR> = 3.14
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:346:7
|
||||
|
|
||||
345 | @disjoint_base
|
||||
346 | class int:
|
||||
| ^^^
|
||||
347 | """int([x]) -> integer
|
||||
348 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: complex = 3.14
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:659:7
|
||||
|
|
||||
658 | @disjoint_base
|
||||
659 | class float:
|
||||
| ^^^^^
|
||||
660 | """Convert a string or number to a floating-point number, if possible."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: complex = 3.14
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:820:7
|
||||
|
|
||||
819 | @disjoint_base
|
||||
820 | class complex:
|
||||
| ^^^^^^^
|
||||
821 | """Create a complex number from a string or numbers.
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: complex = 3.14
|
||||
| ^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/astral-sh/ty/issues/1451>.
|
||||
/// We must ensure we respect re-import convention for stub files for
|
||||
/// imports in builtins.pyi.
|
||||
|
||||
@@ -2634,6 +2634,40 @@ def ab(a: int, *, c: int):
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_float_annotation() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: float<CURSOR> = 3.14
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
int | float
|
||||
---------------------------------------------
|
||||
Convert a string or number to a floating-point number, if possible.
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
int | float
|
||||
```
|
||||
---
|
||||
```text
|
||||
Convert a string or number to a floating-point number, if possible.
|
||||
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: float = 3.14
|
||||
| ^^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn hover(&self) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -369,6 +369,11 @@ reveal_type(c_instance.y) # revealed: Unknown | int
|
||||
|
||||
#### Attributes defined in comprehensions
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class TupleIterator:
|
||||
def __next__(self) -> tuple[int, str]:
|
||||
@@ -380,19 +385,9 @@ class TupleIterable:
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
[... for self.a in range(3)]
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
# error: [unresolved-attribute]
|
||||
[... for (self.b, self.c) in TupleIterable()]
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
# error: [unresolved-attribute]
|
||||
[... for self.d in range(3) for self.e in range(3)]
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
[[... for self.f in range(3)] for _ in range(3)]
|
||||
[[... for self.g in range(3)] for self in [D()]]
|
||||
|
||||
@@ -401,35 +396,74 @@ class D:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | str
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.c) # revealed: Unknown
|
||||
reveal_type(c_instance.c) # revealed: Unknown | str
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.d) # revealed: Unknown
|
||||
reveal_type(c_instance.d) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.e) # revealed: Unknown
|
||||
reveal_type(c_instance.e) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.f) # revealed: Unknown
|
||||
reveal_type(c_instance.f) # revealed: Unknown | int
|
||||
|
||||
# This one is correctly not resolved as an attribute:
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.g) # revealed: Unknown
|
||||
```
|
||||
|
||||
It does not matter how much the comprehension is nested.
|
||||
|
||||
Similarly attributes defined by the comprehension in a generic method are recognized.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def f[T](self):
|
||||
[... for self.a in [1]]
|
||||
[[... for self.b in [1]] for _ in [1]]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
If the comprehension is inside another scope like function then that attribute is not inferred.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
def f():
|
||||
# error: [unresolved-attribute]
|
||||
[... for self.a in [1]]
|
||||
|
||||
def g():
|
||||
# error: [unresolved-attribute]
|
||||
[... for self.b in [1]]
|
||||
g()
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# This attribute is in the function f and is not reachable
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the comprehension is nested in any other eager scope it still can assign attributes.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
class D:
|
||||
[[... for self.a in [1]] for _ in [1]]
|
||||
|
||||
reveal_type(C().a) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
#### Conditionally declared / bound attributes
|
||||
|
||||
We currently treat implicit instance attributes to be bound, even if they are only conditionally
|
||||
|
||||
@@ -33,7 +33,7 @@ g(None)
|
||||
We also support unions in type aliases:
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional
|
||||
from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional, Union
|
||||
from ty_extensions import Unknown
|
||||
|
||||
IntOrStr = int | str
|
||||
@@ -41,6 +41,8 @@ IntOrStrOrBytes1 = int | str | bytes
|
||||
IntOrStrOrBytes2 = (int | str) | bytes
|
||||
IntOrStrOrBytes3 = int | (str | bytes)
|
||||
IntOrStrOrBytes4 = IntOrStr | bytes
|
||||
IntOrStrOrBytes5 = int | Union[str, bytes]
|
||||
IntOrStrOrBytes6 = Union[int, str] | bytes
|
||||
BytesOrIntOrStr = bytes | IntOrStr
|
||||
IntOrNone = int | None
|
||||
NoneOrInt = None | int
|
||||
@@ -70,6 +72,8 @@ reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes2) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes3) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes4) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes5) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes6) # revealed: types.UnionType
|
||||
reveal_type(BytesOrIntOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrNone) # revealed: types.UnionType
|
||||
reveal_type(NoneOrInt) # revealed: types.UnionType
|
||||
@@ -100,6 +104,8 @@ def _(
|
||||
int_or_str_or_bytes2: IntOrStrOrBytes2,
|
||||
int_or_str_or_bytes3: IntOrStrOrBytes3,
|
||||
int_or_str_or_bytes4: IntOrStrOrBytes4,
|
||||
int_or_str_or_bytes5: IntOrStrOrBytes5,
|
||||
int_or_str_or_bytes6: IntOrStrOrBytes6,
|
||||
bytes_or_int_or_str: BytesOrIntOrStr,
|
||||
int_or_none: IntOrNone,
|
||||
none_or_int: NoneOrInt,
|
||||
@@ -129,6 +135,8 @@ def _(
|
||||
reveal_type(int_or_str_or_bytes2) # revealed: int | str | bytes
|
||||
reveal_type(int_or_str_or_bytes3) # revealed: int | str | bytes
|
||||
reveal_type(int_or_str_or_bytes4) # revealed: int | str | bytes
|
||||
reveal_type(int_or_str_or_bytes5) # revealed: int | str | bytes
|
||||
reveal_type(int_or_str_or_bytes6) # revealed: int | str | bytes
|
||||
reveal_type(bytes_or_int_or_str) # revealed: bytes | int | str
|
||||
reveal_type(int_or_none) # revealed: int | None
|
||||
reveal_type(none_or_int) # revealed: None | int
|
||||
@@ -505,13 +513,90 @@ def _(
|
||||
|
||||
## `Tuple`
|
||||
|
||||
We support implicit type aliases using `typing.Tuple`:
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
|
||||
IntAndStr = Tuple[int, str]
|
||||
SingleInt = Tuple[int]
|
||||
Ints = Tuple[int, ...]
|
||||
EmptyTuple = Tuple[()]
|
||||
|
||||
def _(int_and_str: IntAndStr):
|
||||
def _(int_and_str: IntAndStr, single_int: SingleInt, ints: Ints, empty_tuple: EmptyTuple):
|
||||
reveal_type(int_and_str) # revealed: tuple[int, str]
|
||||
reveal_type(single_int) # revealed: tuple[int]
|
||||
reveal_type(ints) # revealed: tuple[int, ...]
|
||||
reveal_type(empty_tuple) # revealed: tuple[()]
|
||||
```
|
||||
|
||||
Invalid uses cause diagnostics:
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
|
||||
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||
Invalid = Tuple[int, 1]
|
||||
|
||||
def _(invalid: Invalid):
|
||||
reveal_type(invalid) # revealed: tuple[int, Unknown]
|
||||
```
|
||||
|
||||
## `Union`
|
||||
|
||||
We support implicit type aliases using `typing.Union`:
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
IntOrStr = Union[int, str]
|
||||
IntOrStrOrBytes = Union[int, Union[str, bytes]]
|
||||
|
||||
reveal_type(IntOrStr) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes) # revealed: types.UnionType
|
||||
|
||||
def _(
|
||||
int_or_str: IntOrStr,
|
||||
int_or_str_or_bytes: IntOrStrOrBytes,
|
||||
):
|
||||
reveal_type(int_or_str) # revealed: int | str
|
||||
reveal_type(int_or_str_or_bytes) # revealed: int | str | bytes
|
||||
```
|
||||
|
||||
If a single type is given, no `types.UnionType` instance is created:
|
||||
|
||||
```py
|
||||
JustInt = Union[int]
|
||||
|
||||
reveal_type(JustInt) # revealed: <class 'int'>
|
||||
|
||||
def _(just_int: JustInt):
|
||||
reveal_type(just_int) # revealed: int
|
||||
```
|
||||
|
||||
An empty `typing.Union` leads to a `TypeError` at runtime, so we emit an error. We still infer
|
||||
`Never` when used as a type expression, which seems reasonable for an empty union:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "`typing.Union` requires at least one type argument"
|
||||
EmptyUnion = Union[()]
|
||||
|
||||
reveal_type(EmptyUnion) # revealed: types.UnionType
|
||||
|
||||
def _(empty: EmptyUnion):
|
||||
reveal_type(empty) # revealed: Never
|
||||
```
|
||||
|
||||
Other invalid uses are also caught:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||
Invalid = Union[str, 1]
|
||||
|
||||
def _(
|
||||
invalid: Invalid,
|
||||
):
|
||||
reveal_type(invalid) # revealed: str | Unknown
|
||||
```
|
||||
|
||||
## Stringified annotations?
|
||||
@@ -544,10 +629,19 @@ We *do* support stringified annotations if they appear in a position where a typ
|
||||
syntactically expected:
|
||||
|
||||
```py
|
||||
ListOfInts = list["int"]
|
||||
from typing import Union
|
||||
|
||||
def _(list_of_ints: ListOfInts):
|
||||
ListOfInts = list["int"]
|
||||
StrOrStyle = Union[str, "Style"]
|
||||
|
||||
class Style: ...
|
||||
|
||||
def _(
|
||||
list_of_ints: ListOfInts,
|
||||
str_or_style: StrOrStyle,
|
||||
):
|
||||
reveal_type(list_of_ints) # revealed: list[int]
|
||||
reveal_type(str_or_style) # revealed: str | Style
|
||||
```
|
||||
|
||||
## Recursive
|
||||
|
||||
@@ -104,3 +104,27 @@ from typing import Callable
|
||||
def _(c: Callable[]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### `typing.Tuple`
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
|
||||
# error: [invalid-syntax] "Expected index or slice expression"
|
||||
InvalidEmptyTuple = Tuple[]
|
||||
|
||||
def _(t: InvalidEmptyTuple):
|
||||
reveal_type(t) # revealed: tuple[Unknown]
|
||||
```
|
||||
|
||||
### `typing.Union`
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
# error: [invalid-syntax] "Expected index or slice expression"
|
||||
InvalidEmptyUnion = Union[]
|
||||
|
||||
def _(u: InvalidEmptyUnion):
|
||||
reveal_type(u) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -147,6 +147,25 @@ def _(x: int | str | bytes):
|
||||
reveal_type(x) # revealed: (int & Unknown) | (str & Unknown) | (bytes & Unknown)
|
||||
```
|
||||
|
||||
## `classinfo` is a `typing.py` special form
|
||||
|
||||
Certain special forms in `typing.py` are aliases to classes elsewhere in the standard library; these
|
||||
can be used in `isinstance()` and `issubclass()` checks. We support narrowing using them:
|
||||
|
||||
```py
|
||||
import typing as t
|
||||
|
||||
def f(x: dict[str, int] | list[str], y: object):
|
||||
if isinstance(x, t.Dict):
|
||||
reveal_type(x) # revealed: dict[str, int]
|
||||
else:
|
||||
reveal_type(x) # revealed: list[str]
|
||||
|
||||
if isinstance(y, t.Callable):
|
||||
# TODO: a better top-materialization for `Callable`s (https://github.com/astral-sh/ty/issues/1426)
|
||||
reveal_type(y) # revealed: () -> object
|
||||
```
|
||||
|
||||
## Class types
|
||||
|
||||
```py
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Invalid key type
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | config: dict[str, int] = {}
|
||||
2 | config[0] = 3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, int].__setitem__(key: str, value: int, /) -> None` cannot be called with a key of type `Literal[0]` and a value of type `Literal[3]` on object of type `dict[str, int]`
|
||||
--> src/mdtest_snippet.py:2:1
|
||||
|
|
||||
1 | config: dict[str, int] = {}
|
||||
2 | config[0] = 3 # error: [invalid-assignment]
|
||||
| ^^^^^^
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Invalid key type for `TypedDict`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import TypedDict
|
||||
2 |
|
||||
3 | class Config(TypedDict):
|
||||
4 | retries: int
|
||||
5 |
|
||||
6 | def _(config: Config) -> None:
|
||||
7 | config[0] = 3 # error: [invalid-key]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Cannot access `Config` with a key of type `Literal[0]`. Only string literals are allowed as keys on TypedDicts.
|
||||
--> src/mdtest_snippet.py:7:12
|
||||
|
|
||||
6 | def _(config: Config) -> None:
|
||||
7 | config[0] = 3 # error: [invalid-key]
|
||||
| ^
|
||||
|
|
||||
info: rule `invalid-key` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Invalid value type
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | config: dict[str, int] = {}
|
||||
2 | config["retries"] = "three" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, int].__setitem__(key: str, value: int, /) -> None` cannot be called with a key of type `Literal["retries"]` and a value of type `Literal["three"]` on object of type `dict[str, int]`
|
||||
--> src/mdtest_snippet.py:2:1
|
||||
|
|
||||
1 | config: dict[str, int] = {}
|
||||
2 | config["retries"] = "three" # error: [invalid-assignment]
|
||||
| ^^^^^^
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Invalid value type for `TypedDict`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import TypedDict
|
||||
2 |
|
||||
3 | class Config(TypedDict):
|
||||
4 | retries: int
|
||||
5 |
|
||||
6 | def _(config: Config) -> None:
|
||||
7 | config["retries"] = "three" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Invalid assignment to key "retries" with declared type `int` on TypedDict `Config`
|
||||
--> src/mdtest_snippet.py:7:5
|
||||
|
|
||||
6 | def _(config: Config) -> None:
|
||||
7 | config["retries"] = "three" # error: [invalid-assignment]
|
||||
| ------ --------- ^^^^^^^ value of type `Literal["three"]`
|
||||
| | |
|
||||
| | key has declared type `int`
|
||||
| TypedDict `Config`
|
||||
|
|
||||
info: Item declaration
|
||||
--> src/mdtest_snippet.py:4:5
|
||||
|
|
||||
3 | class Config(TypedDict):
|
||||
4 | retries: int
|
||||
| ------------ Item declared here
|
||||
5 |
|
||||
6 | def _(config: Config) -> None:
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Misspelled key for `TypedDict`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import TypedDict
|
||||
2 |
|
||||
3 | class Config(TypedDict):
|
||||
4 | retries: int
|
||||
5 |
|
||||
6 | def _(config: Config) -> None:
|
||||
7 | config["Retries"] = 30.0 # error: [invalid-key]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Config`
|
||||
--> src/mdtest_snippet.py:7:5
|
||||
|
|
||||
6 | def _(config: Config) -> None:
|
||||
7 | config["Retries"] = 30.0 # error: [invalid-key]
|
||||
| ------ ^^^^^^^^^ Unknown key "Retries" - did you mean "retries"?
|
||||
| |
|
||||
| TypedDict `Config`
|
||||
|
|
||||
info: rule `invalid-key` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - No `__setitem__` method
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class ReadOnlyDict:
|
||||
2 | def __getitem__(self, key: str) -> int:
|
||||
3 | return 42
|
||||
4 |
|
||||
5 | config = ReadOnlyDict()
|
||||
6 | config["retries"] = 3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Cannot assign to object of type `ReadOnlyDict` with no `__setitem__` method
|
||||
--> src/mdtest_snippet.py:6:1
|
||||
|
|
||||
5 | config = ReadOnlyDict()
|
||||
6 | config["retries"] = 3 # error: [invalid-assignment]
|
||||
| ^^^^^^
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Possibly missing `__setitem__` method
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def _(config: dict[str, int] | None) -> None:
|
||||
2 | config["retries"] = 3 # error: [possibly-missing-implicit-call]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
warning[possibly-missing-implicit-call]: Method `__setitem__` of type `dict[str, int] | None` may be missing
|
||||
--> src/mdtest_snippet.py:2:5
|
||||
|
|
||||
1 | def _(config: dict[str, int] | None) -> None:
|
||||
2 | config["retries"] = 3 # error: [possibly-missing-implicit-call]
|
||||
| ^^^^^^
|
||||
|
|
||||
info: rule `possibly-missing-implicit-call` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for all elemens of a union
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import TypedDict
|
||||
2 |
|
||||
3 | class Person(TypedDict):
|
||||
4 | name: str
|
||||
5 |
|
||||
6 | class Animal(TypedDict):
|
||||
7 | name: str
|
||||
8 | legs: int
|
||||
9 |
|
||||
10 | def _(being: Person | Animal) -> None:
|
||||
11 | being["surname"] = "unknown" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Method `__setitem__` of type `(key: Literal["name"], value: str, /) -> None` cannot be called with a key of type `Literal["surname"]` and a value of type `Literal["unknown"]` on object of type `Person | Animal`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | def _(being: Person | Animal) -> None:
|
||||
11 | being["surname"] = "unknown" # error: [invalid-assignment]
|
||||
| ^^^^^
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for one element of a union
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import TypedDict
|
||||
2 |
|
||||
3 | class Person(TypedDict):
|
||||
4 | name: str
|
||||
5 |
|
||||
6 | class Animal(TypedDict):
|
||||
7 | name: str
|
||||
8 | legs: int
|
||||
9 |
|
||||
10 | def _(being: Person | Animal) -> None:
|
||||
11 | being["legs"] = 4 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Method `__setitem__` of type `(key: Literal["name"], value: str, /) -> None` cannot be called with a key of type `Literal["legs"]` and a value of type `Literal[4]` on object of type `Person | Animal`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | def _(being: Person | Animal) -> None:
|
||||
11 | being["legs"] = 4 # error: [invalid-assignment]
|
||||
| ^^^^^
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Wrong value type for one element of a union
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def _(config: dict[str, int] | dict[str, str]) -> None:
|
||||
2 | config["retries"] = 3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Method `__setitem__` of type `(bound method dict[str, int].__setitem__(key: str, value: int, /) -> None) | (bound method dict[str, str].__setitem__(key: str, value: str, /) -> None)` cannot be called with a key of type `Literal["retries"]` and a value of type `Literal[3]` on object of type `dict[str, int] | dict[str, str]`
|
||||
--> src/mdtest_snippet.py:2:5
|
||||
|
|
||||
1 | def _(config: dict[str, int] | dict[str, str]) -> None:
|
||||
2 | config["retries"] = 3 # error: [invalid-assignment]
|
||||
| ^^^^^^
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Wrong value type for all elements of a union
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def _(config: dict[str, int] | dict[str, str]) -> None:
|
||||
2 | config["retries"] = 3.0 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Method `__setitem__` of type `(bound method dict[str, int].__setitem__(key: str, value: int, /) -> None) | (bound method dict[str, str].__setitem__(key: str, value: str, /) -> None)` cannot be called with a key of type `Literal["retries"]` and a value of type `float` on object of type `dict[str, int] | dict[str, str]`
|
||||
--> src/mdtest_snippet.py:2:5
|
||||
|
|
||||
1 | def _(config: dict[str, int] | dict[str, str]) -> None:
|
||||
2 | config["retries"] = 3.0 # error: [invalid-assignment]
|
||||
| ^^^^^^
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,117 @@
|
||||
# Subscript assignment diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Invalid value type
|
||||
|
||||
```py
|
||||
config: dict[str, int] = {}
|
||||
config["retries"] = "three" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Invalid key type
|
||||
|
||||
```py
|
||||
config: dict[str, int] = {}
|
||||
config[0] = 3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Invalid value type for `TypedDict`
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Config(TypedDict):
|
||||
retries: int
|
||||
|
||||
def _(config: Config) -> None:
|
||||
config["retries"] = "three" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Invalid key type for `TypedDict`
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Config(TypedDict):
|
||||
retries: int
|
||||
|
||||
def _(config: Config) -> None:
|
||||
config[0] = 3 # error: [invalid-key]
|
||||
```
|
||||
|
||||
## Misspelled key for `TypedDict`
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Config(TypedDict):
|
||||
retries: int
|
||||
|
||||
def _(config: Config) -> None:
|
||||
config["Retries"] = 30.0 # error: [invalid-key]
|
||||
```
|
||||
|
||||
## No `__setitem__` method
|
||||
|
||||
```py
|
||||
class ReadOnlyDict:
|
||||
def __getitem__(self, key: str) -> int:
|
||||
return 42
|
||||
|
||||
config = ReadOnlyDict()
|
||||
config["retries"] = 3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Possibly missing `__setitem__` method
|
||||
|
||||
```py
|
||||
def _(config: dict[str, int] | None) -> None:
|
||||
config["retries"] = 3 # error: [possibly-missing-implicit-call]
|
||||
```
|
||||
|
||||
## Unknown key for one element of a union
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
|
||||
class Animal(TypedDict):
|
||||
name: str
|
||||
legs: int
|
||||
|
||||
def _(being: Person | Animal) -> None:
|
||||
being["legs"] = 4 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Unknown key for all elemens of a union
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
|
||||
class Animal(TypedDict):
|
||||
name: str
|
||||
legs: int
|
||||
|
||||
def _(being: Person | Animal) -> None:
|
||||
being["surname"] = "unknown" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Wrong value type for one element of a union
|
||||
|
||||
```py
|
||||
def _(config: dict[str, int] | dict[str, str]) -> None:
|
||||
config["retries"] = 3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Wrong value type for all elements of a union
|
||||
|
||||
```py
|
||||
def _(config: dict[str, int] | dict[str, str]) -> None:
|
||||
config["retries"] = 3.0 # error: [invalid-assignment]
|
||||
```
|
||||
@@ -88,8 +88,6 @@ class C:
|
||||
self.FINAL_C: Final[int] = 1
|
||||
self.FINAL_D: Final = 1
|
||||
self.FINAL_E: Final
|
||||
# TODO: Should not be an error
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `FINAL_E` on type `Self@__init__`"
|
||||
self.FINAL_E = 1
|
||||
|
||||
reveal_type(C.FINAL_A) # revealed: int
|
||||
@@ -186,7 +184,6 @@ class C(metaclass=Meta):
|
||||
self.INSTANCE_FINAL_A: Final[int] = 1
|
||||
self.INSTANCE_FINAL_B: Final = 1
|
||||
self.INSTANCE_FINAL_C: Final[int]
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_C` on type `Self@__init__`"
|
||||
self.INSTANCE_FINAL_C = 1
|
||||
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type `<class 'C'>`"
|
||||
@@ -282,8 +279,6 @@ class C:
|
||||
def __init__(self):
|
||||
self.LEGAL_H: Final[int] = 1
|
||||
self.LEGAL_I: Final[int]
|
||||
# TODO: Should not be an error
|
||||
# error: [invalid-assignment]
|
||||
self.LEGAL_I = 1
|
||||
|
||||
# error: [invalid-type-form] "`Final` is not allowed in function parameter annotations"
|
||||
@@ -392,15 +387,142 @@ class C:
|
||||
# TODO: This should be an error
|
||||
NO_ASSIGNMENT_B: Final[int]
|
||||
|
||||
# This is okay. `DEFINED_IN_INIT` is defined in `__init__`.
|
||||
DEFINED_IN_INIT: Final[int]
|
||||
|
||||
def __init__(self):
|
||||
# TODO: should not be an error
|
||||
# error: [invalid-assignment]
|
||||
self.DEFINED_IN_INIT = 1
|
||||
```
|
||||
|
||||
## Final attributes with Self annotation in `__init__`
|
||||
|
||||
Issue #1409: Final instance attributes should be assignable in `__init__` even when using `Self`
|
||||
type annotation.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Final, Self
|
||||
|
||||
class ClassA:
|
||||
ID4: Final[int] # OK because initialized in __init__
|
||||
|
||||
def __init__(self: Self):
|
||||
self.ID4 = 1 # Should be OK
|
||||
|
||||
def other_method(self: Self):
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `ID4` on type `Self@other_method`"
|
||||
self.ID4 = 2 # Should still error outside __init__
|
||||
|
||||
class ClassB:
|
||||
ID5: Final[int]
|
||||
|
||||
def __init__(self): # Without Self annotation
|
||||
self.ID5 = 1 # Should also be OK
|
||||
|
||||
reveal_type(ClassA().ID4) # revealed: int
|
||||
reveal_type(ClassB().ID5) # revealed: int
|
||||
```
|
||||
|
||||
## Reassignment to Final in `__init__`
|
||||
|
||||
Per PEP 591 and the typing conformance suite, Final attributes can be assigned in `__init__`.
|
||||
Multiple assignments within `__init__` are allowed (matching mypy and pyright behavior). However,
|
||||
assignment in `__init__` is not allowed if the attribute already has a value at class level.
|
||||
|
||||
```py
|
||||
from typing import Final
|
||||
|
||||
# Case 1: Declared in class, assigned once in __init__ - ALLOWED
|
||||
class DeclaredAssignedInInit:
|
||||
attr1: Final[int]
|
||||
|
||||
def __init__(self):
|
||||
self.attr1 = 1 # OK: First and only assignment
|
||||
|
||||
# Case 2: Declared and assigned in class body - ALLOWED (no __init__ assignment)
|
||||
class DeclaredAndAssignedInClass:
|
||||
attr2: Final[int] = 10
|
||||
|
||||
# Case 3: Reassignment when already assigned in class body
|
||||
class ReassignmentFromClass:
|
||||
attr3: Final[int] = 10
|
||||
|
||||
def __init__(self):
|
||||
# error: [invalid-assignment]
|
||||
self.attr3 = 20 # Error: already assigned in class body
|
||||
|
||||
# Case 4: Multiple assignments within __init__ itself
|
||||
# Per conformance suite and PEP 591, all assignments in __init__ are allowed
|
||||
class MultipleAssignmentsInInit:
|
||||
attr4: Final[int]
|
||||
|
||||
def __init__(self):
|
||||
self.attr4 = 1 # OK: Assignment in __init__
|
||||
self.attr4 = 2 # OK: Multiple assignments in __init__ are allowed
|
||||
|
||||
class ConditionalAssignment:
|
||||
X: Final[int]
|
||||
|
||||
def __init__(self, cond: bool):
|
||||
if cond:
|
||||
self.X = 42 # OK: Assignment in __init__
|
||||
else:
|
||||
self.X = 56 # OK: Multiple assignments in __init__ are allowed
|
||||
|
||||
# Case 5: Declaration and assignment in __init__ - ALLOWED
|
||||
class DeclareAndAssignInInit:
|
||||
def __init__(self):
|
||||
self.attr5: Final[int] = 1 # OK: Declare and assign in __init__
|
||||
|
||||
# Case 6: Assignment outside __init__ should still fail
|
||||
class AssignmentOutsideInit:
|
||||
attr6: Final[int]
|
||||
|
||||
def other_method(self):
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `attr6`"
|
||||
self.attr6 = 1 # Error: Not in __init__
|
||||
```
|
||||
|
||||
## Final assignment restrictions in `__init__`
|
||||
|
||||
`__init__` can only assign Final attributes on the class it's defining, and only to the first
|
||||
parameter (`self`).
|
||||
|
||||
```py
|
||||
from typing import Final
|
||||
|
||||
class C:
|
||||
x: Final[int] = 100
|
||||
|
||||
# Assignment from standalone function (even named __init__)
|
||||
def _(c: C):
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `x`"
|
||||
c.x = 1 # Error: Not in C.__init__
|
||||
|
||||
def __init__(c: C):
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `x`"
|
||||
c.x = 1 # Error: Not a method
|
||||
|
||||
# Assignment from another class's __init__
|
||||
class A:
|
||||
def __init__(self, c: C):
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `x`"
|
||||
c.x = 1 # Error: Not C's __init__
|
||||
|
||||
# Assignment to non-self parameter in __init__
|
||||
class D:
|
||||
y: Final[int]
|
||||
|
||||
def __init__(self, other: "D"):
|
||||
self.y = 1 # OK: Assigning to self
|
||||
# TODO: Should error - assigning to non-self parameter
|
||||
# Requires tracking which parameter the base expression refers to
|
||||
other.y = 2
|
||||
```
|
||||
|
||||
## Full diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
@@ -526,10 +526,20 @@ class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
class Animal(TypedDict):
|
||||
name: str
|
||||
|
||||
NAME_FINAL: Final = "name"
|
||||
AGE_FINAL: Final[Literal["age"]] = "age"
|
||||
|
||||
def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age", "name"], str_key: str, unknown_key: Any) -> None:
|
||||
def _(
|
||||
person: Person,
|
||||
being: Person | Animal,
|
||||
literal_key: Literal["age"],
|
||||
union_of_keys: Literal["age", "name"],
|
||||
str_key: str,
|
||||
unknown_key: Any,
|
||||
) -> None:
|
||||
reveal_type(person["name"]) # revealed: str
|
||||
reveal_type(person["age"]) # revealed: int | None
|
||||
|
||||
@@ -548,18 +558,30 @@ def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age",
|
||||
|
||||
# No error here:
|
||||
reveal_type(person[unknown_key]) # revealed: Unknown
|
||||
|
||||
reveal_type(being["name"]) # revealed: str
|
||||
|
||||
# TODO: A type of `int | None | Unknown` might be better here. The `str` is mixed in
|
||||
# because `Animal.__getitem__` can only return `str`.
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Animal`"
|
||||
reveal_type(being["age"]) # revealed: int | None | str
|
||||
```
|
||||
|
||||
### Writing
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, Final, Literal, LiteralString, Any
|
||||
from ty_extensions import Intersection
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
surname: str
|
||||
age: int | None
|
||||
|
||||
class Animal(TypedDict):
|
||||
name: str
|
||||
legs: int
|
||||
|
||||
NAME_FINAL: Final = "name"
|
||||
AGE_FINAL: Final[Literal["age"]] = "age"
|
||||
|
||||
@@ -583,6 +605,23 @@ def _(person: Person, union_of_keys: Literal["name", "surname"]):
|
||||
# error: [invalid-assignment] "Cannot assign value of type `Literal[1]` to key of type `Literal["name", "surname"]` on TypedDict `Person`"
|
||||
person[union_of_keys] = 1
|
||||
|
||||
def _(being: Person | Animal):
|
||||
being["name"] = "Being"
|
||||
|
||||
# error: [invalid-assignment] "Method `__setitem__` of type `(Overload[(key: Literal["name"], value: str, /) -> None, (key: Literal["surname"], value: str, /) -> None, (key: Literal["age"], value: int | None, /) -> None]) | (Overload[(key: Literal["name"], value: str, /) -> None, (key: Literal["legs"], value: int, /) -> None])` cannot be called with a key of type `Literal["name"]` and a value of type `Literal[1]` on object of type `Person | Animal`"
|
||||
being["name"] = 1
|
||||
|
||||
# error: [invalid-assignment] "Method `__setitem__` of type `(Overload[(key: Literal["name"], value: str, /) -> None, (key: Literal["surname"], value: str, /) -> None, (key: Literal["age"], value: int | None, /) -> None]) | (Overload[(key: Literal["name"], value: str, /) -> None, (key: Literal["legs"], value: int, /) -> None])` cannot be called with a key of type `Literal["surname"]` and a value of type `Literal["unknown"]` on object of type `Person | Animal`"
|
||||
being["surname"] = "unknown"
|
||||
|
||||
def _(centaur: Intersection[Person, Animal]):
|
||||
centaur["name"] = "Chiron"
|
||||
centaur["age"] = 100
|
||||
centaur["legs"] = 4
|
||||
|
||||
# TODO: This should be an `invalid-key` error
|
||||
centaur["unknown"] = "value"
|
||||
|
||||
def _(person: Person, union_of_keys: Literal["name", "age"], unknown_value: Any):
|
||||
person[union_of_keys] = unknown_value
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ where
|
||||
///
|
||||
/// This method may panic or produce unspecified results if the provided module is from a
|
||||
/// different file or Salsa revision than the module to which the node belongs.
|
||||
#[track_caller]
|
||||
pub fn node<'ast>(&self, module_ref: &'ast ParsedModuleRef) -> &'ast T {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(module_ref.module().addr(), self.module_addr);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::iter::FusedIterator;
|
||||
use std::iter::{FusedIterator, once};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_db::files::File;
|
||||
@@ -148,29 +148,56 @@ pub(crate) fn attribute_declarations<'db, 's>(
|
||||
///
|
||||
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
|
||||
/// introduces a direct dependency on that file's AST.
|
||||
pub(crate) fn attribute_scopes<'db, 's>(
|
||||
pub(crate) fn attribute_scopes<'db>(
|
||||
db: &'db dyn Db,
|
||||
class_body_scope: ScopeId<'db>,
|
||||
) -> impl Iterator<Item = FileScopeId> + use<'s, 'db> {
|
||||
) -> impl Iterator<Item = FileScopeId> + 'db {
|
||||
let file = class_body_scope.file(db);
|
||||
let index = semantic_index(db, file);
|
||||
let class_scope_id = class_body_scope.file_scope_id(db);
|
||||
ChildrenIter::new(&index.scopes, class_scope_id)
|
||||
.filter_map(move |(child_scope_id, scope)| {
|
||||
let (function_scope_id, function_scope) =
|
||||
if scope.node().scope_kind() == ScopeKind::TypeParams {
|
||||
// This could be a generic method with a type-params scope.
|
||||
// Go one level deeper to find the function scope. The first
|
||||
// descendant is the (potential) function scope.
|
||||
let function_scope_id = scope.descendants().start;
|
||||
(function_scope_id, index.scope(function_scope_id))
|
||||
} else {
|
||||
(child_scope_id, scope)
|
||||
};
|
||||
function_scope.node().as_function()?;
|
||||
Some(function_scope_id)
|
||||
})
|
||||
.flat_map(move |func_id| {
|
||||
// Add any descendent scope that is eager and have eager scopes between the scope
|
||||
// and the method scope. Since attributes can be defined in this scope.
|
||||
let nested = index.descendent_scopes(func_id).filter_map(move |(id, s)| {
|
||||
let is_eager = s.kind().is_eager();
|
||||
let parents_are_eager = {
|
||||
let mut all_parents_eager = true;
|
||||
let mut current = Some(id);
|
||||
|
||||
ChildrenIter::new(&index.scopes, class_scope_id).filter_map(move |(child_scope_id, scope)| {
|
||||
let (function_scope_id, function_scope) =
|
||||
if scope.node().scope_kind() == ScopeKind::TypeParams {
|
||||
// This could be a generic method with a type-params scope.
|
||||
// Go one level deeper to find the function scope. The first
|
||||
// descendant is the (potential) function scope.
|
||||
let function_scope_id = scope.descendants().start;
|
||||
(function_scope_id, index.scope(function_scope_id))
|
||||
} else {
|
||||
(child_scope_id, scope)
|
||||
};
|
||||
while let Some(scope_id) = current {
|
||||
if scope_id == func_id {
|
||||
break;
|
||||
}
|
||||
let scope = index.scope(scope_id);
|
||||
if !scope.is_eager() {
|
||||
all_parents_eager = false;
|
||||
break;
|
||||
}
|
||||
current = scope.parent();
|
||||
}
|
||||
|
||||
function_scope.node().as_function()?;
|
||||
Some(function_scope_id)
|
||||
})
|
||||
all_parents_eager
|
||||
};
|
||||
|
||||
(parents_are_eager && is_eager).then_some(id)
|
||||
});
|
||||
once(func_id).chain(nested)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the module global scope of `file`.
|
||||
|
||||
@@ -186,29 +186,34 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
self.current_scope_info().file_scope_id
|
||||
}
|
||||
|
||||
/// Returns the scope ID of the surrounding class body scope if the current scope
|
||||
/// is a method inside a class body. Returns `None` otherwise, e.g. if the current
|
||||
/// scope is a function body outside of a class, or if the current scope is not a
|
||||
/// Returns the scope ID of the current scope if the current scope
|
||||
/// is a method inside a class body or an eagerly executed scope inside a method.
|
||||
/// Returns `None` otherwise, e.g. if the current scope is a function body outside of a class, or if the current scope is not a
|
||||
/// function body.
|
||||
fn is_method_of_class(&self) -> Option<FileScopeId> {
|
||||
let mut scopes_rev = self.scope_stack.iter().rev();
|
||||
fn is_method_or_eagerly_executed_in_method(&self) -> Option<FileScopeId> {
|
||||
let mut scopes_rev = self
|
||||
.scope_stack
|
||||
.iter()
|
||||
.rev()
|
||||
.skip_while(|scope| self.scopes[scope.file_scope_id].is_eager());
|
||||
let current = scopes_rev.next()?;
|
||||
|
||||
if self.scopes[current.file_scope_id].kind() != ScopeKind::Function {
|
||||
return None;
|
||||
}
|
||||
|
||||
let maybe_method = current.file_scope_id;
|
||||
let parent = scopes_rev.next()?;
|
||||
|
||||
match self.scopes[parent.file_scope_id].kind() {
|
||||
ScopeKind::Class => Some(parent.file_scope_id),
|
||||
ScopeKind::Class => Some(maybe_method),
|
||||
ScopeKind::TypeParams => {
|
||||
// If the function is generic, the parent scope is an annotation scope.
|
||||
// In this case, we need to go up one level higher to find the class scope.
|
||||
let grandparent = scopes_rev.next()?;
|
||||
|
||||
if self.scopes[grandparent.file_scope_id].kind() == ScopeKind::Class {
|
||||
Some(grandparent.file_scope_id)
|
||||
Some(maybe_method)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -217,6 +222,32 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a symbol name is bound in any intermediate eager scopes
|
||||
/// between the current scope and the specified method scope.
|
||||
///
|
||||
fn is_symbol_bound_in_intermediate_eager_scopes(
|
||||
&self,
|
||||
symbol_name: &str,
|
||||
method_scope_id: FileScopeId,
|
||||
) -> bool {
|
||||
for scope_info in self.scope_stack.iter().rev() {
|
||||
let scope_id = scope_info.file_scope_id;
|
||||
|
||||
if scope_id == method_scope_id {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(symbol_id) = self.place_tables[scope_id].symbol_id(symbol_name) {
|
||||
let symbol = self.place_tables[scope_id].symbol(symbol_id);
|
||||
if symbol.is_bound() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Push a new loop, returning the outer loop, if any.
|
||||
fn push_loop(&mut self) -> Option<Loop> {
|
||||
self.current_scope_info_mut()
|
||||
@@ -1700,7 +1731,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
self.visit_expr(&node.annotation);
|
||||
if let Some(value) = &node.value {
|
||||
self.visit_expr(value);
|
||||
if self.is_method_of_class().is_some() {
|
||||
if self.is_method_or_eagerly_executed_in_method().is_some() {
|
||||
// Record the right-hand side of the assignment as a standalone expression
|
||||
// if we're inside a method. This allows type inference to infer the type
|
||||
// of the value for annotated assignments like `self.CONSTANT: Final = 1`,
|
||||
@@ -2372,14 +2403,21 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
| ast::Expr::Attribute(ast::ExprAttribute { ctx, .. })
|
||||
| ast::Expr::Subscript(ast::ExprSubscript { ctx, .. }) => {
|
||||
if let Some(mut place_expr) = PlaceExpr::try_from_expr(expr) {
|
||||
if self.is_method_of_class().is_some() {
|
||||
if let Some(method_scope_id) = self.is_method_or_eagerly_executed_in_method() {
|
||||
if let PlaceExpr::Member(member) = &mut place_expr {
|
||||
if member.is_instance_attribute_candidate() {
|
||||
// We specifically mark attribute assignments to the first parameter of a method,
|
||||
// i.e. typically `self` or `cls`.
|
||||
let accessed_object_refers_to_first_parameter = self
|
||||
.current_first_parameter_name
|
||||
.is_some_and(|first| member.symbol_name() == first);
|
||||
// However, we must check that the symbol hasn't been shadowed by an intermediate
|
||||
// scope (e.g., a comprehension variable: `for self in [...]`).
|
||||
let accessed_object_refers_to_first_parameter =
|
||||
self.current_first_parameter_name.is_some_and(|first| {
|
||||
member.symbol_name() == first
|
||||
&& !self.is_symbol_bound_in_intermediate_eager_scopes(
|
||||
first,
|
||||
method_scope_id,
|
||||
)
|
||||
});
|
||||
|
||||
if accessed_object_refers_to_first_parameter {
|
||||
member.mark_instance_attribute();
|
||||
|
||||
@@ -1034,7 +1034,7 @@ impl<'db> AssignmentDefinitionKind<'db> {
|
||||
self.target_kind
|
||||
}
|
||||
|
||||
pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr {
|
||||
pub fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr {
|
||||
self.value.node(module)
|
||||
}
|
||||
|
||||
|
||||
@@ -761,6 +761,7 @@ pub(crate) struct DeclarationsIterator<'map, 'db> {
|
||||
inner: LiveDeclarationsIterator<'map>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DeclarationWithConstraint<'db> {
|
||||
pub(crate) declaration: DefinitionState<'db>,
|
||||
pub(crate) reachability_constraint: ScopedReachabilityConstraintId,
|
||||
|
||||
@@ -869,7 +869,7 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
|
||||
}
|
||||
|
||||
pub(crate) const fn is_generic_alias(&self) -> bool {
|
||||
pub const fn is_generic_alias(&self) -> bool {
|
||||
matches!(self, Type::GenericAlias(_))
|
||||
}
|
||||
|
||||
@@ -1080,12 +1080,11 @@ impl<'db> Type<'db> {
|
||||
.expect("Expected a Type::ClassLiteral variant")
|
||||
}
|
||||
|
||||
pub(crate) const fn is_subclass_of(&self) -> bool {
|
||||
pub const fn is_subclass_of(&self) -> bool {
|
||||
matches!(self, Type::SubclassOf(..))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const fn is_class_literal(&self) -> bool {
|
||||
pub const fn is_class_literal(&self) -> bool {
|
||||
matches!(self, Type::ClassLiteral(..))
|
||||
}
|
||||
|
||||
@@ -1171,7 +1170,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
pub(crate) const fn expect_union(self) -> UnionType<'db> {
|
||||
self.as_union().expect("Expected a Type::Union variant")
|
||||
@@ -6587,12 +6585,13 @@ impl<'db> Type<'db> {
|
||||
}),
|
||||
KnownInstanceType::UnionType(list) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
let inferred_as = list.inferred_as(db);
|
||||
for element in list.elements(db) {
|
||||
builder = builder.add(element.in_type_expression(
|
||||
db,
|
||||
scope_id,
|
||||
typevar_binding_context,
|
||||
)?);
|
||||
builder = builder.add(if inferred_as.type_expression() {
|
||||
*element
|
||||
} else {
|
||||
element.in_type_expression(db, scope_id, typevar_binding_context)?
|
||||
});
|
||||
}
|
||||
Ok(builder.build())
|
||||
}
|
||||
@@ -8585,7 +8584,7 @@ impl<'db> TypeVarInstance<'db> {
|
||||
self.identity(db).definition(db)
|
||||
}
|
||||
|
||||
pub(crate) fn kind(self, db: &'db dyn Db) -> TypeVarKind {
|
||||
pub fn kind(self, db: &'db dyn Db) -> TypeVarKind {
|
||||
self.identity(db).kind(db)
|
||||
}
|
||||
|
||||
@@ -9165,6 +9164,21 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a given type originates from value expression inference or type expression inference.
|
||||
/// For example, the symbol `int` would be inferred as `<class 'int'>` in value expression context,
|
||||
/// and as `int` (i.e. an instance of the class `int`) in type expression context.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)]
|
||||
pub enum InferredAs {
|
||||
ValueExpression,
|
||||
TypeExpression,
|
||||
}
|
||||
|
||||
impl InferredAs {
|
||||
pub const fn type_expression(self) -> bool {
|
||||
matches!(self, InferredAs::TypeExpression)
|
||||
}
|
||||
}
|
||||
|
||||
/// A salsa-interned list of types.
|
||||
///
|
||||
/// # Ordering
|
||||
@@ -9175,6 +9189,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
|
||||
pub struct InternedTypes<'db> {
|
||||
#[returns(deref)]
|
||||
elements: Box<[Type<'db>]>,
|
||||
inferred_as: InferredAs,
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for InternedTypes<'_> {}
|
||||
@@ -9183,8 +9198,9 @@ impl<'db> InternedTypes<'db> {
|
||||
pub(crate) fn from_elements(
|
||||
db: &'db dyn Db,
|
||||
elements: impl IntoIterator<Item = Type<'db>>,
|
||||
inferred_as: InferredAs,
|
||||
) -> InternedTypes<'db> {
|
||||
InternedTypes::new(db, elements.into_iter().collect::<Box<[_]>>())
|
||||
InternedTypes::new(db, elements.into_iter().collect::<Box<[_]>>(), inferred_as)
|
||||
}
|
||||
|
||||
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
|
||||
@@ -9194,6 +9210,7 @@ impl<'db> InternedTypes<'db> {
|
||||
.iter()
|
||||
.map(|ty| ty.normalized_impl(db, visitor))
|
||||
.collect::<Box<[_]>>(),
|
||||
self.inferred_as(db),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3119,30 +3119,47 @@ impl<'db> ClassLiteral<'db> {
|
||||
union_of_inferred_types = union_of_inferred_types.add(Type::unknown());
|
||||
}
|
||||
|
||||
for (attribute_assignments, method_scope_id) in
|
||||
for (attribute_assignments, attribute_binding_scope_id) in
|
||||
attribute_assignments(db, class_body_scope, &name)
|
||||
{
|
||||
let method_scope = index.scope(method_scope_id);
|
||||
if !is_valid_scope(method_scope) {
|
||||
let binding_scope = index.scope(attribute_binding_scope_id);
|
||||
if !is_valid_scope(binding_scope) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The attribute assignment inherits the reachability of the method which contains it
|
||||
let is_method_reachable = if let Some(method_def) = method_scope.node().as_function() {
|
||||
let method = index.expect_single_definition(method_def);
|
||||
let method_place = class_table
|
||||
.symbol_id(&method_def.node(&module).name)
|
||||
.unwrap();
|
||||
class_map
|
||||
.all_reachable_symbol_bindings(method_place)
|
||||
.find_map(|bind| {
|
||||
(bind.binding.is_defined_and(|def| def == method))
|
||||
.then(|| class_map.binding_reachability(db, &bind))
|
||||
})
|
||||
.unwrap_or(Truthiness::AlwaysFalse)
|
||||
} else {
|
||||
Truthiness::AlwaysFalse
|
||||
let scope_for_reachability_analysis = {
|
||||
if binding_scope.node().as_function().is_some() {
|
||||
binding_scope
|
||||
} else if binding_scope.is_eager() {
|
||||
let mut eager_scope_parent = binding_scope;
|
||||
while eager_scope_parent.is_eager()
|
||||
&& let Some(parent) = eager_scope_parent.parent()
|
||||
{
|
||||
eager_scope_parent = index.scope(parent);
|
||||
}
|
||||
eager_scope_parent
|
||||
} else {
|
||||
binding_scope
|
||||
}
|
||||
};
|
||||
|
||||
// The attribute assignment inherits the reachability of the method which contains it
|
||||
let is_method_reachable =
|
||||
if let Some(method_def) = scope_for_reachability_analysis.node().as_function() {
|
||||
let method = index.expect_single_definition(method_def);
|
||||
let method_place = class_table
|
||||
.symbol_id(&method_def.node(&module).name)
|
||||
.unwrap();
|
||||
class_map
|
||||
.all_reachable_symbol_bindings(method_place)
|
||||
.find_map(|bind| {
|
||||
(bind.binding.is_defined_and(|def| def == method))
|
||||
.then(|| class_map.binding_reachability(db, &bind))
|
||||
})
|
||||
.unwrap_or(Truthiness::AlwaysFalse)
|
||||
} else {
|
||||
Truthiness::AlwaysFalse
|
||||
};
|
||||
if is_method_reachable.is_always_false() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ use crate::semantic_index::scope::ScopeId;
|
||||
use crate::semantic_index::{
|
||||
attribute_scopes, global_scope, place_table, semantic_index, use_def_map,
|
||||
};
|
||||
use crate::types::CallDunderError;
|
||||
use crate::types::call::{CallArguments, MatchedArgument};
|
||||
use crate::types::signatures::Signature;
|
||||
use crate::types::{CallDunderError, UnionType};
|
||||
use crate::types::{
|
||||
ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, TypeContext,
|
||||
TypeVarBoundOrConstraints, class::CodeGeneratorKind,
|
||||
@@ -477,32 +477,17 @@ pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Member<'db>
|
||||
/// Get the primary definition kind for a name expression within a specific file.
|
||||
/// Returns the first definition kind that is reachable for this name in its scope.
|
||||
/// This is useful for IDE features like semantic tokens.
|
||||
pub fn definition_kind_for_name<'db>(
|
||||
pub fn definition_for_name<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
name: &ast::ExprName,
|
||||
) -> Option<DefinitionKind<'db>> {
|
||||
let index = semantic_index(db, file);
|
||||
let name_str = name.id.as_str();
|
||||
|
||||
// Get the scope for this name expression
|
||||
let file_scope = index.expression_scope_id(&ast::ExprRef::from(name));
|
||||
|
||||
// Get the place table for this scope
|
||||
let place_table = index.place_table(file_scope);
|
||||
|
||||
// Look up the place by name
|
||||
let symbol_id = place_table.symbol_id(name_str)?;
|
||||
|
||||
// Get the use-def map and look up definitions for this place
|
||||
let declarations = index
|
||||
.use_def_map(file_scope)
|
||||
.all_reachable_symbol_declarations(symbol_id);
|
||||
) -> Option<Definition<'db>> {
|
||||
let definitions = definitions_for_name(db, file, name);
|
||||
|
||||
// Find the first valid definition and return its kind
|
||||
for declaration in declarations {
|
||||
if let Some(def) = declaration.declaration.definition() {
|
||||
return Some(def.kind(db).clone());
|
||||
for declaration in definitions {
|
||||
if let Some(def) = declaration.definition() {
|
||||
return Some(def);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,8 +602,34 @@ pub fn definitions_for_name<'db>(
|
||||
// If we didn't find any definitions in scopes, fallback to builtins
|
||||
if resolved_definitions.is_empty() {
|
||||
let Some(builtins_scope) = builtins_module_scope(db) else {
|
||||
return Vec::new();
|
||||
return resolved_definitions;
|
||||
};
|
||||
|
||||
// Special cases for `float` and `complex` in type annotation positions.
|
||||
// We don't know whether we're in a type annotation position, so we'll just ask `Name`'s type,
|
||||
// which resolves to `int | float` or `int | float | complex` if `float` or `complex` is used in
|
||||
// a type annotation position and `float` or `complex` otherwise.
|
||||
//
|
||||
// https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
if matches!(name_str, "float" | "complex")
|
||||
&& let Some(union) = name.inferred_type(&SemanticModel::new(db, file)).as_union()
|
||||
&& is_float_or_complex_annotation(db, union, name_str)
|
||||
{
|
||||
return union
|
||||
.elements(db)
|
||||
.iter()
|
||||
// Use `rev` so that `complex` and `float` come first.
|
||||
// This is required for hover to pick up the docstring of `complex` and `float`
|
||||
// instead of `int` (hover only shows the docstring of the first definition).
|
||||
.rev()
|
||||
.filter_map(|ty| ty.as_nominal_instance())
|
||||
.map(|instance| {
|
||||
let definition = instance.class_literal(db).definition(db);
|
||||
ResolvedDefinition::Definition(definition)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
find_symbol_in_scope(db, builtins_scope, name_str)
|
||||
.into_iter()
|
||||
.filter(|def| def.is_reexported(db))
|
||||
@@ -636,6 +647,30 @@ pub fn definitions_for_name<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_float_or_complex_annotation(db: &dyn Db, ty: UnionType, name: &str) -> bool {
|
||||
let float_or_complex_ty = match name {
|
||||
"float" => UnionType::from_elements(
|
||||
db,
|
||||
[
|
||||
KnownClass::Int.to_instance(db),
|
||||
KnownClass::Float.to_instance(db),
|
||||
],
|
||||
),
|
||||
"complex" => UnionType::from_elements(
|
||||
db,
|
||||
[
|
||||
KnownClass::Int.to_instance(db),
|
||||
KnownClass::Float.to_instance(db),
|
||||
KnownClass::Complex.to_instance(db),
|
||||
],
|
||||
),
|
||||
_ => return false,
|
||||
}
|
||||
.expect_union();
|
||||
|
||||
ty == float_or_complex_ty
|
||||
}
|
||||
|
||||
/// Returns all resolved definitions for an attribute expression `x.y`.
|
||||
/// This function duplicates much of the functionality in the semantic
|
||||
/// analyzer, but it has somewhat different behavior so we've decided
|
||||
@@ -1196,6 +1231,14 @@ mod resolve_definition {
|
||||
}
|
||||
|
||||
impl<'db> ResolvedDefinition<'db> {
|
||||
pub(crate) fn definition(&self) -> Option<Definition<'db>> {
|
||||
match self {
|
||||
ResolvedDefinition::Definition(definition) => Some(*definition),
|
||||
ResolvedDefinition::Module(_) => None,
|
||||
ResolvedDefinition::FileWithRange(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn file(&self, db: &'db dyn Db) -> File {
|
||||
match self {
|
||||
ResolvedDefinition::Definition(definition) => definition.file(db),
|
||||
|
||||
@@ -101,10 +101,10 @@ use crate::types::typed_dict::{
|
||||
use crate::types::visitor::any_over_type;
|
||||
use crate::types::{
|
||||
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
|
||||
DynamicType, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, KnownClass,
|
||||
KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
|
||||
ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness,
|
||||
Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
|
||||
DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType,
|
||||
KnownClass, KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
|
||||
Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet,
|
||||
Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
|
||||
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
|
||||
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
|
||||
binding_type, todo_type,
|
||||
@@ -3741,23 +3741,77 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
assignable
|
||||
};
|
||||
|
||||
let emit_invalid_final = |builder: &Self| {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) = builder.context.report_lint(&INVALID_ASSIGNMENT, target) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Cannot assign to final attribute `{attribute}` on type `{}`",
|
||||
object_ty.display(db)
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Return true (and emit a diagnostic) if this is an invalid assignment to a `Final` attribute.
|
||||
// Per PEP 591 and the typing conformance suite, Final instance attributes can be assigned
|
||||
// in __init__ methods. Multiple assignments within __init__ are allowed (matching mypy
|
||||
// and pyright behavior), as long as the attribute doesn't have a class-level value.
|
||||
let invalid_assignment_to_final = |builder: &Self, qualifiers: TypeQualifiers| -> bool {
|
||||
if qualifiers.contains(TypeQualifiers::FINAL) {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) = builder.context.report_lint(&INVALID_ASSIGNMENT, target)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Cannot assign to final attribute `{attribute}` \
|
||||
on type `{}`",
|
||||
object_ty.display(db)
|
||||
));
|
||||
// Check if it's a Final attribute
|
||||
if !qualifiers.contains(TypeQualifiers::FINAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're in an __init__ method (where Final attributes can be initialized).
|
||||
let is_in_init = builder
|
||||
.current_function_definition()
|
||||
.is_some_and(|func| func.name.id == "__init__");
|
||||
|
||||
// Not in __init__ - always disallow
|
||||
if !is_in_init {
|
||||
emit_invalid_final(builder);
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're in __init__ - verify we're in a method of the class being mutated
|
||||
let Some(class_ty) = builder.class_context_of_current_method() else {
|
||||
// Not a method (standalone function named __init__)
|
||||
emit_invalid_final(builder);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Check that object_ty is an instance of the class we're in
|
||||
if !object_ty.is_subtype_of(builder.db(), Type::instance(builder.db(), class_ty)) {
|
||||
// Assigning to a different class's Final attribute
|
||||
emit_invalid_final(builder);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if class-level attribute already has a value
|
||||
{
|
||||
let class_definition = class_ty.class_literal(db).0;
|
||||
let class_scope_id = class_definition.body_scope(db).file_scope_id(db);
|
||||
let place_table = builder.index.place_table(class_scope_id);
|
||||
|
||||
if let Some(symbol) = place_table.symbol_by_name(attribute) {
|
||||
if symbol.is_bound() {
|
||||
if emit_diagnostics {
|
||||
if let Some(diag_builder) =
|
||||
builder.context.report_lint(&INVALID_ASSIGNMENT, target)
|
||||
{
|
||||
diag_builder.into_diagnostic(format_args!(
|
||||
"Cannot assign to final attribute `{attribute}` in `__init__` \
|
||||
because it already has a value at class level"
|
||||
));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
// In __init__ and no class-level value - allow
|
||||
false
|
||||
};
|
||||
|
||||
match object_ty {
|
||||
@@ -9180,7 +9234,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
Some(left_ty)
|
||||
} else {
|
||||
Some(Type::KnownInstance(KnownInstanceType::UnionType(
|
||||
InternedTypes::from_elements(self.db(), [left_ty, right_ty]),
|
||||
InternedTypes::from_elements(
|
||||
self.db(),
|
||||
[left_ty, right_ty],
|
||||
InferredAs::ValueExpression,
|
||||
),
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -9205,7 +9263,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
&& instance.has_known_class(self.db(), KnownClass::NoneType) =>
|
||||
{
|
||||
Some(Type::KnownInstance(KnownInstanceType::UnionType(
|
||||
InternedTypes::from_elements(self.db(), [left_ty, right_ty]),
|
||||
InternedTypes::from_elements(
|
||||
self.db(),
|
||||
[left_ty, right_ty],
|
||||
InferredAs::ValueExpression,
|
||||
),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -10422,9 +10484,46 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
return Type::KnownInstance(KnownInstanceType::UnionType(
|
||||
InternedTypes::from_elements(self.db(), [ty, Type::none(self.db())]),
|
||||
InternedTypes::from_elements(
|
||||
self.db(),
|
||||
[ty, Type::none(self.db())],
|
||||
InferredAs::ValueExpression,
|
||||
),
|
||||
));
|
||||
}
|
||||
Type::SpecialForm(SpecialFormType::Union) => {
|
||||
let db = self.db();
|
||||
|
||||
match **slice {
|
||||
ast::Expr::Tuple(ref tuple) => {
|
||||
let mut elements = tuple
|
||||
.elts
|
||||
.iter()
|
||||
.map(|elt| self.infer_type_expression(elt))
|
||||
.peekable();
|
||||
|
||||
let is_empty = elements.peek().is_none();
|
||||
let union_type = Type::KnownInstance(KnownInstanceType::UnionType(
|
||||
InternedTypes::from_elements(db, elements, InferredAs::TypeExpression),
|
||||
));
|
||||
|
||||
if is_empty {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
||||
{
|
||||
builder.into_diagnostic(
|
||||
"`typing.Union` requires at least one type argument",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return union_type;
|
||||
}
|
||||
_ => {
|
||||
return self.infer_expression(slice, TypeContext::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ use crate::types::enums::{enum_member_literals, enum_metadata};
|
||||
use crate::types::function::KnownFunction;
|
||||
use crate::types::infer::infer_same_file_expression_type;
|
||||
use crate::types::{
|
||||
ClassLiteral, ClassType, IntersectionBuilder, KnownClass, KnownInstanceType, SpecialFormType,
|
||||
SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, TypeVarBoundOrConstraints,
|
||||
UnionBuilder, infer_expression_types,
|
||||
CallableType, ClassLiteral, ClassType, IntersectionBuilder, KnownClass, KnownInstanceType,
|
||||
SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext,
|
||||
TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types,
|
||||
};
|
||||
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
@@ -229,6 +229,18 @@ impl ClassInfoConstraintFunction {
|
||||
)
|
||||
}
|
||||
|
||||
// We don't have a good meta-type for `Callable`s right now,
|
||||
// so only apply `isinstance()` narrowing, not `issubclass()`
|
||||
Type::SpecialForm(SpecialFormType::Callable)
|
||||
if self == ClassInfoConstraintFunction::IsInstance =>
|
||||
{
|
||||
Some(CallableType::unknown(db).top_materialization(db))
|
||||
}
|
||||
|
||||
Type::SpecialForm(special_form) => special_form
|
||||
.aliased_stdlib_class()
|
||||
.and_then(|class| self.generate_constraint(db, class.to_class_literal(db))),
|
||||
|
||||
Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::BooleanLiteral(_)
|
||||
@@ -244,7 +256,6 @@ impl ClassInfoConstraintFunction {
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::SpecialForm(_)
|
||||
| Type::LiteralString
|
||||
| Type::StringLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
|
||||
@@ -328,6 +328,61 @@ impl SpecialFormType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `Some(KnownClass)` if this special form is an alias
|
||||
/// to a standard library class.
|
||||
pub(super) const fn aliased_stdlib_class(self) -> Option<KnownClass> {
|
||||
match self {
|
||||
Self::List => Some(KnownClass::List),
|
||||
Self::Dict => Some(KnownClass::Dict),
|
||||
Self::Set => Some(KnownClass::Set),
|
||||
Self::FrozenSet => Some(KnownClass::FrozenSet),
|
||||
Self::ChainMap => Some(KnownClass::ChainMap),
|
||||
Self::Counter => Some(KnownClass::Counter),
|
||||
Self::DefaultDict => Some(KnownClass::DefaultDict),
|
||||
Self::Deque => Some(KnownClass::Deque),
|
||||
Self::OrderedDict => Some(KnownClass::OrderedDict),
|
||||
Self::Tuple => Some(KnownClass::Tuple),
|
||||
Self::Type => Some(KnownClass::Type),
|
||||
|
||||
Self::AlwaysFalsy
|
||||
| Self::AlwaysTruthy
|
||||
| Self::Annotated
|
||||
| Self::Bottom
|
||||
| Self::CallableTypeOf
|
||||
| Self::ClassVar
|
||||
| Self::Concatenate
|
||||
| Self::Final
|
||||
| Self::Intersection
|
||||
| Self::Literal
|
||||
| Self::LiteralString
|
||||
| Self::Never
|
||||
| Self::NoReturn
|
||||
| Self::Not
|
||||
| Self::ReadOnly
|
||||
| Self::Required
|
||||
| Self::TypeAlias
|
||||
| Self::TypeGuard
|
||||
| Self::NamedTuple
|
||||
| Self::NotRequired
|
||||
| Self::Optional
|
||||
| Self::Top
|
||||
| Self::TypeIs
|
||||
| Self::TypedDict
|
||||
| Self::TypingSelf
|
||||
| Self::Union
|
||||
| Self::Unknown
|
||||
| Self::TypeOf
|
||||
| Self::Any
|
||||
// `typing.Callable` is an alias to `collections.abc.Callable`,
|
||||
// but they're both the same `SpecialFormType` in our model,
|
||||
// and neither is a class in typeshed (even though the `collections.abc` one is at runtime)
|
||||
| Self::Callable
|
||||
| Self::Protocol
|
||||
| Self::Generic
|
||||
| Self::Unpack => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this special form is valid as the second argument
|
||||
/// to `issubclass()` and `isinstance()` calls.
|
||||
pub(super) const fn is_valid_isinstance_target(self) -> bool {
|
||||
|
||||
@@ -70,13 +70,16 @@ export default function Editor({
|
||||
const serverRef = useRef<PlaygroundServer | null>(null);
|
||||
|
||||
if (serverRef.current != null) {
|
||||
serverRef.current.update({
|
||||
files,
|
||||
workspace,
|
||||
onOpenFile,
|
||||
onVendoredFileChange,
|
||||
onBackToUserFile,
|
||||
});
|
||||
serverRef.current.update(
|
||||
{
|
||||
files,
|
||||
workspace,
|
||||
onOpenFile,
|
||||
onVendoredFileChange,
|
||||
onBackToUserFile,
|
||||
},
|
||||
isViewingVendoredFile,
|
||||
);
|
||||
}
|
||||
|
||||
// Update the diagnostics in the editor.
|
||||
@@ -200,6 +203,7 @@ class PlaygroundServer
|
||||
private rangeSemanticTokensDisposable: IDisposable;
|
||||
private signatureHelpDisposable: IDisposable;
|
||||
private documentHighlightDisposable: IDisposable;
|
||||
private inVendoredFileCondition: editor.IContextKey<boolean>;
|
||||
// Cache for vendored file handles
|
||||
private vendoredFileHandles = new Map<string, FileHandle>();
|
||||
|
||||
@@ -249,8 +253,16 @@ class PlaygroundServer
|
||||
this.documentHighlightDisposable =
|
||||
monaco.languages.registerDocumentHighlightProvider("python", this);
|
||||
|
||||
this.inVendoredFileCondition = editor.createContextKey<boolean>(
|
||||
"inVendoredFile",
|
||||
false,
|
||||
);
|
||||
// Register Esc key command
|
||||
editor.addCommand(monaco.KeyCode.Escape, this.props.onBackToUserFile);
|
||||
editor.addCommand(
|
||||
monaco.KeyCode.Escape,
|
||||
() => this.props.onBackToUserFile(),
|
||||
"inVendoredFile",
|
||||
);
|
||||
}
|
||||
|
||||
triggerCharacters: string[] = ["."];
|
||||
@@ -452,8 +464,9 @@ class PlaygroundServer
|
||||
return undefined;
|
||||
}
|
||||
|
||||
update(props: PlaygroundServerProps) {
|
||||
update(props: PlaygroundServerProps, isViewingVendoredFile: boolean) {
|
||||
this.props = props;
|
||||
this.inVendoredFileCondition.set(isViewingVendoredFile);
|
||||
}
|
||||
|
||||
private getOrCreateVendoredFileHandle(vendoredPath: string): FileHandle {
|
||||
|
||||
Reference in New Issue
Block a user