Compare commits

...

7 Commits

Author SHA1 Message Date
Micha Reiser
56a3978479 [ty] Use OrderedSet/Map in more places 2026-01-04 19:54:22 +01:00
Alex Waygood
e1439beab2 [ty] Use UnionType helper methods more consistently (#22357) 2026-01-03 14:19:06 +00:00
Felix Scherz
fd86e699b5 [ty] narrow TypedDict unions with not in (#22349)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2026-01-03 13:12:57 +00:00
Alex Waygood
d0f841bff2 Add help: subdiagnostics for several Ruff rules that can sometimes appear to disagree with ty (#22331) 2026-01-02 22:10:39 +00:00
Alex Waygood
74978cfff2 Error on unused ty: ignore comments when dogfooding ty on our own scripts (#22347) 2026-01-02 20:27:09 +00:00
Alex Waygood
10a417aaf6 [ty] Specify heap_size for SynthesizedTypedDictType (#22345) 2026-01-02 20:09:35 +00:00
Matthew Mckee
a2e0ff57c3 Run cargo sort (#22310) 2026-01-02 19:58:15 +00:00
55 changed files with 471 additions and 256 deletions

View File

@@ -58,8 +58,8 @@ anstream = { version = "0.6.18" }
anstyle = { version = "1.0.10" }
anyhow = { version = "1.0.80" }
arc-swap = { version = "1.7.1" }
assert_fs = { version = "1.1.0" }
argfile = { version = "0.2.0" }
assert_fs = { version = "1.1.0" }
bincode = { version = "2.0.0" }
bitflags = { version = "2.5.0" }
bitvec = { version = "1.0.1", default-features = false, features = [
@@ -71,30 +71,30 @@ camino = { version = "1.1.7" }
clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "4.0.0" }
csv = { version = "1.3.1" }
divan = { package = "codspeed-divan-compat", version = "4.0.4" }
codspeed-criterion-compat = { version = "4.0.4", default-features = false }
colored = { version = "3.0.0" }
compact_str = "0.9.0"
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
compact_str = "0.9.0"
criterion = { version = "0.8.0", default-features = false }
crossbeam = { version = "0.8.4" }
csv = { version = "1.3.1" }
dashmap = { version = "6.0.1" }
datatest-stable = { version = "0.3.3" }
dunce = { version = "1.0.5" }
divan = { package = "codspeed-divan-compat", version = "4.0.4" }
drop_bomb = { version = "0.1.5" }
dunce = { version = "1.0.5" }
etcetera = { version = "0.11.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.7.3", features = [
"derive",
"smallvec",
"hashbrown",
"compact-str",
] }
getrandom = { version = "0.3.1" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
globwalk = { version = "0.9.1" }
@@ -116,8 +116,8 @@ is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.14.0" }
jiff = { version = "0.2.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "1.0.0" }
js-sys = { version = "0.3.69" }
libc = { version = "0.2.153" }
libcst = { version = "1.8.4", default-features = false }
log = { version = "0.4.17" }
@@ -138,9 +138,9 @@ pep440_rs = { version = "0.7.1" }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.79" }
pyproject-toml = { version = "0.13.4" }
quickcheck = { version = "1.0.3", default-features = false}
quickcheck_macros = { version = "1.0.0" }
quick-junit = { version = "0.5.0" }
quickcheck = { version = "1.0.3", default-features = false }
quickcheck_macros = { version = "1.0.0" }
quote = { version = "1.0.23" }
rand = { version = "0.9.0" }
rayon = { version = "1.10.0" }
@@ -197,9 +197,9 @@ tryfn = { version = "0.2.1" }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version = "0.9" }
unicode-ident = { version = "1.0.12" }
unicode-normalization = { version = "0.1.23" }
unicode-width = { version = "0.2.0" }
unicode_names2 = { version = "1.2.2" }
unicode-normalization = { version = "0.1.23" }
url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] }
walkdir = { version = "2.3.2" }
@@ -209,8 +209,13 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom", "ruff_options_metadata", "uuid", "get-size2", "ty_completion_eval"]
ignored = [
"getrandom",
"ruff_options_metadata",
"uuid",
"get-size2",
"ty_completion_eval",
]
[workspace.lints.rust]
unsafe_code = "warn"
@@ -270,17 +275,10 @@ if_not_else = "allow"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
[profile.release]
lto = "fat"
codegen-units = 16
# Profile to build a minimally sized binary for ruff/ty
[profile.minimal-size]
inherits = "release"
opt-level = "z"
codegen-units = 1
# Some crates don't change as much but benefit more from
# more expensive optimization passes, so we selectively
# decrease codegen-units in some cases.
@@ -291,6 +289,12 @@ codegen-units = 1
[profile.release.package.salsa]
codegen-units = 1
# Profile to build a minimally sized binary for ruff/ty
[profile.minimal-size]
inherits = "release"
opt-level = "z"
codegen-units = 1
[profile.dev.package.insta]
opt-level = 3

View File

@@ -12,6 +12,13 @@ license = { workspace = true }
readme = "../../README.md"
default-run = "ruff"
[package.metadata.cargo-shear]
# Used via macro expansion.
ignored = ["jiff"]
[package.metadata.dist]
dist = true
[dependencies]
ruff_cache = { workspace = true }
ruff_db = { workspace = true, default-features = false, features = ["os"] }
@@ -61,6 +68,12 @@ tracing = { workspace = true, features = ["log"] }
walkdir = { workspace = true }
wild = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true }
[dev-dependencies]
# Enable test rules during development
ruff_linter = { workspace = true, features = ["clap", "test-rules"] }
@@ -76,18 +89,5 @@ ruff_python_trivia = { workspace = true }
tempfile = { workspace = true }
test-case = { workspace = true }
[package.metadata.cargo-shear]
# Used via macro expansion.
ignored = ["jiff"]
[package.metadata.dist]
dist = true
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true }
[lints]
workspace = true

View File

@@ -12,10 +12,6 @@ license = "MIT OR Apache-2.0"
[lib]
[features]
default = []
testing-colors = []
[dependencies]
anstyle = { workspace = true }
memchr = { workspace = true }
@@ -23,12 +19,17 @@ unicode-width = { workspace = true }
[dev-dependencies]
ruff_annotate_snippets = { workspace = true, features = ["testing-colors"] }
anstream = { workspace = true }
serde = { workspace = true, features = ["derive"] }
snapbox = { workspace = true, features = ["diff", "term-svg", "cmd", "examples"] }
toml = { workspace = true }
tryfn = { workspace = true }
[features]
default = []
testing-colors = []
[[test]]
name = "fixtures"
harness = false

View File

@@ -16,6 +16,51 @@ bench = false
test = false
doctest = false
[dependencies]
ruff_db = { workspace = true, features = ["testing"] }
ruff_linter = { workspace = true, optional = true }
ruff_python_ast = { workspace = true }
ruff_python_formatter = { workspace = true, optional = true }
ruff_python_parser = { workspace = true, optional = true }
ruff_python_trivia = { workspace = true, optional = true }
ty_project = { workspace = true, optional = true }
anyhow = { workspace = true }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
criterion = { workspace = true, default-features = false, optional = true }
divan = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true, optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true, optional = true }
[dev-dependencies]
rayon = { workspace = true }
rustc-hash = { workspace = true }
[features]
default = ["ty_instrumented", "ty_walltime", "ruff_instrumented"]
# Enables the ruff instrumented benchmarks
ruff_instrumented = [
"criterion",
"ruff_linter",
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"mimalloc",
"tikv-jemallocator",
]
# Enables the ty instrumented benchmarks
ty_instrumented = ["criterion", "ty_project", "ruff_python_trivia"]
codspeed = ["codspeed-criterion-compat"]
# Enables the ty_walltime benchmarks
ty_walltime = ["ruff_db/os", "ty_project", "divan"]
[[bench]]
name = "linter"
harness = false
@@ -46,54 +91,5 @@ name = "ty_walltime"
harness = false
required-features = ["ty_walltime"]
[dependencies]
ruff_db = { workspace = true, features = ["testing"] }
ruff_python_ast = { workspace = true }
ruff_linter = { workspace = true, optional = true }
ruff_python_formatter = { workspace = true, optional = true }
ruff_python_parser = { workspace = true, optional = true }
ruff_python_trivia = { workspace = true, optional = true }
ty_project = { workspace = true, optional = true }
divan = { workspace = true, optional = true }
anyhow = { workspace = true }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
criterion = { workspace = true, default-features = false, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
[lints]
workspace = true
[features]
default = ["ty_instrumented", "ty_walltime", "ruff_instrumented"]
# Enables the ruff instrumented benchmarks
ruff_instrumented = [
"criterion",
"ruff_linter",
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"mimalloc",
"tikv-jemallocator",
]
# Enables the ty instrumented benchmarks
ty_instrumented = [
"criterion",
"ty_project",
"ruff_python_trivia",
]
codspeed = ["codspeed-criterion-compat"]
# Enables the ty_walltime benchmarks
ty_walltime = ["ruff_db/os", "ty_project", "divan"]
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true, optional = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true, optional = true }
[dev-dependencies]
rustc-hash = { workspace = true }
rayon = { workspace = true }

View File

@@ -11,11 +11,11 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
filetime = { workspace = true }
glob = { workspace = true }
globset = { workspace = true }
itertools = { workspace = true }
regex = { workspace = true }
filetime = { workspace = true }
seahash = { workspace = true }
[dev-dependencies]

View File

@@ -48,12 +48,12 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }
zip = { workspace = true }
[target.'cfg(target_arch="wasm32")'.dependencies]
web-time = { version = "1.1.0" }
[target.'cfg(not(target_arch="wasm32"))'.dependencies]
etcetera = { workspace = true, optional = true }
[target.'cfg(target_arch="wasm32")'.dependencies]
web-time = { version = "1.1.0" }
[dev-dependencies]
insta = { workspace = true, features = ["filters"] }
tempfile = { workspace = true }

View File

@@ -11,10 +11,6 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
ty = { workspace = true }
ty_project = { workspace = true, features = ["schemars"] }
ty_python_semantic = { workspace = true }
ty_static = { workspace = true }
ruff = { workspace = true }
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true, features = ["schemars"] }
@@ -26,6 +22,10 @@ ruff_python_formatter = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_workspace = { workspace = true, features = ["schemars"] }
ty = { workspace = true }
ty_project = { workspace = true, features = ["schemars"] }
ty_python_semantic = { workspace = true }
ty_static = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }

