Skip to content

Commit a199217

Browse files
committed
Merge branch 'main' into cjm/fix-spurious-async-None
* main: [`pydocstyle`] Improve discoverability of rules enabled for each convention (#24973) [ty] Deduplicate retained use-def place states (#25450) [ty] reduce features of low-level crates depended on by `ty_python_semantic` (#25524) [ty] Fix narrowing enum literal unions by member identity (#25520) [ty] Test tagged union narrowing for named tuples (#25519) [ty] Disallow file-system access in `ty_python_core` (#25518) [ty] Nominal Tagged Union Narrowing (#24916) Commit `scripts/uv.lock` (#25517) Fix potential index out of range in `LineIndex` computation (#25492) [ty] Sync vendored typeshed stubs (#25514) [ty] Add disjointness for protocol method members (#25315) [ty] Use compact sets for more immutable fields (#25476) [ty] Derive `Default` for `FunctionDecoratorInference` (#25482) [ty] Ignore rejected assignments for synthesized bindings (#25340) [ty] Handle cycles in function decorator inference (#25475) docs: fix typo `bin/active` → `bin/activate` in tutorial (#25473) [ty] Narrow bound method overloads by receiver (#24707)
2 parents 3ebb95e + 7ef96be commit a199217

49 files changed

Lines changed: 2154 additions & 368 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/ruff_source_file/src/line_index.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,15 @@ impl LineIndex {
294294
if row_index.saturating_add(1) >= starts.len() {
295295
contents.text_len()
296296
} else {
297-
starts[row_index + 1] - TextSize::new(1)
297+
let next_line_start = starts[row_index + 1].to_usize();
298+
let bytes = contents.as_bytes();
299+
300+
let line_ending_len = if bytes[..next_line_start].ends_with(b"\r\n") {
301+
2
302+
} else {
303+
1
304+
};
305+
starts[row_index + 1] - TextSize::new(line_ending_len)
298306
}
299307
}
300308

@@ -812,6 +820,42 @@ mod tests {
812820
);
813821
}
814822

