Compare commits
1 Commits
david/comp
...
zb/fix-win
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70cf8a94d5 |
5
.github/workflows/ci.yaml
vendored
5
.github/workflows/ci.yaml
vendored
@@ -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:
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -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
11
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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(): ...
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`"
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
```
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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!(),
|
||||
|
||||
@@ -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 {
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>>>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>`<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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(¤t, &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 })
|
||||
}
|
||||
}
|
||||
@@ -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)?,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 := [])))
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
###
|
||||
|
||||
@@ -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
|
||||
@@ -1,7 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path += [os.path.dirname(__file__)]
|
||||
sys.path += ["../"]
|
||||
|
||||
from package import module
|
||||
@@ -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("")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,13 +12,3 @@ x: TypeAlias = tuple[
|
||||
int, # preserved
|
||||
float,
|
||||
]
|
||||
|
||||
T: TypeAlias = ( # comment0
|
||||
# comment1
|
||||
int # comment2
|
||||
# comment3
|
||||
| # comment4
|
||||
# comment5
|
||||
str # comment6
|
||||
# comment7
|
||||
) # comment8
|
||||
|
||||
@@ -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', ...)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]]): ...
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(¶m_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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user