View File

@@ -10,6 +10,10 @@ documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]
[dependencies]
ruff_cache = { workspace = true }
ruff_macros = { workspace = true }
@@ -25,10 +29,6 @@ unicode-width = { workspace = true }
[dev-dependencies]
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]
[features]
serde = ["dep:serde", "ruff_text_size/serde"]
schemars = ["dep:schemars", "ruff_text_size/schemars"]

View File

@@ -9,6 +9,10 @@ repository.workspace = true
authors.workspace = true
license.workspace = true
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]
[dependencies]
ruff_cache = { workspace = true }
ruff_db = { workspace = true, features = ["os", "serde"] }
@@ -29,7 +33,3 @@ zip = { workspace = true, features = [] }
[lints]
workspace = true
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]

View File

@@ -16,17 +16,17 @@ license = { workspace = true }
ruff_cache = { workspace = true }
ruff_db = { workspace = true, features = ["junit", "serde"] }
ruff_diagnostics = { workspace = true, features = ["serde"] }
ruff_notebook = { workspace = true }
ruff_macros = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true, features = ["serde", "cache"] }
ruff_python_codegen = { workspace = true }
ruff_python_importer = { workspace = true }
ruff_python_index = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_semantic = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_source_file = { workspace = true, features = ["serde"] }
ruff_text_size = { workspace = true }
@@ -44,8 +44,8 @@ imperative = { workspace = true }
is-macro = { workspace = true }
is-wsl = { workspace = true }
itertools = { workspace = true }
libcst = { workspace = true }
jiff = { workspace = true }
libcst = { workspace = true }
log = { workspace = true }
memchr = { workspace = true }
natord = { workspace = true }
@@ -67,17 +67,17 @@ strum_macros = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
typed-arena = { workspace = true }
unicode-normalization = { workspace = true }
unicode-width = { workspace = true }
unicode_names2 = { workspace = true }
unicode-normalization = { workspace = true }
url = { workspace = true }
[dev-dependencies]
insta = { workspace = true, features = ["filters", "json", "redactions"] }
test-case = { workspace = true }
# Disable colored output in tests
colored = { workspace = true, features = ["no-color"] }
insta = { workspace = true, features = ["filters", "json", "redactions"] }
tempfile = { workspace = true }
test-case = { workspace = true }
[features]
default = []