823+
#[test]
824+
fn line_end_exclusive_handles_different_line_endings() {
825+
let lf_contents = "a\nb";
826+
let lf_index = LineIndex::from_source_text(lf_contents);
827+
assert_eq!(
828+
lf_index.line_end_exclusive(OneIndexed::from_zero_indexed(0), lf_contents),
829+
TextSize::from(1)
830+
);
831+
assert_eq!(
832+
lf_index.line_end_exclusive(OneIndexed::from_zero_indexed(1), lf_contents),
833+
TextSize::from(3)
834+
);
835+
836+
let crlf_contents = "a\r\nb";
837+
let crlf_index = LineIndex::from_source_text(crlf_contents);
838+
assert_eq!(
839+
crlf_index.line_end_exclusive(OneIndexed::from_zero_indexed(0), crlf_contents),
840+
TextSize::from(1)
841+
);
842+
assert_eq!(
843+
crlf_index.line_end_exclusive(OneIndexed::from_zero_indexed(1), crlf_contents),
844+
TextSize::from(4)
845+
);
846+
847+
let cr_contents = "a\rb";
848+
let cr_index = LineIndex::from_source_text(cr_contents);
849+
assert_eq!(
850+
cr_index.line_end_exclusive(OneIndexed::from_zero_indexed(0), cr_contents),
851+
TextSize::from(1)
852+
);
853+
assert_eq!(
854+
cr_index.line_end_exclusive(OneIndexed::from_zero_indexed(1), cr_contents),
855+
TextSize::from(3)
856+
);
857+
}
858+
815859
#[test]
816860
fn utf8_index() {
817861
let index = LineIndex::from_source_text("x = '🫣'");

crates/ruff_workspace/src/options.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3313,6 +3313,53 @@ pub struct PydocstyleOptions {
33133313
/// convention = "google"
33143314
/// ```
33153315
///
3316+
/// The PEP 257 convention includes all `D` errors apart from:
3317+
/// [`D203`](rules/incorrect-blank-line-before-class.md),
3318+
/// [`D212`](rules/multi-line-summary-first-line.md),
3319+
/// [`D213`](rules/multi-line-summary-second-line.md),
3320+
/// [`D214`](rules/overindented-section.md),
3321+
/// [`D215`](rules/overindented-section-underline.md),
3322+
/// [`D404`](rules/docstring-starts-with-this.md),
3323+
/// [`D405`](rules/non-capitalized-section-name.md),
3324+
/// [`D406`](rules/missing-new-line-after-section-name.md),
3325+
/// [`D407`](rules/missing-dashed-underline-after-section.md),
3326+
/// [`D408`](rules/missing-section-underline-after-name.md),
3327+
/// [`D409`](rules/mismatched-section-underline-length.md),
3328+
/// [`D410`](rules/no-blank-line-after-section.md),
3329+
/// [`D411`](rules/no-blank-line-before-section.md),
3330+
/// [`D413`](rules/missing-blank-line-after-last-section.md),
3331+
/// [`D415`](rules/missing-terminal-punctuation.md),
3332+
/// [`D416`](rules/missing-section-name-colon.md),
3333+
/// [`D417`](rules/undocumented-param.md), and
3334+
/// [`D420`](rules/incorrect-section-order.md).
3335+
///
3336+
/// The NumPy convention includes all `D` errors apart from:
3337+
/// [`D107`](rules/undocumented-public-init.md),
3338+
/// [`D203`](rules/incorrect-blank-line-before-class.md),
3339+
/// [`D212`](rules/multi-line-summary-first-line.md),
3340+
/// [`D213`](rules/multi-line-summary-second-line.md),
3341+
/// [`D402`](rules/signature-in-docstring.md),
3342+
/// [`D413`](rules/missing-blank-line-after-last-section.md),
3343+
/// [`D415`](rules/missing-terminal-punctuation.md),
3344+
/// [`D416`](rules/missing-section-name-colon.md), and
3345+
/// [`D417`](rules/undocumented-param.md).
3346+
///
3347+
/// The Google convention includes all `D` errors apart from:
3348+
/// [`D203`](rules/incorrect-blank-line-before-class.md),
3349+
/// [`D204`](rules/incorrect-blank-line-after-class.md),
3350+
/// [`D213`](rules/multi-line-summary-second-line.md),
3351+
/// [`D215`](rules/overindented-section-underline.md),
3352+
/// [`D400`](rules/missing-trailing-period.md),
3353+
/// [`D401`](rules/non-imperative-mood.md),
3354+
/// [`D404`](rules/docstring-starts-with-this.md),
3355+
/// [`D406`](rules/missing-new-line-after-section-name.md),
3356+
/// [`D407`](rules/missing-dashed-underline-after-section.md),
3357+
/// [`D408`](rules/missing-section-underline-after-name.md),
3358+
/// [`D409`](rules/mismatched-section-underline-length.md), and
3359+
/// [`D413`](rules/missing-blank-line-after-last-section.md).
3360+
///
3361+
/// For more information see the [FAQ](faq.md#does-ruff-support-numpy-or-google-style-docstrings) entry.
3362+
///
33163363
/// To enable an additional rule that's excluded from the convention,
33173364
/// select the desired rule via its fully qualified rule code (e.g.,
33183365
/// `D400` instead of `D4` or `D40`):

crates/ty_ide/src/hover.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,87 @@ mod tests {
15291529
");
15301530
}
15311531

1532+
#[test]
1533+
fn hover_bound_method_overload_self_type() {
1534+
let string = hover_test(
1535+
r#"
1536+
def f(string: str):
1537+
string.removesuf<CURSOR>fix("suffix")
1538+
"#,
1539+
);
1540+
1541+
assert_snapshot!(string.hover(), @r#"
1542+
bound method str.removesuffix(suffix: str, /) -> str
1543+
---------------------------------------------
1544+
Return a str with the given suffix string removed if present.
1545+
1546+
If the string ends with the suffix string and that suffix is not empty,
1547+
return string[:-len(suffix)]. Otherwise, return a copy of the original
1548+
string.
1549+
1550+
---------------------------------------------
1551+
```python
1552+
bound method str.removesuffix(suffix: str, /) -> str
1553+
```
1554+
---
1555+
Return a str with the given suffix string removed if present.<HB>
1556+
<HB>
1557+
If the string ends with the suffix string and that suffix is not empty,<HB>
1558+
return string[:-len(suffix)]. Otherwise, return a copy of the original<HB>
1559+
string.
1560+
---------------------------------------------
1561+
info[hover]: Hovered content is
1562+
--> main.py:3:12
1563+
|
1564+
3 | string.removesuffix("suffix")
1565+
| ^^^^^^^^^-^^
1566+
| | |
1567+
| | Cursor offset
1568+
| source
1569+
|
1570+
"#);
1571+
1572+
let literal_string = hover_test(
1573+
r#"
1574+
from typing_extensions import LiteralString
1575+
1576+
def f(string: LiteralString):
1577+
string.removesuf<CURSOR>fix("suffix")
1578+
"#,
1579+
);
1580+
1581+
assert_snapshot!(literal_string.hover(), @r#"
1582+
def removesuffix(suffix: LiteralString, /) -> LiteralString
1583+
---------------------------------------------
1584+
Return a str with the given suffix string removed if present.
1585+
1586+
If the string ends with the suffix string and that suffix is not empty,
1587+
return string[:-len(suffix)]. Otherwise, return a copy of the original
1588+
string.
1589+
1590+
---------------------------------------------
1591+
```python
1592+
def removesuffix(suffix: LiteralString, /) -> LiteralString
1593+
```
1594+
---
1595+
Return a str with the given suffix string removed if present.<HB>
1596+
<HB>
1597+
If the string ends with the suffix string and that suffix is not empty,<HB>
1598+
return string[:-len(suffix)]. Otherwise, return a copy of the original<HB>
1599+
string.
1600+
---------------------------------------------
1601+
info[hover]: Hovered content is
1602+
--> main.py:5:12
1603+
|
1604+
5 | string.removesuffix("suffix")
1605+
| ^^^^^^^^^-^^
1606+
| | |
1607+
| | Cursor offset
1608+
| source
1609+
|
1610+
"#);
1611+
}
1612+
15321613
/// When the resolved overload has no docstring and neither does the
15331614
/// implementation, we fall back to showing a sibling overload's docstring.
15341615
#[test]

crates/ty_ide/src/inlay_hints.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4551,6 +4551,82 @@ Source with applied edits:
45514551
");
45524552
}
45534553

4554+
#[test]
4555+
fn instance_method_overload_self_type() {
4556+
let mut test = inlay_hint_test(
4557+
r#"
4558+
from typing import overload
4559+
4560+
class Parent:
4561+
@overload
4562+
def choose(self: "Child", child_value: int) -> None: ...
4563+
@overload
4564+
def choose(self: "Parent", parent_value: int) -> None: ...
4565+
def choose(self, value: int) -> None: ...
4566+
4567+
class Child(Parent): pass
4568+
4569+
def f(parent: Parent, child: Child):
4570+
parent.choose(1)
4571+
child.choose(2)"#,
4572+
);
4573+
4574+
assert_snapshot!(test.inlay_hints(), @r#"
4575+
4576+
from typing import overload
4577+
4578+
class Parent:
4579+
@overload
4580+
def choose(self: "Child", child_value: int) -> None: ...
4581+
@overload
4582+
def choose(self: "Parent", parent_value: int) -> None: ...
4583+
def choose(self, value: int) -> None: ...
4584+
4585+
class Child(Parent): pass
4586+
4587+
def f(parent: Parent, child: Child):
4588+
parent.choose([parent_value=]1)
4589+
child.choose([child_value=]2)
4590+
---------------------------------------------
4591+
info[inlay-hint-location]: Inlay Hint Target
4592+
--> main.py:8:32
4593+
|
4594+
8 | def choose(self: "Parent", parent_value: int) -> None: ...
4595+
| ^^^^^^^^^^^^
4596+
|
4597+
info: Source
4598+
--> main2.py:14:20
4599+
|
4600+
14 | parent.choose([parent_value=]1)
4601+
| ^^^^^^^^^^^^
4602+
|
4603+
4604+
info[inlay-hint-location]: Inlay Hint Target
4605+
--> main.py:6:31
4606+
|
4607+
6 | def choose(self: "Child", child_value: int) -> None: ...
4608+
| ^^^^^^^^^^^
4609+
|
4610+
info: Source
4611+
--> main2.py:15:19
4612+
|
4613+
15 | child.choose([child_value=]2)
4614+
| ^^^^^^^^^^^
4615+
|
4616+
4617+
---------------------------------------------
4618+
info[inlay-hint-edit]: Inlay hint edits
4619+
--> main.py:1:1
4620+
11 | class Child(Parent): pass
4621+
12 |
4622+
13 | def f(parent: Parent, child: Child):
4623+
- parent.choose(1)
4624+
- child.choose(2)
4625+
14 + parent.choose(parent_value=1)
4626+
15 + child.choose(child_value=2)
4627+
"#);
4628+
}
4629+
45544630
#[test]
45554631
fn test_class_method_call() {
45564632
let mut test = inlay_hint_test(

crates/ty_ide/src/signature_help.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,27 @@ mod tests {
353353
}
354354
}
355355

356+
#[test]
357+
fn signature_help_bound_method_overload_self_type() {
358+
let test = cursor_test(
359+
r#"
360+
def f(string: str):
361+
string.removesuffix("suffix"<CURSOR>)
362+
"#,
363+
);
364+
365+
assert_snapshot!(test.signature_help_render(), @"
366+
367+
============== active signature =============
368+
(suffix: str, /) -> str
369+
---------------------------------------------
370+
371+
-------------- active parameter -------------
372+
suffix: str
373+
---------------------------------------------
374+
");
375+
}
376+
356377
#[test]
357378
fn signature_help_nested_function_calls() {
358379
let test = cursor_test(

crates/ty_python_core/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
#![warn(
2+
clippy::disallowed_methods,
3+
reason = "Prefer System trait methods over std methods in ty crates"
4+
)]
15
use ruff_python_ast as ast;
26
use std::iter::{FusedIterator, once};
37
use std::sync::Arc;

crates/ty_python_core/src/place.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,15 +654,30 @@ impl<'db, 'a> PossiblyNarrowedPlacesBuilder<'db, 'a> {
654654
self.add_narrowing_target(comparator, &mut places);
655655
}
656656

657+
let can_narrow_attribute_base =
658+
matches!(&*expr_compare.ops, [ast::CmpOp::Eq | ast::CmpOp::NotEq]);
659+
let can_narrow_subscript_base = matches!(
660+
&*expr_compare.ops,
661+
[ast::CmpOp::Eq | ast::CmpOp::NotEq | ast::CmpOp::Is | ast::CmpOp::IsNot]
662+
);
663+
657664
// For subscript expressions on either side, the subscript base can also be narrowed.
658665
// (TypedDict and tuple discriminated union narrowing.)
659666
for expr in std::iter::once(&*expr_compare.left).chain(&expr_compare.comparators) {
660-
if let ast::Expr::Subscript(subscript) = expr.expression_value()
667+
if can_narrow_subscript_base
668+
&& let ast::Expr::Subscript(subscript) = expr.expression_value()
661669
&& let Some(place_expr) = PlaceExpr::try_from_expr(&subscript.value)
662670
&& let Some(place) = self.places.place_id((&place_expr).into())
663671
{
664672
places.insert(place);
665673
}
674+
if can_narrow_attribute_base
675+
&& let ast::Expr::Attribute(attribute) = expr
676+
&& let Some(place_expr) = PlaceExpr::try_from_expr(&attribute.value)
677+
&& let Some(place) = self.places.place_id((&place_expr).into())
678+
{
679+
places.insert(place);
680+
}
666681
}
667682

668683
places
@@ -767,6 +782,12 @@ impl<'db, 'a> PossiblyNarrowedPlacesBuilder<'db, 'a> {
767782
}
768783
}
769784
}
785+
if let ast::Expr::Attribute(attribute) = subject_node
786+
&& let Some(place_expr) = PlaceExpr::try_from_expr(&attribute.value)
787+
&& let Some(place) = self.places.place_id((&place_expr).into())
788+
{
789+
places.insert(place);
790+
}
770791

771792
// Handle Or patterns by recursing into each alternative
772793
if let PatternPredicateKind::Or(predicates) = kind {

0 commit comments

Comments
 (0)