Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions crates/ty/docs/configuration.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 13 additions & 9 deletions crates/ty_project/src/metadata/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1472,8 +1472,7 @@ pub struct TerminalOptions {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AnalysisOptions {
/// Whether equality-based checks should preserve broad builtin types rather than narrow them to
/// literal types.
/// Whether ty should use conservative equality and inequality semantics.
///
/// By default, ty narrows `value` from `str` to `Literal["a"]` in the positive branch of
/// `value == "a"`. When this option is enabled, `value` remains `str`. This also applies to
Expand Down Expand Up @@ -1523,18 +1522,23 @@ pub struct AnalysisOptions {
/// # Statically `Literal["a"] | None`, but `result` contains `"b"` at runtime.
/// ```
///
/// Enable this option to preserve the broader builtin type instead.
/// ty also assumes subclasses of non-final classes do not override `__eq__` or `__ne__`. This
/// can discard possible union members, affecting equality-result inference and reachability as
/// well as type narrowing.
///
/// Enable this option to avoid unsound assumptions about equality and inequality comparisons.
///
/// Defaults to `false`.
#[option(
default = r#"false"#,
value_type = "bool",
example = r#"
# Preserve broad builtin types instead of narrowing them to literals
strict-literal-narrowing = true
strict-equality-semantics = true
"#
)]
pub strict_literal_narrowing: Option<bool>,
#[serde(alias = "strict-literal-narrowing")]
pub strict_equality_semantics: Option<bool>,

/// Whether ty should respect `type: ignore` comments.
///
Expand Down Expand Up @@ -1612,14 +1616,14 @@ impl AnalysisOptions {
diagnostics: &mut Vec<OptionDiagnostic>,
) -> AnalysisSettings {
let Self {
strict_literal_narrowing,
strict_equality_semantics,
respect_type_ignore_comments,
allowed_unresolved_imports,
replace_imports_with_any,
} = self;

let AnalysisSettings {
strict_literal_narrowing: strict_literal_narrowing_default,
strict_equality_semantics: strict_equality_semantics_default,
respect_type_ignore_comments: respect_type_ignore_default,
allowed_unresolved_imports: allowed_unresolved_imports_default,
replace_imports_with_any: replace_imports_with_any_default,
Expand Down Expand Up @@ -1648,8 +1652,8 @@ impl AnalysisOptions {
};

AnalysisSettings {
strict_literal_narrowing: strict_literal_narrowing
.unwrap_or(strict_literal_narrowing_default),
strict_equality_semantics: strict_equality_semantics
.unwrap_or(strict_equality_semantics_default),
respect_type_ignore_comments: respect_type_ignore_comments
.unwrap_or(respect_type_ignore_default),
allowed_unresolved_imports,
Expand Down
18 changes: 8 additions & 10 deletions crates/ty_python_semantic/resources/mdtest/conditional/match.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,11 @@ def _(target: int):
## Value match

A value pattern matches based on equality: the first `case` branch here will be taken if `subject`
is equal to `2`, even if `subject` is not an instance of `int`. We can't know whether `C` here has a
custom `__eq__` implementation that might cause it to compare equal to `2`, so we have to consider
the possibility that the `case` branch might be taken even though the type `C` is disjoint from the
type `Literal[2]`.
is equal to `2`, even if `subject` is not an instance of `int`. By default, we assume that
subclasses of `C` do not override equality, so the `case` branch cannot be taken when the type `C`
is disjoint from the type `Literal[2]`.

This leads us to infer `Literal[1, 3]` as the type of `y` after the `match` statement, rather than
`Literal[1]`:
This leads us to infer `Literal[1]` as the type of `y` after the `match` statement:

```py
class C: ...
Expand All @@ -137,7 +135,7 @@ def _(subject: C):
match subject:
case 2:
y = 3
reveal_type(y) # revealed: Literal[1, 3]
reveal_type(y) # revealed: Literal[1]
```

However, in this variant, we can prove that `D` here does not have a custom `__eq__` implementation,
Expand Down Expand Up @@ -589,14 +587,14 @@ def _(target: int | str):
reveal_type(y) # revealed: Literal[2, 3, 4]
```

### Enabling strict literal narrowing
### Enabling strict equality narrowing

With strict literal narrowing enabled, broad builtin types are preserved both in the capture and
With strict equality narrowing enabled, broad builtin types are preserved both in the capture and
when narrowing the subject for later cases:

```toml
[analysis]
strict-literal-narrowing = true
strict-equality-semantics = true
```

```py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ reveal_type(alice1.age) # revealed: int | None
reveal_type(repr(alice1)) # revealed: str

reveal_type(alice1 == alice2) # revealed: bool
reveal_type(alice1 == "Alice") # revealed: bool
reveal_type(alice1 == "Alice") # revealed: Literal[False]

bob = Person("Bob")
bob2 = Person("Bob", None)
Expand Down
Loading
Loading