View File

@@ -1,6 +1,8 @@
use ruff_db::diagnostic::Diagnostic;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::function_type::is_subject_to_liskov_substitution_principle;
use crate::checkers::ast::Checker;
use crate::settings::LinterSettings;
@@ -191,3 +193,27 @@ pub(super) fn allow_boolean_trap(call: &ast::ExprCall, checker: &Checker) -> boo
false
}
pub(super) fn add_liskov_substitution_principle_help(
diagnostic: &mut Diagnostic,
function_name: &str,
decorator_list: &[ast::Decorator],
checker: &Checker,
) {
let semantic = checker.semantic();
let parent_scope = semantic.current_scope();
let pep8_settings = &checker.settings().pep8_naming;
if is_subject_to_liskov_substitution_principle(
function_name,
decorator_list,
parent_scope,
semantic,
&pep8_settings.classmethod_decorators,
&pep8_settings.staticmethod_decorators,
) {
diagnostic.help(
"Consider adding `@typing.override` if changing the function signature \
would violate the Liskov Substitution Principle",
);
}
}

View File

@@ -6,7 +6,9 @@ use ruff_python_semantic::analyze::visibility;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
use crate::rules::flake8_boolean_trap::helpers::{
add_liskov_substitution_principle_help, is_allowed_func_def,
};
/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
@@ -139,7 +141,9 @@ pub(crate) fn boolean_default_value_positional_argument(
return;
}
checker.report_diagnostic(BooleanDefaultValuePositionalArgument, param.identifier());
let mut diagnostic = checker
.report_diagnostic(BooleanDefaultValuePositionalArgument, param.identifier());
add_liskov_substitution_principle_help(&mut diagnostic, name, decorator_list, checker);
}
}
}

View File

@@ -7,7 +7,9 @@ use ruff_python_semantic::analyze::visibility;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
use crate::rules::flake8_boolean_trap::helpers::{
add_liskov_substitution_principle_help, is_allowed_func_def,
};
/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
@@ -149,7 +151,10 @@ pub(crate) fn boolean_type_hint_positional_argument(
return;
}
checker.report_diagnostic(BooleanTypeHintPositionalArgument, parameter.identifier());
let mut diagnostic =
checker.report_diagnostic(BooleanTypeHintPositionalArgument, parameter.identifier());
add_liskov_substitution_principle_help(&mut diagnostic, name, decorator_list, checker);
}
}

View File

@@ -97,6 +97,7 @@ FBT001 Boolean-typed positional argument in function definition
| ^^^^^
91 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle
FBT001 Boolean-typed positional argument in function definition
--> FBT.py:100:10

View File

@@ -130,10 +130,16 @@ pub(crate) fn invalid_function_name(
return;
}
checker.report_diagnostic(
let mut diagnostic = checker.report_diagnostic(
InvalidFunctionName {
name: name.to_string(),
},
stmt.identifier(),
);
if parent_class.is_some() {
diagnostic.help(
"Consider adding `@typing.override` if this method \
overrides a method from a superclass",
);
}
}

View File

@@ -42,6 +42,7 @@ N802 Function name `testTest` should be lowercase
| ^^^^^^^^
41 | assert True
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
N802 Function name `bad_Name` should be lowercase
--> N802.py:65:9
@@ -52,6 +53,7 @@ N802 Function name `bad_Name` should be lowercase
| ^^^^^^^^
66 | pass
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
N802 Function name `dont_GET` should be lowercase
--> N802.py:84:9
@@ -62,6 +64,7 @@ N802 Function name `dont_GET` should be lowercase
| ^^^^^^^^
85 | pass
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
N802 Function name `dont_OPTIONS` should be lowercase
--> N802.py:95:9
@@ -72,6 +75,7 @@ N802 Function name `dont_OPTIONS` should be lowercase
| ^^^^^^^^^^^^
96 | pass
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
N802 Function name `dont_OPTIONS` should be lowercase
--> N802.py:106:9
@@ -82,3 +86,4 @@ N802 Function name `dont_OPTIONS` should be lowercase
| ^^^^^^^^^^^^
107 | pass
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

View File

@@ -20,3 +20,4 @@ N802 Function name `stillBad` should be lowercase
| ^^^^^^^^
14 | return super().tearDown()
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

View File

@@ -146,11 +146,15 @@ pub(crate) fn no_self_use(checker: &Checker, scope_id: ScopeId, scope: &Scope) {
.map(|binding_id| semantic.binding(binding_id))
.is_some_and(|binding| binding.kind.is_argument() && binding.is_unused())
{
checker.report_diagnostic(
let mut diagnostic = checker.report_diagnostic(
NoSelfUse {
method_name: name.to_string(),
},
func.identifier(),
);
diagnostic.help(
"Consider adding `@typing.override` if this method overrides \
a method from a superclass",
);
}
}

View File

@@ -1,6 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::identifier::Identifier;
use ruff_python_semantic::analyze::function_type::is_subject_to_liskov_substitution_principle;
use ruff_python_semantic::analyze::{function_type, visibility};
use crate::Violation;
@@ -121,11 +122,24 @@ pub(crate) fn too_many_arguments(checker: &Checker, function_def: &ast::StmtFunc
return;
}
checker.report_diagnostic(
let mut diagnostic = checker.report_diagnostic(
TooManyArguments {
c_args: num_arguments,
max_args: checker.settings().pylint.max_args,
},
function_def.identifier(),
);
if is_subject_to_liskov_substitution_principle(
&function_def.name,
&function_def.decorator_list,
semantic.current_scope(),
semantic,
&checker.settings().pep8_naming.classmethod_decorators,
&checker.settings().pep8_naming.staticmethod_decorators,
) {
diagnostic.help(
"Consider adding `@typing.override` if changing the function signature \
would violate the Liskov Substitution Principle",
);
}
}

View File

@@ -1,5 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, identifier::Identifier};
use ruff_python_semantic::analyze::function_type::is_subject_to_liskov_substitution_principle;
use ruff_python_semantic::analyze::{function_type, visibility};
use crate::Violation;
@@ -125,11 +126,24 @@ pub(crate) fn too_many_positional_arguments(
return;
}
checker.report_diagnostic(
let mut diagnostic = checker.report_diagnostic(
TooManyPositionalArguments {
c_pos: num_positional_args,
max_pos: checker.settings().pylint.max_positional_args,
},
function_def.identifier(),
);
if is_subject_to_liskov_substitution_principle(
&function_def.name,
&function_def.decorator_list,
semantic.current_scope(),
semantic,
&checker.settings().pep8_naming.classmethod_decorators,
&checker.settings().pep8_naming.staticmethod_decorators,
) {
diagnostic.help(
"Consider adding `@typing.override` if changing the function signature \
would violate the Liskov Substitution Principle",
);
}
}

