Compare commits
26 Commits
dcreager/m
...
dcreager/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f33ca3a622 | ||
|
|
a79ba2036b | ||
|
|
d6f28b7428 | ||
|
|
9df9adae1e | ||
|
|
eec4e2ed11 | ||
|
|
a44fbd6658 | ||
|
|
22075d5ed7 | ||
|
|
5d451979c4 | ||
|
|
73773b4ea4 | ||
|
|
7cfdc4a550 | ||
|
|
2f0e7d6af7 | ||
|
|
8d44f8b7b5 | ||
|
|
c0faa2dc3d | ||
|
|
f4fff7fb24 | ||
|
|
a6bd68886f | ||
|
|
5c2c3f00ff | ||
|
|
5affc120b3 | ||
|
|
1e284933ec | ||
|
|
c529ee4f80 | ||
|
|
f88ff62da5 | ||
|
|
0115fd3757 | ||
|
|
cfbd42c22a | ||
|
|
fc3b341529 | ||
|
|
baaa8dad3a | ||
|
|
a21cde8a5a | ||
|
|
64edfb6ef6 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -666,7 +666,7 @@ jobs:
|
||||
- determine_changes
|
||||
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
|
||||
@@ -748,3 +748,7 @@ print(f"{ # Tuple with multiple elements that doesn't fit on a single line gets
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
|
||||
print(f"{ {}, 1, }")
|
||||
|
||||
|
||||
# The inner quotes should not be changed to double quotes before Python 3.12
|
||||
f"{f'''{'nested'} inner'''} outer"
|
||||
|
||||
@@ -144,6 +144,12 @@ pub(crate) enum InterpolatedStringState {
|
||||
///
|
||||
/// The containing `FStringContext` is the surrounding f-string context.
|
||||
InsideInterpolatedElement(InterpolatedStringContext),
|
||||
/// The formatter is inside more than one nested f-string, such as in `nested` in:
|
||||
///
|
||||
/// ```py
|
||||
/// f"{f'''{'nested'} inner'''} outer"
|
||||
/// ```
|
||||
NestedInterpolatedElement(InterpolatedStringContext),
|
||||
/// The formatter is outside an f-string.
|
||||
#[default]
|
||||
Outside,
|
||||
@@ -152,12 +158,18 @@ pub(crate) enum InterpolatedStringState {
|
||||
impl InterpolatedStringState {
|
||||
pub(crate) fn can_contain_line_breaks(self) -> Option<bool> {
|
||||
match self {
|
||||
InterpolatedStringState::InsideInterpolatedElement(context) => {
|
||||
InterpolatedStringState::InsideInterpolatedElement(context)
|
||||
| InterpolatedStringState::NestedInterpolatedElement(context) => {
|
||||
Some(context.is_multiline())
|
||||
}
|
||||
InterpolatedStringState::Outside => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the interpolated string state is [`NestedInterpolatedElement`].
|
||||
pub(crate) fn is_nested(self) -> bool {
|
||||
matches!(self, Self::NestedInterpolatedElement(..))
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of a top-level statement in the module.
|
||||
|
||||
@@ -181,10 +181,16 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
|
||||
|
||||
let item = format_with(|f: &mut PyFormatter| {
|
||||
// Update the context to be inside the f-string expression element.
|
||||
let f = &mut WithInterpolatedStringState::new(
|
||||
InterpolatedStringState::InsideInterpolatedElement(self.context),
|
||||
f,
|
||||
);
|
||||
let state = match f.context().interpolated_string_state() {
|
||||
InterpolatedStringState::InsideInterpolatedElement(_)
|
||||
| InterpolatedStringState::NestedInterpolatedElement(_) => {
|
||||
InterpolatedStringState::NestedInterpolatedElement(self.context)
|
||||
}
|
||||
InterpolatedStringState::Outside => {
|
||||
InterpolatedStringState::InsideInterpolatedElement(self.context)
|
||||
}
|
||||
};
|
||||
let f = &mut WithInterpolatedStringState::new(state, f);
|
||||
|
||||
write!(f, [bracket_spacing, expression.format()])?;
|
||||
|
||||
|
||||
@@ -46,8 +46,15 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
||||
.unwrap_or(self.context.options().quote_style());
|
||||
let supports_pep_701 = self.context.options().target_version().supports_pep_701();
|
||||
|
||||
// Preserve the existing quote style for nested interpolations more than one layer deep, if
|
||||
// PEP 701 isn't supported.
|
||||
if !supports_pep_701 && self.context.interpolated_string_state().is_nested() {
|
||||
return QuoteStyle::Preserve;
|
||||
}
|
||||
|
||||
// For f-strings and t-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
|
||||
if let InterpolatedStringState::InsideInterpolatedElement(parent_context) =
|
||||
if let InterpolatedStringState::InsideInterpolatedElement(parent_context)
|
||||
| InterpolatedStringState::NestedInterpolatedElement(parent_context) =
|
||||
self.context.interpolated_string_state()
|
||||
{
|
||||
let parent_flags = parent_context.flags();
|
||||
|
||||
@@ -28,12 +28,11 @@ but none started with prefix {parentdir_prefix}"
|
||||
f'{{NOT \'a\' "formatted" "value"}}'
|
||||
f"some f-string with {a} {few():.2f} {formatted.values!r}"
|
||||
-f'some f-string with {a} {few(""):.2f} {formatted.values!r}'
|
||||
-f"{f'''{'nested'} inner'''} outer"
|
||||
+f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
|
||||
f"{f'''{'nested'} inner'''} outer"
|
||||
-f"\"{f'{nested} inner'}\" outer"
|
||||
-f"space between opening braces: { {a for a in (1, 2, 3)}}"
|
||||
-f'Hello \'{tricky + "example"}\''
|
||||
+f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
|
||||
+f"{f'''{"nested"} inner'''} outer"
|
||||
+f'"{f"{nested} inner"}" outer'
|
||||
+f"space between opening braces: { {a for a in (1, 2, 3)} }"
|
||||
+f"Hello '{tricky + 'example'}'"
|
||||
@@ -49,7 +48,7 @@ f"{{NOT a formatted value}}"
|
||||
f'{{NOT \'a\' "formatted" "value"}}'
|
||||
f"some f-string with {a} {few():.2f} {formatted.values!r}"
|
||||
f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
|
||||
f"{f'''{"nested"} inner'''} outer"
|
||||
f"{f'''{'nested'} inner'''} outer"
|
||||
f'"{f"{nested} inner"}" outer'
|
||||
f"space between opening braces: { {a for a in (1, 2, 3)} }"
|
||||
f"Hello '{tricky + 'example'}'"
|
||||
@@ -72,17 +71,3 @@ f'Hello \'{tricky + "example"}\''
|
||||
f"Tried directories {str(rootdirs)} \
|
||||
but none started with prefix {parentdir_prefix}"
|
||||
```
|
||||
|
||||
## New Unsupported Syntax Errors
|
||||
|
||||
error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.10 (syntax was added in Python 3.12)
|
||||
--> fstring.py:6:9
|
||||
|
|
||||
4 | f"some f-string with {a} {few():.2f} {formatted.values!r}"
|
||||
5 | f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
|
||||
6 | f"{f'''{"nested"} inner'''} outer"
|
||||
| ^
|
||||
7 | f'"{f"{nested} inner"}" outer'
|
||||
8 | f"space between opening braces: { {a for a in (1, 2, 3)} }"
|
||||
|
|
||||
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.
|
||||
|
||||
@@ -754,6 +754,10 @@ print(f"{ # Tuple with multiple elements that doesn't fit on a single line gets
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
|
||||
print(f"{ {}, 1, }")
|
||||
|
||||
|
||||
# The inner quotes should not be changed to double quotes before Python 3.12
|
||||
f"{f'''{'nested'} inner'''} outer"
|
||||
```
|
||||
|
||||
## Outputs
|
||||
@@ -1532,7 +1536,7 @@ f'{f"""other " """}'
|
||||
f'{1: hy "user"}'
|
||||
f'{1:hy "user"}'
|
||||
f'{1: abcd "{1}" }'
|
||||
f'{1: abcd "{"aa"}" }'
|
||||
f'{1: abcd "{'aa'}" }'
|
||||
f'{1=: "abcd {'aa'}}'
|
||||
f"{x:a{z:hy \"user\"}} '''"
|
||||
|
||||
@@ -1581,6 +1585,10 @@ print(
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
|
||||
print(f"{ {}, 1 }")
|
||||
|
||||
|
||||
# The inner quotes should not be changed to double quotes before Python 3.12
|
||||
f"{f'''{'nested'} inner'''} outer"
|
||||
```
|
||||
|
||||
|
||||
@@ -2359,7 +2367,7 @@ f'{f"""other " """}'
|
||||
f'{1: hy "user"}'
|
||||
f'{1:hy "user"}'
|
||||
f'{1: abcd "{1}" }'
|
||||
f'{1: abcd "{"aa"}" }'
|
||||
f'{1: abcd "{'aa'}" }'
|
||||
f'{1=: "abcd {'aa'}}'
|
||||
f"{x:a{z:hy \"user\"}} '''"
|
||||
|
||||
@@ -2408,6 +2416,10 @@ print(
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
|
||||
print(f"{ {}, 1 }")
|
||||
|
||||
|
||||
# The inner quotes should not be changed to double quotes before Python 3.12
|
||||
f"{f'''{'nested'} inner'''} outer"
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -122,9 +122,6 @@ class CustomerModel(ModelBase):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
# TODO: this is not supported yet
|
||||
# error: [unknown-argument]
|
||||
# error: [unknown-argument]
|
||||
CustomerModel(id=1, name="Test")
|
||||
```
|
||||
|
||||
@@ -216,11 +213,7 @@ class OrderedModelBase: ...
|
||||
class TestWithBase(OrderedModelBase):
|
||||
inner: int
|
||||
|
||||
# TODO: No errors here, should reveal `bool`
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [unsupported-operator]
|
||||
reveal_type(TestWithBase(1) < TestWithBase(2)) # revealed: Unknown
|
||||
reveal_type(TestWithBase(1) < TestWithBase(2)) # revealed: bool
|
||||
```
|
||||
|
||||
### `kw_only_default`
|
||||
@@ -277,8 +270,7 @@ class ModelBase: ...
|
||||
class TestBase(ModelBase):
|
||||
name: str
|
||||
|
||||
# TODO: This should be `(self: TestBase, *, name: str) -> None`
|
||||
reveal_type(TestBase.__init__) # revealed: def __init__(self) -> None
|
||||
reveal_type(TestBase.__init__) # revealed: (self: TestBase, *, name: str) -> None
|
||||
```
|
||||
|
||||
### `frozen_default`
|
||||
@@ -333,12 +325,9 @@ class ModelBase: ...
|
||||
class TestMeta(ModelBase):
|
||||
name: str
|
||||
|
||||
# TODO: no error here
|
||||
# error: [unknown-argument]
|
||||
t = TestMeta(name="test")
|
||||
|
||||
# TODO: this should be an `invalid-assignment` error
|
||||
t.name = "new"
|
||||
t.name = "new" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
### Combining parameters
|
||||
@@ -437,19 +426,15 @@ class DefaultFrozenModel:
|
||||
class Frozen(DefaultFrozenModel):
|
||||
name: str
|
||||
|
||||
# TODO: no error here
|
||||
# error: [unknown-argument]
|
||||
f = Frozen(name="test")
|
||||
# TODO: this should be an `invalid-assignment` error
|
||||
f.name = "new"
|
||||
f.name = "new" # error: [invalid-assignment]
|
||||
|
||||
class Mutable(DefaultFrozenModel, frozen=False):
|
||||
name: str
|
||||
|
||||
# TODO: no error here
|
||||
# error: [unknown-argument]
|
||||
m = Mutable(name="test")
|
||||
m.name = "new" # No error
|
||||
# TODO: This should not be an error
|
||||
m.name = "new" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## `field_specifiers`
|
||||
@@ -532,12 +517,8 @@ class Person(FancyBase):
|
||||
name: str = fancy_field()
|
||||
age: int | None = fancy_field(kw_only=True)
|
||||
|
||||
# TODO: should be (self: Person, name: str = Unknown, *, age: int | None = Unknown) -> None
|
||||
reveal_type(Person.__init__) # revealed: def __init__(self) -> None
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
|
||||
|
||||
# TODO: shouldn't be an error
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [unknown-argument]
|
||||
alice = Person("Alice", age=30)
|
||||
|
||||
reveal_type(alice.id) # revealed: int
|
||||
|
||||
@@ -138,3 +138,27 @@ def _(n: int):
|
||||
# error: [unknown-argument]
|
||||
y = f("foo", name="bar", unknown="quux")
|
||||
```
|
||||
|
||||
### Truncation for long unions and literals
|
||||
|
||||
This test demonstrates a call where the expected type is a large mixed union. The diagnostic must
|
||||
therefore truncate the long expected union type to avoid overwhelming output.
|
||||
|
||||
```py
|
||||
from typing import Literal, Union
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
class E: ...
|
||||
class F: ...
|
||||
|
||||
def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int:
|
||||
return 0
|
||||
|
||||
def _(n: int):
|
||||
x = n
|
||||
# error: [invalid-argument-type]
|
||||
f1(x)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# Legacy namespace packages
|
||||
|
||||
## `__import__("pkgutil").extend_path`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
|
||||
```
|
||||
|
||||
`/airflow-core/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
||||
__version__ = "3.2.0"
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
|
||||
|
||||
```py
|
||||
__version__ = "9.15.0"
|
||||
```
|
||||
|
||||
`test.py`:
|
||||
|
||||
```py
|
||||
from airflow import __version__ as airflow_version
|
||||
from airflow.providers.amazon import __version__ as amazon_provider_version
|
||||
|
||||
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
|
||||
reveal_type(amazon_provider_version) # revealed: Literal["9.15.0"]
|
||||
```
|
||||
|
||||
## `pkgutil.extend_path`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
|
||||
```
|
||||
|
||||
`/airflow-core/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(__path__, __name__)
|
||||
__version__ = "3.2.0"
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(__path__, __name__)
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/__init__.py`:
|
||||
|
||||
```py
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(__path__, __name__)
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
|
||||
|
||||
```py
|
||||
__version__ = "9.15.0"
|
||||
```
|
||||
|
||||
`test.py`:
|
||||
|
||||
```py
|
||||
from airflow import __version__ as airflow_version
|
||||
from airflow.providers.amazon import __version__ as amazon_provider_version
|
||||
|
||||
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
|
||||
reveal_type(amazon_provider_version) # revealed: Literal["9.15.0"]
|
||||
```
|
||||
|
||||
## `extend_path` with keyword arguments
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
|
||||
```
|
||||
|
||||
`/airflow-core/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(name=__name__, path=__path__)
|
||||
__version__ = "3.2.0"
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(name=__name__, path=__path__)
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/__init__.py`:
|
||||
|
||||
```py
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(name=__name__, path=__path__)
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
|
||||
|
||||
```py
|
||||
__version__ = "9.15.0"
|
||||
```
|
||||
|
||||
`test.py`:
|
||||
|
||||
```py
|
||||
from airflow import __version__ as airflow_version
|
||||
from airflow.providers.amazon import __version__ as amazon_provider_version
|
||||
|
||||
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
|
||||
reveal_type(amazon_provider_version) # revealed: Literal["9.15.0"]
|
||||
```
|
||||
|
||||
## incorrect `__import__` arguments
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
|
||||
```
|
||||
|
||||
`/airflow-core/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("not_pkgutil").extend_path(__path__, __name__)
|
||||
__version__ = "3.2.0"
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("not_pkgutil").extend_path(__path__, __name__)
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("not_pkgutil").extend_path(__path__, __name__)
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
|
||||
|
||||
```py
|
||||
__version__ = "9.15.0"
|
||||
```
|
||||
|
||||
`test.py`:
|
||||
|
||||
```py
|
||||
from airflow.providers.amazon import __version__ as amazon_provider_version # error: [unresolved-import]
|
||||
from airflow import __version__ as airflow_version
|
||||
|
||||
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
|
||||
```
|
||||
|
||||
## incorrect `extend_path` arguments
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
|
||||
```
|
||||
|
||||
`/airflow-core/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("pkgutil").extend_path(__path__, "other_module")
|
||||
__version__ = "3.2.0"
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("pkgutil").extend_path(__path__, "other_module")
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/__init__.py`:
|
||||
|
||||
```py
|
||||
__path__ = __import__("pkgutil").extend_path(__path__, "other_module")
|
||||
```
|
||||
|
||||
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
|
||||
|
||||
```py
|
||||
__version__ = "9.15.0"
|
||||
```
|
||||
|
||||
`test.py`:
|
||||
|
||||
```py
|
||||
from airflow.providers.amazon import __version__ as amazon_provider_version # error: [unresolved-import]
|
||||
from airflow import __version__ as airflow_version
|
||||
|
||||
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
assertion_line: 427
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: union_call.md - Calling a union of function types - Try to cover all possible reasons - Truncation for long unions and literals
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import Literal, Union
|
||||
2 |
|
||||
3 | class A: ...
|
||||
4 | class B: ...
|
||||
5 | class C: ...
|
||||
6 | class D: ...
|
||||
7 | class E: ...
|
||||
8 | class F: ...
|
||||
9 |
|
||||
10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int:
|
||||
11 | return 0
|
||||
12 |
|
||||
13 | def _(n: int):
|
||||
14 | x = n
|
||||
15 | # error: [invalid-argument-type]
|
||||
16 | f1(x)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to function `f1` is incorrect
|
||||
--> src/mdtest_snippet.py:16:8
|
||||
|
|
||||
14 | x = n
|
||||
15 | # error: [invalid-argument-type]
|
||||
16 | f1(x)
|
||||
| ^ Expected `Literal[1, 2, 3, 4, 5, ... omitted 3 literals] | A | B | ... omitted 4 union elements`, found `int`
|
||||
|
|
||||
info: Function defined here
|
||||
--> src/mdtest_snippet.py:10:5
|
||||
|
|
||||
8 | class F: ...
|
||||
9 |
|
||||
10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int:
|
||||
| ^^ ----------------------------------------------------------- Parameter declared here
|
||||
11 | return 0
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
@@ -19,7 +19,10 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_python_ast::{PySourceType, PythonVersion};
|
||||
use ruff_python_ast::{
|
||||
self as ast, PySourceType, PythonVersion,
|
||||
visitor::{Visitor, walk_body},
|
||||
};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
@@ -1002,7 +1005,12 @@ where
|
||||
let is_regular_package = package_path.is_regular_package(resolver_state);
|
||||
|
||||
if is_regular_package {
|
||||
in_namespace_package = false;
|
||||
// This is the only place where we need to consider the existence of legacy namespace
|
||||
// packages, as we are explicitly searching for the *parent* package of the module
|
||||
// we actually want. Here, such a package should be treated as a PEP-420 ("modern")
|
||||
// namespace package. In all other contexts it acts like a normal package and needs
|
||||
// no special handling.
|
||||
in_namespace_package = is_legacy_namespace_package(&package_path, resolver_state);
|
||||
} else if package_path.is_directory(resolver_state)
|
||||
// Pure modules hide namespace packages with the same name
|
||||
&& resolve_file_module(&package_path, resolver_state).is_none()
|
||||
@@ -1039,6 +1047,62 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
/// Determines whether a package is a legacy namespace package.
|
||||
///
|
||||
/// Before PEP 420 introduced implicit namespace packages, the ecosystem developed
|
||||
/// its own form of namespace packages. These legacy namespace packages continue to persist
|
||||
/// in modern codebases because they work with ancient Pythons and if it ain't broke, don't fix it.
|
||||
///
|
||||
/// A legacy namespace package is distinguished by having an `__init__.py` that contains an
|
||||
/// expression to the effect of:
|
||||
///
|
||||
/// ```python
|
||||
/// __path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
||||
/// ```
|
||||
///
|
||||
/// The resulting package simultaneously has properties of both regular packages and namespace ones:
|
||||
///
|
||||
/// * Like regular packages, `__init__.py` is defined and can contain items other than submodules
|
||||
/// * Like implicit namespace packages, multiple copies of the package may exist with different
|
||||
/// submodules, and they will be merged into one namespace at runtime by the interpreter
|
||||
///
|
||||
/// Now, you may rightly wonder: "What if the `__init__.py` files have different contents?"
|
||||
/// The apparent official answer is: "Don't do that!"
|
||||
/// And the reality is: "Of course people do that!"
|
||||
///
|
||||
/// In practice we think it's fine to, just like with regular packages, use the first one
|
||||
/// we find on the search paths. To the extent that the different copies "need" to have the same
|
||||
/// contents, they all "need" to have the legacy namespace idiom (we do nothing to enforce that,
|
||||
/// we will just get confused if you mess it up).
|
||||
fn is_legacy_namespace_package(
|
||||
package_path: &ModulePath,
|
||||
resolver_state: &ResolverContext,
|
||||
) -> bool {
|
||||
// Just an optimization, the stdlib and typeshed are never legacy namespace packages
|
||||
if package_path.search_path().is_standard_library() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut package_path = package_path.clone();
|
||||
package_path.push("__init__");
|
||||
let Some(init) = resolve_file_module(&package_path, resolver_state) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// This is all syntax-only analysis so it *could* be fooled but it's really unlikely.
|
||||
//
|
||||
// The benefit of being syntax-only is speed and avoiding circular dependencies
|
||||
// between module resolution and semantic analysis.
|
||||
//
|
||||
// The downside is if you write slightly different syntax we will fail to detect the idiom,
|
||||
// but hey, this is better than nothing!
|
||||
let parsed = ruff_db::parsed::parsed_module(resolver_state.db, init);
|
||||
let mut visitor = LegacyNamespacePackageVisitor::default();
|
||||
visitor.visit_body(parsed.load(resolver_state.db).suite());
|
||||
|
||||
visitor.is_legacy_namespace_package
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ResolvedPackage {
|
||||
path: ModulePath,
|
||||
@@ -1148,6 +1212,124 @@ impl fmt::Display for RelaxedModuleName {
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects if a module contains a statement of the form:
|
||||
/// ```python
|
||||
/// __path__ = pkgutil.extend_path(__path__, __name__)
|
||||
/// ```
|
||||
/// or
|
||||
/// ```python
|
||||
/// __path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
struct LegacyNamespacePackageVisitor {
|
||||
is_legacy_namespace_package: bool,
|
||||
in_body: bool,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for LegacyNamespacePackageVisitor {
|
||||
fn visit_body(&mut self, body: &[ruff_python_ast::Stmt]) {
|
||||
if self.is_legacy_namespace_package {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't traverse into nested bodies.
|
||||
if self.in_body {
|
||||
return;
|
||||
}
|
||||
|
||||
self.in_body = true;
|
||||
|
||||
walk_body(self, body);
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &ast::Stmt) {
|
||||
if self.is_legacy_namespace_package {
|
||||
return;
|
||||
}
|
||||
|
||||
let ast::Stmt::Assign(ast::StmtAssign { value, targets, .. }) = stmt else {
|
||||
return;
|
||||
};
|
||||
|
||||
let [ast::Expr::Name(maybe_path)] = &**targets else {
|
||||
return;
|
||||
};
|
||||
|
||||
if &*maybe_path.id != "__path__" {
|
||||
return;
|
||||
}
|
||||
|
||||
let ast::Expr::Call(ast::ExprCall {
|
||||
func: extend_func,
|
||||
arguments: extend_arguments,
|
||||
..
|
||||
}) = &**value
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: maybe_pkg_util,
|
||||
attr: maybe_extend_path,
|
||||
..
|
||||
}) = &**extend_func
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Match if the left side of the attribute access is either `__import__("pkgutil")` or `pkgutil`
|
||||
match &**maybe_pkg_util {
|
||||
// __import__("pkgutil").extend_path(__path__, __name__)
|
||||
ast::Expr::Call(ruff_python_ast::ExprCall {
|
||||
func: maybe_import,
|
||||
arguments: import_arguments,
|
||||
..
|
||||
}) => {
|
||||
let ast::Expr::Name(maybe_import) = &**maybe_import else {
|
||||
return;
|
||||
};
|
||||
|
||||
if maybe_import.id() != "__import__" {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(ast::Expr::StringLiteral(name)) =
|
||||
import_arguments.find_argument_value("name", 0)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if name.value.to_str() != "pkgutil" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// "pkgutil.extend_path(__path__, __name__)"
|
||||
ast::Expr::Name(name) => {
|
||||
if name.id() != "pkgutil" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Test that this is an `extend_path(__path__, __name__)` call
|
||||
if maybe_extend_path != "extend_path" {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(ast::Expr::Name(path)) = extend_arguments.find_argument_value("path", 0) else {
|
||||
return;
|
||||
};
|
||||
let Some(ast::Expr::Name(name)) = extend_arguments.find_argument_value("name", 1) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.is_legacy_namespace_package = path.id() == "__path__" && name.id() == "__name__";
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::unpack::{Unpack, UnpackPosition};
|
||||
/// before this `Definition`. However, the ID can be considered stable and it is okay to use
|
||||
/// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs.
|
||||
#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
#[derive(Ord, PartialOrd)]
|
||||
pub struct Definition<'db> {
|
||||
/// The file in which the definition occurs.
|
||||
pub file: File,
|
||||
|
||||
@@ -8390,7 +8390,9 @@ fn lazy_bound_cycle_initial<'db>(
|
||||
}
|
||||
|
||||
/// Where a type variable is bound and usable.
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Update, get_size2::GetSize,
|
||||
)]
|
||||
pub enum BindingContext<'db> {
|
||||
/// The definition of the generic class, function, or type alias that binds this typevar.
|
||||
Definition(Definition<'db>),
|
||||
@@ -8424,7 +8426,9 @@ impl<'db> BindingContext<'db> {
|
||||
/// independent of the typevar's bounds or constraints. Two bound typevars have the same identity
|
||||
/// if they represent the same logical typevar bound in the same context, even if their bounds
|
||||
/// have been materialized differently.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, get_size2::GetSize, salsa::Update,
|
||||
)]
|
||||
pub struct BoundTypeVarIdentity<'db> {
|
||||
pub(crate) identity: TypeVarIdentity<'db>,
|
||||
pub(crate) binding_context: BindingContext<'db>,
|
||||
|
||||
@@ -190,7 +190,11 @@ pub(crate) enum CodeGeneratorKind<'db> {
|
||||
}
|
||||
|
||||
impl<'db> CodeGeneratorKind<'db> {
|
||||
pub(crate) fn from_class(db: &'db dyn Db, class: ClassLiteral<'db>) -> Option<Self> {
|
||||
pub(crate) fn from_class(
|
||||
db: &'db dyn Db,
|
||||
class: ClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Option<Self> {
|
||||
#[salsa::tracked(
|
||||
cycle_fn=code_generator_of_class_recover,
|
||||
cycle_initial=code_generator_of_class_initial,
|
||||
@@ -199,11 +203,20 @@ impl<'db> CodeGeneratorKind<'db> {
|
||||
fn code_generator_of_class<'db>(
|
||||
db: &'db dyn Db,
|
||||
class: ClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Option<CodeGeneratorKind<'db>> {
|
||||
if class.dataclass_params(db).is_some() {
|
||||
Some(CodeGeneratorKind::DataclassLike(None))
|
||||
} else if let Ok((_, Some(transformer_params))) = class.try_metaclass(db) {
|
||||
Some(CodeGeneratorKind::DataclassLike(Some(transformer_params)))
|
||||
} else if let Some(transformer_params) =
|
||||
class.iter_mro(db, specialization).skip(1).find_map(|base| {
|
||||
base.into_class().and_then(|class| {
|
||||
class.class_literal(db).0.dataclass_transformer_params(db)
|
||||
})
|
||||
})
|
||||
{
|
||||
Some(CodeGeneratorKind::DataclassLike(Some(transformer_params)))
|
||||
} else if class
|
||||
.explicit_bases(db)
|
||||
.contains(&Type::SpecialForm(SpecialFormType::NamedTuple))
|
||||
@@ -219,6 +232,7 @@ impl<'db> CodeGeneratorKind<'db> {
|
||||
fn code_generator_of_class_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_class: ClassLiteral<'db>,
|
||||
_specialization: Option<Specialization<'db>>,
|
||||
) -> Option<CodeGeneratorKind<'db>> {
|
||||
None
|
||||
}
|
||||
@@ -229,21 +243,37 @@ impl<'db> CodeGeneratorKind<'db> {
|
||||
_value: &Option<CodeGeneratorKind<'db>>,
|
||||
_count: u32,
|
||||
_class: ClassLiteral<'db>,
|
||||
_specialization: Option<Specialization<'db>>,
|
||||
) -> salsa::CycleRecoveryAction<Option<CodeGeneratorKind<'db>>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
code_generator_of_class(db, class)
|
||||
code_generator_of_class(db, class, specialization)
|
||||
}
|
||||
|
||||
pub(super) fn matches(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> bool {
|
||||
pub(super) fn matches(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
class: ClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> bool {
|
||||
matches!(
|
||||
(CodeGeneratorKind::from_class(db, class), self),
|
||||
(
|
||||
CodeGeneratorKind::from_class(db, class, specialization),
|
||||
self
|
||||
),
|
||||
(Some(Self::DataclassLike(_)), Self::DataclassLike(_))
|
||||
| (Some(Self::NamedTuple), Self::NamedTuple)
|
||||
| (Some(Self::TypedDict), Self::TypedDict)
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn dataclass_transformer_params(self) -> Option<DataclassTransformerParams<'db>> {
|
||||
match self {
|
||||
Self::DataclassLike(params) => params,
|
||||
Self::NamedTuple | Self::TypedDict => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A specialization of a generic class with a particular assignment of types to typevars.
|
||||
@@ -2200,7 +2230,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
};
|
||||
}
|
||||
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, self) {
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, self, specialization) {
|
||||
if let Some(field) = self
|
||||
.own_fields(db, specialization, CodeGeneratorKind::NamedTuple)
|
||||
.get(name)
|
||||
@@ -2262,7 +2292,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
) -> Option<Type<'db>> {
|
||||
let dataclass_params = self.dataclass_params(db);
|
||||
|
||||
let field_policy = CodeGeneratorKind::from_class(db, self)?;
|
||||
let field_policy = CodeGeneratorKind::from_class(db, self, specialization)?;
|
||||
|
||||
let transformer_params =
|
||||
if let CodeGeneratorKind::DataclassLike(Some(transformer_params)) = field_policy {
|
||||
@@ -2808,7 +2838,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
.filter_map(|superclass| {
|
||||
if let Some(class) = superclass.into_class() {
|
||||
let (class_literal, specialization) = class.class_literal(db);
|
||||
if field_policy.matches(db, class_literal) {
|
||||
if field_policy.matches(db, class_literal, specialization) {
|
||||
Some((class_literal, specialization))
|
||||
} else {
|
||||
None
|
||||
@@ -3623,7 +3653,7 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
.map(|class| class.variance_of(db, typevar));
|
||||
|
||||
let default_attribute_variance = {
|
||||
let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self);
|
||||
let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self, None);
|
||||
// Python 3.13 introduced a synthesized `__replace__` method on dataclasses which uses
|
||||
// their field types in contravariant position, thus meaning a frozen dataclass must
|
||||
// still be invariant in its field types. Other synthesized methods on dataclasses are
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ pub struct DisplaySettings<'db> {
|
||||
/// Class names that should be displayed fully qualified
|
||||
/// (e.g., `module.ClassName` instead of just `ClassName`)
|
||||
pub qualified: Rc<FxHashMap<&'db str, QualificationLevel>>,
|
||||
/// Whether long unions are displayed in full
|
||||
/// Whether long unions and literals are displayed in full
|
||||
pub preserve_full_unions: bool,
|
||||
}
|
||||
|
||||
@@ -1328,6 +1328,44 @@ impl Display for DisplayParameter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct TruncationPolicy {
|
||||
max: usize,
|
||||
max_when_elided: usize,
|
||||
}
|
||||
|
||||
impl TruncationPolicy {
|
||||
fn display_limit(self, total: usize, preserve_full: bool) -> usize {
|
||||
if preserve_full {
|
||||
return total;
|
||||
}
|
||||
let limit = if total > self.max {
|
||||
self.max_when_elided
|
||||
} else {
|
||||
self.max
|
||||
};
|
||||
limit.min(total)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DisplayOmitted {
|
||||
count: usize,
|
||||
singular: &'static str,
|
||||
plural: &'static str,
|
||||
}
|
||||
|
||||
impl Display for DisplayOmitted {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let noun = if self.count == 1 {
|
||||
self.singular
|
||||
} else {
|
||||
self.plural
|
||||
};
|
||||
write!(f, "... omitted {} {}", self.count, noun)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> UnionType<'db> {
|
||||
fn display_with(
|
||||
&'db self,
|
||||
@@ -1348,8 +1386,10 @@ struct DisplayUnionType<'db> {
|
||||
settings: DisplaySettings<'db>,
|
||||
}
|
||||
|
||||
const MAX_DISPLAYED_UNION_ITEMS: usize = 5;
|
||||
const MAX_DISPLAYED_UNION_ITEMS_WHEN_ELIDED: usize = 3;
|
||||
const UNION_POLICY: TruncationPolicy = TruncationPolicy {
|
||||
max: 5,
|
||||
max_when_elided: 3,
|
||||
};
|
||||
|
||||
impl Display for DisplayUnionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
@@ -1379,16 +1419,8 @@ impl Display for DisplayUnionType<'_> {
|
||||
|
||||
let mut join = f.join(" | ");
|
||||
|
||||
let display_limit = if self.settings.preserve_full_unions {
|
||||
total_entries
|
||||
} else {
|
||||
let limit = if total_entries > MAX_DISPLAYED_UNION_ITEMS {
|
||||
MAX_DISPLAYED_UNION_ITEMS_WHEN_ELIDED
|
||||
} else {
|
||||
MAX_DISPLAYED_UNION_ITEMS
|
||||
};
|
||||
limit.min(total_entries)
|
||||
};
|
||||
let display_limit =
|
||||
UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions);
|
||||
|
||||
let mut condensed_types = Some(condensed_types);
|
||||
let mut displayed_entries = 0usize;
|
||||
@@ -1420,8 +1452,10 @@ impl Display for DisplayUnionType<'_> {
|
||||
if !self.settings.preserve_full_unions {
|
||||
let omitted_entries = total_entries.saturating_sub(displayed_entries);
|
||||
if omitted_entries > 0 {
|
||||
join.entry(&DisplayUnionOmitted {
|
||||
join.entry(&DisplayOmitted {
|
||||
count: omitted_entries,
|
||||
singular: "union element",
|
||||
plural: "union elements",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1437,38 +1471,45 @@ impl fmt::Debug for DisplayUnionType<'_> {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayUnionOmitted {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Display for DisplayUnionOmitted {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let plural = if self.count == 1 {
|
||||
"element"
|
||||
} else {
|
||||
"elements"
|
||||
};
|
||||
write!(f, "... omitted {} union {}", self.count, plural)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayLiteralGroup<'db> {
|
||||
literals: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
settings: DisplaySettings<'db>,
|
||||
}
|
||||
|
||||
const LITERAL_POLICY: TruncationPolicy = TruncationPolicy {
|
||||
max: 7,
|
||||
max_when_elided: 5,
|
||||
};
|
||||
|
||||
impl Display for DisplayLiteralGroup<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Literal[")?;
|
||||
f.join(", ")
|
||||
.entries(
|
||||
self.literals
|
||||
.iter()
|
||||
.map(|ty| ty.representation(self.db, self.settings.singleline())),
|
||||
)
|
||||
.finish()?;
|
||||
|
||||
let total_entries = self.literals.len();
|
||||
|
||||
let display_limit =
|
||||
LITERAL_POLICY.display_limit(total_entries, self.settings.preserve_full_unions);
|
||||
|
||||
let mut join = f.join(", ");
|
||||
|
||||
for lit in self.literals.iter().take(display_limit) {
|
||||
let rep = lit.representation(self.db, self.settings.singleline());
|
||||
join.entry(&rep);
|
||||
}
|
||||
|
||||
if !self.settings.preserve_full_unions {
|
||||
let omitted_entries = total_entries.saturating_sub(display_limit);
|
||||
if omitted_entries > 0 {
|
||||
join.entry(&DisplayOmitted {
|
||||
count: omitted_entries,
|
||||
singular: "literal",
|
||||
plural: "literals",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
join.finish()?;
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ impl<'db> AllMembers<'db> {
|
||||
self.extend_with_instance_members(db, ty, class_literal);
|
||||
|
||||
// If this is a NamedTuple instance, include members from NamedTupleFallback
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, class_literal) {
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, class_literal, None) {
|
||||
self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db));
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ impl<'db> AllMembers<'db> {
|
||||
Type::ClassLiteral(class_literal) => {
|
||||
self.extend_with_class_members(db, ty, class_literal);
|
||||
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, class_literal) {
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, class_literal, None) {
|
||||
self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db));
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ impl<'db> AllMembers<'db> {
|
||||
|
||||
Type::GenericAlias(generic_alias) => {
|
||||
let class_literal = generic_alias.origin(db);
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, class_literal) {
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, class_literal, None) {
|
||||
self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db));
|
||||
}
|
||||
self.extend_with_class_members(db, ty, class_literal);
|
||||
@@ -164,7 +164,7 @@ impl<'db> AllMembers<'db> {
|
||||
let class_literal = class_type.class_literal(db).0;
|
||||
self.extend_with_class_members(db, ty, class_literal);
|
||||
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, class_literal) {
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, class_literal, None) {
|
||||
self.extend_with_type(
|
||||
db,
|
||||
KnownClass::NamedTupleFallback.to_class_literal(db),
|
||||
|
||||
@@ -577,7 +577,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_named_tuple = CodeGeneratorKind::NamedTuple.matches(self.db(), class);
|
||||
let is_named_tuple = CodeGeneratorKind::NamedTuple.matches(self.db(), class, None);
|
||||
|
||||
// (2) If it's a `NamedTuple` class, check that no field without a default value
|
||||
// appears after a field with a default value.
|
||||
@@ -898,7 +898,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
// (7) Check that a dataclass does not have more than one `KW_ONLY`.
|
||||
if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) =
|
||||
CodeGeneratorKind::from_class(self.db(), class)
|
||||
CodeGeneratorKind::from_class(self.db(), class, None)
|
||||
{
|
||||
let specialization = None;
|
||||
|
||||
@@ -4569,11 +4569,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.dataclass_params(db)
|
||||
.map(|params| SmallVec::from(params.field_specifiers(db)))
|
||||
.or_else(|| {
|
||||
class_literal
|
||||
.try_metaclass(db)
|
||||
.ok()
|
||||
.and_then(|(_, params)| params)
|
||||
.map(|params| SmallVec::from(params.field_specifiers(db)))
|
||||
Some(SmallVec::from(
|
||||
CodeGeneratorKind::from_class(db, class_literal, None)?
|
||||
.dataclass_transformer_params()?
|
||||
.field_specifiers(db),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,15 @@ export function persistLocal({
|
||||
settingsSource: string;
|
||||
pythonSource: string;
|
||||
}) {
|
||||
const totalLength = settingsSource.length + pythonSource.length;
|
||||
|
||||
// Don't persist large files to local storage because they can exceed the local storage quota
|
||||
// The number here is picked rarely arbitrarily. Also note, JS uses UTF 16:
|
||||
// that means the limit here is strings larger than 1MB (because UTf 16 uses 2 bytes per character)
|
||||
if (totalLength > 500_000) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
"source",
|
||||
JSON.stringify([settingsSource, pythonSource]),
|
||||
|
||||
@@ -40,6 +40,18 @@ export async function restore(): Promise<Workspace | null> {
|
||||
}
|
||||
|
||||
export function persistLocal(workspace: Workspace) {
|
||||
let totalLength = 0;
|
||||
for (const fileContent of Object.values(workspace.files)) {
|
||||
totalLength += fileContent.length;
|
||||
|
||||
// Don't persist large files to local storage because they can exceed the local storage quota
|
||||
// The number here is picked rarely arbitrarily. Also note, JS uses UTF 16:
|
||||
// that means the limit here is strings larger than 1MB (because UTf 16 uses 2 bytes per character)
|
||||
if (totalLength > 500_000) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem("workspace", JSON.stringify(workspace));
|
||||
}
|
||||
|
||||
|
||||
@@ -152,16 +152,13 @@ class FuzzResult:
|
||||
|
||||
def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult:
|
||||
"""Return a `FuzzResult` instance describing the fuzzing result from this seed."""
|
||||
# TODO debug slowness of these seeds
|
||||
skip_check = seed in {32, 56, 208}
|
||||
|
||||
code = generate_random_code(seed)
|
||||
bug_found = False
|
||||
minimizer_callback: Callable[[str], bool] | None = None
|
||||
|
||||
if args.baseline_executable_path is None:
|
||||
only_new_bugs = False
|
||||
if not skip_check and contains_bug(
|
||||
if contains_bug(
|
||||
code, executable=args.executable, executable_path=args.test_executable_path
|
||||
):
|
||||
bug_found = True
|
||||
@@ -172,7 +169,7 @@ def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult:
|
||||
)
|
||||
else:
|
||||
only_new_bugs = True
|
||||
if not skip_check and contains_new_bug(
|
||||
if contains_new_bug(
|
||||
code,
|
||||
executable=args.executable,
|
||||
test_executable_path=args.test_executable_path,
|
||||
|
||||
Reference in New Issue
Block a user