Compare commits

..

1 Commits

Author SHA1 Message Date
Carl Meyer
ca0e578afa [ty] remove any_over_type 2025-07-02 12:05:59 -07:00
58 changed files with 180 additions and 896 deletions

View File

@@ -1,61 +1,5 @@
# Changelog
## 0.12.2
### Preview features
- \[`flake8-pyi`\] Expand `Optional[A]` to `A | None` (`PYI016`) ([#18572](https://github.com/astral-sh/ruff/pull/18572))
- \[`pyupgrade`\] Mark `UP008` fix safe if no comments are in range ([#18683](https://github.com/astral-sh/ruff/pull/18683))
### Bug fixes
- \[`flake8-comprehensions`\] Fix `C420` to prepend whitespace when needed ([#18616](https://github.com/astral-sh/ruff/pull/18616))
- \[`perflint`\] Fix `PERF403` panic on attribute or subscription loop variable ([#19042](https://github.com/astral-sh/ruff/pull/19042))
- \[`pydocstyle`\] Fix `D413` infinite loop for parenthesized docstring ([#18930](https://github.com/astral-sh/ruff/pull/18930))
- \[`pylint`\] Fix `PLW0108` autofix introducing a syntax error when the lambda's body contains an assignment expression ([#18678](https://github.com/astral-sh/ruff/pull/18678))
- \[`refurb`\] Fix false positive on empty tuples (`FURB168`) ([#19058](https://github.com/astral-sh/ruff/pull/19058))
- \[`ruff`\] Allow more `field` calls from `attrs` (`RUF009`) ([#19021](https://github.com/astral-sh/ruff/pull/19021))
- \[`ruff`\] Fix syntax error introduced for an empty string followed by a u-prefixed string (`UP025`) ([#18899](https://github.com/astral-sh/ruff/pull/18899))
### Rule changes
- \[`flake8-executable`\] Allow `uvx` in shebang line (`EXE003`) ([#18967](https://github.com/astral-sh/ruff/pull/18967))
- \[`pandas`\] Avoid flagging `PD002` if `pandas` is not imported ([#18963](https://github.com/astral-sh/ruff/pull/18963))
- \[`pyupgrade`\] Avoid PEP-604 unions with `typing.NamedTuple` (`UP007`, `UP045`) ([#18682](https://github.com/astral-sh/ruff/pull/18682))
### Documentation
- Document link between `import-outside-top-level (PLC0415)` and `lint.flake8-tidy-imports.banned-module-level-imports` ([#18733](https://github.com/astral-sh/ruff/pull/18733))
- Fix description of the `format.skip-magic-trailing-comma` example ([#19095](https://github.com/astral-sh/ruff/pull/19095))
- \[`airflow`\] Make `AIR302` example error out-of-the-box ([#18988](https://github.com/astral-sh/ruff/pull/18988))
- \[`airflow`\] Make `AIR312` example error out-of-the-box ([#18989](https://github.com/astral-sh/ruff/pull/18989))
- \[`flake8-annotations`\] Make `ANN401` example error out-of-the-box ([#18974](https://github.com/astral-sh/ruff/pull/18974))
- \[`flake8-async`\] Make `ASYNC100` example error out-of-the-box ([#18993](https://github.com/astral-sh/ruff/pull/18993))
- \[`flake8-async`\] Make `ASYNC105` example error out-of-the-box ([#19002](https://github.com/astral-sh/ruff/pull/19002))
- \[`flake8-async`\] Make `ASYNC110` example error out-of-the-box ([#18975](https://github.com/astral-sh/ruff/pull/18975))
- \[`flake8-async`\] Make `ASYNC210` example error out-of-the-box ([#18977](https://github.com/astral-sh/ruff/pull/18977))
- \[`flake8-async`\] Make `ASYNC220`, `ASYNC221`, and `ASYNC222` examples error out-of-the-box ([#18978](https://github.com/astral-sh/ruff/pull/18978))
- \[`flake8-async`\] Make `ASYNC251` example error out-of-the-box ([#18990](https://github.com/astral-sh/ruff/pull/18990))
- \[`flake8-bandit`\] Make `S201` example error out-of-the-box ([#19017](https://github.com/astral-sh/ruff/pull/19017))
- \[`flake8-bandit`\] Make `S604` and `S609` examples error out-of-the-box ([#19049](https://github.com/astral-sh/ruff/pull/19049))
- \[`flake8-bugbear`\] Make `B028` example error out-of-the-box ([#19054](https://github.com/astral-sh/ruff/pull/19054))
- \[`flake8-bugbear`\] Make `B911` example error out-of-the-box ([#19051](https://github.com/astral-sh/ruff/pull/19051))
- \[`flake8-datetimez`\] Make `DTZ011` example error out-of-the-box ([#19055](https://github.com/astral-sh/ruff/pull/19055))
- \[`flake8-datetimez`\] Make `DTZ901` example error out-of-the-box ([#19056](https://github.com/astral-sh/ruff/pull/19056))
- \[`flake8-pyi`\] Make `PYI032` example error out-of-the-box ([#19061](https://github.com/astral-sh/ruff/pull/19061))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI014`, `PYI015`) ([#19097](https://github.com/astral-sh/ruff/pull/19097))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI042`) ([#19101](https://github.com/astral-sh/ruff/pull/19101))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI059`) ([#19080](https://github.com/astral-sh/ruff/pull/19080))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI062`) ([#19079](https://github.com/astral-sh/ruff/pull/19079))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT023`) ([#19104](https://github.com/astral-sh/ruff/pull/19104))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT030`) ([#19105](https://github.com/astral-sh/ruff/pull/19105))
- \[`flake8-quotes`\] Make example error out-of-the-box (`Q003`) ([#19106](https://github.com/astral-sh/ruff/pull/19106))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM110`) ([#19113](https://github.com/astral-sh/ruff/pull/19113))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM113`) ([#19109](https://github.com/astral-sh/ruff/pull/19109))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM401`) ([#19110](https://github.com/astral-sh/ruff/pull/19110))
- \[`pyflakes`\] Fix backslash in docs (`F621`) ([#19098](https://github.com/astral-sh/ruff/pull/19098))
- \[`pylint`\] Fix `PLC0415` example ([#18970](https://github.com/astral-sh/ruff/pull/18970))
## 0.12.1
### Preview features

6
Cargo.lock generated
View File

@@ -2724,7 +2724,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.2"
version = "0.12.1"
dependencies = [
"anyhow",
"argfile",
@@ -2970,7 +2970,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.2"
version = "0.12.1"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3302,7 +3302,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.2"
version = "0.12.1"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.12.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.2/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.1/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
rev: v0.12.1
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.12.2"
version = "0.12.1"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -242,20 +242,19 @@ fn large(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
// Currently disabled because the benchmark is too noisy (± 10%) to give useful feedback.
// #[bench(args=[&*PYDANTIC], sample_size=3, sample_count=3)]
// fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
// let thread_pool = ThreadPoolBuilder::new().build().unwrap();
#[bench(args=[&*PYDANTIC], sample_size=3, sample_count=3)]
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
// bencher
// .with_inputs(|| benchmark.setup_iteration())
// .bench_local_values(|db| {
// thread_pool.install(|| {
// check_project(&db, benchmark.max_diagnostics);
// db
// })
// });
// }
bencher
.with_inputs(|| benchmark.setup_iteration())
.bench_local_values(|db| {
thread_pool.install(|| {
check_project(&db, benchmark.max_diagnostics);
db
})
});
}
fn main() {
ThreadPoolBuilder::new()

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.12.2"
version = "0.12.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -125,20 +125,3 @@ class J:
class K:
f: F = F()
g: G = G()
# Regression test for https://github.com/astral-sh/ruff/issues/19014
# These are all valid field calls and should not cause diagnostics.
@attr.define
class TestAttrField:
attr_field_factory: list[int] = attr.field(factory=list)
attr_field_default: list[int] = attr.field(default=attr.Factory(list))
attr_factory: list[int] = attr.Factory(list)
attr_ib: list[int] = attr.ib(factory=list)
attr_attr: list[int] = attr.attr(factory=list)
attr_attrib: list[int] = attr.attrib(factory=list)
@attr.attributes
class TestAttrAttributes:
x: list[int] = list() # RUF009

View File

@@ -75,7 +75,7 @@ impl AlwaysFixableViolation for TypedArgumentDefaultInStub {
/// ## Example
///
/// ```pyi
/// def foo(arg=bar()) -> None: ...
/// def foo(arg=[]) -> None: ...
/// ```
///
/// Use instead:
@@ -120,7 +120,7 @@ impl AlwaysFixableViolation for ArgumentDefaultInStub {
///
/// ## Example
/// ```pyi
/// foo: str = bar()
/// foo: str = "..."
/// ```
///
/// Use instead:

View File

@@ -14,15 +14,11 @@ use crate::checkers::ast::Checker;
///
/// ## Example
/// ```pyi
/// from typing import TypeAlias
///
/// type_alias_name: TypeAlias = int
/// ```
///
/// Use instead:
/// ```pyi
/// from typing import TypeAlias
///
/// TypeAliasName: TypeAlias = int
/// ```
#[derive(ViolationMetadata)]

View File

@@ -31,7 +31,7 @@ use crate::rules::flake8_pytest_style::helpers::{Parentheses, get_mark_decorator
/// import pytest
///
///
/// @pytest.mark.foo()
/// @pytest.mark.foo
/// def test_something(): ...
/// ```
///
@@ -41,7 +41,7 @@ use crate::rules::flake8_pytest_style::helpers::{Parentheses, get_mark_decorator
/// import pytest
///
///
/// @pytest.mark.foo
/// @pytest.mark.foo()
/// def test_something(): ...
/// ```
///

View File

@@ -76,11 +76,11 @@ impl Violation for PytestWarnsWithMultipleStatements {
///
///
/// def test_foo():
/// with pytest.warns(Warning):
/// with pytest.warns(RuntimeWarning):
/// ...
///
/// # empty string is also an error
/// with pytest.warns(Warning, match=""):
/// with pytest.warns(RuntimeWarning, match=""):
/// ...
/// ```
///
@@ -90,7 +90,7 @@ impl Violation for PytestWarnsWithMultipleStatements {
///
///
/// def test_foo():
/// with pytest.warns(Warning, match="expected message"):
/// with pytest.warns(RuntimeWarning, match="expected message"):
/// ...
/// ```
///

View File

@@ -19,12 +19,12 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
///
/// ## Example
/// ```python
/// foo = "bar\"s"
/// foo = 'bar\'s'
/// ```
///
/// Use instead:
/// ```python
/// foo = 'bar"s'
/// foo = "bar's"
/// ```
///
/// ## Formatter compatibility

View File

@@ -20,7 +20,6 @@ use crate::checkers::ast::Checker;
/// ## Example
/// ```python
/// fruits = ["apple", "banana", "cherry"]
/// i = 0
/// for fruit in fruits:
/// print(f"{i + 1}. {fruit}")
/// i += 1

View File

@@ -27,7 +27,6 @@ use crate::{Edit, Fix, FixAvailability, Violation};
///
/// ## Example
/// ```python
/// foo = {}
/// if "bar" in foo:
/// value = foo["bar"]
/// else:
@@ -36,7 +35,6 @@ use crate::{Edit, Fix, FixAvailability, Violation};
///
/// Use instead:
/// ```python
/// foo = {}
/// value = foo.get("bar", 0)
/// ```
///

View File

@@ -23,17 +23,15 @@ use crate::{Edit, Fix, FixAvailability, Violation};
///
/// ## Example
/// ```python
/// def foo():
/// for item in iterable:
/// if predicate(item):
/// return True
/// return False
/// for item in iterable:
/// if predicate(item):
/// return True
/// return False
/// ```
///
/// Use instead:
/// ```python
/// def foo():
/// return any(predicate(item) for item in iterable)
/// return any(predicate(item) for item in iterable)
/// ```
///
/// ## Fix safety

View File

@@ -11,8 +11,8 @@ use crate::{Violation, checkers::ast::Checker};
/// ## Why is this bad?
/// In assignment statements, starred expressions can be used to unpack iterables.
///
/// In Python 3, no more than `1 << 8` assignments are allowed before a starred
/// expression, and no more than `1 << 24` expressions are allowed after a starred
/// In Python 3, no more than 1 << 8 assignments are allowed before a starred
/// expression, and no more than 1 << 24 expressions are allowed after a starred
/// expression.
///
/// ## References

View File

@@ -44,13 +44,6 @@ use crate::{
/// print(platform.python_version())
/// ```
///
/// ## See also
/// This rule will ignore import statements configured in
/// [`lint.flake8-tidy-imports.banned-module-level-imports`][banned-module-level-imports]
/// if the rule [`banned-module-level-imports`][TID253] is enabled.
///
/// [banned-module-level-imports]: https://docs.astral.sh/ruff/settings/#lint_flake8-tidy-imports_banned-module-level-imports
/// [TID253]: https://docs.astral.sh/ruff/rules/banned-module-level-imports/
/// [PEP 8]: https://peps.python.org/pep-0008/#imports
#[derive(ViolationMetadata)]
pub(crate) struct ImportOutsideTopLevel;

View File

@@ -25,16 +25,14 @@ fn is_stdlib_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool {
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["dataclasses", "field"]))
}
/// Returns `true` if the given [`Expr`] is a call to an `attrs` field function.
/// Returns `true` if the given [`Expr`] is a call to `attr.ib()` or `attrs.field()`.
fn is_attrs_field(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["attrs", "field" | "Factory"]
// See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L33
| ["attr", "ib" | "attr" | "attrib" | "field" | "Factory"]
["attrs", "field" | "Factory"] | ["attr", "ib"]
)
})
}
@@ -122,8 +120,7 @@ pub(super) fn dataclass_kind<'a>(
match qualified_name.segments() {
["attrs" | "attr", func @ ("define" | "frozen" | "mutable")]
// See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L32
| ["attr", func @ ("s" | "attributes" | "attrs")] => {
| ["attr", func @ ("s" | "attrs")] => {
// `.define`, `.frozen` and `.mutable` all default `auto_attribs` to `None`,
// whereas `@attr.s` implicitly sets `auto_attribs=False`.
// https://www.attrs.org/en/stable/api.html#attrs.define

View File

@@ -98,11 +98,3 @@ RUF009_attrs.py:127:12: RUF009 Do not perform function call `G` in dataclass def
127 | g: G = G()
| ^^^ RUF009
|
RUF009_attrs.py:144:20: RUF009 Do not perform function call `list` in dataclass defaults
|
142 | @attr.attributes
143 | class TestAttrAttributes:
144 | x: list[int] = list() # RUF009
| ^^^^^^ RUF009
|

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.12.2"
version = "0.12.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1996,8 +1996,7 @@ pub struct Flake8TidyImportsOptions {
/// List of specific modules that may not be imported at module level, and should instead be
/// imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:`
/// block, or some other nested context). This also affects the rule `import-outside-top-level`
/// if `banned-module-level-imports` is enabled.
/// block, or some other nested context).
#[option(
default = r#"[]"#,
value_type = r#"list[str]"#,
@@ -3587,7 +3586,7 @@ pub struct FormatOptions {
/// Setting `skip-magic-trailing-comma = true` changes the formatting to:
///
/// ```python
/// # The arguments are collapsed to a single line because the trailing comma is ignored
/// # The arguments remain on separate lines because of the trailing comma after `b`
/// def test(a, b):
/// pass
/// ```

View File

@@ -5,7 +5,7 @@ use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast;
use ruff_python_parser::{Token, TokenAt, TokenKind};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::{Completion, NameKind, SemanticModel};
use ty_python_semantic::{Completion, SemanticModel};
use crate::Db;
use crate::find_node::covering_node;
@@ -325,7 +325,38 @@ fn import_from_tokens(tokens: &[Token]) -> Option<&Token> {
/// This has the effect of putting all dunder attributes after "normal"
/// attributes, and all single-underscore attributes after dunder attributes.
fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering {
let (kind1, kind2) = (NameKind::classify(&c1.name), NameKind::classify(&c2.name));
/// A helper type for sorting completions based only on name.
///
/// This sorts "normal" names first, then dunder names and finally
/// single-underscore names. This matches the order of the variants defined for
/// this enum, which is in turn picked up by the derived trait implementation
/// for `Ord`.
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
enum Kind {
Normal,
Dunder,
Sunder,
}
impl Kind {
fn classify(c: &Completion) -> Kind {
// Dunder needs a prefix and suffix double underscore.
// When there's only a prefix double underscore, this
// results in explicit name mangling. We let that be
// classified as-if they were single underscore names.
//
// Ref: <https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers>
if c.name.starts_with("__") && c.name.ends_with("__") {
Kind::Dunder
} else if c.name.starts_with('_') {
Kind::Sunder
} else {
Kind::Normal
}
}
}
let (kind1, kind2) = (Kind::classify(c1), Kind::classify(c2));
kind1.cmp(&kind2).then_with(|| c1.name.cmp(&c2.name))
}
@@ -441,11 +472,6 @@ mod tests {
",
);
test.assert_completions_include("filter");
// Sunder items should be filtered out
test.assert_completions_do_not_include("_T");
// Dunder attributes should not be stripped
test.assert_completions_include("__annotations__");
// See `private_symbols_in_stub` for more comprehensive testing private of symbol filtering.
}
#[test]
@@ -510,112 +536,6 @@ re.<CURSOR>
test.assert_completions_include("findall");
}
#[test]
fn private_symbols_in_stub() {
let test = CursorTest::builder()
.source(
"package/__init__.pyi",
r#"\
from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol
public_name = 1
_private_name = 1
__mangled_name = 1
__dunder_name__ = 1
public_type_var = TypeVar("public_type_var")
_private_type_var = TypeVar("_private_type_var")
__mangled_type_var = TypeVar("__mangled_type_var")
public_param_spec = ParamSpec("public_param_spec")
_private_param_spec = ParamSpec("_private_param_spec")
public_type_var_tuple = TypeVarTuple("public_type_var_tuple")
_private_type_var_tuple = TypeVarTuple("_private_type_var_tuple")
public_explicit_type_alias: TypeAlias = Literal[1]
_private_explicit_type_alias: TypeAlias = Literal[1]
class PublicProtocol(Protocol):
def method(self) -> None: ...
class _PrivateProtocol(Protocol):
def method(self) -> None: ...
"#,
)
.source("main.py", "import package; package.<CURSOR>")
.build();
test.assert_completions_include("public_name");
test.assert_completions_include("_private_name");
test.assert_completions_include("__mangled_name");
test.assert_completions_include("__dunder_name__");
test.assert_completions_include("public_type_var");
test.assert_completions_do_not_include("_private_type_var");
test.assert_completions_do_not_include("__mangled_type_var");
test.assert_completions_include("public_param_spec");
test.assert_completions_do_not_include("_private_param_spec");
test.assert_completions_include("public_type_var_tuple");
test.assert_completions_do_not_include("_private_type_var_tuple");
test.assert_completions_include("public_explicit_type_alias");
test.assert_completions_include("_private_explicit_type_alias");
test.assert_completions_include("PublicProtocol");
test.assert_completions_do_not_include("_PrivateProtocol");
}
/// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names
/// are visible.
#[test]
fn private_symbols_in_module() {
let test = CursorTest::builder()
.source(
"package/__init__.py",
r#"\
from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol
public_name = 1
_private_name = 1
__mangled_name = 1
__dunder_name__ = 1
public_type_var = TypeVar("public_type_var")
_private_type_var = TypeVar("_private_type_var")
__mangled_type_var = TypeVar("__mangled_type_var")
public_param_spec = ParamSpec("public_param_spec")
_private_param_spec = ParamSpec("_private_param_spec")
public_type_var_tuple = TypeVarTuple("public_type_var_tuple")
_private_type_var_tuple = TypeVarTuple("_private_type_var_tuple")
public_explicit_type_alias: TypeAlias = Literal[1]
_private_explicit_type_alias: TypeAlias = Literal[1]
class PublicProtocol(Protocol):
def method(self) -> None: ...
class _PrivateProtocol(Protocol):
def method(self) -> None: ...
"#,
)
.source("main.py", "import package; package.<CURSOR>")
.build();
test.assert_completions_include("public_name");
test.assert_completions_include("_private_name");
test.assert_completions_include("__mangled_name");
test.assert_completions_include("__dunder_name__");
test.assert_completions_include("public_type_var");
test.assert_completions_include("_private_type_var");
test.assert_completions_include("__mangled_type_var");
test.assert_completions_include("public_param_spec");
test.assert_completions_include("_private_param_spec");
test.assert_completions_include("public_type_var_tuple");
test.assert_completions_include("_private_type_var_tuple");
test.assert_completions_include("public_explicit_type_alias");
test.assert_completions_include("_private_explicit_type_alias");
test.assert_completions_include("PublicProtocol");
test.assert_completions_include("_PrivateProtocol");
}
#[test]
fn one_function_prefix() {
let test = cursor_test(

View File

@@ -118,7 +118,7 @@ impl fmt::Display for DisplayHoverContent<'_, '_> {
match self.content {
HoverContent::Type(ty) => self
.kind
.fenced_code_block(ty.display(self.db), "python")
.fenced_code_block(ty.display(self.db), "text")
.fmt(f),
}
}
@@ -148,7 +148,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
Literal[10]
---------------------------------------------
```python
```text
Literal[10]
```
---------------------------------------------
@@ -184,7 +184,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
```python
```text
int
```
---------------------------------------------
@@ -214,7 +214,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
def foo(a, b) -> Unknown
---------------------------------------------
```python
```text
def foo(a, b) -> Unknown
```
---------------------------------------------
@@ -243,7 +243,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
bool
---------------------------------------------
```python
```text
bool
```
---------------------------------------------
@@ -274,7 +274,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
Literal[123]
---------------------------------------------
```python
```text
Literal[123]
```
---------------------------------------------
@@ -312,7 +312,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
(def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown)
---------------------------------------------
```python
```text
(def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown)
```
---------------------------------------------
@@ -344,7 +344,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
<module 'lib'>
---------------------------------------------
```python
```text
<module 'lib'>
```
---------------------------------------------
@@ -373,7 +373,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
T
---------------------------------------------
```python
```text
T
```
---------------------------------------------
@@ -399,7 +399,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
@Todo
---------------------------------------------
```python
```text
@Todo
```
---------------------------------------------
@@ -425,7 +425,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
@Todo
---------------------------------------------
```python
```text
@Todo
```
---------------------------------------------
@@ -451,7 +451,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
```python
```text
Literal[1]
```
---------------------------------------------
@@ -482,7 +482,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
```python
```text
Literal[1]
```
---------------------------------------------
@@ -512,7 +512,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
Literal[2]
---------------------------------------------
```python
```text
Literal[2]
```
---------------------------------------------
@@ -545,7 +545,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
Unknown | Literal[1]
---------------------------------------------
```python
```text
Unknown | Literal[1]
```
---------------------------------------------
@@ -574,7 +574,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
```python
```text
int
```
---------------------------------------------
@@ -602,7 +602,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
```python
```text
Literal[1]
```
---------------------------------------------
@@ -631,7 +631,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
```python
```text
int
```
---------------------------------------------
@@ -661,7 +661,7 @@ mod tests {
assert_snapshot!(test.hover(), @r"
str
---------------------------------------------
```python
```text
str
```
---------------------------------------------

View File

@@ -83,20 +83,15 @@ impl ProjectDatabase {
/// Checks all open files in the project and its dependencies.
pub fn check(&self) -> Vec<Diagnostic> {
self.check_with_mode(CheckMode::OpenFiles)
let mut reporter = DummyReporter;
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn Reporter);
self.project().check(self, reporter)
}
/// Checks all open files in the project and its dependencies, using the given reporter.
pub fn check_with_reporter(&self, reporter: &mut dyn Reporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, CheckMode::OpenFiles, reporter)
}
/// Check the project with the given mode.
pub fn check_with_mode(&self, mode: CheckMode) -> Vec<Diagnostic> {
let mut reporter = DummyReporter;
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn Reporter);
self.project().check(self, mode, reporter)
self.project().check(self, reporter)
}
#[tracing::instrument(level = "debug", skip(self))]
@@ -162,17 +157,6 @@ impl std::fmt::Debug for ProjectDatabase {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CheckMode {
/// Checks only the open files in the project.
OpenFiles,
/// Checks all files in the project, ignoring the open file set.
///
/// This includes virtual files, such as those created by the language server.
AllFiles,
}
/// Stores memory usage information.
pub struct SalsaMemoryDump {
total_fields: usize,

View File

@@ -1,7 +1,7 @@
use crate::glob::{GlobFilterCheckMode, IncludeResult};
use crate::metadata::options::{OptionDiagnostic, ToSettingsError};
use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
pub use db::{CheckMode, Db, ProjectDatabase, SalsaMemoryDump};
pub use db::{Db, ProjectDatabase, SalsaMemoryDump};
use files::{Index, Indexed, IndexedFiles};
use metadata::settings::Settings;
pub use metadata::{ProjectMetadata, ProjectMetadataError};
@@ -214,7 +214,6 @@ impl Project {
pub(crate) fn check(
self,
db: &ProjectDatabase,
mode: CheckMode,
mut reporter: AssertUnwindSafe<&mut dyn Reporter>,
) -> Vec<Diagnostic> {
let project_span = tracing::debug_span!("Project::check");
@@ -229,11 +228,7 @@ impl Project {
.map(OptionDiagnostic::to_diagnostic),
);
let files = match mode {
CheckMode::OpenFiles => ProjectFiles::new(db, self),
// TODO: Consider open virtual files as well
CheckMode::AllFiles => ProjectFiles::Indexed(self.files(db)),
};
let files = ProjectFiles::new(db, self);
reporter.set_files(files.len());
diagnostics.extend(

View File

@@ -4,7 +4,7 @@ expression: snapshot
---
---
mdtest name: dataclasses.md - Dataclasses - `dataclasses.KW_ONLY`
mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses.md
---
# Python source files

View File

@@ -1064,37 +1064,6 @@ static_assert(not is_assignable_to(A, Callable[[int], int]))
reveal_type(A()(1)) # revealed: str
```
### Subclass of
#### Type of a class with constructor methods
```py
from typing import Callable
from ty_extensions import static_assert, is_assignable_to
class A:
def __init__(self, x: int) -> None: ...
class B:
def __new__(cls, x: str) -> "B":
return super().__new__(cls)
static_assert(is_assignable_to(type[A], Callable[[int], A]))
static_assert(not is_assignable_to(type[A], Callable[[str], A]))
static_assert(is_assignable_to(type[B], Callable[[str], B]))
static_assert(not is_assignable_to(type[B], Callable[[int], B]))
```
#### Type with no generic parameters
```py
from typing import Callable, Any
from ty_extensions import static_assert, is_assignable_to
static_assert(is_assignable_to(type, Callable[..., Any]))
```
## Generics
### Assignability of generic types parameterized by gradual types

View File

@@ -1752,28 +1752,6 @@ static_assert(not is_subtype_of(TypeOf[F], Callable[[], str]))
static_assert(not is_subtype_of(TypeOf[F], Callable[[int], F]))
```
### Subclass of
#### Type of a class with constructor methods
```py
from typing import Callable
from ty_extensions import TypeOf, static_assert, is_subtype_of
class A:
def __init__(self, x: int) -> None: ...
class B:
def __new__(cls, x: str) -> "B":
return super().__new__(cls)
static_assert(is_subtype_of(type[A], Callable[[int], A]))
static_assert(not is_subtype_of(type[A], Callable[[str], A]))
static_assert(is_subtype_of(type[B], Callable[[str], B]))
static_assert(not is_subtype_of(type[B], Callable[[int], B]))
```
### Bound methods
```py

View File

@@ -15,7 +15,7 @@ pub use program::{
PythonVersionWithSource, SearchPathSettings,
};
pub use python_platform::PythonPlatform;
pub use semantic_model::{Completion, HasType, NameKind, SemanticModel};
pub use semantic_model::{Completion, HasType, SemanticModel};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;

View File

@@ -68,10 +68,12 @@ impl<'db> SemanticModel<'db> {
return vec![];
};
let ty = Type::module_literal(self.db, self.file, &module);
let builtin = module.is_known(KnownModule::Builtins);
crate::types::all_members(self.db, ty)
.into_iter()
.map(|name| Completion { name, builtin })
.map(|name| Completion {
name,
builtin: module.is_known(KnownModule::Builtins),
})
.collect()
}
@@ -128,39 +130,6 @@ impl<'db> SemanticModel<'db> {
}
}
/// A classification of symbol names.
///
/// The ordering here is used for sorting completions.
///
/// This sorts "normal" names first, then dunder names and finally
/// single-underscore names. This matches the order of the variants defined for
/// this enum, which is in turn picked up by the derived trait implementation
/// for `Ord`.
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
pub enum NameKind {
Normal,
Dunder,
Sunder,
}
impl NameKind {
pub fn classify(name: &Name) -> NameKind {
// Dunder needs a prefix and suffix double underscore.
// When there's only a prefix double underscore, this
// results in explicit name mangling. We let that be
// classified as-if they were single underscore names.
//
// Ref: <https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers>
if name.starts_with("__") && name.ends_with("__") {
NameKind::Dunder
} else if name.starts_with('_') {
NameKind::Sunder
} else {
NameKind::Normal
}
}
}
/// A suggestion for code completion.
#[derive(Clone, Debug)]
pub struct Completion {

View File

@@ -412,14 +412,6 @@ impl<'db> PropertyInstanceType<'db> {
self.setter(db).map(|ty| ty.materialize(db, variance)),
)
}
fn any_over_type(self, db: &'db dyn Db, type_fn: &dyn Fn(Type<'db>) -> bool) -> bool {
self.getter(db)
.is_some_and(|ty| ty.any_over_type(db, type_fn))
|| self
.setter(db)
.is_some_and(|ty| ty.any_over_type(db, type_fn))
}
}
bitflags! {
@@ -753,110 +745,6 @@ impl<'db> Type<'db> {
}
}
/// Return `true` if `self`, or any of the types contained in `self`, match the closure passed in.
pub fn any_over_type(self, db: &'db dyn Db, type_fn: &dyn Fn(Type<'db>) -> bool) -> bool {
if type_fn(self) {
return true;
}
match self {
Self::AlwaysFalsy
| Self::AlwaysTruthy
| Self::Never
| Self::BooleanLiteral(_)
| Self::BytesLiteral(_)
| Self::ModuleLiteral(_)
| Self::FunctionLiteral(_)
| Self::ClassLiteral(_)
| Self::SpecialForm(_)
| Self::KnownInstance(_)
| Self::StringLiteral(_)
| Self::IntLiteral(_)
| Self::LiteralString
| Self::Dynamic(_)
| Self::BoundMethod(_)
| Self::WrapperDescriptor(_)
| Self::MethodWrapper(_)
| Self::DataclassDecorator(_)
| Self::DataclassTransformer(_) => false,
Self::GenericAlias(generic) => generic
.specialization(db)
.types(db)
.iter()
.copied()
.any(|ty| ty.any_over_type(db, type_fn)),
Self::Callable(callable) => {
let signatures = callable.signatures(db);
signatures.iter().any(|signature| {
signature.parameters().iter().any(|param| {
param
.annotated_type()
.is_some_and(|ty| ty.any_over_type(db, type_fn))
}) || signature
.return_ty
.is_some_and(|ty| ty.any_over_type(db, type_fn))
})
}
Self::SubclassOf(subclass_of) => {
Type::from(subclass_of.subclass_of()).any_over_type(db, type_fn)
}
Self::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.any_over_type(db, type_fn)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.any(|constraint| constraint.any_over_type(db, type_fn)),
},
Self::BoundSuper(bound_super) => {
Type::from(bound_super.pivot_class(db)).any_over_type(db, type_fn)
|| Type::from(bound_super.owner(db)).any_over_type(db, type_fn)
}
Self::Tuple(tuple) => tuple
.tuple(db)
.all_elements()
.any(|ty| ty.any_over_type(db, type_fn)),
Self::Union(union) => union
.elements(db)
.iter()
.any(|ty| ty.any_over_type(db, type_fn)),
Self::Intersection(intersection) => {
intersection
.positive(db)
.iter()
.any(|ty| ty.any_over_type(db, type_fn))
|| intersection
.negative(db)
.iter()
.any(|ty| ty.any_over_type(db, type_fn))
}
Self::ProtocolInstance(protocol) => protocol.any_over_type(db, type_fn),
Self::PropertyInstance(property) => property.any_over_type(db, type_fn),
Self::NominalInstance(instance) => match instance.class {
ClassType::NonGeneric(_) => false,
ClassType::Generic(generic) => generic
.specialization(db)
.types(db)
.iter()
.any(|ty| ty.any_over_type(db, type_fn)),
},
Self::TypeIs(type_is) => type_is.return_type(db).any_over_type(db, type_fn),
}
}
pub const fn into_class_literal(self) -> Option<ClassLiteral<'db>> {
match self {
Type::ClassLiteral(class_type) => Some(class_type),
@@ -1559,16 +1447,6 @@ impl<'db> Type<'db> {
.into_callable(db)
.has_relation_to(db, target, relation),
// TODO: This is unsound so in future we can consider an opt-in option to disable it.
(Type::SubclassOf(subclass_of_ty), Type::Callable(_))
if subclass_of_ty.subclass_of().into_class().is_some() =>
{
let class = subclass_of_ty.subclass_of().into_class().unwrap();
class
.into_callable(db)
.has_relation_to(db, target, relation)
}
// `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`.
// `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object
// is an instance of its metaclass `abc.ABCMeta`.

View File

@@ -1145,11 +1145,11 @@ impl KnownFunction {
let [Some(casted_type), Some(source_type)] = parameter_types else {
return None;
};
let contains_unknown_or_todo =
let is_unknown_or_todo =
|ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
if source_type.is_equivalent_to(db, *casted_type)
&& !casted_type.any_over_type(db, &|ty| contains_unknown_or_todo(ty))
&& !source_type.any_over_type(db, &|ty| contains_unknown_or_todo(ty))
&& !is_unknown_or_todo(*casted_type)
&& !is_unknown_or_todo(*source_type)
{
let builder = context.report_lint(&REDUNDANT_CAST, call_expression)?;
builder.into_diagnostic(format_args!(

View File

@@ -1,10 +1,10 @@
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
use crate::Db;
use crate::place::{imported_symbol, place_from_bindings, place_from_declarations};
use crate::semantic_index::place::ScopeId;
use crate::semantic_index::{
attribute_scopes, global_scope, imported_modules, place_table, semantic_index, use_def_map,
};
use crate::types::{ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type};
use crate::{Db, NameKind};
use crate::types::{ClassBase, ClassLiteral, KnownClass, Type};
use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet;
@@ -144,41 +144,13 @@ impl AllMembers {
let Some(symbol_name) = place_table.place_expr(symbol_id).as_name() else {
continue;
};
let Place::Type(ty, _) = imported_symbol(db, file, symbol_name, None).place
else {
continue;
};
// Filter private symbols from stubs if they appear to be internal types
let is_stub_file = file.path(db).extension() == Some("pyi");
let is_private_symbol = match NameKind::classify(symbol_name) {
NameKind::Dunder | NameKind::Normal => false,
NameKind::Sunder => true,
};
if is_private_symbol && is_stub_file {
match ty {
Type::NominalInstance(instance)
if matches!(
instance.class.known(db),
Some(
KnownClass::TypeVar
| KnownClass::TypeVarTuple
| KnownClass::ParamSpec
)
) =>
{
continue;
}
Type::ClassLiteral(class) if class.is_protocol(db) => continue,
Type::KnownInstance(
KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_),
) => continue,
_ => {}
}
if !imported_symbol(db, file, symbol_name, None)
.place
.is_unbound()
{
self.members
.insert(place_table.place_expr(symbol_id).expect_name().clone());
}
self.members
.insert(place_table.place_expr(symbol_id).expect_name().clone());
}
let module_name = module.name();

View File

@@ -229,15 +229,6 @@ impl<'db> ProtocolInstanceType<'db> {
}
}
/// Return `true` if the types of any of the members match the closure passed in.
pub(super) fn any_over_type(
self,
db: &'db dyn Db,
type_fn: &dyn Fn(Type<'db>) -> bool,
) -> bool {
self.inner.interface(db).any_over_type(db, type_fn)
}
/// Return `true` if this protocol type has the given type relation to the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence

View File

@@ -152,16 +152,6 @@ impl<'db> ProtocolInterface<'db> {
.all(|member_name| other.inner(db).contains_key(member_name))
}
/// Return `true` if the types of any of the members match the closure passed in.
pub(super) fn any_over_type(
self,
db: &'db dyn Db,
type_fn: &dyn Fn(Type<'db>) -> bool,
) -> bool {
self.members(db)
.any(|member| member.any_over_type(db, type_fn))
}
pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeVisitor<'db>) -> Self {
Self::new(
db,
@@ -371,14 +361,6 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
}
}
}
fn any_over_type(&self, db: &'db dyn Db, type_fn: &dyn Fn(Type<'db>) -> bool) -> bool {
match &self.kind {
ProtocolMemberKind::Method(callable) => callable.any_over_type(db, type_fn),
ProtocolMemberKind::Property(property) => property.any_over_type(db, type_fn),
ProtocolMemberKind::Other(ty) => ty.any_over_type(db, type_fn),
}
}
}
/// Returns `true` if a declaration or binding to a given name in a protocol class body

View File

@@ -4,9 +4,10 @@
//! are written to `stderr` by default, which should appear in the logs for most LSP clients. A
//! `logFile` path can also be specified in the settings, and output will be directed there
//! instead.
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use ruff_db::system::{SystemPath, SystemPathBuf};
use serde::Deserialize;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::Layer;
@@ -14,14 +15,14 @@ use tracing_subscriber::fmt::time::ChronoLocal;
use tracing_subscriber::fmt::writer::BoxMakeWriter;
use tracing_subscriber::layer::SubscriberExt;
pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&SystemPath>) {
pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&Path>) {
let log_file = log_file
.map(|path| {
// this expands `logFile` so that tildes and environment variables
// are replaced with their values, if possible.
if let Some(expanded) = shellexpand::full(&path.to_string())
if let Some(expanded) = shellexpand::full(&path.to_string_lossy())
.ok()
.map(|path| SystemPathBuf::from(&*path))
.and_then(|path| PathBuf::from_str(&path).ok())
{
expanded
} else {
@@ -32,11 +33,14 @@ pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&SystemPath>) {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path.as_std_path())
.open(&path)
.map_err(|err| {
#[expect(clippy::print_stderr)]
{
eprintln!("Failed to open file at {path} for logging: {err}");
eprintln!(
"Failed to open file at {} for logging: {err}",
path.display()
);
}
})
.ok()

View File

@@ -173,7 +173,6 @@ impl Server {
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
identifier: Some(crate::DIAGNOSTIC_NAME.into()),
inter_file_dependencies: true,
workspace_diagnostics: true,
..Default::default()
})),
text_document_sync: Some(TextDocumentSyncCapability::Options(

View File

@@ -28,28 +28,23 @@ pub(super) fn request(req: server::Request) -> Task {
let id = req.id.clone();
match req.method.as_str() {
requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::<
requests::DocumentDiagnosticRequestHandler::METHOD => background_request_task::<
requests::DocumentDiagnosticRequestHandler,
>(
req, BackgroundSchedule::Worker
),
requests::WorkspaceDiagnosticRequestHandler::METHOD => background_request_task::<
requests::WorkspaceDiagnosticRequestHandler,
>(
req, BackgroundSchedule::Worker
),
requests::GotoTypeDefinitionRequestHandler::METHOD => background_document_request_task::<
requests::GotoTypeDefinitionRequestHandler::METHOD => background_request_task::<
requests::GotoTypeDefinitionRequestHandler,
>(
req, BackgroundSchedule::Worker
),
requests::HoverRequestHandler::METHOD => background_document_request_task::<
requests::HoverRequestHandler::METHOD => background_request_task::<
requests::HoverRequestHandler,
>(req, BackgroundSchedule::Worker),
requests::InlayHintRequestHandler::METHOD => background_document_request_task::<
requests::InlayHintRequestHandler::METHOD => background_request_task::<
requests::InlayHintRequestHandler,
>(req, BackgroundSchedule::Worker),
requests::CompletionRequestHandler::METHOD => background_document_request_task::<
requests::CompletionRequestHandler::METHOD => background_request_task::<
requests::CompletionRequestHandler,
>(
req, BackgroundSchedule::LatencySensitive
@@ -140,51 +135,7 @@ where
}))
}
fn background_request_task<R: traits::BackgroundRequestHandler>(
req: server::Request,
schedule: BackgroundSchedule,
) -> Result<Task>
where
<<R as RequestHandler>::RequestType as Request>::Params: UnwindSafe,
{
let retry = R::RETRY_ON_CANCELLATION.then(|| req.clone());
let (id, params) = cast_request::<R>(req)?;
Ok(Task::background(schedule, move |session: &Session| {
let cancellation_token = session
.request_queue()
.incoming()
.cancellation_token(&id)
.expect("request should have been tested for cancellation before scheduling");
let snapshot = session.take_workspace_snapshot();
Box::new(move |client| {
let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered();
// Test again if the request was cancelled since it was scheduled on the background task
// and, if so, return early
if cancellation_token.is_cancelled() {
tracing::trace!(
"Ignoring request id={id} method={} because it was cancelled",
R::METHOD
);
// We don't need to send a response here because the `cancel` notification
// handler already responded with a message.
return;
}
let result = ruff_db::panic::catch_unwind(|| R::run(snapshot, client, params));
if let Some(response) = request_result_to_response::<R>(&id, client, result, retry) {
respond::<R>(&id, response, client);
}
})
}))
}
fn background_document_request_task<R: traits::BackgroundDocumentRequestHandler>(
fn background_request_task<R: traits::BackgroundDocumentRequestHandler>(
req: server::Request,
schedule: BackgroundSchedule,
) -> Result<Task>
@@ -217,7 +168,7 @@ where
};
let Some(snapshot) = session.take_snapshot(url) else {
tracing::warn!("Ignoring request because snapshot for path `{path:?}` doesn't exist");
tracing::warn!("Ignoring request because snapshot for path `{path:?}` doesn't exist.");
return Box::new(|_| {});
};
@@ -258,7 +209,7 @@ fn request_result_to_response<R>(
request: Option<lsp_server::Request>,
) -> Option<Result<<<R as RequestHandler>::RequestType as Request>::Result>>
where
R: traits::RetriableRequestHandler,
R: traits::BackgroundDocumentRequestHandler,
{
match result {
Ok(response) => Some(response),

View File

@@ -166,7 +166,7 @@ pub(super) fn compute_diagnostics(
/// Converts the tool specific [`Diagnostic`][ruff_db::diagnostic::Diagnostic] to an LSP
/// [`Diagnostic`].
pub(super) fn to_lsp_diagnostic(
fn to_lsp_diagnostic(
db: &dyn Db,
diagnostic: &ruff_db::diagnostic::Diagnostic,
encoding: PositionEncoding,

View File

@@ -41,12 +41,7 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
);
}
if !session.global_settings().diagnostic_mode().is_workspace() {
// The server needs to clear the diagnostics regardless of whether the client supports
// pull diagnostics or not. This is because the client only has the capability to fetch
// the diagnostics but does not automatically clear them when a document is closed.
clear_diagnostics(&key, client);
}
clear_diagnostics(&key, client);
Ok(())
}

View File

@@ -4,7 +4,6 @@ mod goto_type_definition;
mod hover;
mod inlay_hints;
mod shutdown;
mod workspace_diagnostic;
pub(super) use completion::CompletionRequestHandler;
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
@@ -12,4 +11,3 @@ pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler;
pub(super) use hover::HoverRequestHandler;
pub(super) use inlay_hints::InlayHintRequestHandler;
pub(super) use shutdown::ShutdownHandler;
pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler;

View File

@@ -8,9 +8,7 @@ use ty_project::ProjectDatabase;
use crate::DocumentSnapshot;
use crate::document::PositionExt;
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::session::client::Client;
pub(crate) struct CompletionRequestHandler;
@@ -20,6 +18,8 @@ impl RequestHandler for CompletionRequestHandler {
}
impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
const RETRY_ON_CANCELLATION: bool = true;
fn document_url(params: &CompletionParams) -> Cow<Url> {
Cow::Borrowed(&params.text_document_position.text_document.uri)
}
@@ -65,7 +65,3 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
Ok(Some(response))
}
}
impl RetriableRequestHandler for CompletionRequestHandler {
const RETRY_ON_CANCELLATION: bool = true;
}

View File

@@ -8,9 +8,7 @@ use lsp_types::{
use crate::server::Result;
use crate::server::api::diagnostics::{Diagnostics, compute_diagnostics};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::session::DocumentSnapshot;
use crate::session::client::Client;
use ty_project::ProjectDatabase;
@@ -45,9 +43,7 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
}),
))
}
}
impl RetriableRequestHandler for DocumentDiagnosticRequestHandler {
fn salsa_cancellation_error() -> lsp_server::ResponseError {
lsp_server::ResponseError {
code: lsp_server::ErrorCode::ServerCancelled as i32,

View File

@@ -8,9 +8,7 @@ use ty_project::ProjectDatabase;
use crate::DocumentSnapshot;
use crate::document::{PositionExt, ToLink};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::session::client::Client;
pub(crate) struct GotoTypeDefinitionRequestHandler;
@@ -72,5 +70,3 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler {
}
}
}
impl RetriableRequestHandler for GotoTypeDefinitionRequestHandler {}

View File

@@ -2,9 +2,7 @@ use std::borrow::Cow;
use crate::DocumentSnapshot;
use crate::document::{PositionExt, ToRangeExt};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::session::client::Client;
use lsp_types::request::HoverRequest;
use lsp_types::{HoverContents, HoverParams, MarkupContent, Url};
@@ -75,5 +73,3 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
}))
}
}
impl RetriableRequestHandler for HoverRequestHandler {}

View File

@@ -2,9 +2,7 @@ use std::borrow::Cow;
use crate::DocumentSnapshot;
use crate::document::{RangeExt, TextSizeExt};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::session::client::Client;
use lsp_types::request::InlayHintRequest;
use lsp_types::{InlayHintParams, Url};
@@ -66,5 +64,3 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
Ok(Some(inlay_hints))
}
}
impl RetriableRequestHandler for InlayHintRequestHandler {}

View File

@@ -1,108 +0,0 @@
use lsp_types::request::WorkspaceDiagnosticRequest;
use lsp_types::{
FullDocumentDiagnosticReport, Url, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport,
WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport,
WorkspaceFullDocumentDiagnosticReport,
};
use rustc_hash::FxHashMap;
use ty_project::CheckMode;
use crate::server::Result;
use crate::server::api::diagnostics::to_lsp_diagnostic;
use crate::server::api::traits::{
BackgroundRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::WorkspaceSnapshot;
use crate::session::client::Client;
use crate::system::file_to_url;
pub(crate) struct WorkspaceDiagnosticRequestHandler;
impl RequestHandler for WorkspaceDiagnosticRequestHandler {
type RequestType = WorkspaceDiagnosticRequest;
}
impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
fn run(
snapshot: WorkspaceSnapshot,
_client: &Client,
_params: WorkspaceDiagnosticParams,
) -> Result<WorkspaceDiagnosticReportResult> {
let index = snapshot.index();
if !index.global_settings().diagnostic_mode().is_workspace() {
tracing::debug!("Workspace diagnostics is disabled; returning empty report");
return Ok(WorkspaceDiagnosticReportResult::Report(
WorkspaceDiagnosticReport { items: vec![] },
));
}
let mut items = Vec::new();
for db in snapshot.projects() {
let diagnostics = db.check_with_mode(CheckMode::AllFiles);
// Group diagnostics by URL
let mut diagnostics_by_url: FxHashMap<Url, Vec<_>> = FxHashMap::default();
for diagnostic in diagnostics {
if let Some(span) = diagnostic.primary_span() {
let file = span.expect_ty_file();
let Some(url) = file_to_url(db, file) else {
tracing::debug!("Failed to convert file to URL at {}", file.path(db));
continue;
};
diagnostics_by_url.entry(url).or_default().push(diagnostic);
}
}
items.reserve(diagnostics_by_url.len());
// Convert to workspace diagnostic report format
for (url, file_diagnostics) in diagnostics_by_url {
let version = index
.key_from_url(url.clone())
.ok()
.and_then(|key| index.make_document_ref(&key))
.map(|doc| i64::from(doc.version()));
// Convert diagnostics to LSP format
let lsp_diagnostics = file_diagnostics
.into_iter()
.map(|diagnostic| {
to_lsp_diagnostic(db, &diagnostic, snapshot.position_encoding())
})
.collect::<Vec<_>>();
items.push(WorkspaceDocumentDiagnosticReport::Full(
WorkspaceFullDocumentDiagnosticReport {
uri: url,
version,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
// TODO: We don't implement result ID caching yet
result_id: None,
items: lsp_diagnostics,
},
},
));
}
}
Ok(WorkspaceDiagnosticReportResult::Report(
WorkspaceDiagnosticReport { items },
))
}
}
impl RetriableRequestHandler for WorkspaceDiagnosticRequestHandler {
fn salsa_cancellation_error() -> lsp_server::ResponseError {
lsp_server::ResponseError {
code: lsp_server::ErrorCode::ServerCancelled as i32,
message: "server cancelled the request".to_owned(),
data: serde_json::to_value(lsp_types::DiagnosticServerCancellationData {
retrigger_request: true,
})
.ok(),
}
}
}

View File

@@ -1,7 +1,7 @@
//! A stateful LSP implementation that calls into the ty API.
use crate::session::client::Client;
use crate::session::{DocumentSnapshot, Session, WorkspaceSnapshot};
use crate::session::{DocumentSnapshot, Session};
use lsp_types::notification::Notification as LSPNotification;
use lsp_types::request::Request;
@@ -25,24 +25,11 @@ pub(super) trait SyncRequestHandler: RequestHandler {
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
}
pub(super) trait RetriableRequestHandler: RequestHandler {
/// Whether this request can be cancelled if the Salsa database is modified.
/// A request handler that can be run on a background thread.
pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
/// Whether this request be retried if it was cancelled due to a modification to the Salsa database.
const RETRY_ON_CANCELLATION: bool = false;
/// The error to return if the request was cancelled due to a modification to the Salsa database.
fn salsa_cancellation_error() -> lsp_server::ResponseError {
lsp_server::ResponseError {
code: lsp_server::ErrorCode::ContentModified as i32,
message: "content modified".to_string(),
data: None,
}
}
}
/// A request handler that can be run on a background thread.
///
/// This handler is specific to requests that operate on a single document.
pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
fn document_url(
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
) -> std::borrow::Cow<lsp_types::Url>;
@@ -53,15 +40,14 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
client: &Client,
params: <<Self as RequestHandler>::RequestType as Request>::Params,
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
}
/// A request handler that can be run on a background thread.
pub(super) trait BackgroundRequestHandler: RetriableRequestHandler {
fn run(
snapshot: WorkspaceSnapshot,
client: &Client,
params: <<Self as RequestHandler>::RequestType as Request>::Params,
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
fn salsa_cancellation_error() -> lsp_server::ResponseError {
lsp_server::ResponseError {
code: lsp_server::ErrorCode::ContentModified as i32,
message: "content modified".to_string(),
data: None,
}
}
}
/// A supertrait for any server notification handler.

View File

@@ -2,7 +2,7 @@
use std::collections::{BTreeMap, VecDeque};
use std::ops::{Deref, DerefMut};
use std::panic::AssertUnwindSafe;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context, anyhow};
@@ -224,14 +224,6 @@ impl Session {
self.index().key_from_url(url)
}
pub(crate) fn take_workspace_snapshot(&self) -> WorkspaceSnapshot {
WorkspaceSnapshot {
projects: AssertUnwindSafe(self.projects.values().cloned().collect()),
index: self.index.clone().unwrap(),
position_encoding: self.position_encoding,
}
}
pub(crate) fn initialize_workspaces(&mut self, workspace_settings: Vec<(Url, ClientOptions)>) {
assert!(!self.workspaces.all_initialized());
@@ -243,7 +235,14 @@ impl Session {
// In the future, index the workspace directories to find all projects
// and create a project database for each.
let system = LSPSystem::new(self.index.as_ref().unwrap().clone());
let system_path = workspace.root();
let Some(system_path) = SystemPath::from_std_path(workspace.root()) else {
tracing::warn!(
"Ignore workspace `{}` because it's root contains non UTF8 characters",
workspace.root().display()
);
continue;
};
let root = system_path.to_path_buf();
let project = ProjectMetadata::discover(&root, &system)
@@ -383,10 +382,6 @@ impl Session {
pub(crate) fn client_capabilities(&self) -> &ResolvedClientCapabilities {
&self.resolved_client_capabilities
}
pub(crate) fn global_settings(&self) -> Arc<ClientSettings> {
self.index().global_settings()
}
}
/// A guard that holds the only reference to the index and allows modifying it.
@@ -466,27 +461,6 @@ impl DocumentSnapshot {
}
}
/// An immutable snapshot of the current state of [`Session`].
pub(crate) struct WorkspaceSnapshot {
projects: AssertUnwindSafe<Vec<ProjectDatabase>>,
index: Arc<index::Index>,
position_encoding: PositionEncoding,
}
impl WorkspaceSnapshot {
pub(crate) fn projects(&self) -> &[ProjectDatabase] {
&self.projects
}
pub(crate) fn index(&self) -> &index::Index {
&self.index
}
pub(crate) fn position_encoding(&self) -> PositionEncoding {
self.position_encoding
}
}
#[derive(Debug, Default)]
pub(crate) struct Workspaces {
workspaces: BTreeMap<Url, Workspace>,
@@ -499,15 +473,11 @@ impl Workspaces {
.to_file_path()
.map_err(|()| anyhow!("Workspace URL is not a file or directory: {url:?}"))?;
// Realistically I don't think this can fail because we got the path from a Url
let system_path = SystemPathBuf::from_path_buf(path)
.map_err(|_| anyhow!("Workspace URL is not valid UTF8"))?;
self.workspaces.insert(
url,
Workspace {
options,
root: system_path,
root: path,
},
);
@@ -550,12 +520,12 @@ impl<'a> IntoIterator for &'a Workspaces {
#[derive(Debug)]
pub(crate) struct Workspace {
root: SystemPathBuf,
root: PathBuf,
options: ClientOptions,
}
impl Workspace {
pub(crate) fn root(&self) -> &SystemPath {
pub(crate) fn root(&self) -> &Path {
&self.root
}
}

View File

@@ -1,5 +1,6 @@
use std::path::PathBuf;
use lsp_types::Url;
use ruff_db::system::SystemPathBuf;
use rustc_hash::FxHashMap;
use serde::Deserialize;
@@ -46,26 +47,6 @@ pub(crate) struct ClientOptions {
/// Settings under the `python.*` namespace in VS Code that are useful for the ty language
/// server.
python: Option<Python>,
/// Diagnostic mode for the language server.
diagnostic_mode: Option<DiagnosticMode>,
}
/// Diagnostic mode for the language server.
#[derive(Clone, Copy, Debug, Default, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) enum DiagnosticMode {
/// Check only currently open files.
#[default]
OpenFilesOnly,
/// Check all files in the workspace.
Workspace,
}
impl DiagnosticMode {
pub(crate) fn is_workspace(self) -> bool {
matches!(self, DiagnosticMode::Workspace)
}
}
impl ClientOptions {
@@ -77,7 +58,6 @@ impl ClientOptions {
.and_then(|python| python.ty)
.and_then(|ty| ty.disable_language_services)
.unwrap_or_default(),
diagnostic_mode: self.diagnostic_mode.unwrap_or_default(),
}
}
}
@@ -109,7 +89,7 @@ pub(crate) struct TracingOptions {
pub(crate) log_level: Option<LogLevel>,
/// Path to the log file - tildes and environment variables are supported.
pub(crate) log_file: Option<SystemPathBuf>,
pub(crate) log_file: Option<PathBuf>,
}
/// This is the exact schema for initialization options sent in by the client during

View File

@@ -1,5 +1,3 @@
use super::options::DiagnosticMode;
/// Resolved client settings for a specific document. These settings are meant to be
/// used directly by the server, and are *not* a 1:1 representation with how the client
/// sends them.
@@ -7,15 +5,10 @@ use super::options::DiagnosticMode;
#[cfg_attr(test, derive(PartialEq, Eq))]
pub(crate) struct ClientSettings {
pub(super) disable_language_services: bool,
pub(super) diagnostic_mode: DiagnosticMode,
}
impl ClientSettings {
pub(crate) fn is_language_services_disabled(&self) -> bool {
self.disable_language_services
}
pub(crate) fn diagnostic_mode(&self) -> DiagnosticMode {
self.diagnostic_mode
}
}

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.12.2-alpine
name: ghcr.io/astral-sh/ruff:0.12.1-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
rev: v0.12.1
hooks:
# Run the linter.
- id: ruff
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
rev: v0.12.1
hooks:
# Run the linter.
- id: ruff
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
rev: v0.12.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
rev: v0.12.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.12.2"
version = "0.12.1"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"

4
ruff.schema.json generated
View File

@@ -1438,7 +1438,7 @@
}
},
"banned-module-level-imports": {
"description": "List of specific modules that may not be imported at module level, and should instead be imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:` block, or some other nested context). This also affects the rule `import-outside-top-level` if `banned-module-level-imports` is enabled.",
"description": "List of specific modules that may not be imported at module level, and should instead be imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:` block, or some other nested context).",
"type": [
"array",
"null"
@@ -1588,7 +1588,7 @@
]
},
"skip-magic-trailing-comma": {
"description": "Ruff uses existing trailing commas as an indication that short lines should be left separate. If this option is set to `true`, the magic trailing comma is ignored.\n\nFor example, Ruff leaves the arguments separate even though collapsing the arguments to a single line doesn't exceed the line length if `skip-magic-trailing-comma = false`:\n\n```python # The arguments remain on separate lines because of the trailing comma after `b` def test( a, b, ): pass ```\n\nSetting `skip-magic-trailing-comma = true` changes the formatting to:\n\n```python # The arguments are collapsed to a single line because the trailing comma is ignored def test(a, b): pass ```",
"description": "Ruff uses existing trailing commas as an indication that short lines should be left separate. If this option is set to `true`, the magic trailing comma is ignored.\n\nFor example, Ruff leaves the arguments separate even though collapsing the arguments to a single line doesn't exceed the line length if `skip-magic-trailing-comma = false`:\n\n```python # The arguments remain on separate lines because of the trailing comma after `b` def test( a, b, ): pass ```\n\nSetting `skip-magic-trailing-comma = true` changes the formatting to:\n\n```python # The arguments remain on separate lines because of the trailing comma after `b` def test(a, b): pass ```",
"type": [
"boolean",
"null"

View File

@@ -1,6 +1,6 @@
[project]
name = "scripts"
version = "0.12.2"
version = "0.12.1"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]