View File

@@ -49,6 +49,7 @@ PLR0913 Too many arguments in function definition (8 > 5)
| ^
52 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle
PLR0913 Too many arguments in function definition (8 > 5)
--> too_many_arguments.py:58:9
@@ -58,6 +59,7 @@ PLR0913 Too many arguments in function definition (8 > 5)
| ^
59 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle
PLR0913 Too many arguments in function definition (8 > 5)
--> too_many_arguments.py:66:9
@@ -67,6 +69,7 @@ PLR0913 Too many arguments in function definition (8 > 5)
| ^
67 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle
PLR0913 Too many arguments in function definition (6 > 5)
--> too_many_arguments.py:70:9
@@ -76,3 +79,4 @@ PLR0913 Too many arguments in function definition (6 > 5)
| ^
71 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle

View File

@@ -34,6 +34,7 @@ PLR0917 Too many positional arguments (6/5)
| ^
44 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle
PLR0917 Too many positional arguments (6/5)
--> too_many_positional_arguments.py:47:9
@@ -43,3 +44,4 @@ PLR0917 Too many positional arguments (6/5)
| ^
48 | pass
|
help: Consider adding `@typing.override` if changing the function signature would violate the Liskov Substitution Principle

View File

@@ -9,6 +9,7 @@ PLR6301 Method `developer_greeting` could be a function, class method, or static
| ^^^^^^^^^^^^^^^^^^
8 | print(f"Greetings {name}!")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `greeting_1` could be a function, class method, or static method
--> no_self_use.py:10:9
@@ -19,6 +20,7 @@ PLR6301 Method `greeting_1` could be a function, class method, or static method
| ^^^^^^^^^^
11 | print("Hello!")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `greeting_2` could be a function, class method, or static method
--> no_self_use.py:13:9
@@ -29,6 +31,7 @@ PLR6301 Method `greeting_2` could be a function, class method, or static method
| ^^^^^^^^^^
14 | print("Hi!")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `validate_y` could be a function, class method, or static method
--> no_self_use.py:103:9
@@ -39,6 +42,7 @@ PLR6301 Method `validate_y` could be a function, class method, or static method
104 | if value <= 0:
105 | raise ValueError("y must be a positive integer")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `non_simple_assignment` could be a function, class method, or static method
--> no_self_use.py:128:9
@@ -50,6 +54,7 @@ PLR6301 Method `non_simple_assignment` could be a function, class method, or sta
129 | msg = foo = ""
130 | raise NotImplementedError(msg)
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `non_simple_assignment_2` could be a function, class method, or static method
--> no_self_use.py:132:9
@@ -61,6 +66,7 @@ PLR6301 Method `non_simple_assignment_2` could be a function, class method, or s
133 | msg[0] = ""
134 | raise NotImplementedError(msg)
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `unused_message` could be a function, class method, or static method
--> no_self_use.py:136:9
@@ -72,6 +78,7 @@ PLR6301 Method `unused_message` could be a function, class method, or static met
137 | msg = ""
138 | raise NotImplementedError("")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `unused_message_2` could be a function, class method, or static method
--> no_self_use.py:140:9
@@ -83,6 +90,7 @@ PLR6301 Method `unused_message_2` could be a function, class method, or static m
141 | msg = ""
142 | raise NotImplementedError(x)
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `developer_greeting` could be a function, class method, or static method
--> no_self_use.py:145:9
@@ -92,6 +100,7 @@ PLR6301 Method `developer_greeting` could be a function, class method, or static
| ^^^^^^^^^^^^^^^^^^
146 | print(t"Greetings {name}!")
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass
PLR6301 Method `tstring` could be a function, class method, or static method
--> no_self_use.py:151:9
@@ -103,3 +112,4 @@ PLR6301 Method `tstring` could be a function, class method, or static method
152 | msg = t"{x}"
153 | raise NotImplementedError(msg)
|
help: Consider adding `@typing.override` if this method overrides a method from a superclass

View File

@@ -18,10 +18,10 @@ doctest = false
ruff_python_trivia = { workspace = true }
heck = { workspace = true }
itertools = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true, features = ["derive", "parsing", "extra-traits", "full"] }
itertools = { workspace = true }
[lints]
workspace = true

View File

@@ -20,12 +20,12 @@ ruff_text_size = { workspace = true }
anyhow = { workspace = true }
itertools = { workspace = true }
rand = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true, default-features = false, features = ["macros"] }
thiserror = { workspace = true }
uuid = { workspace = true }
rand = { workspace = true }
[dev-dependencies]
test-case = { workspace = true }

View File

@@ -10,6 +10,10 @@ documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]
[lib]
[dependencies]
@@ -42,14 +46,7 @@ serde = [
"dep:ruff_cache",
"compact_str/serde",
]
get-size = [
"dep:get-size2",
"ruff_text_size/get-size"
]
get-size = ["dep:get-size2", "ruff_text_size/get-size"]
[lints]
workspace = true
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]

View File

@@ -12,8 +12,8 @@ license.workspace = true
[dependencies]
[dev-dependencies]
ruff_python_parser = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_text_size = { workspace = true }

View File

