Compare commits

..

1 Commits

Author SHA1 Message Date
Zanie Blue
70cf8a94d5 Disable CRL checks during Windows test CI 2025-02-05 15:24:44 -06:00
686 changed files with 4077 additions and 7665 deletions

View File

@@ -217,6 +217,11 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
# There are spurious CRL server offline errors when downloading
# `cargo-bloat` with curl below, so we just disable them for now
- name: "Disable SChannel CRL checks"
run: |
reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /v EnableCRLCheck /t REG_DWORD /d 0 /f
- name: "Install cargo nextest"
uses: taiki-e/install-action@v2
with:

View File

@@ -1,64 +1,5 @@
# Changelog
## 0.9.5
### Preview features
- Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#15719](https://github.com/astral-sh/ruff/pull/15719))
- \[`flake8-comprehensions`\] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#15837](https://github.com/astral-sh/ruff/pull/15837))
- \[`flake8-logging`\] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#15799](https://github.com/astral-sh/ruff/pull/15799))
- \[`flake8-pyi`\] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#15853](https://github.com/astral-sh/ruff/pull/15853))
- \[`flake8-pyi`\] Fix more complex cases (`PYI019`) ([#15821](https://github.com/astral-sh/ruff/pull/15821))
- \[`flake8-pyi`\] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#15889](https://github.com/astral-sh/ruff/pull/15889))
- \[`flake8-pyi`\] Remove type parameter correctly when it is the last (`PYI019`) ([#15854](https://github.com/astral-sh/ruff/pull/15854))
- \[`pylint`\] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#15762](https://github.com/astral-sh/ruff/pull/15762))
- \[`pyupgrade`\] Better messages and diagnostic range (`UP015`) ([#15872](https://github.com/astral-sh/ruff/pull/15872))
- \[`pyupgrade`\] Rename private type parameters in PEP 695 generics (`UP049`) ([#15862](https://github.com/astral-sh/ruff/pull/15862))
- \[`refurb`\] Also report non-name expressions (`FURB169`) ([#15905](https://github.com/astral-sh/ruff/pull/15905))
- \[`refurb`\] Mark fix as unsafe if there are comments (`FURB171`) ([#15832](https://github.com/astral-sh/ruff/pull/15832))
- \[`ruff`\] Classes with mixed type variable style (`RUF053`) ([#15841](https://github.com/astral-sh/ruff/pull/15841))
- \[`airflow`\] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#15922](https://github.com/astral-sh/ruff/pull/15922))
- \[`flake8-pyi`\] Add autofix for unused-private-type-var (`PYI018`) ([#15999](https://github.com/astral-sh/ruff/pull/15999))
- \[`flake8-pyi`\] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#15888](https://github.com/astral-sh/ruff/pull/15888))
### Rule changes
- Preserve triple quotes and prefixes for strings ([#15818](https://github.com/astral-sh/ruff/pull/15818))
- \[`flake8-comprehensions`\] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#15838](https://github.com/astral-sh/ruff/pull/15838))
- \[`flake8-pyi`\] Rename `PYI019` and improve its diagnostic message ([#15885](https://github.com/astral-sh/ruff/pull/15885))
- \[`pep8-naming`\] Ignore `@override` methods (`N803`) ([#15954](https://github.com/astral-sh/ruff/pull/15954))
- \[`pyupgrade`\] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#15840](https://github.com/astral-sh/ruff/pull/15840))
- \[`ruff`\] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#15921](https://github.com/astral-sh/ruff/pull/15921))
- \[`pycodestyle`\] Exempt `sys.path += ...` calls (`E402`) ([#15980](https://github.com/astral-sh/ruff/pull/15980))
### Configuration
- Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#15918](https://github.com/astral-sh/ruff/pull/15918))
- Workaround Even Better TOML crash related to `allOf` ([#15992](https://github.com/astral-sh/ruff/pull/15992))
### Bug fixes
- \[`flake8-comprehensions`\] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#15877](https://github.com/astral-sh/ruff/pull/15877))
- \[`flake8-comprehensions`\] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#15929](https://github.com/astral-sh/ruff/pull/15929))
- \[`flake8-pyi`\] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#15851](https://github.com/astral-sh/ruff/pull/15851))
- \[`pep8-naming`\] Consider any number of leading underscore for `N801` ([#15988](https://github.com/astral-sh/ruff/pull/15988))
- \[`pyflakes`\] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#15829](https://github.com/astral-sh/ruff/pull/15829))
- \[`pylint`\] Correct min/max auto-fix and suggestion for (`PL1730`) ([#15930](https://github.com/astral-sh/ruff/pull/15930))
- \[`refurb`\] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#15953](https://github.com/astral-sh/ruff/pull/15953))
- \[`refurb`\] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#15779](https://github.com/astral-sh/ruff/pull/15779))
### Documentation
- Add deprecation warning for `ruff-lsp` related settings ([#15850](https://github.com/astral-sh/ruff/pull/15850))
- Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#15882](https://github.com/astral-sh/ruff/pull/15882))
- Fix a typo in `non_pep695_generic_class.rs` ([#15946](https://github.com/astral-sh/ruff/pull/15946))
- Improve Docs: Pylint subcategories' codes ([#15909](https://github.com/astral-sh/ruff/pull/15909))
- Remove non-existing `lint.extendIgnore` editor setting ([#15844](https://github.com/astral-sh/ruff/pull/15844))
- Update black deviations ([#15928](https://github.com/astral-sh/ruff/pull/15928))
- Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#15956](https://github.com/astral-sh/ruff/pull/15956))
- Add instance variable examples to `RUF012` ([#15982](https://github.com/astral-sh/ruff/pull/15982))
- Explain precedence for `ignore` and `select` config ([#15883](https://github.com/astral-sh/ruff/pull/15883))
## 0.9.4
### Preview features

11
Cargo.lock generated
View File

@@ -2439,7 +2439,6 @@ dependencies = [
"ruff_text_size",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
"thiserror 2.0.11",
"toml",
@@ -2479,7 +2478,6 @@ dependencies = [
"ruff_text_size",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
"smallvec",
"static_assertions",
@@ -2527,7 +2525,6 @@ dependencies = [
"regex",
"ruff_db",
"ruff_index",
"ruff_python_ast",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
@@ -2642,7 +2639,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"anyhow",
"argfile",
@@ -2770,7 +2767,6 @@ dependencies = [
"ruff_text_size",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
"tempfile",
"thiserror 2.0.11",
@@ -2795,7 +2791,6 @@ dependencies = [
"libcst",
"pretty_assertions",
"rayon",
"red_knot_project",
"regex",
"ruff",
"ruff_diagnostics",
@@ -2876,7 +2871,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3194,7 +3189,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -149,8 +149,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.9.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.5/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.9.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.4/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -183,7 +183,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.9.5
rev: v0.9.4
hooks:
# Run the linter.
- id: ruff
@@ -452,7 +452,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus))
- [Ibis](https://github.com/ibis-project/ibis)
- [ivy](https://github.com/unifyai/ivy)
- [JAX](https://github.com/jax-ml/jax)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Kraken Tech](https://kraken.tech/)
- [LangChain](https://github.com/hwchase17/langchain)

View File

@@ -28,7 +28,6 @@ pep440_rs = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
@@ -41,9 +40,8 @@ insta = { workspace = true, features = ["redactions", "ron"] }
[features]
default = ["zstd"]
deflate = ["red_knot_vendored/deflate"]
schemars = ["dep:schemars", "ruff_db/schemars", "red_knot_python_semantic/schemars"]
zstd = ["red_knot_vendored/zstd"]
deflate = ["red_knot_vendored/deflate"]
[lints]
workspace = true

View File

@@ -18,16 +18,13 @@ use thiserror::Error;
/// The options for the project.
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {
/// Configures the type checking environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<EnvironmentOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub src: Option<SrcOptions>,
/// Configures the enabled lints and their severity.
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Rules>,
}
@@ -180,22 +177,10 @@ impl Options {
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct EnvironmentOptions {
/// Specifies the version of Python that will be used to execute the source code.
/// The version should be specified as a string in the format `M.m` where `M` is the major version
/// and `m` is the minor (e.g. "3.0" or "3.6").
/// If a version is provided, knot will generate errors if the source code makes use of language features
/// that are not supported in that version.
/// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.
#[serde(skip_serializing_if = "Option::is_none")]
pub python_version: Option<RangedValue<PythonVersion>>,
/// Specifies the target platform that will be used to execute the source code.
/// If specified, Red Knot will tailor its use of type stub files,
/// which conditionalize type definitions based on the platform.
///
/// If no platform is specified, knot will use `all` or the current platform in the LSP use case.
#[serde(skip_serializing_if = "Option::is_none")]
pub python_platform: Option<RangedValue<PythonPlatform>>,
@@ -219,7 +204,6 @@ pub struct EnvironmentOptions {
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SrcOptions {
/// The root of the project, used for finding first-party modules.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -228,9 +212,7 @@ pub struct SrcOptions {
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Rules {
#[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))]
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
}
@@ -244,69 +226,6 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
}
}
#[cfg(feature = "schemars")]
mod schema {
use crate::DEFAULT_LINT_REGISTRY;
use red_knot_python_semantic::lint::Level;
use schemars::gen::SchemaGenerator;
use schemars::schema::{
InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation,
};
use schemars::JsonSchema;
pub(super) struct Rules;
impl JsonSchema for Rules {
fn schema_name() -> String {
"Rules".to_string()
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let registry = &*DEFAULT_LINT_REGISTRY;
let level_schema = gen.subschema_for::<Level>();
let properties: schemars::Map<String, Schema> = registry
.lints()
.iter()
.map(|lint| {
(
lint.name().to_string(),
Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
title: Some(lint.summary().to_string()),
description: Some(lint.documentation()),
deprecated: lint.status.is_deprecated(),
default: Some(lint.default_level.to_string().into()),
..Metadata::default()
})),
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![level_schema.clone()]),
..Default::default()
})),
..Default::default()
}),
)
})
.collect();
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties,
// Allow unknown rules: Red Knot will warn about them.
// It gives a better experience when using an older Red Knot version because
// the schema will not deny rules that have been removed in newer versions.
additional_properties: Some(Box::new(level_schema)),
..ObjectValidation::default()
})),
..Default::default()
})
}
}
}
#[derive(Error, Debug)]
pub enum KnotTomlError {
#[error(transparent)]

View File

@@ -1,9 +1,8 @@
use crate::combine::Combine;
use crate::Db;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_macros::Combine;
use ruff_text_size::{TextRange, TextSize};
use serde::{Deserialize, Deserializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::fmt;
@@ -71,19 +70,15 @@ impl Drop for ValueSourceGuard {
///
/// This ensures that two resolved configurations are identical even if the position of a value has changed
/// or if the values were loaded from different sources.
#[derive(Clone, serde::Serialize)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone)]
pub struct RangedValue<T> {
value: T,
#[serde(skip)]
source: ValueSource,
/// The byte range of `value` in `source`.
///
/// Can be `None` because not all sources support a range.
/// For example, arguments provided on the CLI won't have a range attached.
#[serde(skip)]
range: Option<TextRange>,
}
@@ -271,6 +266,18 @@ where
}
}
impl<T> Serialize for RangedValue<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.value.serialize(serializer)
}
}
/// A possibly relative path in a configuration file.
///
/// Relative paths in configuration files or from CLI options
@@ -279,19 +286,9 @@ where
/// * CLI: The path is relative to the current working directory
/// * Configuration file: The path is relative to the project's root.
#[derive(
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Combine,
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RelativePathBuf(RangedValue<SystemPathBuf>);
impl RelativePathBuf {
@@ -328,3 +325,13 @@ impl RelativePathBuf {
SystemPath::absolute(&self.0, relative_to)
}
}
impl Combine for RelativePathBuf {
fn combine(self, other: Self) -> Self {
Self(self.0.combine(other.0))
}
fn combine_with(&mut self, other: Self) {
self.0.combine_with(other.0);
}
}

View File

@@ -270,8 +270,6 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
/// Whether or not the .py/.pyi version of this file is expected to fail
#[rustfmt::skip]
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
// related to circular references in nested functions
("crates/ruff_linter/resources/test/fixtures/flake8_return/RET503.py", false, true),
// related to circular references in class definitions
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true),

View File

@@ -36,7 +36,6 @@ thiserror = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
hashbrown = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }

View File

@@ -232,36 +232,6 @@ reveal_type(c_instance.y) # revealed: int
reveal_type(c_instance.z) # revealed: int
```
#### Attributes defined in multi-target assignments
```py
class C:
def __init__(self) -> None:
self.a = self.b = 1
c_instance = C()
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
reveal_type(c_instance.b) # revealed: Unknown | Literal[1]
```
#### Augmented assignments
```py
class Weird:
def __iadd__(self, other: None) -> str:
return "a"
class C:
def __init__(self) -> None:
self.w = Weird()
self.w += None
# TODO: Mypy and pyright do not support this, but it would be great if we could
# infer `Unknown | str` or at least `Unknown | Weird | str` here.
reveal_type(C().w) # revealed: Unknown | Weird
```
#### Attributes defined in tuple unpackings
```py
@@ -283,24 +253,19 @@ reveal_type(c_instance.b1) # revealed: Unknown | Literal["a"]
reveal_type(c_instance.c1) # revealed: Unknown | int
reveal_type(c_instance.d1) # revealed: Unknown | str
reveal_type(c_instance.a2) # revealed: Unknown | Literal[1]
# TODO: This should be supported (no error; type should be: `Unknown | Literal[1]`)
# error: [unresolved-attribute]
reveal_type(c_instance.a2) # revealed: Unknown
reveal_type(c_instance.b2) # revealed: Unknown | Literal["a"]
# TODO: This should be supported (no error; type should be: `Unknown | Literal["a"]`)
# error: [unresolved-attribute]
reveal_type(c_instance.b2) # revealed: Unknown
reveal_type(c_instance.c2) # revealed: Unknown | int
reveal_type(c_instance.d2) # revealed: Unknown | str
```
#### Starred assignments
```py
class C:
def __init__(self) -> None:
self.a, *self.b = (1, 2, 3)
c_instance = C()
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking)
# TODO: Similar for these two (should be `Unknown | int` and `Unknown | str`, respectively)
# error: [unresolved-attribute]
reveal_type(c_instance.c2) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(c_instance.d2) # revealed: Unknown
```
#### Attributes defined in for-loop (unpacking)
@@ -322,8 +287,6 @@ class TupleIterable:
def __iter__(self) -> TupleIterator:
return TupleIterator()
class NonIterable: ...
class C:
def __init__(self):
for self.x in IntIterable():
@@ -332,54 +295,14 @@ class C:
for _, self.y in TupleIterable():
pass
# TODO: We should emit a diagnostic here
for self.z in NonIterable():
pass
# TODO: Pyright fully supports these, mypy detects the presence of the attributes,
# but infers type `Any` for both of them. We should infer `int` and `str` here:
reveal_type(C().x) # revealed: Unknown | int
reveal_type(C().y) # revealed: Unknown | str
```
#### Attributes defined in `with` statements
```py
class ContextManager:
def __enter__(self) -> int | None: ...
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
class C:
def __init__(self) -> None:
with ContextManager() as self.x:
pass
c_instance = C()
# TODO: Should be `Unknown | int | None`
# error: [unresolved-attribute]
reveal_type(c_instance.x) # revealed: Unknown
```
reveal_type(C().x) # revealed: Unknown
#### Attributes defined in comprehensions
```py
class IntIterator:
def __next__(self) -> int:
return 1
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
class C:
def __init__(self) -> None:
[... for self.a in IntIterable()]
c_instance = C()
# TODO: Should be `Unknown | int`
# error: [unresolved-attribute]
reveal_type(c_instance.a) # revealed: Unknown
reveal_type(C().y) # revealed: Unknown
```
#### Conditionally declared / bound attributes
@@ -470,6 +393,8 @@ reveal_type(D().x) # revealed: Unknown | Literal[1]
If `staticmethod` is something else, that should not influence the behavior:
`other.py`:
```py
def staticmethod(f):
return f
@@ -484,6 +409,8 @@ reveal_type(C().x) # revealed: Unknown | Literal[1]
And if `staticmethod` is fully qualified, that should also be recognized:
`fully_qualified.py`:
```py
import builtins
@@ -520,15 +447,6 @@ class C:
reveal_type(C().x) # revealed: str
```
#### Diagnostics are reported for the right-hand side of attribute assignments
```py
class C:
def __init__(self) -> None:
# error: [too-many-positional-arguments]
self.x: int = len(1, 2, 3)
```
### Pure class variables (`ClassVar`)
#### Annotated with `ClassVar` type qualifier
@@ -931,6 +849,8 @@ outer.nested.inner.Outer.Nested.Inner.attr = "a"
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
functions are instances of that class:
`a.py`:
```py
def f(): ...
@@ -940,7 +860,11 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
Some attributes are special-cased, however:
`b.py`:
```py
def f(): ...
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
```
@@ -950,6 +874,8 @@ reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
integers are instances of that class:
`a.py`:
```py
reveal_type((2).bit_length) # revealed: @Todo(bound method)
reveal_type((2).denominator) # revealed: @Todo(@property)
@@ -957,6 +883,8 @@ reveal_type((2).denominator) # revealed: @Todo(@property)
Some attributes are special-cased, however:
`b.py`:
```py
reveal_type((2).numerator) # revealed: Literal[2]
reveal_type((2).real) # revealed: Literal[2]
@@ -967,6 +895,8 @@ reveal_type((2).real) # revealed: Literal[2]
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
bols are instances of that class:
`a.py`:
```py
reveal_type(True.__and__) # revealed: @Todo(bound method)
reveal_type(False.__or__) # revealed: @Todo(bound method)
@@ -974,6 +904,8 @@ reveal_type(False.__or__) # revealed: @Todo(bound method)
Some attributes are special-cased, however:
`b.py`:
```py
reveal_type(True.numerator) # revealed: Literal[1]
reveal_type(False.real) # revealed: Literal[0]

View File

@@ -33,6 +33,8 @@ reveal_type(a >= b) # revealed: Literal[False]
Even when tuples have different lengths, comparisons should be handled appropriately.
`different_length.py`:
```py
a = (1, 2, 3)
b = (1, 2, 3, 4)
@@ -102,6 +104,8 @@ reveal_type(a >= b) # revealed: bool
However, if the lexicographic comparison completes without reaching a point where str and int are
compared, Python will still produce a result based on the prior elements.
`short_circuit.py`:
```py
a = (1, 2)
b = (999999, "hello")

View File

@@ -1,21 +0,0 @@
# Unpacking
<!-- snapshot-diagnostics -->
## Right hand side not iterable
```py
a, b = 1 # error: [not-iterable]
```
## Too many values to unpack
```py
a, b = (1, 2, 3) # error: [invalid-assignment]
```
## Too few values to unpack
```py
a, b = (1,) # error: [invalid-assignment]
```

View File

@@ -124,49 +124,42 @@ def _(e: Exception | type[Exception] | None):
## Exception cause is not an exception
```py
def _():
try:
raise EOFError() from GeneratorExit # fine
except:
...
try:
raise EOFError() from GeneratorExit # fine
except:
...
def _():
try:
raise StopIteration from MemoryError() # fine
except:
...
try:
raise StopIteration from MemoryError() # fine
except:
...
def _():
try:
raise BufferError() from None # fine
except:
...
try:
raise BufferError() from None # fine
except:
...
def _():
try:
raise ZeroDivisionError from False # error: [invalid-raise]
except:
...
try:
raise ZeroDivisionError from False # error: [invalid-raise]
except:
...
def _():
try:
raise SystemExit from bool() # error: [invalid-raise]
except:
...
try:
raise SystemExit from bool() # error: [invalid-raise]
except:
...
def _():
try:
raise
except KeyboardInterrupt as e: # fine
reveal_type(e) # revealed: KeyboardInterrupt
raise LookupError from e # fine
try:
raise
except KeyboardInterrupt as e: # fine
reveal_type(e) # revealed: KeyboardInterrupt
raise LookupError from e # fine
def _():
try:
raise
except int as e: # error: [invalid-exception-caught]
reveal_type(e) # revealed: Unknown
raise KeyError from e
try:
raise
except int as e: # error: [invalid-exception-caught]
reveal_type(e) # revealed: Unknown
raise KeyError from e
def _(e: Exception | type[Exception]):
raise ModuleNotFoundError from e # fine

View File

@@ -29,6 +29,8 @@ completing. The type of `x` at the beginning of the `except` suite in this examp
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
*after* that redefinition.
`union_type_inferred.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -50,7 +52,12 @@ reveal_type(x) # revealed: str | Literal[2]
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
inferred as having a union type following the `try`/`except` block:
`branches_unify_to_non_union_type.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -130,6 +137,8 @@ the `except` suite:
- At the end of `else`, `x == 3`
- At the end of `except`, `x == 2`
`single_except.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -158,6 +167,9 @@ been executed in its entirety, or the `try` suite and the `else` suite must both
in their entireties:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -186,6 +198,8 @@ A `finally` suite is *always* executed. As such, if we reach the `reveal_type` c
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
type of `x` at the end of the example is therefore `Literal[2]`:
`redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -211,7 +225,12 @@ at this point than there were when we were inside the `finally` block.
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
still a TODO item for us.)
`no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -240,6 +259,8 @@ suites:
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
`except` suite ran to completion
`redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -277,7 +298,18 @@ itself. (In some control-flow possibilities, some exceptions were merely *suspen
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
suite.)
`no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
x = 1
try:
@@ -299,7 +331,18 @@ reveal_type(x) # revealed: str | bool
An example with multiple `except` branches and a `finally` branch:
`multiple_except_branches.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
@@ -337,6 +380,8 @@ If the exception handler has an `else` branch, we must also take into account th
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
an exception raised *there*.
`single_except_branch.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -380,7 +425,24 @@ reveal_type(x) # revealed: bool | float
The same again, this time with multiple `except` branches:
`multiple_except_branches.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_range() -> range:
return range(42)

View File

@@ -218,21 +218,3 @@ import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
reveal_type(package.foo.X) # revealed: Unknown
```
## Relative imports at the top of a search path
Relative imports at the top of a search path result in a runtime error:
`ImportError: attempted relative import with no known parent package`. That's why Red Knot should
disallow them.
`parser.py`:
```py
X: int = 42
```
`__main__.py`:
```py
from .parser import X # error: [unresolved-import]
```

View File

@@ -29,6 +29,8 @@ def foo():
However, three attributes on `types.ModuleType` are not present as implicit module globals; these
are excluded:
`unbound_dunders.py`:
```py
# error: [unresolved-reference]
# revealed: Unknown
@@ -70,7 +72,11 @@ Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType`
dynamic imports; but we ignore that for module-literal types where we know exactly which module
we're dealing with:
`__getattr__.py`:
```py
import typing
# error: [unresolved-attribute]
reveal_type(typing.__getattr__) # revealed: Unknown
```

View File

@@ -5,6 +5,8 @@
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function.
No diagnostics should be generated.
`a.py`:
```py
def f(x: str):
x: int = int(x)
@@ -12,6 +14,8 @@ def f(x: str):
## Implicit error
`a.py`:
```py
def f(): ...
@@ -20,6 +24,8 @@ f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explici
## Explicit shadowing
`a.py`:
```py
def f(): ...

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
@@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:8
--> /src/mdtest_snippet__1.py:1:8
|
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | # Topmost component resolvable, submodule not resolvable:
@@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:2:8
--> /src/mdtest_snippet__1.py:2:8
|
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
@@ -41,7 +41,7 @@ error: lint:unresolved-import
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:5:8
--> /src/mdtest_snippet__1.py:5:8
|
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Right hand side not iterable
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = 1 # error: [not-iterable]
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:1:8
|
1 | a, b = 1 # error: [not-iterable]
| ^ Object of type `Literal[1]` is not iterable
|
```

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Too few values to unpack
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = (1,) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1,) # error: [invalid-assignment]
| ^^^^ Not enough values to unpack (expected 2, got 1)
|
```

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Too many values to unpack
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
| ^^^^ Too many values to unpack (expected 2, got 3)
|
```

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | import does_not_exist # error: [unresolved-import]
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:8
--> /src/mdtest_snippet__1.py:1:8
|
1 | import does_not_exist # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`

View File

@@ -16,7 +16,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
2 | does_exist2 = 2
```
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
@@ -26,7 +26,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:28
--> /src/mdtest_snippet__1.py:1:28
|
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Module `a` has no member `does_not_exist`

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | from .does_not_exist import add # error: [unresolved-import]
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:7
--> /src/mdtest_snippet__1.py:1:7
|
1 | from .does_not_exist import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist`

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:7
--> /src/mdtest_snippet__1.py:1:7
|
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar`

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | from does_not_exist import add # error: [unresolved-import]
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:6
--> /src/mdtest_snippet__1.py:1:6
|
1 | from does_not_exist import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`

View File

@@ -7,36 +7,43 @@ branches whose conditions we can statically determine to be always true or alway
useful for `sys.version_info` branches, which can make new features available based on the Python
version:
If we can statically determine that the condition is always true, then we can also understand that
`SomeFeature` is always bound, without raising any errors:
`module1.py`:
```py
import sys
class C:
if sys.version_info >= (3, 9):
SomeFeature: str = "available"
if sys.version_info >= (3, 9):
SomeFeature: str = "available"
```
# C.SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
reveal_type(C.SomeFeature) # revealed: str
If we can statically determine that the condition is always true, then we can also understand that
`SomeFeature` is always bound, without raising any errors:
`test1.py`:
```py
from module1 import SomeFeature
# SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
reveal_type(SomeFeature) # revealed: str
```
Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used
for conditional imports:
`module.py`:
`module2.py`:
```py
class SomeType: ...
```
`main.py`:
`test2.py`:
```py
import typing
if typing.TYPE_CHECKING:
from module import SomeType
from module2 import SomeType
# `SomeType` is unconditionally available here for type checkers:
def f(s: SomeType) -> None: ...

View File

@@ -37,6 +37,8 @@ child expression now suppresses errors in the outer expression.
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
`"test"` and adding `"other"` to the result of the cast.
`nested.py`:
```py
# fmt: off
from typing import cast

View File

@@ -109,6 +109,8 @@ reveal_type(version_info >= (3, 9)) # revealed: bool
The fields of `sys.version_info` can be accessed by name:
`a.py`:
```py
import sys
@@ -120,7 +122,11 @@ reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
properties on instance types:
`b.py`:
```py
import sys
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property)
reveal_type(sys.version_info.serial) # revealed: @Todo(@property)

View File

@@ -452,9 +452,6 @@ def raise_in_both_branches(cond: bool):
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
else:
# This branch is unreachable, since all control flows in the `try` clause raise exceptions.
# As a result, this binding should never be reachable, since new bindings are visible only
# when they are reachable.
x = "unreachable"
finally:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
@@ -626,9 +623,9 @@ def return_from_nested_if(cond1: bool, cond2: bool):
## Statically known terminal statements
We model reachability using the same visibility constraints that we use to model statically known
bounds. In this example, we see that the `return` statement is always executed, and therefore that
the `"b"` assignment is not visible to the `reveal_type`.
Terminal statements do not yet interact correctly with statically known bounds. In this example, we
should see that the `return` statement is always executed, and therefore that the `"b"` assignment
is not visible to the `reveal_type`.
```py
def _(cond: bool):
@@ -638,26 +635,6 @@ def _(cond: bool):
if True:
return
reveal_type(x) # revealed: Literal["a"]
```
## Bindings after a terminal statement are unreachable
Any bindings introduced after a terminal statement are unreachable, and are currently considered not
visible. We [anticipate](https://github.com/astral-sh/ruff/issues/15797) that we want to provide a
more useful analysis for code after terminal statements.
```py
def f(cond: bool) -> str:
x = "before"
if cond:
reveal_type(x) # revealed: Literal["before"]
return
x = "after-return"
# TODO: no unresolved-reference error
# error: [unresolved-reference]
reveal_type(x) # revealed: Unknown
else:
x = "else"
reveal_type(x) # revealed: Literal["else"]
# TODO: Literal["a"]
reveal_type(x) # revealed: Literal["a", "b"]
```

View File

@@ -84,11 +84,8 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: tuple[str, Unknown]
reveal_type(z) # revealed: Unknown | Literal[1]
```
`Unknown` can be subclassed, just like `Any`:
```py
# Unknown can be subclassed, just like Any
class C(Unknown): ...
# revealed: tuple[Literal[C], Unknown, Literal[object]]
@@ -241,12 +238,9 @@ error_message = "A custom message "
error_message += "constructed from multiple string literals"
# error: "Static assertion error: A custom message constructed from multiple string literals"
static_assert(False, error_message)
```
There are limitations to what we can still infer as a string literal. In those cases, we simply fall
back to the default message:
```py
# There are limitations to what we can still infer as a string literal. In those cases,
# we simply fall back to the default message.
shouted_message = "A custom message".upper()
# error: "Static assertion error: argument evaluates to `False`"
static_assert(False, shouted_message)
@@ -377,11 +371,8 @@ static_assert(is_subtype_of(TypeOf[str], type[str]))
class Base: ...
class Derived(Base): ...
```
`TypeOf` can also be used in annotations:
```py
# `TypeOf` can be used in annotations:
def type_of_annotation() -> None:
t1: TypeOf[Base] = Base
t2: TypeOf[Base] = Derived # error: [invalid-assignment]

View File

@@ -132,27 +132,6 @@ static_assert(not is_disjoint_from(Intersection[X, Z], Y))
static_assert(not is_disjoint_from(Intersection[Y, Z], X))
```
## Negation / complement
The complement of a type `T` is disjoint from `T`. In fact, it is disjoint from every subtype of
`T`:
```py
from knot_extensions import Not, Intersection, is_disjoint_from, static_assert
class T: ...
class S(T): ...
static_assert(is_disjoint_from(Not[T], T))
static_assert(is_disjoint_from(Not[T], S))
static_assert(is_disjoint_from(Intersection[T, Any], Not[T]))
static_assert(is_disjoint_from(Not[T], Intersection[T, Any]))
static_assert(is_disjoint_from(Intersection[S, Any], Not[T]))
static_assert(is_disjoint_from(Not[T], Intersection[S, Any]))
```
## Special types
### `Never`
@@ -265,7 +244,7 @@ static_assert(not is_disjoint_from(TypeOf[f], object))
### `AlwaysTruthy` and `AlwaysFalsy`
```py
from knot_extensions import AlwaysFalsy, AlwaysTruthy, Intersection, Not, is_disjoint_from, static_assert
from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert
from typing import Literal
static_assert(is_disjoint_from(None, AlwaysTruthy))
@@ -277,14 +256,6 @@ static_assert(not is_disjoint_from(str, AlwaysTruthy))
static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy))
static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy))
type Truthy = Not[AlwaysFalsy]
type Falsy = Not[AlwaysTruthy]
type AmbiguousTruthiness = Intersection[Truthy, Falsy]
static_assert(is_disjoint_from(AlwaysTruthy, AmbiguousTruthiness))
static_assert(is_disjoint_from(AlwaysFalsy, AmbiguousTruthiness))
```
### Instance types versus `type[T]` types

View File

@@ -19,17 +19,11 @@ static_assert(is_equivalent_to(Never, tuple[int, Never]))
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
```
The empty `tuple` is *not* equivalent to `Never`!
```py
# The empty tuple is *not* equivalent to Never!
static_assert(not is_equivalent_to(Never, tuple[()]))
```
`NoReturn` is just a different spelling of `Never`, so the same is true for `NoReturn`:
```py
# NoReturn is just a different spelling of Never, so the same is true for NoReturn
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn]))
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn, int]))
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn]))

View File

@@ -1,8 +1,6 @@
use core::fmt;
use itertools::Itertools;
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
use rustc_hash::FxHashMap;
use std::fmt::Formatter;
use std::hash::Hasher;
use thiserror::Error;
@@ -38,20 +36,13 @@ pub struct LintMetadata {
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Level {
/// # Ignore
///
/// The lint is disabled and should not run.
Ignore,
/// # Warn
///
/// The lint is enabled and diagnostic should have a warning severity.
Warn,
/// # Error
///
/// The lint is enabled and diagnostics have an error severity.
Error,
}
@@ -70,16 +61,6 @@ impl Level {
}
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Level::Ignore => f.write_str("ignore"),
Level::Warn => f.write_str("warn"),
Level::Error => f.write_str("error"),
}
}
}
impl TryFrom<Level> for Severity {
type Error = ();
@@ -103,11 +84,9 @@ impl LintMetadata {
/// Returns the documentation line by line with one leading space and all trailing whitespace removed.
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
self.raw_documentation.lines().map(|line| {
line.strip_prefix(char::is_whitespace)
.unwrap_or(line)
.trim_end()
})
self.raw_documentation
.lines()
.map(|line| line.strip_prefix(' ').unwrap_or(line).trim_end())
}
/// Returns the documentation as a single string.
@@ -201,10 +180,6 @@ impl LintStatus {
pub const fn is_removed(&self) -> bool {
matches!(self, LintStatus::Removed { .. })
}
pub const fn is_deprecated(&self) -> bool {
matches!(self, LintStatus::Deprecated { .. })
}
}
/// Declares a lint rule with the given metadata.
@@ -248,7 +223,7 @@ macro_rules! declare_lint {
$vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
summary: $summary,
raw_documentation: concat!($($doc, '\n',)+),
raw_documentation: concat!($($doc,)+ "\n"),
status: $status,
file: file!(),
line: line!(),

View File

@@ -11,7 +11,6 @@ pub enum PythonPlatform {
/// Do not make any assumptions about the target platform.
#[default]
All,
/// Assume a specific target platform like `linux`, `darwin` or `win32`.
///
/// We use a string (instead of individual enum variants), as the set of possible platforms
@@ -29,77 +28,3 @@ impl Display for PythonPlatform {
}
}
}
#[cfg(feature = "schemars")]
mod schema {
use crate::PythonPlatform;
use schemars::_serde_json::Value;
use schemars::gen::SchemaGenerator;
use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation};
use schemars::JsonSchema;
impl JsonSchema for PythonPlatform {
fn schema_name() -> String {
"PythonPlatform".to_string()
}
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
// Hard code some well known values, but allow any other string as well.
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..SchemaObject::default()
}),
// Promote well-known values for better auto-completion.
// Using `const` over `enumValues` as recommended [here](https://github.com/SchemaStore/schemastore/blob/master/CONTRIBUTING.md#documenting-enums).
Schema::Object(SchemaObject {
const_value: Some(Value::String("all".to_string())),
metadata: Some(Box::new(Metadata {
description: Some(
"Do not make any assumptions about the target platform."
.to_string(),
),
..Metadata::default()
})),
..SchemaObject::default()
}),
Schema::Object(SchemaObject {
const_value: Some(Value::String("darwin".to_string())),
metadata: Some(Box::new(Metadata {
description: Some("Darwin".to_string()),
..Metadata::default()
})),
..SchemaObject::default()
}),
Schema::Object(SchemaObject {
const_value: Some(Value::String("linux".to_string())),
metadata: Some(Box::new(Metadata {
description: Some("Linux".to_string()),
..Metadata::default()
})),
..SchemaObject::default()
}),
Schema::Object(SchemaObject {
const_value: Some(Value::String("win32".to_string())),
metadata: Some(Box::new(Metadata {
description: Some("Windows".to_string()),
..Metadata::default()
})),
..SchemaObject::default()
}),
]),
..SubschemaValidation::default()
})),
..SchemaObject::default()
})
}
}
}

View File

@@ -31,20 +31,6 @@ impl PythonVersion {
minor: 13,
};
pub fn iter() -> impl Iterator<Item = PythonVersion> {
[
PythonVersion::PY37,
PythonVersion::PY38,
PythonVersion::PY39,
PythonVersion::PY310,
PythonVersion::PY311,
PythonVersion::PY312,
PythonVersion::PY313,
]
.iter()
.copied()
}
pub fn free_threaded_build_available(self) -> bool {
self >= PythonVersion::PY313
}
@@ -83,86 +69,40 @@ impl fmt::Display for PythonVersion {
}
#[cfg(feature = "serde")]
mod serde {
use crate::PythonVersion;
impl<'de> serde::Deserialize<'de> for PythonVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let as_str = String::deserialize(deserializer)?;
impl<'de> serde::Deserialize<'de> for PythonVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let as_str = String::deserialize(deserializer)?;
if let Some((major, minor)) = as_str.split_once('.') {
let major = major
.parse()
.map_err(|err| serde::de::Error::custom(format!("invalid major version: {err}")))?;
let minor = minor
.parse()
.map_err(|err| serde::de::Error::custom(format!("invalid minor version: {err}")))?;
if let Some((major, minor)) = as_str.split_once('.') {
let major = major.parse().map_err(|err| {
serde::de::Error::custom(format!("invalid major version: {err}"))
})?;
let minor = minor.parse().map_err(|err| {
serde::de::Error::custom(format!("invalid minor version: {err}"))
})?;
Ok((major, minor).into())
} else {
let major = as_str.parse().map_err(|err| {
serde::de::Error::custom(format!(
"invalid python-version: {err}, expected: `major.minor`"
))
})?;
Ok((major, minor).into())
} else {
let major = as_str.parse().map_err(|err| {
serde::de::Error::custom(format!(
"invalid python-version: {err}, expected: `major.minor`"
))
})?;
Ok((major, 0).into())
}
}
}
impl serde::Serialize for PythonVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
Ok((major, 0).into())
}
}
}
#[cfg(feature = "schemars")]
mod schemars {
use super::PythonVersion;
use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation};
use schemars::JsonSchema;
use schemars::_serde_json::Value;
impl JsonSchema for PythonVersion {
fn schema_name() -> String {
"PythonVersion".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> Schema {
let sub_schemas = std::iter::once(Schema::Object(SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
string: Some(Box::new(schemars::schema::StringValidation {
pattern: Some(r"^\d+\.\d+$".to_string()),
..Default::default()
})),
..Default::default()
}))
.chain(Self::iter().map(|v| {
Schema::Object(SchemaObject {
const_value: Some(Value::String(v.to_string())),
metadata: Some(Box::new(Metadata {
description: Some(format!("Python {v}")),
..Metadata::default()
})),
..SchemaObject::default()
})
}));
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(sub_schemas.collect()),
..Default::default()
})),
..SchemaObject::default()
})
}
#[cfg(feature = "serde")]
impl serde::Serialize for PythonVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

View File

@@ -1,7 +1,4 @@
use crate::{
semantic_index::{ast_ids::ScopedExpressionId, expression::Expression},
unpack::Unpack,
};
use crate::semantic_index::expression::Expression;
use ruff_python_ast::name::Name;
@@ -17,17 +14,6 @@ pub(crate) enum AttributeAssignment<'db> {
/// An attribute assignment without a type annotation, e.g. `self.x = <value>`.
Unannotated { value: Expression<'db> },
/// An attribute assignment where the right-hand side is an iterable, for example
/// `for self.x in <iterable>`.
Iterable { iterable: Expression<'db> },
/// An attribute assignment where the left-hand side is an unpacking expression,
/// e.g. `self.x, self.y = <value>`.
Unpack {
attribute_expression_id: ScopedExpressionId,
unpack: Unpack<'db>,
},
}
pub(crate) type AttributeAssignments<'db> = FxHashMap<Name, Vec<AttributeAssignment<'db>>>;

View File

@@ -6,9 +6,9 @@ use rustc_hash::{FxHashMap, FxHashSet};
use ruff_db::files::File;
use ruff_db::parsed::ParsedModule;
use ruff_index::IndexVec;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
use ruff_python_ast::{self as ast, ExprContext};
use crate::ast_node_ref::AstNodeRef;
use crate::module_name::ModuleName;
@@ -793,30 +793,9 @@ where
&mut builder.current_first_parameter_name,
&mut first_parameter_name,
);
// TODO: Fix how we determine the public types of symbols in a
// function-like scope: https://github.com/astral-sh/ruff/issues/15777
//
// In the meantime, visit the function body, but treat the last statement
// specially if it is a return. If it is, this would cause all definitions
// in the function to be marked as non-visible with our current treatment
// of terminal statements. Since we currently model the externally visible
// definitions in a function scope as the set of bindings that are visible
// at the end of the body, we then consider this function to have no
// externally visible definitions. To get around this, we take a flow
// snapshot just before processing the return statement, and use _that_ as
// the "end-of-body" state that we resolve external references against.
if let Some((last_stmt, first_stmts)) = body.split_last() {
builder.visit_body(first_stmts);
let pre_return_state = matches!(last_stmt, ast::Stmt::Return(_))
.then(|| builder.flow_snapshot());
builder.visit_stmt(last_stmt);
if let Some(pre_return_state) = pre_return_state {
builder.flow_restore(pre_return_state);
}
}
builder.visit_body(body);
builder.current_first_parameter_name = first_parameter_name;
builder.pop_scope()
},
);
@@ -1231,20 +1210,6 @@ where
unpack: None,
first: false,
}),
ast::Expr::Attribute(ast::ExprAttribute {
value: object,
attr,
..
}) => {
self.register_attribute_assignment(
object,
attr,
AttributeAssignment::Iterable {
iterable: iter_expr,
},
);
None
}
_ => None,
};
@@ -1473,7 +1438,7 @@ where
fn visit_expr(&mut self, expr: &'ast ast::Expr) {
self.scopes_by_expression
.insert(expr.into(), self.current_scope());
let expression_id = self.current_ast_ids().record_expression(expr);
self.current_ast_ids().record_expression(expr);
match expr {
ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => {
@@ -1732,35 +1697,6 @@ where
self.simplify_visibility_constraints(pre_op);
}
ast::Expr::Attribute(ast::ExprAttribute {
value: object,
attr,
ctx: ExprContext::Store,
range: _,
}) => {
if let Some(
CurrentAssignment::Assign {
unpack: Some(unpack),
..
}
| CurrentAssignment::For {
unpack: Some(unpack),
..
},
) = self.current_assignment()
{
self.register_attribute_assignment(
object,
attr,
AttributeAssignment::Unpack {
attribute_expression_id: expression_id,
unpack,
},
);
}
walk_expr(self, expr);
}
_ => {
walk_expr(self, expr);
}

View File

@@ -478,6 +478,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
scope_start_visibility: ScopedVisibilityConstraintId,
reachable: bool,
}
#[derive(Debug)]
@@ -505,6 +506,8 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Currently live bindings and declarations for each symbol.
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
reachable: bool,
}
impl Default for UseDefMapBuilder<'_> {
@@ -517,13 +520,14 @@ impl Default for UseDefMapBuilder<'_> {
bindings_by_use: IndexVec::new(),
definitions_by_definition: FxHashMap::default(),
symbol_states: IndexVec::new(),
reachable: true,
}
}
}
impl<'db> UseDefMapBuilder<'db> {
pub(super) fn mark_unreachable(&mut self) {
self.record_visibility_constraint(ScopedVisibilityConstraintId::ALWAYS_FALSE);
self.reachable = false;
}
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
@@ -540,7 +544,7 @@ impl<'db> UseDefMapBuilder<'db> {
binding,
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
);
symbol_state.record_binding(def_id, self.scope_start_visibility);
symbol_state.record_binding(def_id);
}
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
@@ -592,11 +596,7 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
// If there are any control flow paths that have become unreachable between `snapshot` and
// now, then it's not valid to simplify any visibility constraints to `snapshot`.
if self.scope_start_visibility != snapshot.scope_start_visibility {
return;
}
self.scope_start_visibility = snapshot.scope_start_visibility;
// Note that this loop terminates when we reach a symbol not present in the snapshot.
// This means we keep visibility constraints for all new symbols, which is intended,
@@ -632,7 +632,7 @@ impl<'db> UseDefMapBuilder<'db> {
let def_id = self.all_definitions.push(Some(definition));
let symbol_state = &mut self.symbol_states[symbol];
symbol_state.record_declaration(def_id);
symbol_state.record_binding(def_id, self.scope_start_visibility);
symbol_state.record_binding(def_id);
}
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
@@ -649,6 +649,7 @@ impl<'db> UseDefMapBuilder<'db> {
FlowSnapshot {
symbol_states: self.symbol_states.clone(),
scope_start_visibility: self.scope_start_visibility,
reachable: self.reachable,
}
}
@@ -671,23 +672,21 @@ impl<'db> UseDefMapBuilder<'db> {
num_symbols,
SymbolState::undefined(self.scope_start_visibility),
);
self.reachable = snapshot.reachable;
}
/// Merge the given snapshot into the current state, reflecting that we might have taken either
/// path to get here. The new state for each symbol should include definitions from both the
/// prior state and the snapshot.
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
// As an optimization, if we know statically that either of the snapshots is always
// unreachable, we can leave it out of the merged result entirely. Note that we cannot
// perform any type inference at this point, so this is largely limited to unreachability
// via terminal statements. If a flow's reachability depends on an expression in the code,
// we will include the flow in the merged result; the visibility constraints of its
// bindings will include this reachability condition, so that later during type inference,
// we can determine whether any particular binding is non-visible due to unreachability.
if snapshot.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
// Unreachable snapshots should not be merged: If the current snapshot is unreachable, it
// should be completely overwritten by the snapshot we're merging in. If the other snapshot
// is unreachable, we should return without merging.
if !snapshot.reachable {
return;
}
if self.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
if !self.reachable {
self.restore(snapshot);
return;
}
@@ -713,6 +712,9 @@ impl<'db> UseDefMapBuilder<'db> {
self.scope_start_visibility = self
.visibility_constraints
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
// Both of the snapshots are reachable, so the merged result is too.
self.reachable = true;
}
pub(super) fn finish(mut self) -> UseDefMap<'db> {

View File

@@ -237,11 +237,7 @@ impl SymbolBindings {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
visibility_constraint: ScopedVisibilityConstraintId,
) {
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
// The new binding replaces all previous live bindings in this path, and has no
// constraints.
self.live_bindings = Bindings::with(binding_id.into());
@@ -249,7 +245,8 @@ impl SymbolBindings {
self.constraints.push(Constraints::default());
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
self.visibility_constraints.push(visibility_constraint);
self.visibility_constraints
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
}
/// Add given constraint to all live bindings.
@@ -352,14 +349,9 @@ impl SymbolState {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
visibility_constraint: ScopedVisibilityConstraintId,
) {
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
self.bindings
.record_binding(binding_id, visibility_constraint);
self.bindings.record_binding(binding_id);
}
/// Add given constraint to all live bindings.
@@ -565,10 +557,7 @@ mod tests {
#[test]
fn with() {
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym.record_binding(ScopedDefinitionId::from_u32(1));
assert_bindings(&sym, &["1<>"]);
}
@@ -576,10 +565,7 @@ mod tests {
#[test]
fn record_constraint() {
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym.record_binding(ScopedDefinitionId::from_u32(1));
sym.record_constraint(ScopedConstraintId::from_u32(0));
assert_bindings(&sym, &["1<0>"]);
@@ -591,17 +577,11 @@ mod tests {
// merging the same definition with the same constraint keeps the constraint
let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1a.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
sym1a.record_constraint(ScopedConstraintId::from_u32(0));
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
sym1b.record_constraint(ScopedConstraintId::from_u32(0));
sym1a.merge(sym1b, &mut visibility_constraints);
@@ -610,17 +590,11 @@ mod tests {
// merging the same definition with differing constraints drops all constraints
let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym2a.record_binding(
ScopedDefinitionId::from_u32(2),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
sym2a.record_constraint(ScopedConstraintId::from_u32(1));
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(
ScopedDefinitionId::from_u32(2),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1b.record_binding(ScopedDefinitionId::from_u32(2));
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
sym2a.merge(sym1b, &mut visibility_constraints);
@@ -629,10 +603,7 @@ mod tests {
// merging a constrained definition with unbound keeps both
let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym3a.record_binding(
ScopedDefinitionId::from_u32(3),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym3a.record_binding(ScopedDefinitionId::from_u32(3));
sym3a.record_constraint(ScopedConstraintId::from_u32(3));
let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);

View File

@@ -40,7 +40,6 @@ use crate::types::call::{
};
use crate::types::class_base::ClassBase;
use crate::types::diagnostic::INVALID_TYPE_FORM;
use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
@@ -1261,42 +1260,19 @@ impl<'db> Type<'db> {
.iter()
.all(|e| e.is_disjoint_from(db, other)),
(Type::Intersection(inter_left), Type::Intersection(inter_right)) => {
// We explicitly make this case a symmetric version of the case below, as there
// are some type pairs like `Any & T` and `~T` that would otherwise lead to non-
// symmetric results.
inter_left
(Type::Intersection(intersection), other)
| (other, Type::Intersection(intersection)) => {
if intersection
.positive(db)
.iter()
.any(|p| p.is_disjoint_from(db, other))
|| inter_right
.positive(db)
.iter()
.any(|p| p.is_disjoint_from(db, self))
|| inter_left.negative(db).iter().any(|n| {
other.is_subtype_of(db, *n)
&& self.is_fully_static(db)
&& other.is_fully_static(db)
})
|| inter_right.negative(db).iter().any(|n| {
self.is_subtype_of(db, *n)
&& self.is_fully_static(db)
&& other.is_fully_static(db)
})
}
(Type::Intersection(intersection), t) | (t, Type::Intersection(intersection)) => {
// TODO: There are certainly more cases that could be handled here. For example,
// it is possible that both A and B overlap with C, but the intersection A & B
// does not overlap with C.
intersection
.positive(db)
.iter()
.any(|p| p.is_disjoint_from(db, t))
|| intersection.negative(db).iter().any(|n| {
t.is_subtype_of(db, *n)
&& self.is_fully_static(db)
&& other.is_fully_static(db)
})
{
true
} else {
// TODO we can do better here. For example:
// X & ~Literal[1] is disjoint from Literal[1]
false
}
}
// any single-valued type is disjoint from another single-valued type
@@ -4255,32 +4231,6 @@ impl<'db> Class<'db> {
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
AttributeAssignment::Iterable { iterable } => {
// We found an attribute assignment like:
//
// for self.name in <iterable>:
// TODO: Potential diagnostics resulting from the iterable are currently not reported.
let iterable_ty = infer_expression_type(db, *iterable);
let inferred_ty = iterable_ty.iterate(db).unwrap_without_diagnostic();
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
AttributeAssignment::Unpack {
attribute_expression_id,
unpack,
} => {
// We found an unpacking assignment like:
//
// .., self.name, .. = <value>
// (.., self.name, ..) = <value>
// [.., self.name, ..] = <value>
let inferred_ty =
infer_unpack_types(db, *unpack).expression_type(*attribute_expression_id);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
}
}

View File

@@ -200,7 +200,7 @@ pub(crate) fn infer_expression_types<'db>(
/// type of the variables involved in this unpacking along with any violations that are detected
/// during this unpacking.
#[salsa::tracked(return_ref)]
pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> {
fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> {
let file = unpack.file(db);
let _span =
tracing::trace_span!("infer_unpack_types", range=?unpack.range(db), file=%file.path(db))
@@ -2085,7 +2085,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
unpacked.expression_type(name_ast_id)
unpacked.get(name_ast_id).unwrap_or(Type::unknown())
}
TargetKind::Name => {
if self.in_stub() && value.is_ellipsis_literal_expr() {
@@ -2356,7 +2356,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.context.extend(unpacked);
}
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
unpacked.expression_type(name_ast_id)
unpacked.get(name_ast_id).unwrap_or(Type::unknown())
}
TargetKind::Name => iterable_ty
.iterate(self.db())
@@ -2512,22 +2512,19 @@ impl<'db> TypeInferenceBuilder<'db> {
let module = file_to_module(self.db(), self.file())
.ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
let mut level = level.get();
if module.kind().is_package() {
level = level.saturating_sub(1);
level -= 1;
}
let mut module_name = module.name().clone();
for _ in 0..level {
module_name = module_name
.parent()
.ok_or(ModuleNameResolutionError::TooManyDots)?;
}
let mut module_name = module
.name()
.ancestors()
.nth(level as usize)
.ok_or(ModuleNameResolutionError::TooManyDots)?;
if let Some(tail) = tail {
let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
module_name.extend(&tail);
}
Ok(module_name)
}

View File

@@ -62,22 +62,19 @@ impl<'db> Unpacker<'db> {
.unwrap_with_diagnostic(&self.context, value.as_any_node_ref(self.db()));
}
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_ty);
self.unpack_inner(target, value_ty);
}
fn unpack_inner(
&mut self,
target: &ast::Expr,
value_expr: AnyNodeRef<'db>,
value_ty: Type<'db>,
) {
fn unpack_inner(&mut self, target: &ast::Expr, value_ty: Type<'db>) {
match target {
ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
self.targets
.insert(target.scoped_expression_id(self.db(), self.scope), value_ty);
ast::Expr::Name(target_name) => {
self.targets.insert(
target_name.scoped_expression_id(self.db(), self.scope),
value_ty,
);
}
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
self.unpack_inner(value, value_expr, value_ty);
self.unpack_inner(value, value_ty);
}
ast::Expr::List(ast::ExprList { elts, .. })
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
@@ -156,7 +153,7 @@ impl<'db> Unpacker<'db> {
Type::LiteralString
} else {
ty.iterate(self.db())
.unwrap_with_diagnostic(&self.context, value_expr)
.unwrap_with_diagnostic(&self.context, AnyNodeRef::from(target))
};
for target_type in &mut target_types {
target_type.push(ty);
@@ -170,7 +167,7 @@ impl<'db> Unpacker<'db> {
[] => Type::unknown(),
types => UnionType::from_elements(self.db(), types),
};
self.unpack_inner(element, value_expr, element_ty);
self.unpack_inner(element, element_ty);
}
}
_ => {}
@@ -268,14 +265,8 @@ pub(crate) struct UnpackResult<'db> {
}
impl<'db> UnpackResult<'db> {
/// Returns the inferred type for a given sub-expression of the left-hand side target
/// of an unpacking assignment.
///
/// Panics if a scoped expression ID is passed in that does not correspond to a sub-
/// expression of the target.
#[track_caller]
pub(crate) fn expression_type(&self, expr_id: ScopedExpressionId) -> Type<'db> {
self.targets[&expr_id]
pub(crate) fn get(&self, expr_id: ScopedExpressionId) -> Option<Type<'db>> {
self.targets.get(&expr_id).copied()
}
}

View File

@@ -18,7 +18,6 @@ ruff_index = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_ast = { workspace = true }
anyhow = { workspace = true }
camino = { workspace = true }

View File

@@ -20,7 +20,7 @@ reveal_type(1) # revealed: Literal[1]
````
When running this test, the mdtest framework will write a file with these contents to the default
file path (`/src/mdtest_snippet.py`) in its in-memory file system, run a type check on that file,
file path (`/src/mdtest_snippet__1.py`) in its in-memory file system, run a type check on that file,
and then match the resulting diagnostics with the assertions in the test. Assertions are in the form
of Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise,
it fails.
@@ -34,8 +34,7 @@ syntax, it's just how this README embeds an example mdtest Markdown document.)
See actual example mdtest suites in
[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest).
> [!NOTE]
> If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory,
> Note: If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory,
> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`,
> you will likely want to also make sure that the crate the tests are in is rebuilt every time a
> Markdown file is added or removed from the directory. See
@@ -127,31 +126,6 @@ Intervening empty lines or non-assertion comments are not allowed; an assertion
assertion per line, immediately following each other, with the line immediately following the last
assertion as the line of source code on which the matched diagnostics are emitted.
## Literate style
If multiple code blocks (without an explicit path, see below) are present in a single test, they will
be merged into a single file in the order they appear in the Markdown file. This allows for tests that
interleave code and explanations:
````markdown
# My literate test
This first snippet here:
```py
from typing import Literal
def f(x: Literal[1]):
pass
```
will be merged with this second snippet here, i.e. `f` is defined here:
```py
f(2) # error: [invalid-argument-type]
```
````
## Diagnostic Snapshotting
In addition to inline assertions, one can also snapshot the full diagnostic
@@ -182,8 +156,13 @@ snapshotting of specific diagnostics.
## Multi-file tests
Some tests require multiple files, with imports from one file into another. For this purpose,
tests can specify explicit file paths in a separate line before the code block (`b.py` below):
Some tests require multiple files, with imports from one file into another. Multiple fenced code
blocks represent multiple embedded files. If there are multiple unnamed files, mdtest will name them
according to the numbered scheme `/src/mdtest_snippet__1.py`, `/src/mdtest_snippet__2.py`, etc. (If
they are `pyi` files, they will be named with a `pyi` extension instead.)
Tests should not rely on these default names. If a test must import from a file, then it should
explicitly specify the file name:
````markdown
```py
@@ -204,8 +183,8 @@ is, the equivalent of a runtime entry on `sys.path`).
The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but
this is a feature we will want to add in the future.
So the above test creates two files, `/src/mdtest_snippet.py` and `/src/b.py`, and sets the workspace
root to `/src/`, allowing imports from `b.py` using the module name `b`.
So the above test creates two files, `/src/mdtest_snippet__1.py` and `/src/b.py`, and sets the
workspace root to `/src/`, allowing imports from `b.py` using the module name `b`.
## Multi-test suites
@@ -419,7 +398,7 @@ This is just an example, not a proposal that red-knot would ever actually output
precisely this format:
```output
mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]'
mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
```
````
@@ -427,7 +406,7 @@ We will want to build tooling to automatically capture and update these “full
blocks, when tests are run in an update-output mode (probably specified by an environment variable.)
By default, an `output` block will specify diagnostic output for the file
`<workspace-root>/mdtest_snippet.py`. An `output` block can be prefixed by a
`<workspace-root>/mdtest_snippet__1.py`. An `output` block can be prefixed by a
<code>`&lt;path>`:</code> label as usual, to explicitly specify the Python file for which it asserts
diagnostic output.
@@ -463,7 +442,7 @@ x = 1
Initial expected output for the unnamed file:
```output
/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]'
/src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
```
Now in our first incremental stage, modify the contents of `b.py`:
@@ -478,12 +457,12 @@ x = 2
And this is our updated expected output for the unnamed file at stage 1:
```output stage=1
/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[2]'
/src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[2]'
```
(One reason to use full-diagnostic-output blocks in this test is that updating inline-comment
diagnostic assertions for `mdtest_snippet.py` would require specifying new contents for
`mdtest_snippet.py` in stage 1, which we don't want to do in this test.)
diagnostic assertions for `mdtest_snippet__1.py` would require specifying new contents for
`mdtest_snippet__1.py` in stage 1, which we don't want to do in this test.)
````
It will be possible to provide any number of stages in an incremental test. If a stage re-specifies

View File

@@ -1,5 +1,4 @@
use crate::config::Log;
use crate::parser::{BacktickOffsets, EmbeddedFileSourceMap};
use camino::Utf8Path;
use colored::Colorize;
use parser as test_parser;
@@ -12,6 +11,7 @@ use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextSize;
use std::fmt::Write;
mod assertion;
@@ -67,14 +67,12 @@ pub fn run(
let md_index = LineIndex::from_source_text(&source);
for test_failures in failures {
let source_map =
EmbeddedFileSourceMap::new(&md_index, test_failures.backtick_offsets);
let backtick_line = md_index.line_index(test_failures.backtick_offset);
for (relative_line_number, failures) in test_failures.by_line.iter() {
let absolute_line_number =
source_map.to_absolute_line_number(relative_line_number);
for failure in failures {
let absolute_line_number =
backtick_line.checked_add(relative_line_number).unwrap();
let line_info =
format!("{relative_fixture_path}:{absolute_line_number}").cyan();
println!(" {line_info} {failure}");
@@ -122,7 +120,11 @@ fn run_test(
"Supported file types are: py, pyi, text"
);
let full_path = embedded.full_path(&project_root);
let full_path = if embedded.path.starts_with('/') {
SystemPathBuf::from(embedded.path.clone())
} else {
project_root.join(&embedded.path)
};
if let Some(ref typeshed_path) = custom_typeshed_path {
if let Ok(relative_path) = full_path.strip_prefix(typeshed_path.join("stdlib")) {
@@ -134,7 +136,7 @@ fn run_test(
}
}
db.write_file(&full_path, &embedded.code).unwrap();
db.write_file(&full_path, embedded.code).unwrap();
if !full_path.starts_with(&src_path) || embedded.lang == "text" {
// These files need to be written to the file system (above), but we don't run any checks on them.
@@ -145,7 +147,7 @@ fn run_test(
Some(TestFile {
file,
backtick_offsets: embedded.backtick_offsets.clone(),
backtick_offset: embedded.backtick_offset,
})
})
.collect();
@@ -228,7 +230,7 @@ fn run_test(
}
by_line.push(OneIndexed::from_zero_indexed(0), messages);
return Some(FileFailures {
backtick_offsets: test_file.backtick_offsets,
backtick_offset: test_file.backtick_offset,
by_line,
});
}
@@ -242,7 +244,7 @@ fn run_test(
match matcher::match_file(db, test_file.file, diagnostics.iter().map(|d| &**d)) {
Ok(()) => None,
Err(line_failures) => Some(FileFailures {
backtick_offsets: test_file.backtick_offsets,
backtick_offset: test_file.backtick_offset,
by_line: line_failures,
}),
};
@@ -278,11 +280,11 @@ fn run_test(
type Failures = Vec<FileFailures>;
/// The failures for a single file in a test by line number.
#[derive(Debug)]
struct FileFailures {
/// Positional information about the code block(s) to reconstruct absolute line numbers.
backtick_offsets: Vec<BacktickOffsets>,
/// The failures by lines in the file.
/// The offset of the backticks that starts the code block in the Markdown file
backtick_offset: TextSize,
/// The failures by lines in the code block.
by_line: matcher::FailuresByLine,
}
@@ -290,8 +292,8 @@ struct FileFailures {
struct TestFile {
file: File,
/// Positional information about the code block(s) to reconstruct absolute line numbers.
backtick_offsets: Vec<BacktickOffsets>,
// Offset of the backticks that starts the code block in the Markdown file
backtick_offset: TextSize,
}
fn create_diagnostic_snapshot<D: Diagnostic>(
@@ -315,7 +317,7 @@ fn create_diagnostic_snapshot<D: Diagnostic>(
writeln!(snapshot, "# Python source files").unwrap();
writeln!(snapshot).unwrap();
for file in test.files() {
writeln!(snapshot, "## {}", file.relative_path()).unwrap();
writeln!(snapshot, "## {}", file.path).unwrap();
writeln!(snapshot).unwrap();
// Note that we don't use ```py here because the line numbering
// we add makes it invalid Python. This sacrifices syntax

View File

@@ -1,13 +1,9 @@
use std::{borrow::Cow, collections::hash_map::Entry};
use anyhow::bail;
use ruff_db::system::{SystemPath, SystemPathBuf};
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast::PySourceType;
use ruff_python_trivia::Cursor;
use ruff_source_file::{LineIndex, LineRanges, OneIndexed};
use ruff_source_file::LineRanges;
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::config::MarkdownTestConfig;
@@ -136,112 +132,6 @@ struct Section<'s> {
#[newtype_index]
struct EmbeddedFileId;
/// Holds information about the start and the end of a code block in a Markdown file.
///
/// The start is the offset of the first triple-backtick in the code block, and the end is the
/// offset of the (start of the) closing triple-backtick.
#[derive(Debug, Clone)]
pub(crate) struct BacktickOffsets(TextSize, TextSize);
/// Holds information about the position and length of all code blocks that are part of
/// a single embedded file in a Markdown file. This is used to reconstruct absolute line
/// numbers (in the Markdown file) from relative line numbers (in the embedded file).
///
/// If we have a Markdown section with multiple code blocks like this:
///
/// 01 # Test
/// 02
/// 03 Part 1:
/// 04
/// 05 ```py
/// 06 a = 1 # Relative line number: 1
/// 07 b = 2 # Relative line number: 2
/// 08 ```
/// 09
/// 10 Part 2:
/// 11
/// 12 ```py
/// 13 c = 3 # Relative line number: 3
/// 14 ```
///
/// We want to reconstruct the absolute line number (left) from relative
/// line numbers. The information we have is the start line and the line
/// count of each code block:
///
/// - Block 1: (start = 5, count = 2)
/// - Block 2: (start = 12, count = 1)
///
/// For example, if we see a relative line number of 3, we see that it is
/// larger than the line count of the first block, so we subtract the line
/// count of the first block, and then add the new relative line number (1)
/// to the absolute start line of the second block (12), resulting in an
/// absolute line number of 13.
pub(crate) struct EmbeddedFileSourceMap {
start_line_and_line_count: Vec<(usize, usize)>,
}
impl EmbeddedFileSourceMap {
pub(crate) fn new(
md_index: &LineIndex,
dimensions: impl IntoIterator<Item = BacktickOffsets>,
) -> EmbeddedFileSourceMap {
EmbeddedFileSourceMap {
start_line_and_line_count: dimensions
.into_iter()
.map(|d| {
let start_line = md_index.line_index(d.0).get();
let end_line = md_index.line_index(d.1).get();
let code_line_count = (end_line - start_line) - 1;
(start_line, code_line_count)
})
.collect(),
}
}
pub(crate) fn to_absolute_line_number(&self, relative_line_number: OneIndexed) -> OneIndexed {
let mut absolute_line_number = 0;
let mut relative_line_number = relative_line_number.get();
for (start_line, line_count) in &self.start_line_and_line_count {
if relative_line_number > *line_count {
relative_line_number -= *line_count;
} else {
absolute_line_number = start_line + relative_line_number;
break;
}
}
OneIndexed::new(absolute_line_number).expect("Relative line number out of bounds")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum EmbeddedFilePath<'s> {
Autogenerated(PySourceType),
Explicit(&'s str),
}
impl EmbeddedFilePath<'_> {
pub(crate) fn as_str(&self) -> &str {
match self {
EmbeddedFilePath::Autogenerated(PySourceType::Python) => "mdtest_snippet.py",
EmbeddedFilePath::Autogenerated(PySourceType::Stub) => "mdtest_snippet.pyi",
EmbeddedFilePath::Autogenerated(PySourceType::Ipynb) => "mdtest_snippet.ipynb",
EmbeddedFilePath::Explicit(path) => path,
}
}
fn is_explicit(&self) -> bool {
matches!(self, EmbeddedFilePath::Explicit(_))
}
fn is_allowed_explicit_path(path: &str) -> bool {
[PySourceType::Python, PySourceType::Stub]
.iter()
.all(|source_type| path != EmbeddedFilePath::Autogenerated(*source_type).as_str())
}
}
/// A single file embedded in a [`Section`] as a fenced code block.
///
/// Currently must be a Python file (`py` language), a type stub (`pyi`) or a [typeshed `VERSIONS`]
@@ -258,39 +148,12 @@ impl EmbeddedFilePath<'_> {
#[derive(Debug)]
pub(crate) struct EmbeddedFile<'s> {
section: SectionId,
path: EmbeddedFilePath<'s>,
pub(crate) path: String,
pub(crate) lang: &'s str,
pub(crate) code: Cow<'s, str>,
pub(crate) backtick_offsets: Vec<BacktickOffsets>,
}
pub(crate) code: &'s str,
impl EmbeddedFile<'_> {
fn append_code(&mut self, backtick_offsets: BacktickOffsets, new_code: &str) {
// Treat empty code blocks as non-existent, instead of creating
// an additional empty line:
if new_code.is_empty() {
return;
}
self.backtick_offsets.push(backtick_offsets);
let existing_code = self.code.to_mut();
existing_code.push('\n');
existing_code.push_str(new_code);
}
pub(crate) fn relative_path(&self) -> &str {
self.path.as_str()
}
pub(crate) fn full_path(&self, project_root: &SystemPath) -> SystemPathBuf {
let relative_path = self.relative_path();
if relative_path.starts_with('/') {
SystemPathBuf::from(relative_path)
} else {
project_root.join(relative_path)
}
}
/// The offset of the backticks beginning the code block within the markdown file
pub(crate) backtick_offset: TextSize,
}
#[derive(Debug)]
@@ -332,6 +195,12 @@ struct Parser<'s> {
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
/// The counts are done by section. This gives each code block a
/// somewhat locally derived name. That is, adding new sections
/// won't change the names of files in other sections. This is
/// important for avoiding snapshot churn.
unnamed_file_count: FxHashMap<SectionId, usize>,
/// The unparsed remainder of the Markdown source.
cursor: Cursor<'s>,
@@ -348,7 +217,7 @@ struct Parser<'s> {
stack: SectionStack,
/// Names of embedded files in current active section.
current_section_files: FxHashMap<EmbeddedFilePath<'s>, EmbeddedFileId>,
current_section_files: Option<FxHashSet<String>>,
/// Whether or not the current section has a config block.
current_section_has_config: bool,
@@ -368,12 +237,13 @@ impl<'s> Parser<'s> {
sections,
source,
files: IndexVec::default(),
unnamed_file_count: FxHashMap::default(),
cursor: Cursor::new(source),
preceding_blank_lines: 0,
explicit_path: None,
source_len: source.text_len(),
stack: SectionStack::new(root_section_id),
current_section_files: FxHashMap::default(),
current_section_files: None,
current_section_has_config: false,
}
}
@@ -464,7 +334,7 @@ impl<'s> Parser<'s> {
if self.cursor.eat_char2('`', '`') {
// We saw the triple-backtick beginning of a code block.
let backtick_offset_start = self.offset() - "```".text_len();
let backtick_offset = self.offset() - "```".text_len();
if self.preceding_blank_lines < 1 && self.explicit_path.is_none() {
bail!("Code blocks must start on a new line and be preceded by at least one blank line.");
@@ -493,13 +363,7 @@ impl<'s> Parser<'s> {
code = &code[..code.len() - '\n'.len_utf8()];
}
let backtick_offset_end = self.offset() - "```".text_len();
self.process_code_block(
lang,
code,
BacktickOffsets(backtick_offset_start, backtick_offset_end),
)?;
self.process_code_block(lang, code, backtick_offset)?;
} else {
let code_block_start = self.cursor.token_len();
let line = self.source.count_lines(TextRange::up_to(code_block_start));
@@ -564,7 +428,7 @@ impl<'s> Parser<'s> {
snapshot_diagnostics: self.sections[parent].snapshot_diagnostics,
};
if !self.current_section_files.is_empty() {
if self.current_section_files.is_some() {
bail!(
"Header '{}' not valid inside a test case; parent '{}' has code files.",
section.title,
@@ -575,7 +439,7 @@ impl<'s> Parser<'s> {
let section_id = self.sections.push(section);
self.stack.push(section_id);
self.current_section_files.clear();
self.current_section_files = None;
self.current_section_has_config = false;
Ok(())
@@ -585,11 +449,10 @@ impl<'s> Parser<'s> {
&mut self,
lang: &'s str,
code: &'s str,
backtick_offsets: BacktickOffsets,
backtick_offset: TextSize,
) -> anyhow::Result<()> {
// We never pop the implicit root section.
let section = self.stack.top();
let test_name = self.sections[section].title;
if lang == "toml" {
return self.process_config_block(code);
@@ -602,86 +465,52 @@ impl<'s> Parser<'s> {
&& !explicit_path.ends_with(&format!(".{lang}"))
{
bail!(
"File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block"
"File ending of test file path `{explicit_path}` does not match `lang={lang}` of code block"
);
}
}
let path = match self.explicit_path {
Some(path) => {
if !EmbeddedFilePath::is_allowed_explicit_path(path) {
bail!(
"The file name `{path}` in test `{test_name}` must not be used explicitly.",
);
}
Some(path) => path.to_string(),
None => {
let unnamed_file_count = self.unnamed_file_count.entry(section).or_default();
*unnamed_file_count += 1;
EmbeddedFilePath::Explicit(path)
match lang {
"py" | "pyi" => format!("mdtest_snippet__{unnamed_file_count}.{lang}"),
"" => format!("mdtest_snippet__{unnamed_file_count}.py"),
_ => {
bail!(
"Cannot generate name for `lang={}`: Unsupported extension",
lang
);
}
}
}
None => match lang {
"py" => EmbeddedFilePath::Autogenerated(PySourceType::Python),
"pyi" => EmbeddedFilePath::Autogenerated(PySourceType::Stub),
"" => {
bail!("Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`");
}
_ => {
bail!(
"Cannot auto-generate file name for code block with language `{}` in test `{test_name}`",
lang
);
}
},
};
let has_merged_snippets = self.current_section_has_merged_snippets();
let has_explicit_file_paths = self.current_section_has_explicit_file_paths();
self.files.push(EmbeddedFile {
path: path.clone(),
section,
lang,
code,
backtick_offset,
});
match self.current_section_files.entry(path.clone()) {
Entry::Vacant(entry) => {
if has_merged_snippets {
bail!("Merged snippets in test `{test_name}` are not allowed in the presence of other files.");
}
let index = self.files.push(EmbeddedFile {
path: path.clone(),
section,
lang,
code: Cow::Borrowed(code),
backtick_offsets: vec![backtick_offsets],
});
entry.insert(index);
}
Entry::Occupied(entry) => {
if path.is_explicit() {
bail!(
"Test `{test_name}` has duplicate files named `{}`.",
path.as_str(),
);
};
if has_explicit_file_paths {
bail!("Merged snippets in test `{test_name}` are not allowed in the presence of other files.");
}
let index = *entry.get();
self.files[index].append_code(backtick_offsets, code);
}
if let Some(current_files) = &mut self.current_section_files {
if !current_files.insert(path.clone()) {
bail!(
"Test `{}` has duplicate files named `{path}`.",
self.sections[section].title
);
};
} else {
self.current_section_files = Some(FxHashSet::from_iter([path]));
}
Ok(())
}
fn current_section_has_explicit_file_paths(&self) -> bool {
self.current_section_files
.iter()
.any(|(path, _)| path.is_explicit())
}
fn current_section_has_merged_snippets(&self) -> bool {
self.current_section_files
.values()
.any(|id| self.files[*id].backtick_offsets.len() > 1)
}
fn process_config_block(&mut self, code: &str) -> anyhow::Result<()> {
if self.current_section_has_config {
bail!("Multiple TOML configuration blocks in the same section are not allowed.");
@@ -702,7 +531,7 @@ impl<'s> Parser<'s> {
everything else (including TOML configuration blocks).",
);
}
if !self.current_section_files.is_empty() {
if self.current_section_files.is_some() {
bail!(
"Section config to enable snapshotting diagnostics must come before \
everything else (including embedded files).",
@@ -726,7 +555,7 @@ impl<'s> Parser<'s> {
self.stack.pop();
// We would have errored before pushing a child section if there were files, so we know
// no parent section can have files.
self.current_section_files.clear();
self.current_section_files = None;
}
}
@@ -738,11 +567,8 @@ impl<'s> Parser<'s> {
#[cfg(test)]
mod tests {
use ruff_python_ast::PySourceType;
use ruff_python_trivia::textwrap::dedent;
use crate::parser::EmbeddedFilePath;
#[test]
fn empty() {
let mf = super::parse("file.md", "").unwrap();
@@ -771,10 +597,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1");
}
@@ -799,10 +622,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1");
}
@@ -825,14 +645,10 @@ mod tests {
# Three
`mod_a.pyi`:
```pyi
a: int
```
`mod_b.pyi`:
```pyi
b: str
```
@@ -852,10 +668,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1");
@@ -863,10 +676,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "y = 2");
@@ -874,11 +684,11 @@ mod tests {
panic!("expected two files");
};
assert_eq!(file_1.relative_path(), "mod_a.pyi");
assert_eq!(file_1.path, "mdtest_snippet__1.pyi");
assert_eq!(file_1.lang, "pyi");
assert_eq!(file_1.code, "a: int");
assert_eq!(file_2.relative_path(), "mod_b.pyi");
assert_eq!(file_2.path, "mdtest_snippet__2.pyi");
assert_eq!(file_2.lang, "pyi");
assert_eq!(file_2.code, "b: str");
}
@@ -921,11 +731,11 @@ mod tests {
panic!("expected two files");
};
assert_eq!(main.relative_path(), "main.py");
assert_eq!(main.path, "main.py");
assert_eq!(main.lang, "py");
assert_eq!(main.code, "from foo import y");
assert_eq!(foo.relative_path(), "foo.py");
assert_eq!(foo.path, "foo.py");
assert_eq!(foo.lang, "py");
assert_eq!(foo.code, "y = 2");
@@ -933,157 +743,11 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "y = 2");
}
#[test]
fn merged_snippets() {
let source = dedent(
"
# One
This is the first part of the embedded file:
```py
x = 1
```
And this is the second part:
```py
y = 2
```
And this is the third part:
```py
z = 3
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1\ny = 2\nz = 3");
}
#[test]
fn no_merged_snippets_for_explicit_paths() {
let source = dedent(
"
# One
`foo.py`:
```py
x = 1
```
`foo.py`:
```py
y = 2
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Test `One` has duplicate files named `foo.py`."
);
}
#[test]
fn disallow_merged_snippets_in_presence_of_explicit_paths() {
for source in [
// Merged snippets first
"
# One
```py
x = 1
```
```py
y = 2
```
`foo.py`:
```py
print('hello')
```
",
// Explicit path first
"
# One
`foo.py`:
```py
print('hello')
```
```py
x = 1
```
```py
y = 2
```
",
] {
let err = super::parse("file.md", &dedent(source)).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Merged snippets in test `One` are not allowed in the presence of other files."
);
}
}
#[test]
fn disallow_pyi_snippets_in_presence_of_merged_py_snippets() {
let source = dedent(
"
# One
```py
x = 1
```
```py
y = 2
```
```pyi
x: int
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Merged snippets in test `One` are not allowed in the presence of other files."
);
}
#[test]
fn custom_file_path() {
let source = dedent(
@@ -1104,7 +768,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo.py");
assert_eq!(file.path, "foo.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1");
}
@@ -1156,27 +820,28 @@ mod tests {
fn no_lang() {
let source = dedent(
"
# No language specifier
```
x = 10
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Cannot auto-generate file name for code block with empty language specifier in test `No language specifier`"
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.code, "x = 10");
}
#[test]
fn cannot_generate_name_for_lang() {
let source = dedent(
"
# JSON test?
```json
{}
```
@@ -1185,7 +850,7 @@ mod tests {
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Cannot auto-generate file name for code block with language `json` in test `JSON test?`"
"Cannot generate name for `lang=json`: Unsupported extension"
);
}
@@ -1193,8 +858,6 @@ mod tests {
fn mismatching_lang() {
let source = dedent(
"
# Accidental stub
`a.py`:
```pyi
@@ -1205,7 +868,7 @@ mod tests {
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block"
"File ending of test file path `a.py` does not match `lang=pyi` of code block"
);
}
@@ -1230,7 +893,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "lorem");
assert_eq!(file.path, "lorem");
assert_eq!(file.code, "x = 1");
}
@@ -1255,7 +918,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "lorem.yaml");
assert_eq!(file.path, "lorem.yaml");
assert_eq!(file.code, "x = 1");
}
@@ -1277,13 +940,13 @@ mod tests {
"
## A well-fenced block
```py
```
y = 2
```
## A not-so-well-fenced block
```py
```
x = 1
",
);
@@ -1324,8 +987,7 @@ mod tests {
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(err.to_string(), "Indented code blocks are not supported.");
super::parse("file.md", &source).expect_err("Indented code blocks are not supported.");
}
#[test]
@@ -1371,10 +1033,7 @@ mod tests {
};
assert_eq!(test.section.title, "file.md");
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1403,10 +1062,7 @@ mod tests {
};
assert_eq!(test.section.title, "Foo");
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1435,12 +1091,10 @@ mod tests {
}
#[test]
fn no_usage_of_autogenerated_name() {
fn no_duplicate_name_files_in_test_2() {
let source = dedent(
"
# Name clash
`mdtest_snippet.py`:
`mdtest_snippet__1.py`:
```py
x = 1
@@ -1454,7 +1108,7 @@ mod tests {
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"The file name `mdtest_snippet.py` in test `Name clash` must not be used explicitly."
"Test `file.md` has duplicate files named `mdtest_snippet__1.py`."
);
}
@@ -1479,7 +1133,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo.py");
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
@@ -1504,7 +1158,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo.py");
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
@@ -1528,7 +1182,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo.py");
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
@@ -1553,7 +1207,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo bar.py");
assert_eq!(file.path, "foo bar.py");
assert_eq!(file.code, "x = 1");
}
@@ -1579,10 +1233,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1607,10 +1258,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1636,10 +1284,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1665,10 +1310,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}

View File

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

View File

@@ -3,9 +3,9 @@
#![cfg(not(target_family = "wasm"))]
use regex::escape;
use std::fs;
use std::process::Command;
use std::str;
use std::{fs, path::Path};
use anyhow::Result;
use assert_fs::fixture::{ChildPath, FileTouch, PathChild};
@@ -2218,11 +2218,13 @@ def func(t: _T) -> _T:
return x
"#
),
@r"
success: true
exit_code: 0
@r#"
success: false
exit_code: 1
----- stdout -----
from typing import TypeVar
_T = TypeVar("_T")
class OldStyle[T]:
var: T
@@ -2232,153 +2234,8 @@ def func(t: _T) -> _T:
return x
----- stderr -----
Found 7 errors (7 fixed, 0 remaining).
"
test.py:3:1: PYI018 Private TypeVar `_T` is never used
Found 6 errors (5 fixed, 1 remaining).
"#
);
}
/// Test that we do not rename two different type parameters to the same name
/// in one execution of Ruff (autofixing this to `class Foo[T, T]: ...` would
/// introduce invalid syntax)
#[test]
fn type_parameter_rename_isolation() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--select", "UP049"])
.args(["--stdin-filename", "test.py"])
.arg("--unsafe-fixes")
.arg("--fix")
.arg("--preview")
.arg("--target-version=py312")
.arg("-")
.pass_stdin(
r#"
class Foo[_T, __T]:
pass
"#
),
@r"
success: false
exit_code: 1
----- stdout -----
class Foo[T, __T]:
pass
----- stderr -----
test.py:2:14: UP049 Generic class uses private type parameters
Found 2 errors (1 fixed, 1 remaining).
"
);
}
#[test]
fn a005_module_shadowing_strict() -> Result<()> {
fn create_module(path: &Path) -> Result<()> {
fs::create_dir(path)?;
fs::File::create(path.join("__init__.py"))?;
Ok(())
}
// construct a directory tree with this structure:
// .
// ├── abc
// │   └── __init__.py
// ├── collections
// │   ├── __init__.py
// │   ├── abc
// │   │   └── __init__.py
// │   └── foobar
// │   └── __init__.py
// ├── foobar
// │   ├── __init__.py
// │   ├── abc
// │   │   └── __init__.py
// │   └── collections
// │   ├── __init__.py
// │   ├── abc
// │   │   └── __init__.py
// │   └── foobar
// │   └── __init__.py
// ├── ruff.toml
// └── urlparse
// └── __init__.py
let tempdir = TempDir::new()?;
let foobar = tempdir.path().join("foobar");
create_module(&foobar)?;
for base in [&tempdir.path().into(), &foobar] {
for dir in ["abc", "collections"] {
create_module(&base.join(dir))?;
}
create_module(&base.join("collections").join("abc"))?;
create_module(&base.join("collections").join("foobar"))?;
}
create_module(&tempdir.path().join("urlparse"))?;
// also create a ruff.toml to mark the project root
fs::File::create(tempdir.path().join("ruff.toml"))?;
insta::with_settings!({
filters => vec![(r"\\", "/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--select", "A005"])
.current_dir(tempdir.path()),
@r"
success: false
exit_code: 1
----- stdout -----
abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
Found 6 errors.
----- stderr -----
");
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--select", "A005"])
.current_dir(tempdir.path()),
@r"
success: false
exit_code: 1
----- stdout -----
abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
Found 6 errors.
----- stderr -----
");
// TODO(brent) Default should currently match the strict version, but after the next minor
// release it will match the non-strict version directly above
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--select", "A005"])
.current_dir(tempdir.path()),
@r"
success: false
exit_code: 1
----- stdout -----
abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
Found 6 errors.
----- stderr -----
");
});
Ok(())
}

View File

@@ -30,7 +30,6 @@ glob = { workspace = true }
ignore = { workspace = true, optional = true }
matchit = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
path-slash = { workspace = true }
thiserror = { workspace = true }

View File

@@ -471,13 +471,7 @@ impl ToOwned for SystemPath {
/// The path is guaranteed to be valid UTF-8.
#[repr(transparent)]
#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(transparent)
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SystemPathBuf(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Utf8PathBuf);
pub struct SystemPathBuf(Utf8PathBuf);
impl SystemPathBuf {
pub fn new() -> Self {
@@ -664,6 +658,27 @@ impl ruff_cache::CacheKey for SystemPathBuf {
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for SystemPath {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for SystemPathBuf {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for SystemPathBuf {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Utf8PathBuf::deserialize(deserializer).map(SystemPathBuf)
}
}
/// A slice of a virtual path on [`System`](super::System) (akin to [`str`]).
#[repr(transparent)]
pub struct SystemVirtualPath(str);

View File

@@ -11,7 +11,6 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
red_knot_project = { workspace = true, features = ["schemars"] }
ruff = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_formatter = { workspace = true }

View File

@@ -2,7 +2,7 @@
use anyhow::Result;
use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_knot_schema};
use crate::{generate_cli_help, generate_docs, generate_json_schema};
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
@@ -33,7 +33,6 @@ impl Mode {
pub(crate) fn main(args: &Args) -> Result<()> {
generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?;
generate_knot_schema::main(&generate_knot_schema::Args { mode: args.mode })?;
generate_cli_help::main(&generate_cli_help::Args { mode: args.mode })?;
generate_docs::main(&generate_docs::Args {
dry_run: args.mode.is_dry_run(),

View File

@@ -1,72 +0,0 @@
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;
use anyhow::{bail, Result};
use pretty_assertions::StrComparison;
use schemars::schema_for;
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
use crate::ROOT_DIR;
use red_knot_project::metadata::options::Options;
#[derive(clap::Args)]
pub(crate) struct Args {
/// Write the generated table to stdout (rather than to `knot.schema.json`).
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
pub(crate) fn main(args: &Args) -> Result<()> {
let schema = schema_for!(Options);
let schema_string = serde_json::to_string_pretty(&schema).unwrap();
let filename = "knot.schema.json";
let schema_path = PathBuf::from(ROOT_DIR).join(filename);
match args.mode {
Mode::DryRun => {
println!("{schema_string}");
}
Mode::Check => {
let current = fs::read_to_string(schema_path)?;
if current == schema_string {
println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &schema_string);
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
}
}
Mode::Write => {
let current = fs::read_to_string(&schema_path)?;
if current == schema_string {
println!("Up-to-date: {filename}");
} else {
println!("Updating: {filename}");
fs::write(schema_path, schema_string.as_bytes())?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use std::env;
use crate::generate_all::Mode;
use super::{main, Args};
#[test]
fn test_generate_json_schema() -> Result<()> {
let mode = if env::var("KNOT_UPDATE_SCHEMA").as_deref() == Ok("1") {
Mode::Write
} else {
Mode::Check
};
main(&Args { mode })
}
}

View File

@@ -13,7 +13,6 @@ mod generate_all;
mod generate_cli_help;
mod generate_docs;
mod generate_json_schema;
mod generate_knot_schema;
mod generate_options;
mod generate_rules_table;
mod print_ast;
@@ -40,8 +39,6 @@ enum Command {
GenerateAll(generate_all::Args),
/// Generate JSON schema for the TOML configuration file.
GenerateJSONSchema(generate_json_schema::Args),
/// Generate JSON schema for the Red Knot TOML configuration file.
GenerateKnotSchema(generate_knot_schema::Args),
/// Generate a Markdown-compatible table of supported lint rules.
GenerateRulesTable,
/// Generate a Markdown-compatible listing of configuration options.
@@ -86,7 +83,6 @@ fn main() -> Result<ExitCode> {
match command {
Command::GenerateAll(args) => generate_all::main(&args)?,
Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
Command::GenerateKnotSchema(args) => generate_knot_schema::main(&args)?,
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
Command::GenerateOptions => println!("{}", generate_options::generate()),
Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,

View File

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

View File

@@ -75,10 +75,15 @@ from airflow.secrets.local_filesystem import LocalFilesystemBackend, load_connec
from airflow.security.permissions import RESOURCE_DATASET
from airflow.sensors.base_sensor_operator import BaseSensorOperator
from airflow.sensors.date_time_sensor import DateTimeSensor
from airflow.sensors.external_task import (
ExternalTaskSensorLink as ExternalTaskSensorLinkFromExternalTask,
)
from airflow.sensors.external_task_sensor import (
ExternalTaskMarker,
ExternalTaskSensor,
ExternalTaskSensorLink,
)
from airflow.sensors.external_task_sensor import (
ExternalTaskSensorLink as ExternalTaskSensorLinkFromExternalTaskSensor,
)
from airflow.sensors.time_delta_sensor import TimeDeltaSensor
from airflow.timetables.datasets import DatasetOrTimeSchedule
@@ -244,13 +249,11 @@ BaseSensorOperator()
DateTimeSensor()
# airflow.sensors.external_task
ExternalTaskSensorLink()
ExternalTaskMarker()
ExternalTaskSensor()
ExternalTaskSensorLinkFromExternalTask()
# airflow.sensors.external_task_sensor
ExternalTaskMarkerFromExternalTaskSensor()
ExternalTaskSensorFromExternalTaskSensor()
ExternalTaskMarker()
ExternalTaskSensor()
ExternalTaskSensorLinkFromExternalTaskSensor()
# airflow.sensors.time_delta_sensor

View File

@@ -1,28 +0,0 @@
##### https://github.com/astral-sh/ruff/issues/15809
### Errors
def overshadowed_list():
list = ...
list(map(lambda x: x, []))
### No errors
dict(map(lambda k: (k,), a))
dict(map(lambda k: (k, v, 0), a))
dict(map(lambda k: [k], a))
dict(map(lambda k: [k, v, 0], a))
dict(map(lambda k: {k, v}, a))
dict(map(lambda k: {k: 0, v: 1}, a))
a = [(1, 2), (3, 4)]
map(lambda x: [*x, 10], *a)
map(lambda x: [*x, 10], *a, *b)
map(lambda x: [*x, 10], a, *b)
map(lambda x: x + 10, (a := []))
list(map(lambda x: x + 10, (a := [])))
set(map(lambda x: x + 10, (a := [])))
dict(map(lambda x: (x, 10), (a := [])))

View File

@@ -70,32 +70,6 @@ foo({**foo, **{"bar": True}}) # PIE800
,
})
{
"data": [],
** # Foo
( # Comment
{ "a": b,
# Comment
}
) ,
c: 9,
}
# https://github.com/astral-sh/ruff/issues/15997
{"a": [], **{},}
{"a": [], **({}),}
{"a": [], **{}, 6: 3}
{"a": [], **({}), 6: 3}
{"a": [], **{
# Comment
}, 6: 3}
{"a": [], **({
# Comment
}), 6: 3}
{**foo, "bar": True } # OK

View File

@@ -2,8 +2,6 @@
# Positive cases
###
a_dict = {}
# SIM401 (pattern-1)
if key in a_dict:
var = a_dict[key]
@@ -28,8 +26,6 @@ if keys[idx] in a_dict:
else:
var = "default"
dicts = {"key": a_dict}
# SIM401 (complex expression in dict)
if key in dicts[idx]:
var = dicts[idx][key]
@@ -119,28 +115,6 @@ elif key in a_dict:
else:
vars[idx] = "default"
class NotADictionary:
def __init__(self):
self._dict = {}
def __getitem__(self, key):
return self._dict[key]
def __setitem__(self, key, value):
self._dict[key] = value
def __iter__(self):
return self._dict.__iter__()
not_dict = NotADictionary()
not_dict["key"] = "value"
# OK (type `NotADictionary` is not a known dictionary type)
if "key" in not_dict:
value = not_dict["key"]
else:
value = None
###
# Positive cases (preview)
###

View File

@@ -6,10 +6,6 @@ class _bad:
pass
class __bad:
pass
class bad_class:
pass
@@ -17,8 +13,6 @@ class bad_class:
class Bad_Class:
pass
class Bad__Class:
pass
class BAD_CLASS:
pass
@@ -38,6 +32,3 @@ class GoodClass:
class GOOD:
pass
class __GoodClass:
pass

View File

@@ -1,7 +0,0 @@
import os
import sys
sys.path += [os.path.dirname(__file__)]
sys.path += ["../"]
from package import module

View File

@@ -64,42 +64,10 @@ u''.strip('http://')
u''.lstrip('http://')
# PLE1310
b''.rstrip(b'http://')
b''.rstrip('http://')
# OK
''.strip('Hi')
# OK
''.strip()
### https://github.com/astral-sh/ruff/issues/15968
# Errors: Multiple backslashes
''.strip('\\b\\x09')
''.strip(r'\b\x09')
''.strip('\\\x5C')
# OK: Different types
b"".strip("//")
"".strip(b"//")
# OK: Escapes
'\\test'.strip('\\')
# OK: Extra/missing arguments
"".strip("//", foo)
b"".lstrip(b"//", foo = "bar")
"".rstrip()
# OK: Not literals
foo: str = ""; bar: bytes = b""
"".strip(foo)
b"".strip(bar)
# False negative
foo.rstrip("//")
bar.lstrip(b"//")
# OK: Not `.[lr]?strip`
"".mobius_strip("")

View File

@@ -113,18 +113,3 @@ PositiveList = TypeAliasType(
Annotated[T, Gt(0)], # preserved comment
], type_params=(T,)
)
T: TypeAlias = (
int
| str
)
T: TypeAlias = ( # comment0
# comment1
int # comment2
# comment3
| # comment4
# comment5
str # comment6
# comment7
) # comment8

View File

@@ -12,13 +12,3 @@ x: TypeAlias = tuple[
int, # preserved
float,
]
T: TypeAlias = ( # comment0
# comment1
int # comment2
# comment3
| # comment4
# comment5
str # comment6
# comment7
) # comment8

View File

@@ -54,52 +54,3 @@ def f[_](x: _) -> _: ...
def g[__](x: __) -> __: ...
def h[_T_](x: _T_) -> _T_: ...
def i[__T__](x: __T__) -> __T__: ...
# https://github.com/astral-sh/ruff/issues/16024
from typing import cast, Literal
class C[_0]: ...
class C[T, _T]: ...
class C[_T, T]: ...
class C[_T]:
v1 = cast(_T, ...)
v2 = cast('_T', ...)
v3 = cast("\u005fT", ...)
def _(self):
v1 = cast(_T, ...)
v2 = cast('_T', ...)
v3 = cast("\u005fT", ...)
class C[_T]:
v = cast('Literal[\'foo\'] | _T', ...)
## Name collision
class C[T]:
def f[_T](self): # No fix, collides with `T` from outer scope
v1 = cast(_T, ...)
v2 = cast('_T', ...)
# Unfixable as the new name collides with a variable visible from one of the inner scopes
class C[_T]:
T = 42
v1 = cast(_T, ...)
v2 = cast('_T', ...)
# Unfixable as the new name collides with a variable visible from one of the inner scopes
class C[_T]:
def f[T](self):
v1 = cast(_T, ...)
v2 = cast('_T', ...)

View File

@@ -176,22 +176,3 @@ class Node:
_seen.add(self)
for other in self.connected:
other.recurse(_seen=_seen)
def foo():
_dummy_var = 42
def bar():
dummy_var = 43
print(_dummy_var)
def foo():
# Unfixable because both possible candidates for the new name are shadowed
# in the scope of one of the references to the variable
_dummy_var = 42
def bar():
dummy_var = 43
dummy_var_ = 44
print(_dummy_var)

View File

@@ -1,109 +0,0 @@
from typing import Generic, ParamSpec, TypeVar, TypeVarTuple, Unpack
_A = TypeVar('_A')
_B = TypeVar('_B', bound=int)
_C = TypeVar('_C', str, bytes)
_D = TypeVar('_D', default=int)
_E = TypeVar('_E', bound=int, default=int)
_F = TypeVar('_F', str, bytes, default=str)
_G = TypeVar('_G', str, a := int)
_As = TypeVarTuple('_As')
_Bs = TypeVarTuple('_Bs', bound=tuple[int, str])
_Cs = TypeVarTuple('_Cs', default=tuple[int, str])
_P1 = ParamSpec('_P1')
_P2 = ParamSpec('_P2', bound=[bytes, bool])
_P3 = ParamSpec('_P3', default=[int, str])
### Errors
class C[T](Generic[_A]): ...
class C[T](Generic[_B], str): ...
class C[T](int, Generic[_C]): ...
class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
class C[*Ts](Generic[*_As]): ...
class C[*Ts](Generic[Unpack[_As]]): ...
class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
class C[**P](Generic[_P1]): ...
class C[**P](Generic[_P2]): ...
class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
class C[T](Generic[T, _A]): ...
# See `is_existing_param_of_same_class`.
# `expr_name_to_type_var` doesn't handle named expressions,
# only simple assignments, so there is no fix.
class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ...
class C(Generic[_B]):
class D[T](Generic[_B, T]): ...
class C[T]:
class D[U](Generic[T, U]): ...
# In a single run, only the first is reported.
# Others will be reported/fixed in following iterations.
class C[T](Generic[_C], Generic[_D]): ...
class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
class C[
T # Comment
](Generic[_E]): ... # TODO: Type parameter defaults
class C[T](Generic[Generic[_F]]): ...
class C[T](Generic[Unpack[_A]]): ...
class C[T](Generic[Unpack[_P1]]): ...
class C[T](Generic[Unpack[Unpack[_P2]]]): ...
class C[T](Generic[Unpack[*_As]]): ...
class C[T](Generic[Unpack[_As, _Bs]]): ...
class C[T](Generic[_A, _A]): ...
class C[T](Generic[_A, Unpack[_As]]): ...
class C[T](Generic[*_As, _A]): ...
from somewhere import APublicTypeVar
class C[T](Generic[APublicTypeVar]): ...
class C[T](Generic[APublicTypeVar, _A]): ...
# `_G` has two constraints: `str` and `a := int`.
# The latter cannot be used as a PEP 695 constraint,
# as named expressions are forbidden within type parameter lists.
# See also the `_Z` example above.
class C[T](Generic[_G]): ... # Should be moved down below eventually
# Single-element constraints should not be converted to a bound.
class C[T: (str,)](Generic[_A]): ...
class C[T: [a]](Generic[_A]): ...
# Existing bounds should not be deparenthesized.
# class C[T: (_Y := int)](Generic[_A]): ... # TODO: Uncomment this
# class C[T: (*a,)](Generic[_A]): ... # TODO: Uncomment this
### No errors
class C(Generic[_A]): ...
class C[_A]: ...
class C[_A](list[_A]): ...
class C[_A](list[Generic[_A]]): ...

View File

@@ -9,7 +9,7 @@ use crate::rules::{
};
/// Run lint rules over the [`Binding`]s.
pub(crate) fn bindings(checker: &Checker) {
pub(crate) fn bindings(checker: &mut Checker) {
if !checker.any_enabled(&[
Rule::AssignmentInAssert,
Rule::InvalidAllFormat,
@@ -48,22 +48,22 @@ pub(crate) fn bindings(checker: &Checker) {
pyflakes::fixes::remove_exception_handler_assignment(binding, checker.locator)
.map(Fix::safe_edit)
});
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::InvalidAllFormat) {
if let Some(diagnostic) = pylint::rules::invalid_all_format(binding) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::InvalidAllObject) {
if let Some(diagnostic) = pylint::rules::invalid_all_object(binding) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::NonAsciiName) {
if let Some(diagnostic) = pylint::rules::non_ascii_name(binding, checker.locator) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::UnconventionalImportAlias) {
@@ -72,61 +72,61 @@ pub(crate) fn bindings(checker: &Checker) {
binding,
&checker.settings.flake8_import_conventions.aliases,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::UnaliasedCollectionsAbcSetImport) {
if let Some(diagnostic) =
flake8_pyi::rules::unaliased_collections_abc_set_import(checker, binding)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if !checker.source_type.is_stub() && checker.enabled(Rule::UnquotedTypeAlias) {
if let Some(diagnostics) =
flake8_type_checking::rules::unquoted_type_alias(checker, binding)
{
checker.report_diagnostics(diagnostics);
checker.diagnostics.extend(diagnostics);
}
}
if checker.enabled(Rule::UnsortedDunderSlots) {
if let Some(diagnostic) = ruff::rules::sort_dunder_slots(checker, binding) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::UsedDummyVariable) {
if let Some(diagnostic) = ruff::rules::used_dummy_variable(checker, binding, binding_id)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::AssignmentInAssert) {
if let Some(diagnostic) = ruff::rules::assignment_in_assert(checker, binding) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::PytestUnittestRaisesAssertion) {
if let Some(diagnostic) =
flake8_pytest_style::rules::unittest_raises_assertion_binding(checker, binding)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::ForLoopWrites) {
if let Some(diagnostic) = refurb::rules::for_loop_writes_binding(checker, binding) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CustomTypeVarForSelf) {
if let Some(diagnostic) =
flake8_pyi::rules::custom_type_var_instead_of_self(checker, binding)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::PrivateTypeParameter) {
if let Some(diagnostic) = pyupgrade::rules::private_type_parameter(checker, binding) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
}

View File

@@ -5,7 +5,7 @@ use crate::codes::Rule;
use crate::rules::{flake8_simplify, pylint, refurb};
/// Run lint rules over a [`Comprehension`] syntax nodes.
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &Checker) {
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker) {
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_comprehension(checker, comprehension);
}

View File

@@ -13,7 +13,7 @@ use crate::rules::{
};
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
pub(crate) fn deferred_scopes(checker: &Checker) {
pub(crate) fn deferred_scopes(checker: &mut Checker) {
if !checker.any_enabled(&[
Rule::AsyncioDanglingTask,
Rule::BadStaticmethodArgument,
@@ -85,11 +85,12 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
vec![]
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for scope_id in checker.analyze.scopes.iter().rev().copied() {
let scope = &checker.semantic.scopes[scope_id];
if checker.enabled(Rule::UndefinedLocal) {
pyflakes::rules::undefined_local(checker, scope_id, scope);
pyflakes::rules::undefined_local(checker, scope_id, scope, &mut diagnostics);
}
if checker.enabled(Rule::GlobalVariableNotAssigned) {
@@ -111,7 +112,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
.map(|id| checker.semantic.reference(*id))
.all(ResolvedReference::is_load)
{
checker.report_diagnostic(Diagnostic::new(
diagnostics.push(Diagnostic::new(
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
@@ -145,7 +146,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
if scope.kind.is_generator() {
continue;
}
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pylint::rules::RedefinedArgumentFromLocal {
name: name.to_string(),
},
@@ -185,7 +186,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
continue;
}
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
row: checker.compute_source_row(shadowed.start()),
@@ -346,7 +347,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
diagnostic.set_fix(fix.clone());
}
checker.report_diagnostic(diagnostic);
diagnostics.push(diagnostic);
}
}
}
@@ -355,47 +356,55 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
|| matches!(scope.kind, ScopeKind::Module | ScopeKind::Function(_))
{
if checker.enabled(Rule::UnusedPrivateTypeVar) {
flake8_pyi::rules::unused_private_type_var(checker, scope);
flake8_pyi::rules::unused_private_type_var(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateProtocol) {
flake8_pyi::rules::unused_private_protocol(checker, scope);
flake8_pyi::rules::unused_private_protocol(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateTypeAlias) {
flake8_pyi::rules::unused_private_type_alias(checker, scope);
flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateTypedDict) {
flake8_pyi::rules::unused_private_typed_dict(checker, scope);
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
}
}
if checker.enabled(Rule::AsyncioDanglingTask) {
ruff::rules::asyncio_dangling_binding(scope, checker);
ruff::rules::asyncio_dangling_binding(scope, &checker.semantic, &mut diagnostics);
}
if let Some(class_def) = scope.kind.as_class() {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, scope_id, scope, class_def,
checker,
scope_id,
scope,
class_def,
&mut diagnostics,
);
}
if checker.enabled(Rule::FunctionCallInDataclassDefaultArgument) {
ruff::rules::function_call_in_dataclass_default(checker, class_def);
ruff::rules::function_call_in_dataclass_default(
checker,
class_def,
&mut diagnostics,
);
}
if checker.enabled(Rule::MutableClassDefault) {
ruff::rules::mutable_class_default(checker, class_def);
ruff::rules::mutable_class_default(checker, class_def, &mut diagnostics);
}
if checker.enabled(Rule::MutableDataclassDefault) {
ruff::rules::mutable_dataclass_default(checker, class_def);
ruff::rules::mutable_dataclass_default(checker, class_def, &mut diagnostics);
}
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope);
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedAnnotation) {
pyflakes::rules::unused_annotation(checker, scope);
pyflakes::rules::unused_annotation(checker, scope, &mut diagnostics);
}
if !checker.source_type.is_stub() {
@@ -406,7 +415,11 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
Rule::UnusedMethodArgument,
Rule::UnusedStaticMethodArgument,
]) {
flake8_unused_arguments::rules::unused_arguments(checker, scope);
flake8_unused_arguments::rules::unused_arguments(
checker,
scope,
&mut diagnostics,
);
}
}
}
@@ -415,7 +428,11 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
if !checker.source_type.is_stub()
&& checker.enabled(Rule::RuntimeImportInTypeCheckingBlock)
{
flake8_type_checking::rules::runtime_import_in_type_checking_block(checker, scope);
flake8_type_checking::rules::runtime_import_in_type_checking_block(
checker,
scope,
&mut diagnostics,
);
}
if enforce_typing_only_imports {
let runtime_imports: Vec<&Binding> = checker
@@ -430,45 +447,47 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
checker,
scope,
&runtime_imports,
&mut diagnostics,
);
}
if checker.enabled(Rule::UnusedImport) {
pyflakes::rules::unused_import(checker, scope);
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::ImportPrivateName) {
pylint::rules::import_private_name(checker, scope);
pylint::rules::import_private_name(checker, scope, &mut diagnostics);
}
}
if scope.kind.is_function() {
if checker.enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope_id, scope);
pylint::rules::no_self_use(checker, scope_id, scope, &mut diagnostics);
}
if checker.enabled(Rule::TooManyLocals) {
pylint::rules::too_many_locals(checker, scope);
pylint::rules::too_many_locals(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::SingledispatchMethod) {
pylint::rules::singledispatch_method(checker, scope);
pylint::rules::singledispatch_method(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::SingledispatchmethodFunction) {
pylint::rules::singledispatchmethod_function(checker, scope);
pylint::rules::singledispatchmethod_function(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::BadStaticmethodArgument) {
pylint::rules::bad_staticmethod_argument(checker, scope);
pylint::rules::bad_staticmethod_argument(checker, scope, &mut diagnostics);
}
if checker.any_enabled(&[
Rule::InvalidFirstArgumentNameForClassMethod,
Rule::InvalidFirstArgumentNameForMethod,
]) {
pep8_naming::rules::invalid_first_argument_name(checker, scope);
pep8_naming::rules::invalid_first_argument_name(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);
}

View File

@@ -139,11 +139,13 @@ pub(crate) fn definitions(checker: &mut Checker) {
&checker.semantic,
)
}) {
checker.report_diagnostics(flake8_annotations::rules::definition(
checker,
definition,
*visibility,
));
checker
.diagnostics
.extend(flake8_annotations::rules::definition(
checker,
definition,
*visibility,
));
}
overloaded_name =
flake8_annotations::helpers::overloaded_name(definition, &checker.semantic);

View File

@@ -8,7 +8,7 @@ use crate::rules::{
};
/// Run lint rules over an [`ExceptHandler`] syntax node.
pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &Checker) {
pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &mut Checker) {
match except_handler {
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_,
@@ -23,7 +23,7 @@ pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &Checker)
except_handler,
checker.locator,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::RaiseWithoutFromInsideExcept) {

View File

@@ -21,7 +21,7 @@ use crate::rules::{
use crate::settings::types::PythonVersion;
/// Run lint rules over an [`Expr`] syntax node.
pub(crate) fn expression(expr: &Expr, checker: &Checker) {
pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
match expr {
Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => {
// Ex) Optional[...], Union[...]
@@ -201,7 +201,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
check_two_starred_expressions,
expr.range(),
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
}
@@ -515,7 +515,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
match pyflakes::format::FormatSummary::try_from(string_value.to_str()) {
Err(e) => {
if checker.enabled(Rule::StringDotFormatInvalidFormat) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::StringDotFormatInvalidFormat {
message: pyflakes::format::error_to_string(&e),
},
@@ -842,7 +842,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_comprehensions::rules::unnecessary_subscript_reversal(checker, call);
}
if checker.enabled(Rule::UnnecessaryMap) {
flake8_comprehensions::rules::unnecessary_map(checker, call);
flake8_comprehensions::rules::unnecessary_map(
checker,
expr,
checker.semantic.current_expression_parent(),
func,
args,
);
}
if checker.enabled(Rule::UnnecessaryComprehensionInCall) {
flake8_comprehensions::rules::unnecessary_comprehension_in_call(
@@ -906,7 +912,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
pylint::rules::bad_open_mode(checker, call);
}
if checker.enabled(Rule::BadStrStripCall) {
pylint::rules::bad_str_strip_call(checker, call);
pylint::rules::bad_str_strip_call(checker, func, args);
}
if checker.enabled(Rule::ShallowCopyEnviron) {
pylint::rules::shallow_copy_environ(checker, call);
@@ -925,7 +931,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
}
if checker.enabled(Rule::PytestPatchWithLambda) {
if let Some(diagnostic) = flake8_pytest_style::rules::patch_with_lambda(call) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.any_enabled(&[
@@ -1281,7 +1287,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
..
}) => {
if checker.enabled(Rule::PercentFormatUnsupportedFormatCharacter) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::PercentFormatUnsupportedFormatCharacter {
char: c,
},
@@ -1291,7 +1297,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
}
Err(e) => {
if checker.enabled(Rule::PercentFormatInvalidFormat) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::PercentFormatInvalidFormat {
message: e.to_string(),
},
@@ -1365,7 +1371,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
checker.locator,
checker.settings,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CollectionLiteralConcatenation) {

View File

@@ -5,7 +5,7 @@ use crate::codes::Rule;
use crate::rules::{flake8_bugbear, ruff};
/// Run lint rules over a module.
pub(crate) fn module(suite: &Suite, checker: &Checker) {
pub(crate) fn module(suite: &Suite, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, suite);
}

View File

@@ -6,7 +6,7 @@ use crate::codes::Rule;
use crate::rules::{flake8_builtins, pycodestyle};
/// Run lint rules over a [`Parameter`] syntax node.
pub(crate) fn parameter(parameter: &Parameter, checker: &Checker) {
pub(crate) fn parameter(parameter: &Parameter, checker: &mut Checker) {
if checker.enabled(Rule::AmbiguousVariableName) {
pycodestyle::rules::ambiguous_variable_name(
checker,

View File

@@ -5,7 +5,7 @@ use crate::codes::Rule;
use crate::rules::{flake8_bugbear, flake8_pyi, ruff};
/// Run lint rules over a [`Parameters`] syntax node.
pub(crate) fn parameters(parameters: &Parameters, checker: &Checker) {
pub(crate) fn parameters(parameters: &Parameters, checker: &mut Checker) {
if checker.enabled(Rule::FunctionCallInDefaultArgument) {
flake8_bugbear::rules::function_call_in_argument_default(checker, parameters);
}

View File

@@ -39,7 +39,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if !checker.semantic.scope_id.is_global() {
for name in names {
if checker.semantic.nonlocal(name).is_none() {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pylint::rules::NonlocalWithoutBinding {
name: name.to_string(),
},
@@ -59,7 +59,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
&mut checker.semantic.current_statements().skip(1),
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
}
@@ -69,7 +69,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
&mut checker.semantic.current_statements().skip(1),
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
}
@@ -99,7 +99,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
if checker.enabled(Rule::AmbiguousFunctionName) {
if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::InvalidBoolReturnType) {
@@ -128,7 +128,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.pep8_naming.ignore_names,
&checker.semantic,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.source_type.is_stub() {
@@ -187,7 +187,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
name,
&checker.settings.pep8_naming.ignore_names,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::GlobalStatement) {
@@ -239,7 +239,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body,
checker.settings.mccabe.max_complexity,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::HardcodedPasswordDefault) {
@@ -265,7 +265,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body,
checker.settings.pylint.max_returns,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::TooManyBranches) {
@@ -274,7 +274,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body,
checker.settings.pylint.max_branches,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::TooManyStatements) {
@@ -283,7 +283,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body,
checker.settings.pylint.max_statements,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.any_enabled(&[
@@ -351,7 +351,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
#[cfg(any(feature = "test-rules", test))]
if checker.enabled(Rule::UnreachableCode) {
checker.report_diagnostics(pylint::rules::in_function(name, body));
checker
.diagnostics
.extend(pylint::rules::in_function(name, body));
}
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &function_def.into());
@@ -454,7 +456,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
if checker.enabled(Rule::AmbiguousClassName) {
if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::InvalidClassName) {
@@ -463,7 +465,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
name,
&checker.settings.pep8_naming.ignore_names,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::ErrorSuffixOnExceptionName) {
@@ -473,7 +475,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
name,
&checker.settings.pep8_naming.ignore_names,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if !checker.source_type.is_stub() {
@@ -552,9 +554,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonPEP695GenericClass) {
pyupgrade::rules::non_pep695_generic_class(checker, class_def);
}
if checker.enabled(Rule::ClassWithMixedTypeVars) {
ruff::rules::class_with_mixed_type_vars(checker, class_def);
}
}
Stmt::Import(ast::StmtImport { names, range: _ }) => {
if checker.enabled(Rule::MultipleImportsOnOneLine) {
@@ -613,7 +612,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
flake8_debugger::rules::debugger_import(stmt, None, &alias.name)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::BannedApi) {
@@ -640,7 +639,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
pylint::rules::import_self(alias, checker.module.qualified_name())
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if let Some(asname) = &alias.asname {
@@ -655,7 +654,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.pep8_naming.ignore_names,
)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::LowercaseImportedAsNonLowercase) {
@@ -668,7 +667,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.pep8_naming.ignore_names,
)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CamelcaseImportedAsLowercase) {
@@ -681,7 +680,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.pep8_naming.ignore_names,
)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CamelcaseImportedAsConstant) {
@@ -692,14 +691,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
&checker.settings.pep8_naming.ignore_names,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CamelcaseImportedAsAcronym) {
if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym(
name, asname, alias, stmt, checker,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
}
@@ -713,7 +712,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.flake8_import_conventions.banned_aliases,
)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
}
@@ -723,7 +722,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&alias.name,
alias.asname.as_deref(),
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::BuiltinImportShadowing) {
@@ -839,7 +838,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
flake8_pytest_style::rules::import_from(stmt, module, level)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.source_type.is_stub() {
@@ -854,7 +853,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
if checker.enabled(Rule::LateFutureImport) {
if checker.semantic.seen_futures_boundary() {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::LateFutureImport,
stmt.range(),
));
@@ -863,7 +862,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
} else if &alias.name == "*" {
if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithNestedImportStarUsage {
name: helpers::format_import_from(level, module).to_string(),
},
@@ -872,7 +871,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::UndefinedLocalWithImportStar) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStar {
name: helpers::format_import_from(level, module).to_string(),
},
@@ -889,14 +888,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.module.qualified_name(),
checker.settings.flake8_tidy_imports.ban_relative_imports,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::Debugger) {
if let Some(diagnostic) =
flake8_debugger::rules::debugger_import(stmt, module, &alias.name)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::BannedImportAlias) {
@@ -911,7 +910,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.flake8_import_conventions.banned_aliases,
)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
}
@@ -926,7 +925,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.pep8_naming.ignore_names,
)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::LowercaseImportedAsNonLowercase) {
@@ -939,7 +938,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.pep8_naming.ignore_names,
)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CamelcaseImportedAsLowercase) {
@@ -952,7 +951,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&checker.settings.pep8_naming.ignore_names,
)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CamelcaseImportedAsConstant) {
@@ -963,7 +962,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
&checker.settings.pep8_naming.ignore_names,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CamelcaseImportedAsAcronym) {
@@ -974,7 +973,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
checker,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if !checker.source_type.is_stub() {
@@ -994,7 +993,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
names,
checker.module.qualified_name(),
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::BannedImportFrom) {
@@ -1003,7 +1002,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&helpers::format_import_from(level, module),
&checker.settings.flake8_import_conventions.banned_from,
) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::ByteStringUsage) {
@@ -1172,7 +1171,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
if checker.any_enabled(&[Rule::BadVersionInfoComparison, Rule::BadVersionInfoOrder]) {
fn bad_version_info_comparison(
checker: &Checker,
checker: &mut Checker,
test: &Expr,
has_else_clause: bool,
) {
@@ -1219,7 +1218,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
) => {
if !checker.semantic.in_type_checking_block() {
if checker.enabled(Rule::Assert) {
checker.report_diagnostic(flake8_bandit::rules::assert_used(stmt));
checker
.diagnostics
.push(flake8_bandit::rules::assert_used(stmt));
}
}
if checker.enabled(Rule::AssertTuple) {
@@ -1436,7 +1437,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
pyflakes::rules::default_except_not_last(handlers, checker.locator)
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.any_enabled(&[
@@ -1533,7 +1534,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
if checker.enabled(Rule::PandasDfVariableName) {
if let Some(diagnostic) = pandas_vet::rules::assignment_to_df(targets) {
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker
@@ -1731,7 +1732,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
ruff::rules::asyncio_dangling_task(value, checker.semantic())
{
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::RepeatedAppend) {

View File

@@ -5,7 +5,7 @@ use crate::codes::Rule;
use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, pycodestyle, ruff};
/// Run lint rules over a [`StringLike`] syntax nodes.
pub(crate) fn string_like(string_like: StringLike, checker: &Checker) {
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
if checker.any_enabled(&[
Rule::AmbiguousUnicodeCharacterString,
Rule::AmbiguousUnicodeCharacterDocstring,

View File

@@ -6,7 +6,7 @@ use crate::rules::flake8_pie;
use crate::rules::refurb;
/// Run lint rules over a suite of [`Stmt`] syntax nodes.
pub(crate) fn suite(suite: &[Stmt], checker: &Checker) {
pub(crate) fn suite(suite: &[Stmt], checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryPlaceholder) {
flake8_pie::rules::unnecessary_placeholder(checker, suite);
}

View File

@@ -7,7 +7,7 @@ use crate::codes::Rule;
use crate::rules::pyflakes;
/// Run lint rules over all [`UnresolvedReference`] entities in the [`SemanticModel`].
pub(crate) fn unresolved_references(checker: &Checker) {
pub(crate) fn unresolved_references(checker: &mut Checker) {
if !checker.any_enabled(&[Rule::UndefinedLocalWithImportStarUsage, Rule::UndefinedName]) {
return;
}
@@ -15,7 +15,7 @@ pub(crate) fn unresolved_references(checker: &Checker) {
for reference in checker.semantic.unresolved_references() {
if reference.is_wildcard_import() {
if checker.enabled(Rule::UndefinedLocalWithImportStarUsage) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: reference.name(checker.source()).to_string(),
},
@@ -42,7 +42,7 @@ pub(crate) fn unresolved_references(checker: &Checker) {
let symbol_name = reference.name(checker.source());
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: symbol_name.to_string(),
minor_version_builtin_added: version_builtin_was_added(symbol_name),

View File

@@ -9,6 +9,11 @@
//! parent scopes have been fully traversed. Individual rules may also perform internal traversals
//! of the AST.
//!
//! While the [`Checker`] is typically passed by mutable reference to the individual lint rule
//! implementations, most of its constituent components are intended to be treated immutably, with
//! the exception of the [`Diagnostic`] vector, which is intended to be mutated by the individual
//! lint rules. In the future, this should be formalized in the API.
//!
//! The individual [`Visitor`] implementations within the [`Checker`] typically proceed in four
//! steps:
//!
@@ -26,7 +31,7 @@ use std::path::Path;
use itertools::Itertools;
use log::debug;
use rustc_hash::{FxHashMap, FxHashSet};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, IsolationLevel};
use ruff_notebook::{CellOffsets, NotebookIndex};
@@ -216,9 +221,9 @@ pub(crate) struct Checker<'a> {
/// A set of deferred nodes to be analyzed after the AST traversal (e.g., `for` loops).
analyze: deferred::Analyze,
/// The cumulative set of diagnostics computed across all lint rules.
diagnostics: RefCell<Vec<Diagnostic>>,
pub(crate) diagnostics: Vec<Diagnostic>,
/// The list of names already seen by flake8-bugbear diagnostics, to avoid duplicate violations.
flake8_bugbear_seen: RefCell<FxHashSet<TextRange>>,
pub(crate) flake8_bugbear_seen: Vec<TextRange>,
/// The end offset of the last visited statement.
last_stmt_end: TextSize,
/// A state describing if a docstring is expected or not.
@@ -243,11 +248,6 @@ impl<'a> Checker<'a> {
cell_offsets: Option<&'a CellOffsets>,
notebook_index: Option<&'a NotebookIndex>,
) -> Checker<'a> {
let mut semantic = SemanticModel::new(&settings.typing_modules, path, module);
if settings.preview.is_enabled() {
// Set the feature flag to test `TYPE_CHECKING` semantic changes
semantic.flags |= SemanticModelFlags::NEW_TYPE_CHECKING_BLOCK_DETECTION;
}
Self {
parsed,
parsed_type_annotation: None,
@@ -263,11 +263,11 @@ impl<'a> Checker<'a> {
stylist,
indexer,
importer: Importer::new(parsed, locator, stylist),
semantic,
semantic: SemanticModel::new(&settings.typing_modules, path, module),
visit: deferred::Visit::default(),
analyze: deferred::Analyze::default(),
diagnostics: RefCell::default(),
flake8_bugbear_seen: RefCell::default(),
diagnostics: Vec::default(),
flake8_bugbear_seen: Vec::default(),
cell_offsets,
notebook_index,
last_stmt_end: TextSize::default(),
@@ -357,30 +357,6 @@ impl<'a> Checker<'a> {
self.indexer.comment_ranges()
}
/// Push a new [`Diagnostic`] to the collection in the [`Checker`]
pub(crate) fn report_diagnostic(&self, diagnostic: Diagnostic) {
let mut diagnostics = self.diagnostics.borrow_mut();
diagnostics.push(diagnostic);
}
/// Extend the collection of [`Diagnostic`] objects in the [`Checker`]
pub(crate) fn report_diagnostics<I>(&self, diagnostics: I)
where
I: IntoIterator<Item = Diagnostic>,
{
let mut checker_diagnostics = self.diagnostics.borrow_mut();
checker_diagnostics.extend(diagnostics);
}
/// Adds a [`TextRange`] to the set of ranges of variable names
/// flagged in `flake8-bugbear` violations so far.
///
/// Returns whether the value was newly inserted.
pub(crate) fn insert_flake8_bugbear_range(&self, range: TextRange) -> bool {
let mut ranges = self.flake8_bugbear_seen.borrow_mut();
ranges.insert(range)
}
/// Returns the [`Tokens`] for the parsed type annotation if the checker is in a typing context
/// or the parsed source code.
pub(crate) fn tokens(&self) -> &'a Tokens {
@@ -495,9 +471,9 @@ impl<'a> Checker<'a> {
}
/// Push `diagnostic` if the checker is not in a `@no_type_check` context.
pub(crate) fn report_type_diagnostic(&self, diagnostic: Diagnostic) {
pub(crate) fn push_type_diagnostic(&mut self, diagnostic: Diagnostic) {
if !self.semantic.in_no_type_check() {
self.report_diagnostic(diagnostic);
self.diagnostics.push(diagnostic);
}
}
}
@@ -2438,7 +2414,7 @@ impl<'a> Checker<'a> {
self.semantic.restore(snapshot);
if self.enabled(Rule::ForwardAnnotationSyntaxError) {
self.report_type_diagnostic(Diagnostic::new(
self.push_type_diagnostic(Diagnostic::new(
pyflakes::rules::ForwardAnnotationSyntaxError {
parse_error: parse_error.error.to_string(),
},
@@ -2580,7 +2556,7 @@ impl<'a> Checker<'a> {
} else {
if self.semantic.global_scope().uses_star_imports() {
if self.enabled(Rule::UndefinedLocalWithImportStarUsage) {
self.diagnostics.get_mut().push(
self.diagnostics.push(
Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: name.to_string(),
@@ -2595,7 +2571,7 @@ impl<'a> Checker<'a> {
if self.settings.preview.is_enabled()
|| !self.path.ends_with("__init__.py")
{
self.diagnostics.get_mut().push(
self.diagnostics.push(
Diagnostic::new(
pyflakes::rules::UndefinedExport {
name: name.to_string(),
@@ -2719,13 +2695,13 @@ pub(crate) fn check_ast(
analyze::deferred_lambdas(&mut checker);
analyze::deferred_for_loops(&mut checker);
analyze::definitions(&mut checker);
analyze::bindings(&checker);
analyze::unresolved_references(&checker);
analyze::bindings(&mut checker);
analyze::unresolved_references(&mut checker);
// Reset the scope to module-level, and check all consumed scopes.
checker.semantic.scope_id = ScopeId::global();
checker.analyze.scopes.push(ScopeId::global());
analyze::deferred_scopes(&checker);
analyze::deferred_scopes(&mut checker);
checker.diagnostics.take()
checker.diagnostics
}

View File

@@ -46,7 +46,12 @@ pub(crate) fn check_file_path(
// flake8-builtins
if settings.rules.enabled(Rule::StdlibModuleShadowing) {
if let Some(diagnostic) = stdlib_module_shadowing(path, settings) {
if let Some(diagnostic) = stdlib_module_shadowing(
path,
package,
&settings.flake8_builtins.builtins_allowed_modules,
settings.target_version,
) {
diagnostics.push(diagnostic);
}
}

View File

@@ -1005,7 +1005,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "049") => (RuleGroup::Preview, rules::ruff::rules::DataclassEnum),
(Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel),
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
(Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),

View File

@@ -9,7 +9,7 @@ use anyhow::Result;
use libcst_native::{ImportAlias, Name as cstName, NameOrAttribute};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Expr, ModModule, Stmt};
use ruff_python_ast::{self as ast, ModModule, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_parser::{Parsed, Tokens};
use ruff_python_semantic::{
@@ -125,7 +125,7 @@ impl<'a> Importer<'a> {
&self,
import: &ImportedMembers,
at: TextSize,
semantic: &SemanticModel<'a>,
semantic: &SemanticModel,
) -> Result<TypingImportEdit> {
// Generate the modified import statement.
let content = fix::codemods::retain_imports(
@@ -135,39 +135,6 @@ impl<'a> Importer<'a> {
self.stylist,
)?;
// Add the import to an existing `TYPE_CHECKING` block.
if let Some(block) = self.preceding_type_checking_block(at) {
// Add the import to the existing `TYPE_CHECKING` block.
let type_checking_edit =
if let Some(statement) = Self::type_checking_binding_statement(semantic, block) {
if statement == import.statement {
// Special-case: if the `TYPE_CHECKING` symbol is imported as part of the same
// statement that we're modifying, avoid adding a no-op edit. For example, here,
// the `TYPE_CHECKING` no-op edit would overlap with the edit to remove `Final`
// from the import:
// ```python
// from __future__ import annotations
//
// from typing import Final, TYPE_CHECKING
//
// Const: Final[dict] = {}
// ```
None
} else {
Some(Edit::range_replacement(
self.locator.slice(statement.range()).to_string(),
statement.range(),
))
}
} else {
None
};
return Ok(TypingImportEdit {
type_checking_edit,
add_import_edit: self.add_to_type_checking_block(&content, block.start()),
});
}
// Import the `TYPE_CHECKING` symbol from the typing module.
let (type_checking_edit, type_checking) =
if let Some(type_checking) = Self::find_type_checking(at, semantic)? {
@@ -212,10 +179,13 @@ impl<'a> Importer<'a> {
(Some(edit), name)
};
// Add the import to a new `TYPE_CHECKING` block.
Ok(TypingImportEdit {
type_checking_edit,
add_import_edit: self.add_type_checking_block(
// Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
// Add the import to the `TYPE_CHECKING` block.
self.add_to_type_checking_block(&content, block.start())
} else {
// Add the import to a new `TYPE_CHECKING` block.
self.add_type_checking_block(
&format!(
"{}if {type_checking}:{}{}",
self.stylist.line_ending().as_str(),
@@ -223,25 +193,13 @@ impl<'a> Importer<'a> {
indent(&content, self.stylist.indentation())
),
at,
)?,
})
}
fn type_checking_binding_statement(
semantic: &SemanticModel<'a>,
type_checking_block: &Stmt,
) -> Option<&'a Stmt> {
let Stmt::If(ast::StmtIf { test, .. }) = type_checking_block else {
return None;
)?
};
let mut source = test;
while let Expr::Attribute(ast::ExprAttribute { value, .. }) = source.as_ref() {
source = value;
}
semantic
.binding(semantic.resolve_name(source.as_name_expr()?)?)
.statement(semantic)
Ok(TypingImportEdit {
type_checking_edit,
add_import_edit,
})
}
/// Find a reference to `typing.TYPE_CHECKING`.

View File

@@ -387,36 +387,25 @@ pub(crate) enum ShadowedKind {
}
impl ShadowedKind {
/// Determines the kind of shadowing or conflict for the proposed new name of a given [`Binding`].
/// Determines the kind of shadowing or conflict for a given variable name.
///
/// This function is useful for checking whether or not the `target` of a [`Renamer::rename`]
/// This function is useful for checking whether or not the `target` of a [`Rename::rename`]
/// will shadow another binding.
pub(crate) fn new(binding: &Binding, new_name: &str, checker: &Checker) -> ShadowedKind {
pub(crate) fn new(name: &str, checker: &Checker, scope_id: ScopeId) -> ShadowedKind {
// Check the kind in order of precedence
if is_keyword(new_name) {
if is_keyword(name) {
return ShadowedKind::Keyword;
}
if is_python_builtin(
new_name,
name,
checker.settings.target_version.minor(),
checker.source_type.is_ipynb(),
) {
return ShadowedKind::BuiltIn;
}
let semantic = checker.semantic();
if !semantic.is_available_in_scope(new_name, binding.scope) {
return ShadowedKind::Some;
}
if binding
.references()
.map(|reference_id| semantic.reference(reference_id).scope_id())
.dedup()
.any(|scope| !semantic.is_available_in_scope(new_name, scope))
{
if !checker.semantic().is_available_in_scope(name, scope_id) {
return ShadowedKind::Some;
}

View File

@@ -50,7 +50,7 @@ impl Violation for AirflowDagNoScheduleArgument {
}
/// AIR301
pub(crate) fn dag_no_schedule_argument(checker: &Checker, expr: &Expr) {
pub(crate) fn dag_no_schedule_argument(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::AIRFLOW) {
return;
}
@@ -86,5 +86,5 @@ pub(crate) fn dag_no_schedule_argument(checker: &Checker, expr: &Expr) {
// Produce a diagnostic when the `schedule` keyword argument is not found.
let diagnostic = Diagnostic::new(AirflowDagNoScheduleArgument, expr.range());
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}

View File

@@ -83,7 +83,7 @@ impl Violation for Airflow3MovedToProvider {
}
/// AIR303
pub(crate) fn moved_to_provider_in_3(checker: &Checker, expr: &Expr) {
pub(crate) fn moved_to_provider_in_3(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::AIRFLOW) {
return;
}
@@ -112,7 +112,7 @@ enum Replacement {
},
}
fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRange) {
fn check_names_moved_to_provider(checker: &mut Checker, expr: &Expr, ranged: TextRange) {
let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) else {
return;
};
@@ -1018,7 +1018,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
},
_ => return,
};
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3MovedToProvider {
deprecated: qualified_name.to_string(),
replacement,

View File

@@ -80,7 +80,7 @@ enum Replacement {
}
/// AIR302
pub(crate) fn airflow_3_removal_expr(checker: &Checker, expr: &Expr) {
pub(crate) fn airflow_3_removal_expr(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::AIRFLOW) {
return;
}
@@ -117,7 +117,10 @@ pub(crate) fn airflow_3_removal_expr(checker: &Checker, expr: &Expr) {
}
/// AIR302
pub(crate) fn airflow_3_removal_function_def(checker: &Checker, function_def: &StmtFunctionDef) {
pub(crate) fn airflow_3_removal_function_def(
checker: &mut Checker,
function_def: &StmtFunctionDef,
) {
if !checker.semantic().seen_module(Modules::AIRFLOW) {
return;
}
@@ -153,7 +156,7 @@ const REMOVED_CONTEXT_KEYS: [&str; 12] = [
/// # 'execution_date' is removed in Airflow 3.0
/// pass
/// ```
fn check_function_parameters(checker: &Checker, function_def: &StmtFunctionDef) {
fn check_function_parameters(checker: &mut Checker, function_def: &StmtFunctionDef) {
if !is_airflow_task(function_def, checker.semantic())
&& !is_execute_method_inherits_from_airflow_operator(function_def, checker.semantic())
{
@@ -163,7 +166,7 @@ fn check_function_parameters(checker: &Checker, function_def: &StmtFunctionDef)
for param in function_def.parameters.iter_non_variadic_params() {
let param_name = param.name();
if REMOVED_CONTEXT_KEYS.contains(&param_name.as_str()) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: param_name.to_string(),
replacement: Replacement::None,
@@ -183,25 +186,29 @@ fn check_function_parameters(checker: &Checker, function_def: &StmtFunctionDef)
///
/// DAG(schedule_interval="@daily")
/// ```
fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, arguments: &Arguments) {
fn check_call_arguments(
checker: &mut Checker,
qualified_name: &QualifiedName,
arguments: &Arguments,
) {
match qualified_name.segments() {
["airflow", .., "DAG" | "dag"] => {
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"schedule_interval",
Some("schedule"),
));
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"timetable",
Some("schedule"),
));
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"sla_miss_callback",
None,
));
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"fail_stop",
Some("fail_fast"),
@@ -210,7 +217,7 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
_ => {
if is_airflow_auth_manager(qualified_name.segments()) {
if !arguments.is_empty() {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: String::from("appbuilder"),
replacement: Replacement::Message(
@@ -221,42 +228,44 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
));
}
} else if is_airflow_task_handler(qualified_name.segments()) {
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"filename_template",
None,
));
} else if is_airflow_operator(qualified_name.segments()) {
checker.report_diagnostics(diagnostic_for_argument(arguments, "sla", None));
checker.report_diagnostics(diagnostic_for_argument(
checker
.diagnostics
.extend(diagnostic_for_argument(arguments, "sla", None));
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"task_concurrency",
Some("max_active_tis_per_dag"),
));
match qualified_name.segments() {
["airflow", .., "operators", "trigger_dagrun", "TriggerDagRunOperator"] => {
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"execution_date",
Some("logical_date"),
));
}
["airflow", .., "operators", "datetime", "BranchDateTimeOperator"] => {
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
["airflow", .., "operators", "weekday", "DayOfWeekSensor"] => {
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
["airflow", .., "operators", "weekday", "BranchDayOfWeekOperator"] => {
checker.report_diagnostics(diagnostic_for_argument(
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
@@ -279,7 +288,7 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
/// info = DatasetLineageInfo()
/// info.dataset
/// ```
fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) {
fn check_class_attribute(checker: &mut Checker, attribute_expr: &ExprAttribute) {
let ExprAttribute { value, attr, .. } = attribute_expr;
let Some(qualname) = typing::resolve_assignment(value, checker.semantic()) else {
@@ -303,7 +312,7 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) {
};
if let Some(replacement) = replacement {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: attr.to_string(),
replacement,
@@ -341,7 +350,7 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) {
/// def my_task(**context):
/// context.get("conf") # 'conf' is removed in Airflow 3.0
/// ```
fn check_context_key_usage_in_call(checker: &Checker, call_expr: &ExprCall) {
fn check_context_key_usage_in_call(checker: &mut Checker, call_expr: &ExprCall) {
if !in_airflow_task_function(checker.semantic()) {
return;
}
@@ -377,7 +386,7 @@ fn check_context_key_usage_in_call(checker: &Checker, call_expr: &ExprCall) {
continue;
};
if value == removed_key {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: removed_key.to_string(),
replacement: Replacement::None,
@@ -390,7 +399,7 @@ fn check_context_key_usage_in_call(checker: &Checker, call_expr: &ExprCall) {
/// Check if a subscript expression accesses a removed Airflow context variable.
/// If a removed key is found, push a corresponding diagnostic.
fn check_context_key_usage_in_subscript(checker: &Checker, subscript: &ExprSubscript) {
fn check_context_key_usage_in_subscript(checker: &mut Checker, subscript: &ExprSubscript) {
if !in_airflow_task_function(checker.semantic()) {
return;
}
@@ -418,7 +427,7 @@ fn check_context_key_usage_in_subscript(checker: &Checker, subscript: &ExprSubsc
}
if REMOVED_CONTEXT_KEYS.contains(&key.to_str()) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: key.to_string(),
replacement: Replacement::None,
@@ -454,7 +463,7 @@ fn is_kwarg_parameter(semantic: &SemanticModel, name: &ExprName) -> bool {
/// manager = DatasetManager()
/// manager.register_datsaet_change()
/// ```
fn check_method(checker: &Checker, call_expr: &ExprCall) {
fn check_method(checker: &mut Checker, call_expr: &ExprCall) {
let Expr::Attribute(ExprAttribute { attr, value, .. }) = &*call_expr.func else {
return;
};
@@ -519,7 +528,7 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
}
};
if let Some(replacement) = replacement {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: attr.to_string(),
replacement,
@@ -543,7 +552,7 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
/// # Or, directly
/// SubDagOperator()
/// ```
fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
fn check_name(checker: &mut Checker, expr: &Expr, range: TextRange) {
let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) else {
return;
};
@@ -681,7 +690,16 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
["airflow", "operators", "branch_operator", "BaseBranchOperator"] => {
Replacement::Name("airflow.operators.branch.BaseBranchOperator")
}
["airflow", "operators", "dummy" | "dummy_operator", "EmptyOperator" | "DummyOperator"] => {
["airflow", "operators", " dummy", "EmptyOperator"] => {
Replacement::Name("airflow.operators.empty.EmptyOperator")
}
["airflow", "operators", "dummy", "DummyOperator"] => {
Replacement::Name("airflow.operators.empty.EmptyOperator")
}
["airflow", "operators", "dummy_operator", "EmptyOperator"] => {
Replacement::Name("airflow.operators.empty.EmptyOperator")
}
["airflow", "operators", "dummy_operator", "DummyOperator"] => {
Replacement::Name("airflow.operators.empty.EmptyOperator")
}
["airflow", "operators", "email_operator", "EmailOperator"] => {
@@ -710,21 +728,24 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
}
// airflow.sensors
["airflow", "sensors", "external_task", "ExternalTaskSensorLink"] => {
Replacement::Name("airflow.sensors.external_task.ExternalDagLink")
}
["airflow", "sensors", "base_sensor_operator", "BaseSensorOperator"] => {
Replacement::Name("airflow.sensors.base.BaseSensorOperator")
}
["airflow", "sensors", "date_time_sensor", "DateTimeSensor"] => {
Replacement::Name("airflow.sensors.date_time.DateTimeSensor")
}
["airflow", "sensors", "external_task" | "external_task_sensor", "ExternalTaskMarker"] => {
["airflow", "sensors", "external_task_sensor", "ExternalTaskMarker"] => {
Replacement::Name("airflow.sensors.external_task.ExternalTaskMarker")
}
["airflow", "sensors", "external_task" | "external_task_sensor", "ExternalTaskSensorLink"] => {
Replacement::Name("airflow.sensors.external_task.ExternalDagLink")
}
["airflow", "sensors", "external_task" | "external_task_sensor", "ExternalTaskSensor"] => {
["airflow", "sensors", "external_task_sensor", "ExternalTaskSensor"] => {
Replacement::Name("airflow.sensors.external_task.ExternalTaskSensor")
}
["airflow", "sensors", "external_task_sensor", "ExternalTaskSensorLink"] => {
Replacement::Name("airflow.sensors.external_task.ExternalDagLink")
}
["airflow", "sensors", "time_delta_sensor", "TimeDeltaSensor"] => {
Replacement::Name("airflow.sensors.time_delta.TimeDeltaSensor")
}
@@ -743,9 +764,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
["airflow", "utils", "dates", "days_ago"] => {
Replacement::Name("pendulum.today('UTC').add(days=-N, ...)")
}
["airflow", "utils", "dates", "parse_execution_date" | "round_time" | "scale_time_units" | "infer_time_unit"] => {
Replacement::None
}
["airflow", "utils", "dates", "parse_execution_date"] => Replacement::None,
["airflow", "utils", "dates", "round_time"] => Replacement::None,
["airflow", "utils", "dates", "scale_time_units"] => Replacement::None,
["airflow", "utils", "dates", "infer_time_unit"] => Replacement::None,
// airflow.utils.file
["airflow", "utils", "file", "TemporaryDirectory"] => Replacement::None,
@@ -762,10 +784,12 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
}
// airflow.utils.state
["airflow", "utils", "state", "SHUTDOWN" | "terminating_states"] => Replacement::None,
["airflow", "utils", "state", "SHUTDOWN"] => Replacement::None,
["airflow", "utils", "state", "terminating_states"] => Replacement::None,
// airflow.utils.trigger_rule
["airflow", "utils", "trigger_rule", "TriggerRule", "DUMMY" | "NONE_FAILED_OR_SKIPPED"] => {
["airflow", "utils", "trigger_rule", "TriggerRule", "DUMMY"] => Replacement::None,
["airflow", "utils", "trigger_rule", "TriggerRule", "NONE_FAILED_OR_SKIPPED"] => {
Replacement::None
}
@@ -867,7 +891,7 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
_ => return,
};
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: qualified_name.to_string(),
replacement,
@@ -888,7 +912,7 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
/// executors = "some.third.party.executor"
/// ```
fn check_airflow_plugin_extension(
checker: &Checker,
checker: &mut Checker,
expr: &Expr,
name: &str,
class_def: &StmtClassDef,
@@ -905,7 +929,7 @@ fn check_airflow_plugin_extension(
)
})
}) {
checker.report_diagnostic(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: name.to_string(),
replacement: Replacement::Message(

View File

@@ -45,7 +45,7 @@ impl Violation for AirflowVariableNameTaskIdMismatch {
}
/// AIR001
pub(crate) fn variable_name_task_id(checker: &Checker, targets: &[Expr], value: &Expr) {
pub(crate) fn variable_name_task_id(checker: &mut Checker, targets: &[Expr], value: &Expr) {
if !checker.semantic().seen_module(Modules::AIRFLOW) {
return;
}
@@ -116,5 +116,5 @@ pub(crate) fn variable_name_task_id(checker: &Checker, targets: &[Expr], value:
},
target.range(),
);
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}

View File

@@ -88,7 +88,7 @@ impl Violation for FastApiNonAnnotatedDependency {
/// FAST002
pub(crate) fn fastapi_non_annotated_dependency(
checker: &Checker,
checker: &mut Checker,
function_def: &ast::StmtFunctionDef,
) {
if !checker.semantic().seen_module(Modules::FASTAPI)
@@ -219,7 +219,7 @@ impl<'a> DependencyCall<'a> {
/// necessary to determine this while generating the fix, thus the need to return an updated
/// `seen_default` here.
fn create_diagnostic(
checker: &Checker,
checker: &mut Checker,
parameter: &DependencyParameter,
dependency_call: Option<DependencyCall>,
mut seen_default: bool,
@@ -304,7 +304,7 @@ fn create_diagnostic(
}
diagnostic.try_set_optional_fix(|| fix);
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
seen_default
}

View File

@@ -74,7 +74,10 @@ impl AlwaysFixableViolation for FastApiRedundantResponseModel {
}
/// FAST001
pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def: &StmtFunctionDef) {
pub(crate) fn fastapi_redundant_response_model(
checker: &mut Checker,
function_def: &StmtFunctionDef,
) {
if !checker.semantic().seen_module(Modules::FASTAPI) {
return;
}
@@ -95,7 +98,7 @@ pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def:
)
.map(Fix::unsafe_edit)
});
checker.report_diagnostic(diagnostic);
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -105,7 +105,7 @@ impl Violation for FastApiUnusedPathParameter {
/// FAST003
pub(crate) fn fastapi_unused_path_parameter(
checker: &Checker,
checker: &mut Checker,
function_def: &ast::StmtFunctionDef,
) {
if !checker.semantic().seen_module(Modules::FASTAPI) {
@@ -163,6 +163,7 @@ pub(crate) fn fastapi_unused_path_parameter(
}
// Check if any of the path parameters are not in the function signature.
let mut diagnostics = vec![];
for (path_param, range) in path_params {
// Ignore invalid identifiers (e.g., `user-id`, as opposed to `user_id`)
if !is_identifier(path_param) {
@@ -202,8 +203,10 @@ pub(crate) fn fastapi_unused_path_parameter(
checker.locator().contents(),
)));
}
checker.report_diagnostic(diagnostic);
diagnostics.push(diagnostic);
}
checker.diagnostics.extend(diagnostics);
}
/// Returns an iterator over the non-positional-only, non-variadic parameters of a function.

View File

@@ -223,7 +223,7 @@ impl Violation for SysVersionCmpStr10 {
}
/// YTT103, YTT201, YTT203, YTT204, YTT302
pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators: &[Expr]) {
pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], comparators: &[Expr]) {
match left {
Expr::Subscript(ast::ExprSubscript { value, slice, .. })
if is_sys(value, "version_info", checker.semantic()) =>
@@ -243,10 +243,9 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators
) = (ops, comparators)
{
if *n == 3 && checker.enabled(Rule::SysVersionInfo0Eq3) {
checker.report_diagnostic(Diagnostic::new(
SysVersionInfo0Eq3,
left.range(),
));
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfo0Eq3, left.range()));
}
}
} else if *i == 1 {
@@ -259,10 +258,9 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators
) = (ops, comparators)
{
if checker.enabled(Rule::SysVersionInfo1CmpInt) {
checker.report_diagnostic(Diagnostic::new(
SysVersionInfo1CmpInt,
left.range(),
));
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfo1CmpInt, left.range()));
}
}
}
@@ -281,10 +279,9 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators
) = (ops, comparators)
{
if checker.enabled(Rule::SysVersionInfoMinorCmpInt) {
checker.report_diagnostic(Diagnostic::new(
SysVersionInfoMinorCmpInt,
left.range(),
));
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfoMinorCmpInt, left.range()));
}
}
}
@@ -300,10 +297,14 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators
{
if value.len() == 1 {
if checker.enabled(Rule::SysVersionCmpStr10) {
checker.report_diagnostic(Diagnostic::new(SysVersionCmpStr10, left.range()));
checker
.diagnostics
.push(Diagnostic::new(SysVersionCmpStr10, left.range()));
}
} else if checker.enabled(Rule::SysVersionCmpStr3) {
checker.report_diagnostic(Diagnostic::new(SysVersionCmpStr3, left.range()));
checker
.diagnostics
.push(Diagnostic::new(SysVersionCmpStr3, left.range()));
}
}
}

View File

@@ -46,7 +46,7 @@ impl Violation for SixPY3 {
}
/// YTT202
pub(crate) fn name_or_attribute(checker: &Checker, expr: &Expr) {
pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::SIX) {
return;
}
@@ -56,6 +56,8 @@ pub(crate) fn name_or_attribute(checker: &Checker, expr: &Expr) {
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["six", "PY3"]))
{
checker.report_diagnostic(Diagnostic::new(SixPY3, expr.range()));
checker
.diagnostics
.push(Diagnostic::new(SixPY3, expr.range()));
}
}

View File

@@ -168,7 +168,7 @@ impl Violation for SysVersionSlice1 {
}
/// YTT101, YTT102, YTT301, YTT303
pub(crate) fn subscript(checker: &Checker, value: &Expr, slice: &Expr) {
pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
if is_sys(value, "version", checker.semantic()) {
match slice {
Expr::Slice(ast::ExprSlice {
@@ -183,9 +183,13 @@ pub(crate) fn subscript(checker: &Checker, value: &Expr, slice: &Expr) {
}) = upper.as_ref()
{
if *i == 1 && checker.enabled(Rule::SysVersionSlice1) {
checker.report_diagnostic(Diagnostic::new(SysVersionSlice1, value.range()));
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice1, value.range()));
} else if *i == 3 && checker.enabled(Rule::SysVersionSlice3) {
checker.report_diagnostic(Diagnostic::new(SysVersionSlice3, value.range()));
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice3, value.range()));
}
}
}
@@ -195,9 +199,13 @@ pub(crate) fn subscript(checker: &Checker, value: &Expr, slice: &Expr) {
..
}) => {
if *i == 2 && checker.enabled(Rule::SysVersion2) {
checker.report_diagnostic(Diagnostic::new(SysVersion2, value.range()));
checker
.diagnostics
.push(Diagnostic::new(SysVersion2, value.range()));
} else if *i == 0 && checker.enabled(Rule::SysVersion0) {
checker.report_diagnostic(Diagnostic::new(SysVersion0, value.range()));
checker
.diagnostics
.push(Diagnostic::new(SysVersion0, value.range()));
}
}

Some files were not shown because too many files have changed in this diff Show More