@@ -10,6 +10,10 @@ documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]
[lib]
doctest = false
@@ -18,10 +22,10 @@ ruff_cache = { workspace = true }
ruff_db = { workspace = true }
ruff_formatter = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
anyhow = { workspace = true }
@@ -32,8 +36,8 @@ memchr = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
serde = { workspace = true, optional = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
@@ -50,16 +54,6 @@ serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true }
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]
[[test]]
name = "fixtures"
harness = false
test = true
required-features = ["serde"]
[features]
default = ["serde"]
serde = [
@@ -70,5 +64,11 @@ serde = [
]
schemars = ["dep:schemars", "dep:serde_json", "ruff_formatter/schemars"]
[[test]]
name = "fixtures"
harness = false
test = true
required-features = ["serde"]
[lints]
workspace = true

View File

@@ -12,10 +12,6 @@ license = { workspace = true }
[lib]
[[test]]
name = "fixtures"
harness = false
[dependencies]
ruff_python_ast = { workspace = true, features = ["get-size"] }
ruff_python_trivia = { workspace = true }
@@ -29,8 +25,8 @@ memchr = { workspace = true }
rustc-hash = { workspace = true }
static_assertions = { workspace = true }
unicode-ident = { workspace = true }
unicode_names2 = { workspace = true }
unicode-normalization = { workspace = true }
unicode_names2 = { workspace = true }
[dev-dependencies]
ruff_annotate_snippets = { workspace = true }
@@ -45,5 +41,9 @@ serde = { workspace = true }
serde_json = { workspace = true }
walkdir = { workspace = true }
[[test]]
name = "fixtures"
harness = false
[lints]
workspace = true

View File

@@ -10,6 +10,10 @@ documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]
[dependencies]
ruff_cache = { workspace = true }
ruff_index = { workspace = true }
@@ -28,12 +32,8 @@ smallvec = { workspace = true }
[dev-dependencies]
insta = { workspace = true, features = ["filters", "json", "redactions"] }
test-case = { workspace = true }
ruff_python_parser = { workspace = true }
test-case = { workspace = true }
[lints]
workspace = true
[package.metadata.cargo-shear]
# Used via `CacheKey` macro expansion.
ignored = ["ruff_cache"]

View File

@@ -47,6 +47,35 @@ pub fn classify(
}
}
/// Return `true` if this function is subject to the Liskov Substitution Principle.
///
/// Type checkers will check nearly all methods for compliance with the Liskov Substitution
/// Principle, but some methods are exempt.
pub fn is_subject_to_liskov_substitution_principle(
function_name: &str,
decorator_list: &[Decorator],
parent_scope: &Scope,
semantic: &SemanticModel,
classmethod_decorators: &[String],
staticmethod_decorators: &[String],
) -> bool {
let kind = classify(
function_name,
decorator_list,
parent_scope,
semantic,
classmethod_decorators,
staticmethod_decorators,
);
match (kind, function_name) {
(FunctionType::Function | FunctionType::NewMethod, _) => false,
(FunctionType::Method, "__init__" | "__post_init__" | "__replace__") => false,
(_, "__init_subclass__") => false,
(FunctionType::Method | FunctionType::ClassMethod | FunctionType::StaticMethod, _) => true,
}
}
/// Return `true` if a [`Decorator`] is indicative of a static method.
/// Note: Implicit static methods like `__new__` are not considered.
fn is_static_method(

View File

@@ -13,8 +13,8 @@ license = { workspace = true }
[lib]
[dependencies]
ruff_text_size = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
itertools = { workspace = true }
unicode-ident = { workspace = true }

View File

@@ -44,12 +44,12 @@ tracing = { workspace = true }
tracing-log = { workspace = true }
tracing-subscriber = { workspace = true, features = ["chrono"] }
[dev-dependencies]
insta = { workspace = true }
[target.'cfg(target_vendor = "apple")'.dependencies]
libc = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
[features]
test-uv = []

View File

@@ -11,9 +11,9 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
serde = { workspace = true, optional = true }
get-size2 = { workspace = true, optional = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
[dev-dependencies]
serde_test = { workspace = true }

View File

@@ -15,14 +15,11 @@ description = "WebAssembly bindings for Ruff"
crate-type = ["cdylib", "rlib"]
doctest = false
[features]
default = ["console_error_panic_hook"]
[dependencies]
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_codegen = { workspace = true }
ruff_formatter = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_index = { workspace = true }
ruff_python_parser = { workspace = true }
@@ -33,19 +30,22 @@ ruff_workspace = { workspace = true }
console_error_panic_hook = { workspace = true, optional = true }
console_log = { workspace = true }
js-sys = { workspace = true }
log = { workspace = true }
# Not a direct dependency but required to enable the `wasm_js` feature.
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
getrandom = { workspace = true, features = ["wasm_js"] }
js-sys = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
serde-wasm-bindgen = { workspace = true }
wasm-bindgen = { workspace = true }
# Not a direct dependency but required to compile for Wasm.
uuid = { workspace = true, features = ["js"] }
wasm-bindgen = { workspace = true }
[dev-dependencies]
wasm-bindgen-test = { workspace = true }
[features]
default = ["console_error_panic_hook"]
[lints]
workspace = true

View File

@@ -10,6 +10,10 @@ documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[package.metadata.cargo-shear]
# Used via macro expansion.
ignored = ["colored"]
[lib]
[dependencies]
@@ -27,14 +31,14 @@ ruff_source_file = { workspace = true }
anyhow = { workspace = true }
colored = { workspace = true }
glob = { workspace = true }
globset = { workspace = true }
ignore = { workspace = true }
indexmap = { workspace = true }
is-macro = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
matchit = { workspace = true }
glob = { workspace = true }
globset = { workspace = true }
path-absolutize = { workspace = true }
path-slash = { workspace = true }
pep440_rs = { workspace = true }
@@ -56,10 +60,6 @@ etcetera = { workspace = true }
ruff_linter = { workspace = true, features = ["clap", "test-rules"] }
tempfile = { workspace = true }
[package.metadata.cargo-shear]
# Used via macro expansion.
ignored = ["colored"]
[features]
default = []
schemars = [

View File

@@ -17,8 +17,8 @@ license.workspace = true
ruff_db = { workspace = true, features = ["os", "cache"] }
ruff_python_ast = { workspace = true }
ty_combine = { workspace = true }
ty_python_semantic = { workspace = true }
ty_project = { workspace = true, features = ["zstd"] }
ty_python_semantic = { workspace = true }
ty_server = { workspace = true }
ty_static = { workspace = true }
@@ -35,19 +35,22 @@ jiff = { workspace = true }
rayon = { workspace = true }
salsa = { workspace = true }
tracing = { workspace = true, features = ["release_max_level_debug"] }
tracing-subscriber = { workspace = true }
tracing-flame = { workspace = true }
tracing-subscriber = { workspace = true }
wild = { workspace = true }
[target.'cfg(all(not(target_os = "macos"), not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing"] }
ruff_python_trivia = { workspace = true }
ty_module_resolver = { workspace = true }
dunce = { workspace = true }
filetime = { workspace = true }
insta = { workspace = true, features = ["filters"] }
insta-cmd = { workspace = true }
filetime = { workspace = true }
regex = { workspace = true }
tempfile = { workspace = true }
toml = { workspace = true }
@@ -55,8 +58,5 @@ toml = { workspace = true }
[features]
default = []
[target.'cfg(all(not(target_os = "macos"), not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true }
[lints]
workspace = true

View File

@@ -23,8 +23,8 @@ ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ty_module_resolver = { workspace = true }
ty_python_semantic = { workspace = true }
ty_project = { workspace = true, features = ["testing"] }
ty_python_semantic = { workspace = true }
ty_vendored = { workspace = true }
get-size2 = { workspace = true }

View File

@@ -21,10 +21,10 @@ compact_str = { workspace = true }
get-size2 = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }

View File

@@ -33,8 +33,8 @@ crossbeam = { workspace = true }
get-size2 = { workspace = true }
globset = { workspace = true }
notify = { workspace = true }
pep440_rs = { workspace = true, features = ["version-ranges"] }
ordermap = { workspace = true, features = ["serde"] }
pep440_rs = { workspace = true, features = ["version-ranges"] }
rayon = { workspace = true }
regex = { workspace = true }
regex-automata = { workspace = true }
@@ -48,8 +48,8 @@ toml = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing"] }
insta = { workspace = true, features = ["redactions", "ron"] }
ruff_db = { workspace = true, features = ["testing"] }
[features]
default = ["zstd"]

View File

@@ -11,21 +11,21 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_annotate_snippets = { workspace = true }
ruff_db = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_index = { workspace = true, features = ["salsa"] }
ruff_macros = { workspace = true }
ruff_memory_usage = { workspace = true }
ruff_python_ast = { workspace = true, features = ["salsa"] }
ruff_python_literal = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_trivia = { workspace = true }
ty_module_resolver = { workspace = true }
ty_combine = { workspace = true }
ty_module_resolver = { workspace = true }
ty_static = { workspace = true }
anyhow = { workspace = true }
@@ -36,24 +36,24 @@ colored = { workspace = true }
compact_str = { workspace = true }
drop_bomb = { workspace = true }
get-size2 = { workspace = true, features = ["indexmap", "ordermap"] }
hashbrown = { workspace = true }
indexmap = { workspace = true }
itertools = { workspace = true }
memchr = { workspace = true }
ordermap = { workspace = true }
salsa = { workspace = true, features = ["compact_str", "ordermap"] }
thiserror = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
hashbrown = { workspace = true }
salsa = { workspace = true, features = ["compact_str", "ordermap"] }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
test-case = { workspace = true }
memchr = { workspace = true }
strsim = "0.11.1"
strum = { workspace = true }
strum_macros = { workspace = true }
strsim = "0.11.1"
test-case = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }
@@ -69,7 +69,7 @@ indoc = { workspace = true }
insta = { workspace = true }
pretty_assertions = { workspace = true }
quickcheck = { workspace = true }
quickcheck_macros = { workspace = true}
quickcheck_macros = { workspace = true }
[features]
schemars = ["dep:schemars", "dep:serde_json"]

View File

@@ -2124,20 +2124,26 @@ shows up in a subset of the union members) is present, but that isn't generally
field, it could be *assigned to* with another `TypedDict` that does:
```py
from typing_extensions import Literal
class Foo(TypedDict):
foo: int
class Bar(TypedDict):
bar: int
def disappointment(u: Foo | Bar):
def disappointment(u: Foo | Bar, v: Literal["foo"]):
if "foo" in u:
# We can't narrow the union here...
reveal_type(u) # revealed: Foo | Bar
else:
# ...(even though we *can* narrow it here)...
# TODO: This should narrow to `Bar`, because "foo" is required in `Foo`.
reveal_type(u) # revealed: Bar
if v in u:
reveal_type(u) # revealed: Foo | Bar
else:
reveal_type(u) # revealed: Bar
# ...because `u` could turn out to be one of these.
class FooBar(TypedDict):
@@ -2148,6 +2154,39 @@ static_assert(is_assignable_to(FooBar, Foo))
static_assert(is_assignable_to(FooBar, Bar))
```
`not in` works in the opposite way to `in`: we can narrow in the positive case, but we cannot narrow
in the negative case. The following snippet also tests our narrowing behaviour for intersections
that contain `TypedDict`s, and unions that contain intersections that contain `TypedDict`s:
```py
from typing_extensions import Literal, Any
from ty_extensions import Intersection, is_assignable_to, static_assert
def _(t: Bar, u: Foo | Intersection[Bar, Any], v: Intersection[Bar, Any], w: Literal["bar"]):
reveal_type(u) # revealed: Foo | (Bar & Any)
reveal_type(v) # revealed: Bar & Any
if "bar" not in t:
reveal_type(t) # revealed: Never
else:
reveal_type(t) # revealed: Bar
if "bar" not in u:
reveal_type(u) # revealed: Foo
else:
reveal_type(u) # revealed: Foo | (Bar & Any)
if "bar" not in v:
reveal_type(v) # revealed: Never
else:
reveal_type(v) # revealed: Bar & Any
if w not in u:
reveal_type(u) # revealed: Foo
else:
reveal_type(u) # revealed: Foo | (Bar & Any)
```
TODO: The narrowing that we didn't do above will become possible when we add support for
`closed=True`. This is [one of the main use cases][closed] that motivated the `closed` feature.

View File

@@ -7267,10 +7267,7 @@ impl<'db> Type<'db> {
}
(Some(Place::Defined(new_method, ..)), Place::Defined(init_method, ..)) => {
let callable = UnionBuilder::new(db)
.add(*new_method)
.add(*init_method)
.build();
let callable = UnionType::from_elements(db, [new_method, init_method]);
let new_method_bindings = new_method
.bindings(db)
@@ -10758,11 +10755,7 @@ fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
impl<'db> TypeVarConstraints<'db> {
fn as_type(self, db: &'db dyn Db) -> Type<'db> {
let mut builder = UnionBuilder::new(db);
for ty in self.elements(db) {
builder = builder.add(*ty);
}
builder.build()
UnionType::from_elements(db, self.elements(db))
}
fn to_instance(self, db: &'db dyn Db) -> Option<TypeVarConstraints<'db>> {

View File

@@ -8,6 +8,7 @@ use super::{
SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase,
function::FunctionType,
};
use crate::FxOrderMap;
use crate::place::TypeOrigin;
use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::semantic_index::scope::{NodeWithScopeKind, Scope, ScopeKind};
@@ -161,8 +162,8 @@ fn fields_cycle_initial<'db>(
_self: ClassLiteral<'db>,
_specialization: Option<Specialization<'db>>,
_field_policy: CodeGeneratorKind<'db>,
) -> FxIndexMap<Name, Field<'db>> {
FxIndexMap::default()
) -> FxOrderMap<Name, Field<'db>> {
FxOrderMap::default()
}
/// A category of classes with code generation capabilities (with synthesized methods).
@@ -3147,7 +3148,7 @@ impl<'db> ClassLiteral<'db> {
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
field_policy: CodeGeneratorKind<'db>,
) -> FxIndexMap<Name, Field<'db>> {
) -> FxOrderMap<Name, Field<'db>> {
if field_policy == CodeGeneratorKind::NamedTuple {
// NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the
// fields of this class only.
@@ -3195,8 +3196,8 @@ impl<'db> ClassLiteral<'db> {
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
field_policy: CodeGeneratorKind,
) -> FxIndexMap<Name, Field<'db>> {
let mut attributes = FxIndexMap::default();
) -> FxOrderMap<Name, Field<'db>> {
let mut attributes = FxOrderMap::default();
let class_body_scope = self.body_scope(db);
let table = place_table(db, class_body_scope);

View File

@@ -31,7 +31,7 @@ use crate::types::{
protocol_class::ProtocolClass,
};
use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance};
use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint};
use crate::{Db, DisplaySettings, FxOrderMap, Program, declare_lint};
use itertools::Itertools;
use ruff_db::{
diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity},
@@ -3001,7 +3001,7 @@ pub(crate) fn report_instance_layout_conflict(
/// The inner data is an `IndexMap` to ensure that diagnostics regarding conflicting disjoint bases
/// are reported in a stable order.
#[derive(Debug, Default)]
pub(super) struct IncompatibleBases<'db>(FxIndexMap<DisjointBase<'db>, IncompatibleBaseInfo<'db>>);
pub(super) struct IncompatibleBases<'db>(FxOrderMap<DisjointBase<'db>, IncompatibleBaseInfo<'db>>);
impl<'db> IncompatibleBases<'db> {
pub(super) fn insert(

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::name::Name;
use rustc_hash::FxHashMap;
use crate::{
Db, FxIndexMap,
Db, FxOrderMap,
place::{Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations},
semantic_index::{place_table, use_def_map},
types::{
@@ -13,7 +13,7 @@ use crate::{
#[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct EnumMetadata<'db> {
pub(crate) members: FxIndexMap<Name, Type<'db>>,
pub(crate) members: FxOrderMap<Name, Type<'db>>,
pub(crate) aliases: FxHashMap<Name, Name>,
}
@@ -22,7 +22,7 @@ impl get_size2::GetSize for EnumMetadata<'_> {}
impl EnumMetadata<'_> {
fn empty() -> Self {
EnumMetadata {
members: FxIndexMap::default(),
members: FxOrderMap::default(),
aliases: FxHashMap::default(),
}
}
@@ -253,7 +253,7 @@ pub(crate) fn enum_metadata<'db>(
Some((name.clone(), value_ty))
})
.collect::<FxIndexMap<_, _>>();
.collect::<FxOrderMap<_, _>>();
if members.is_empty() {
// Enum subclasses without members are not considered enums.

View File

@@ -1083,13 +1083,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
&mut self.inner_expression_inference_state,
InnerExpressionInferenceState::Get,
);
let union = union
.elements(self.db())
.iter()
.fold(UnionBuilder::new(self.db()), |builder, elem| {
builder.add(self.infer_subscript_type_expression(subscript, *elem))
})
.build();
let union = union.map(self.db(), |element| {
self.infer_subscript_type_expression(subscript, *element)
});
self.inner_expression_inference_state = previous_slice_inference_state;
union
}

View File

@@ -12,7 +12,7 @@ use crate::types::enums::{enum_member_literals, enum_metadata};
use crate::types::function::KnownFunction;
use crate::types::infer::{ExpressionInference, infer_same_file_expression_type};
use crate::types::typed_dict::{
SynthesizedTypedDictType, TypedDictFieldBuilder, TypedDictSchema, TypedDictType,
SynthesizedTypedDictType, TypedDictField, TypedDictFieldBuilder, TypedDictSchema, TypedDictType,
};
use crate::types::{
CallableType, ClassLiteral, ClassType, IntersectionBuilder, IntersectionType, KnownClass,
@@ -926,10 +926,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
.build();
// Keep order: first literal complement, then broader arms.
let result = UnionBuilder::new(self.db)
.add(narrowed_single)
.add(rest_union)
.build();
let result = UnionType::from_elements(self.db, [narrowed_single, rest_union]);
Some(result)
} else {
None
@@ -1099,6 +1096,75 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
}
}
// Narrow unions and intersections of `TypedDict` in cases where required keys are
// excluded:
//
// class Foo(TypedDict):
// foo: int
// class Bar(TypedDict):
// bar: int
//
// def _(u: Foo | Bar):
// if "foo" not in u:
// reveal_type(u) # revealed: Bar
if matches!(&**ops, [ast::CmpOp::In | ast::CmpOp::NotIn])
&& let Type::StringLiteral(key) = inference.expression_type(&**left)
&& let Some(rhs_place_expr) = place_expr(&comparators[0])
&& let rhs_type = inference.expression_type(&comparators[0])
&& is_typeddict_or_union_with_typeddicts(self.db, rhs_type)
{
let is_negative_check = is_positive == (ops[0] == ast::CmpOp::NotIn);
if is_negative_check {
let requires_key = |td: TypedDictType<'db>| -> bool {
td.items(self.db)
.get(key.value(self.db))
.is_some_and(TypedDictField::is_required)
};
let narrowed = match rhs_type {
Type::TypedDict(td) => {
if requires_key(td) {
Type::Never
} else {
rhs_type
}
}
Type::Intersection(intersection) => {
if intersection
.positive(self.db)
.iter()
.copied()
.filter_map(Type::as_typed_dict)
.any(requires_key)
{
Type::Never
} else {
rhs_type
}
}
Type::Union(union) => {
// remove all members of the union that would require the key
union.filter(self.db, |ty| match ty {
Type::TypedDict(td) => !requires_key(*td),
Type::Intersection(intersection) => !intersection
.positive(self.db)
.iter()
.copied()
.filter_map(Type::as_typed_dict)
.any(requires_key),
_ => true,
})
}
_ => rhs_type,
};
if narrowed != rhs_type {
let place = self.expect_place(&rhs_place_expr);
constraints.insert(place, NarrowingConstraint::typeguard(narrowed));
}
}
}
let mut last_rhs_ty: Option<Type> = None;
for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) {
@@ -1677,18 +1743,13 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
fn is_typeddict_or_union_with_typeddicts<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
match ty {
Type::TypedDict(_) => true,
Type::Union(union) => {
union
.elements(db)
.iter()
.any(|union_member_ty| match union_member_ty {
Type::TypedDict(_) => true,
Type::Intersection(intersection) => {
intersection.positive(db).iter().any(Type::is_typed_dict)
}
_ => false,
})
Type::Intersection(intersection) => {
intersection.positive(db).iter().any(Type::is_typed_dict)
}
Type::Union(union) => union
.elements(db)
.iter()
.any(|union_member_ty| is_typeddict_or_union_with_typeddicts(db, *union_member_ty)),
_ => false,
}
}

View File

@@ -879,7 +879,7 @@ pub(super) fn validate_typed_dict_dict_literal<'db>(
}
}
#[salsa::interned(debug)]
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct SynthesizedTypedDictType<'db> {
#[returns(ref)]
pub(crate) items: TypedDictSchema<'db>,

View File

@@ -40,15 +40,15 @@ thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["chrono"] }
[target.'cfg(target_vendor = "apple")'.dependencies]
libc = { workspace = true }
[dev-dependencies]
dunce = { workspace = true }
insta = { workspace = true, features = ["filters", "json"] }
regex = { workspace = true }
tempfile = { workspace = true }
smallvec = { workspace = true }
[target.'cfg(target_vendor = "apple")'.dependencies]
libc = { workspace = true }
tempfile = { workspace = true }
[lints]
workspace = true

View File

@@ -12,8 +12,8 @@ license = { workspace = true }
[lib]
doctest = false
[lints]
workspace = true
[dependencies]
ruff_macros = { workspace = true }
[lints]
workspace = true

View File

@@ -15,10 +15,10 @@ ruff_db = { workspace = true, features = ["os", "testing"] }
ruff_diagnostics = { workspace = true }
ruff_index = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_ast = { workspace = true }
ty_module_resolver = { workspace = true }
ty_python_semantic = { workspace = true, features = ["serde", "testing"] }
ty_static = { workspace = true }
@@ -26,8 +26,8 @@ ty_vendored = { workspace = true }
anyhow = { workspace = true }
camino = { workspace = true }
dunce = { workspace = true }
colored = { workspace = true }
dunce = { workspace = true }
insta = { workspace = true, features = ["filters"] }
memchr = { workspace = true }
path-slash = { workspace = true }
@@ -35,12 +35,12 @@ regex = { workspace = true }
rustc-hash = { workspace = true }
rustc-stable-hash = { workspace = true }
salsa = { workspace = true }
smallvec = { workspace = true }
serde = { workspace = true }
smallvec = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
[lints]
workspace = true

View File

@@ -11,13 +11,14 @@ repository = { workspace = true }
license = { workspace = true }
description = "WebAssembly bindings for ty"
[package.metadata.cargo-shear]
# Depended on only to enable `log` feature as of 2025-10-03.
ignored = ["tracing"]
[lib]
crate-type = ["cdylib", "rlib"]
doctest = false
[features]
default = ["console_error_panic_hook"]
[dependencies]
ty_ide = { workspace = true }
ty_project = { workspace = true, default-features = false, features = [
@@ -35,11 +36,11 @@ ruff_text_size = { workspace = true }
console_error_panic_hook = { workspace = true, optional = true }
console_log = { workspace = true }
js-sys = { workspace = true }
log = { workspace = true }
# Not a direct dependency but required to enable the `wasm_js` feature.
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
getrandom = { workspace = true, features = ["wasm_js"] }
js-sys = { workspace = true }
log = { workspace = true }
serde-wasm-bindgen = { workspace = true }
tracing = { workspace = true, features = ["log"] }
# Not a direct dependency but required to compile for Wasm.
@@ -50,9 +51,8 @@ wasm-bindgen = { workspace = true }
[dev-dependencies]
wasm-bindgen-test = { workspace = true }
[features]
default = ["console_error_panic_hook"]
[lints]
workspace = true
[package.metadata.cargo-shear]
# Depended on only to enable `log` feature as of 2025-10-03.
ignored = ["tracing"]

View File

@@ -17,3 +17,4 @@ exclude = ["./ty_benchmark"]
[tool.ty.rules]
possibly-unresolved-reference = "error"
division-by-zero = "error"
unused-ignore-comment = "error"

View File

@@ -34,3 +34,4 @@ ignore = [
[tool.ty.rules]
possibly-unresolved-reference = "error"
division-by-zero = "error"
unused-ignore-comment = "error"