Compare commits
2 Commits
0.9.4
...
david/unio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c366fb642a | ||
|
|
a3d730bc7c |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -430,7 +430,7 @@ jobs:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
name: Download baseline Ruff binary
|
||||
with:
|
||||
name: ruff
|
||||
|
||||
4
.github/workflows/pr-comment.yaml
vendored
4
.github/workflows/pr-comment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
name: Download pull request number
|
||||
with:
|
||||
name: pr-number
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
name: "Download ecosystem results"
|
||||
id: download-ecosystem-result
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -30,7 +30,7 @@ tracing-flamechart.svg
|
||||
tracing-flamegraph.svg
|
||||
|
||||
# insta
|
||||
*.rs.pending-snap
|
||||
.rs.pending-snap
|
||||
|
||||
|
||||
###
|
||||
|
||||
@@ -36,7 +36,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.44.0
|
||||
rev: v0.43.0
|
||||
hooks:
|
||||
- id: markdownlint-fix
|
||||
exclude: |
|
||||
@@ -56,7 +56,7 @@ repos:
|
||||
.*?invalid(_.+)*_syntax\.md
|
||||
)$
|
||||
additional_dependencies:
|
||||
- black==25.1.0
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.29.4
|
||||
@@ -73,7 +73,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.3
|
||||
rev: v0.9.2
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,43 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Extend airflow context parameter check for `BaseOperator.execute` (`AIR302`) ([#15713](https://github.com/astral-sh/ruff/pull/15713))
|
||||
- \[`airflow`\] Update `AIR302` to check for deprecated context keys ([#15144](https://github.com/astral-sh/ruff/pull/15144))
|
||||
- \[`flake8-bandit`\] Permit suspicious imports within stub files (`S4`) ([#15822](https://github.com/astral-sh/ruff/pull/15822))
|
||||
- \[`pylint`\] Do not trigger `PLR6201` on empty collections ([#15732](https://github.com/astral-sh/ruff/pull/15732))
|
||||
- \[`refurb`\] Do not emit diagnostic when loop variables are used outside loop body (`FURB122`) ([#15757](https://github.com/astral-sh/ruff/pull/15757))
|
||||
- \[`ruff`\] Add support for more `re` patterns (`RUF055`) ([#15764](https://github.com/astral-sh/ruff/pull/15764))
|
||||
- \[`ruff`\] Check for shadowed `map` before suggesting fix (`RUF058`) ([#15790](https://github.com/astral-sh/ruff/pull/15790))
|
||||
- \[`ruff`\] Do not emit diagnostic when all arguments to `zip()` are variadic (`RUF058`) ([#15744](https://github.com/astral-sh/ruff/pull/15744))
|
||||
- \[`ruff`\] Parenthesize fix when argument spans multiple lines for `unnecessary-round` (`RUF057`) ([#15703](https://github.com/astral-sh/ruff/pull/15703))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Preserve quote style in generated code ([#15726](https://github.com/astral-sh/ruff/pull/15726), [#15778](https://github.com/astral-sh/ruff/pull/15778), [#15794](https://github.com/astral-sh/ruff/pull/15794))
|
||||
- \[`flake8-bugbear`\] Exempt `NewType` calls where the original type is immutable (`B008`) ([#15765](https://github.com/astral-sh/ruff/pull/15765))
|
||||
- \[`pylint`\] Honor banned top-level imports by `TID253` in `PLC0415`. ([#15628](https://github.com/astral-sh/ruff/pull/15628))
|
||||
- \[`pyupgrade`\] Ignore `is_typeddict` and `TypedDict` for `deprecated-import` (`UP035`) ([#15800](https://github.com/astral-sh/ruff/pull/15800))
|
||||
|
||||
### CLI
|
||||
|
||||
- Fix formatter warning message for `flake8-quotes` option ([#15788](https://github.com/astral-sh/ruff/pull/15788))
|
||||
- Implement tab autocomplete for `ruff config` ([#15603](https://github.com/astral-sh/ruff/pull/15603))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-comprehensions`\] Do not emit `unnecessary-map` diagnostic when lambda has different arity (`C417`) ([#15802](https://github.com/astral-sh/ruff/pull/15802))
|
||||
- \[`flake8-comprehensions`\] Parenthesize `sorted` when needed for `unnecessary-call-around-sorted` (`C413`) ([#15825](https://github.com/astral-sh/ruff/pull/15825))
|
||||
- \[`pyupgrade`\] Handle end-of-line comments for `quoted-annotation` (`UP037`) ([#15824](https://github.com/astral-sh/ruff/pull/15824))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add missing config docstrings ([#15803](https://github.com/astral-sh/ruff/pull/15803))
|
||||
- Add references to `trio.run_process` and `anyio.run_process` ([#15761](https://github.com/astral-sh/ruff/pull/15761))
|
||||
- Use `uv init --lib` in tutorial ([#15718](https://github.com/astral-sh/ruff/pull/15718))
|
||||
|
||||
## 0.9.3
|
||||
|
||||
### Preview features
|
||||
|
||||
605
Cargo.lock
generated
605
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.4/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.9.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.3/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.4
|
||||
rev: v0.9.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
use crate::logging::Verbosity;
|
||||
use crate::python_version::PythonVersion;
|
||||
use clap::{ArgAction, ArgMatches, Error, Parser};
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use red_knot_python_semantic::lint;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
author,
|
||||
name = "red-knot",
|
||||
about = "An extremely fast Python type checker."
|
||||
)]
|
||||
#[command(version)]
|
||||
pub(crate) struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub(crate) enum Command {
|
||||
/// Check a project for type errors.
|
||||
Check(CheckCommand),
|
||||
|
||||
/// Start the language server
|
||||
Server,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub(crate) struct CheckCommand {
|
||||
/// Run the command within the given project directory.
|
||||
///
|
||||
/// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory,
|
||||
/// as will the project's virtual environment (`.venv`) unless the `venv-path` option is set.
|
||||
///
|
||||
/// Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
|
||||
#[arg(long, value_name = "PROJECT")]
|
||||
pub(crate) project: Option<SystemPathBuf>,
|
||||
|
||||
/// Path to the virtual environment the project uses.
|
||||
///
|
||||
/// If provided, red-knot will use the `site-packages` directory of this virtual environment
|
||||
/// to resolve type information for the project's third-party dependencies.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
pub(crate) venv_path: Option<SystemPathBuf>,
|
||||
|
||||
/// Custom directory to use for stdlib typeshed stubs.
|
||||
#[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")]
|
||||
pub(crate) typeshed: Option<SystemPathBuf>,
|
||||
|
||||
/// Additional path to use as a module-resolution source (can be passed multiple times).
|
||||
#[arg(long, value_name = "PATH")]
|
||||
pub(crate) extra_search_path: Option<Vec<SystemPathBuf>>,
|
||||
|
||||
/// Python version to assume when resolving types.
|
||||
#[arg(long, value_name = "VERSION", alias = "target-version")]
|
||||
pub(crate) python_version: Option<PythonVersion>,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) verbosity: Verbosity,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) rules: RulesArg,
|
||||
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
#[arg(long, short = 'W')]
|
||||
pub(crate) watch: bool,
|
||||
}
|
||||
|
||||
impl CheckCommand {
|
||||
pub(crate) fn into_options(self) -> Options {
|
||||
let rules = if self.rules.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
self.rules
|
||||
.into_iter()
|
||||
.map(|(rule, level)| (RangedValue::cli(rule), RangedValue::cli(level)))
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: self
|
||||
.python_version
|
||||
.map(|version| RangedValue::cli(version.into())),
|
||||
venv_path: self.venv_path.map(RelativePathBuf::cli),
|
||||
typeshed: self.typeshed.map(RelativePathBuf::cli),
|
||||
extra_paths: self.extra_search_path.map(|extra_search_paths| {
|
||||
extra_search_paths
|
||||
.into_iter()
|
||||
.map(RelativePathBuf::cli)
|
||||
.collect()
|
||||
}),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
rules,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of rules to enable or disable with a given severity.
|
||||
///
|
||||
/// This type is used to parse the `--error`, `--warn`, and `--ignore` arguments
|
||||
/// while preserving the order in which they were specified (arguments last override previous severities).
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RulesArg(Vec<(String, lint::Level)>);
|
||||
|
||||
impl RulesArg {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
fn into_iter(self) -> impl Iterator<Item = (String, lint::Level)> {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl clap::FromArgMatches for RulesArg {
|
||||
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
|
||||
let mut rules = Vec::new();
|
||||
|
||||
for (level, arg_id) in [
|
||||
(lint::Level::Ignore, "ignore"),
|
||||
(lint::Level::Warn, "warn"),
|
||||
(lint::Level::Error, "error"),
|
||||
] {
|
||||
let indices = matches.indices_of(arg_id).into_iter().flatten();
|
||||
let levels = matches.get_many::<String>(arg_id).into_iter().flatten();
|
||||
rules.extend(
|
||||
indices
|
||||
.zip(levels)
|
||||
.map(|(index, rule)| (index, rule, level)),
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by their index so that values specified later override earlier ones.
|
||||
rules.sort_by_key(|(index, _, _)| *index);
|
||||
|
||||
Ok(Self(
|
||||
rules
|
||||
.into_iter()
|
||||
.map(|(_, rule, level)| (rule.to_owned(), level))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
|
||||
self.0 = Self::from_arg_matches(matches)?.0;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl clap::Args for RulesArg {
|
||||
fn augment_args(cmd: clap::Command) -> clap::Command {
|
||||
const HELP_HEADING: &str = "Enabling / disabling rules";
|
||||
|
||||
cmd.arg(
|
||||
clap::Arg::new("error")
|
||||
.long("error")
|
||||
.action(ArgAction::Append)
|
||||
.help("Treat the given rule as having severity 'error'. Can be specified multiple times.")
|
||||
.value_name("RULE")
|
||||
.help_heading(HELP_HEADING),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("warn")
|
||||
.long("warn")
|
||||
.action(ArgAction::Append)
|
||||
.help("Treat the given rule as having severity 'warn'. Can be specified multiple times.")
|
||||
.value_name("RULE")
|
||||
.help_heading(HELP_HEADING),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("ignore")
|
||||
.long("ignore")
|
||||
.action(ArgAction::Append)
|
||||
.help("Disables the rule. Can be specified multiple times.")
|
||||
.value_name("RULE")
|
||||
.help_heading(HELP_HEADING),
|
||||
)
|
||||
}
|
||||
|
||||
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
|
||||
Self::augment_args(cmd)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::process::{ExitCode, Termination};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::args::{Args, CheckCommand, Command};
|
||||
use crate::logging::setup_tracing;
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use red_knot_project::metadata::options::Options;
|
||||
use python_version::PythonVersion;
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use red_knot_project::watch;
|
||||
use red_knot_project::watch::ProjectWatcher;
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
@@ -16,11 +16,87 @@ use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
mod args;
|
||||
use crate::logging::{setup_tracing, Verbosity};
|
||||
|
||||
mod logging;
|
||||
mod python_version;
|
||||
mod verbosity;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
author,
|
||||
name = "red-knot",
|
||||
about = "An extremely fast Python type checker."
|
||||
)]
|
||||
#[command(version)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Option<Command>,
|
||||
|
||||
/// Run the command within the given project directory.
|
||||
///
|
||||
/// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory,
|
||||
/// as will the project's virtual environment (`.venv`) unless the `venv-path` option is set.
|
||||
///
|
||||
/// Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
|
||||
#[arg(long, value_name = "PROJECT")]
|
||||
project: Option<SystemPathBuf>,
|
||||
|
||||
/// Path to the virtual environment the project uses.
|
||||
///
|
||||
/// If provided, red-knot will use the `site-packages` directory of this virtual environment
|
||||
/// to resolve type information for the project's third-party dependencies.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
venv_path: Option<SystemPathBuf>,
|
||||
|
||||
/// Custom directory to use for stdlib typeshed stubs.
|
||||
#[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")]
|
||||
typeshed: Option<SystemPathBuf>,
|
||||
|
||||
/// Additional path to use as a module-resolution source (can be passed multiple times).
|
||||
#[arg(long, value_name = "PATH")]
|
||||
extra_search_path: Option<Vec<SystemPathBuf>>,
|
||||
|
||||
/// Python version to assume when resolving types.
|
||||
#[arg(long, value_name = "VERSION", alias = "target-version")]
|
||||
python_version: Option<PythonVersion>,
|
||||
|
||||
#[clap(flatten)]
|
||||
verbosity: Verbosity,
|
||||
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
#[arg(long, short = 'W')]
|
||||
watch: bool,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn to_options(&self) -> Options {
|
||||
Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: self
|
||||
.python_version
|
||||
.map(|version| RangedValue::cli(version.into())),
|
||||
venv_path: self.venv_path.as_ref().map(RelativePathBuf::cli),
|
||||
typeshed: self.typeshed.as_ref().map(RelativePathBuf::cli),
|
||||
extra_paths: self.extra_search_path.as_ref().map(|extra_search_paths| {
|
||||
extra_search_paths
|
||||
.iter()
|
||||
.map(RelativePathBuf::cli)
|
||||
.collect()
|
||||
}),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum Command {
|
||||
/// Start the language server
|
||||
Server,
|
||||
}
|
||||
|
||||
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
|
||||
pub fn main() -> ExitStatus {
|
||||
run().unwrap_or_else(|error| {
|
||||
@@ -46,13 +122,10 @@ pub fn main() -> ExitStatus {
|
||||
fn run() -> anyhow::Result<ExitStatus> {
|
||||
let args = Args::parse_from(std::env::args());
|
||||
|
||||
match args.command {
|
||||
Command::Server => run_server().map(|()| ExitStatus::Success),
|
||||
Command::Check(check_args) => run_check(check_args),
|
||||
if matches!(args.command, Some(Command::Server)) {
|
||||
return run_server().map(|()| ExitStatus::Success);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
let verbosity = args.verbosity.level();
|
||||
countme::enable(verbosity.is_trace());
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
@@ -83,8 +156,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
.unwrap_or_else(|| cli_base_path.clone());
|
||||
|
||||
let system = OsSystem::new(cwd);
|
||||
let watch = args.watch;
|
||||
let cli_options = args.into_options();
|
||||
let cli_options = args.to_options();
|
||||
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
|
||||
workspace_metadata.apply_cli_options(cli_options.clone());
|
||||
|
||||
@@ -102,7 +174,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
}
|
||||
})?;
|
||||
|
||||
let exit_status = if watch {
|
||||
let exit_status = if args.watch {
|
||||
main_loop.watch(&mut db)?
|
||||
} else {
|
||||
main_loop.run(&mut db)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use insta::Settings;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
@@ -28,22 +28,24 @@ fn config_override() -> anyhow::Result<()> {
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-attribute] <temp_dir>/test.py:5:7 Type `<module 'sys'>` has no attribute `last_exc`
|
||||
case.insta_settings().bind(|| {
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-attribute] <temp_dir>/test.py:5:7 Type `<module 'sys'>` has no attribute `last_exc`
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -90,23 +92,25 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
),
|
||||
])?;
|
||||
|
||||
// Make sure that the CLI fails when the `libs` directory is not in the search path.
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-import] <temp_dir>/child/test.py:2:1 Cannot resolve import `utils`
|
||||
case.insta_settings().bind(|| {
|
||||
// Make sure that the CLI fails when the `libs` directory is not in the search path.
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-import] <temp_dir>/child/test.py:2:1 Cannot resolve import `utils`
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
----- stderr -----
|
||||
"#);
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -152,20 +156,22 @@ fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Re
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
case.insta_settings().bind(|| {
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The rule severity can be changed in the configuration file
|
||||
#[test]
|
||||
fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
fn rule_severity() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
@@ -178,146 +184,41 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
|
||||
case.insta_settings().bind(|| {
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
case.write_file(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.knot.rules]
|
||||
division-by-zero = "warn" # demote to warn
|
||||
possibly-unresolved-reference = "ignore"
|
||||
"#,
|
||||
)?;
|
||||
case.write_file("pyproject.toml", r#"
|
||||
[tool.knot.rules]
|
||||
division-by-zero = "warn" # demote to warn
|
||||
possibly-unresolved-reference = "ignore"
|
||||
"#)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// The rule severity can be changed using `--ignore`, `--warn`, and `--error`
|
||||
/// Red Knot warns about unknown rules
|
||||
#[test]
|
||||
fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
import does_not_exit
|
||||
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, y):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-import] <temp_dir>/test.py:2:8 Cannot resolve import `does_not_exit`
|
||||
error[lint:division-by-zero] <temp_dir>/test.py:4:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:9:7 Name `x` used when possibly not defined
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case
|
||||
.command()
|
||||
.arg("--ignore")
|
||||
.arg("possibly-unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("division-by-zero")
|
||||
.arg("--warn")
|
||||
.arg("unresolved-import"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[lint:unresolved-import] <temp_dir>/test.py:2:8 Cannot resolve import `does_not_exit`
|
||||
warning[lint:division-by-zero] <temp_dir>/test.py:4:5 Cannot divide object of type `Literal[4]` by zero
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` and
|
||||
/// values specified last override previous severities.
|
||||
#[test]
|
||||
fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, y):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case
|
||||
.command()
|
||||
.arg("--error")
|
||||
.arg("possibly-unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("division-by-zero")
|
||||
// Override the error severity with warning
|
||||
.arg("--ignore")
|
||||
.arg("possibly-unresolved-reference"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Red Knot warns about unknown rules specified in a configuration file
|
||||
#[test]
|
||||
fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
fn unknown_rules() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
@@ -329,38 +230,22 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
("test.py", "print(10)"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unknown-rule] <temp_dir>/pyproject.toml:3:1 Unknown lint rule `division-by-zer`
|
||||
case.insta_settings().bind(|| {
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unknown-rule] <temp_dir>/pyproject.toml:3:1 Unknown lint rule `division-by-zer`
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Red Knot warns about unknown rules specified in a CLI argument
|
||||
#[test]
|
||||
fn cli_unknown_rules() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", "print(10)")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unknown-rule] Unknown lint rule `division-by-zer`
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
_temp_dir: TempDir,
|
||||
_settings_scope: SettingsBindDropGuard,
|
||||
project_dir: PathBuf,
|
||||
}
|
||||
|
||||
@@ -375,16 +260,9 @@ impl TestCase {
|
||||
.canonicalize()
|
||||
.context("Failed to canonicalize project path")?;
|
||||
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(&tempdir_filter(&project_dir), "<temp_dir>/");
|
||||
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||
|
||||
let settings_scope = settings.bind_to_scope();
|
||||
|
||||
Ok(Self {
|
||||
project_dir,
|
||||
_temp_dir: temp_dir,
|
||||
_settings_scope: settings_scope,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -429,9 +307,17 @@ impl TestCase {
|
||||
&self.project_dir
|
||||
}
|
||||
|
||||
// Returns the insta filters to escape paths in snapshots
|
||||
fn insta_settings(&self) -> Settings {
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(&tempdir_filter(&self.project_dir), "<temp_dir>/");
|
||||
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||
settings
|
||||
}
|
||||
|
||||
fn command(&self) -> Command {
|
||||
let mut command = Command::new(get_cargo_bin("red_knot"));
|
||||
command.current_dir(&self.project_dir).arg("check");
|
||||
command.current_dir(&self.project_dir);
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ impl TestCase {
|
||||
#[track_caller]
|
||||
fn panic_with_formatted_events(events: Vec<ChangeEvent>) -> Vec<ChangeEvent> {
|
||||
panic!(
|
||||
"Didn't observe the expected event. The following events occurred:\n{}",
|
||||
"Didn't observe expected change:\n{}",
|
||||
events
|
||||
.into_iter()
|
||||
.map(|event| format!(" - {event:?}"))
|
||||
|
||||
@@ -149,16 +149,6 @@ impl Options {
|
||||
format!("Unknown lint rule `{rule_name}`"),
|
||||
Severity::Warning,
|
||||
),
|
||||
GetLintError::PrefixedWithCategory { suggestion, .. } => {
|
||||
OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!(
|
||||
"Unknown lint rule `{rule_name}`. Did you mean `{suggestion}`?"
|
||||
),
|
||||
Severity::Warning,
|
||||
)
|
||||
}
|
||||
|
||||
GetLintError::Removed(_) => OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!("Unknown lint rule `{rule_name}`"),
|
||||
@@ -216,16 +206,6 @@ pub struct Rules {
|
||||
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
|
||||
}
|
||||
|
||||
impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
|
||||
fn from_iter<T: IntoIterator<Item = (RangedValue<String>, RangedValue<Level>)>>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KnotTomlError {
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -11,7 +11,7 @@ x: Any = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
reveal_type(x) # revealed: Any | Literal["foo"]
|
||||
```
|
||||
|
||||
## Aliased to a different name
|
||||
@@ -25,7 +25,7 @@ x: RenamedAny = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
reveal_type(x) # revealed: Any | Literal["foo"]
|
||||
```
|
||||
|
||||
## Shadowed class
|
||||
|
||||
@@ -106,7 +106,7 @@ def union_example(
|
||||
Literal["B"],
|
||||
Literal[True],
|
||||
None,
|
||||
],
|
||||
]
|
||||
):
|
||||
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
reveal_type(Alias) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(Alias) # revealed: @Todo(Unsupported or invalid type in a type expression) | Literal[int]
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def h() -> TypeIs[int]: ...
|
||||
|
||||
@@ -49,12 +49,12 @@ reveal_type(c) # revealed: tuple[str, int]
|
||||
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
|
||||
# TODO: homogeneous tuples, PEP-646 tuples
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support) | tuple[()]
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support) | tuple[Literal["42"], Literal[b"42"]]
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support) | tuple[Literal["42"], Literal[b"42"]]
|
||||
|
||||
# TODO: support more kinds of type expressions in annotations
|
||||
reveal_type(h) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(h) # revealed: @Todo(full tuple[...] support) | tuple[list, list]
|
||||
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
reveal_type(j) # revealed: tuple[str | int]
|
||||
|
||||
@@ -13,94 +13,123 @@ accessed on the class itself.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, param: int | None, flag: bool = False) -> None:
|
||||
value = 1 if flag else "a"
|
||||
self.inferred_from_value = value
|
||||
self.inferred_from_other_attribute = self.inferred_from_value
|
||||
self.inferred_from_param = param
|
||||
self.declared_only: bytes
|
||||
self.declared_and_bound: bool = True
|
||||
def __init__(self, value2: int, flag: bool = False) -> None:
|
||||
# bound but not declared
|
||||
self.pure_instance_variable1 = "value set in __init__"
|
||||
|
||||
# bound but not declared - with type inferred from parameter
|
||||
self.pure_instance_variable2 = value2
|
||||
|
||||
# declared but not bound
|
||||
self.pure_instance_variable3: bytes
|
||||
|
||||
# declared and bound
|
||||
self.pure_instance_variable4: bool = True
|
||||
|
||||
# possibly undeclared/unbound
|
||||
if flag:
|
||||
self.possibly_undeclared_unbound: str = "possibly set in __init__"
|
||||
self.pure_instance_variable5: str = "possibly set in __init__"
|
||||
|
||||
c_instance = C(1)
|
||||
|
||||
# TODO: Mypy/pyright infer `int | str` here. We want this to be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: @Todo(implicit instance attribute)
|
||||
# TODO: should be `Literal["value set in __init__"]`, or `Unknown | Literal[…]` to allow
|
||||
# assignments to this unannotated attribute from other scopes.
|
||||
reveal_type(c_instance.pure_instance_variable1) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: @Todo(implicit instance attribute)
|
||||
# TODO: should be `int`
|
||||
reveal_type(c_instance.pure_instance_variable2) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should be `bytes`
|
||||
reveal_type(c_instance.declared_only) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.pure_instance_variable3) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should be `bool`
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should be `str`
|
||||
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound.
|
||||
# mypy and pyright do not show an error here.
|
||||
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.pure_instance_variable5) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`.
|
||||
c_instance.inferred_from_value = "value set on instance"
|
||||
|
||||
# This assignment is also fine:
|
||||
c_instance.inferred_from_param = None
|
||||
# TODO: If we choose to infer a precise `Literal[…]` type for the instance attribute (see
|
||||
# above), this should be an error: incompatible types in assignment. If we choose to infer
|
||||
# a gradual `Unknown | Literal[…]` type, this assignment is fine.
|
||||
c_instance.pure_instance_variable1 = "value set on instance"
|
||||
|
||||
# TODO: this should be an error (incompatible types in assignment)
|
||||
c_instance.inferred_from_param = "incompatible"
|
||||
c_instance.pure_instance_variable2 = "incompatible"
|
||||
|
||||
# TODO: we already show an error here but the message might be improved?
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
# error: [unresolved-attribute] "Type `Literal[C]` has no attribute `inferred_from_value`"
|
||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Type `Literal[C]` has no attribute `pure_instance_variable1`"
|
||||
reveal_type(C.pure_instance_variable1) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error (pure instance variables cannot be accessed on the class)
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
C.inferred_from_value = "overwritten on class"
|
||||
C.pure_instance_variable1 = "overwritten on class"
|
||||
|
||||
# This assignment is fine:
|
||||
c_instance.declared_and_bound = False
|
||||
c_instance.pure_instance_variable4 = False
|
||||
|
||||
# TODO: After this assignment to the attribute within this scope, we may eventually want to narrow
|
||||
# the `bool` type (see above) for this instance variable to `Literal[False]` here. This is unsound
|
||||
# in general (we don't know what else happened to `c_instance` between the assignment and the use
|
||||
# here), but mypy and pyright support this. In conclusion, this could be `bool` but should probably
|
||||
# be `Literal[False]`.
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(implicit instance attribute)
|
||||
```
|
||||
|
||||
#### Variable declared in class body and possibly bound in `__init__`
|
||||
#### Variable declared in class body and declared/bound in `__init__`
|
||||
|
||||
The same rule applies even if the variable is *declared* (not bound!) in the class body: it is still
|
||||
a pure instance variable.
|
||||
|
||||
```py
|
||||
class C:
|
||||
declared_and_bound: str | None
|
||||
pure_instance_variable: str
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.declared_and_bound = "value set in __init__"
|
||||
self.pure_instance_variable = "value set in __init__"
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: str | None
|
||||
reveal_type(c_instance.pure_instance_variable) # revealed: str
|
||||
|
||||
# TODO: we currently plan to emit a diagnostic here. Note that both mypy
|
||||
# and pyright show no error in this case! So we may reconsider this in
|
||||
# the future, if it turns out to produce too many false positives.
|
||||
reveal_type(C.declared_and_bound) # revealed: str | None
|
||||
reveal_type(C.pure_instance_variable) # revealed: str
|
||||
|
||||
# TODO: same as above. We plan to emit a diagnostic here, even if both mypy
|
||||
# and pyright allow this.
|
||||
C.declared_and_bound = "overwritten on class"
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `declared_and_bound` of type `str | None`"
|
||||
c_instance.declared_and_bound = 1
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_instance_variable` of type `str`"
|
||||
c_instance.pure_instance_variable = 1
|
||||
```
|
||||
|
||||
#### Variable only defined in unrelated method
|
||||
|
||||
We also recognize pure instance variables if they are defined in a method that is not `__init__`.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def set_instance_variable(self) -> None:
|
||||
self.pure_instance_variable = "value set in method"
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# Not that we would use this in static analysis, but for a more realistic example, let's actually
|
||||
# call the method, so that the attribute is bound if this example is actually run.
|
||||
c_instance.set_instance_variable()
|
||||
|
||||
# TODO: should be `Literal["value set in method"]` or `Unknown | Literal[…]` (see above).
|
||||
reveal_type(c_instance.pure_instance_variable) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: We already show an error here, but the message might be improved?
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_instance_variable) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
```
|
||||
|
||||
#### Variable declared in class body and not bound anywhere
|
||||
@@ -110,86 +139,18 @@ instance variable and allow access to it via instances.
|
||||
|
||||
```py
|
||||
class C:
|
||||
only_declared: str
|
||||
pure_instance_variable: str
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.only_declared) # revealed: str
|
||||
reveal_type(c_instance.pure_instance_variable) # revealed: str
|
||||
|
||||
# TODO: mypy and pyright do not show an error here, but we plan to emit a diagnostic.
|
||||
# The type could be changed to 'Unknown' if we decide to emit an error?
|
||||
reveal_type(C.only_declared) # revealed: str
|
||||
reveal_type(C.pure_instance_variable) # revealed: str
|
||||
|
||||
# TODO: mypy and pyright do not show an error here, but we plan to emit one.
|
||||
C.only_declared = "overwritten on class"
|
||||
```
|
||||
|
||||
#### Variable only defined in unrelated method
|
||||
|
||||
We also recognize pure instance variables if they are defined in a method that is not `__init__`.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, param: int | None, flag: bool = False) -> None:
|
||||
self.initialize(param, flag)
|
||||
|
||||
def initialize(self, param: int | None, flag: bool) -> None:
|
||||
value = 1 if flag else "a"
|
||||
self.inferred_from_value = value
|
||||
self.inferred_from_other_attribute = self.inferred_from_value
|
||||
self.inferred_from_param = param
|
||||
self.declared_only: bytes
|
||||
self.declared_and_bound: bool = True
|
||||
|
||||
c_instance = C(1)
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: Should be `int | None`
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: Should be `bytes`
|
||||
reveal_type(c_instance.declared_only) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: Should be `bool`
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: We already show an error here, but the message might be improved?
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error
|
||||
C.inferred_from_value = "overwritten on class"
|
||||
```
|
||||
|
||||
#### Methods that does not use `self` as a first parameter
|
||||
|
||||
```py
|
||||
class C:
|
||||
# This might trigger a stylistic lint like `invalid-first-argument-name-for-method`, but
|
||||
# it should be supported in general:
|
||||
def __init__(this) -> None:
|
||||
this.declared_and_bound: str | None = "a"
|
||||
|
||||
# TODO: should be `str | None`
|
||||
reveal_type(C().declared_and_bound) # revealed: @Todo(implicit instance attribute)
|
||||
```
|
||||
|
||||
#### Aliased `self` parameter
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
this = self
|
||||
this.declared_and_bound: str | None = "a"
|
||||
|
||||
# TODO: This would ideally be `str | None`, but mypy/pyright don't support this either,
|
||||
# so `Unknown` + a diagnostic is also fine.
|
||||
reveal_type(C().declared_and_bound) # revealed: @Todo(implicit instance attribute)
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
```
|
||||
|
||||
### Pure class variables (`ClassVar`)
|
||||
@@ -214,16 +175,14 @@ class C:
|
||||
|
||||
reveal_type(C.pure_class_variable1) # revealed: str
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1]`.
|
||||
reveal_type(C.pure_class_variable2) # revealed: Unknown
|
||||
reveal_type(C.pure_class_variable2) # revealed: Unknown | Literal[1]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# It is okay to access a pure class variable on an instance.
|
||||
reveal_type(c_instance.pure_class_variable1) # revealed: str
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1]`.
|
||||
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown
|
||||
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown | Literal[1]
|
||||
|
||||
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `C`"
|
||||
c_instance.pure_class_variable1 = "value set on instance"
|
||||
@@ -316,54 +275,6 @@ reveal_type(C.variable_with_class_default1) # revealed: str
|
||||
reveal_type(c_instance.variable_with_class_default1) # revealed: str
|
||||
```
|
||||
|
||||
### Inheritance of class/instance attributes
|
||||
|
||||
#### Instance variable defined in a base class
|
||||
|
||||
```py
|
||||
class Base:
|
||||
declared_in_body: int | None = 1
|
||||
|
||||
base_class_attribute_1: str | None
|
||||
base_class_attribute_2: str | None
|
||||
base_class_attribute_3: str | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.defined_in_init: str | None = "value in base"
|
||||
|
||||
class Intermediate(Base):
|
||||
# Re-declaring base class attributes with the *same *type is fine:
|
||||
base_class_attribute_1: str | None = None
|
||||
|
||||
# Re-declaring them with a *narrower type* is unsound, because modifications
|
||||
# through a `Base` reference could violate that constraint.
|
||||
#
|
||||
# Mypy does not report an error here, but pyright does: "… overrides symbol
|
||||
# of same name in class "Base". Variable is mutable so its type is invariant"
|
||||
#
|
||||
# We should introduce a diagnostic for this. Whether or not that should be
|
||||
# enabled by default can still be discussed.
|
||||
#
|
||||
# TODO: This should be an error
|
||||
base_class_attribute_2: str
|
||||
|
||||
# Re-declaring attributes with a *wider type* directly violates LSP.
|
||||
#
|
||||
# In this case, both mypy and pyright report an error.
|
||||
#
|
||||
# TODO: This should be an error
|
||||
base_class_attribute_3: str | int | None
|
||||
|
||||
class Derived(Intermediate): ...
|
||||
|
||||
reveal_type(Derived.declared_in_body) # revealed: int | None
|
||||
|
||||
reveal_type(Derived().declared_in_body) # revealed: int | None
|
||||
|
||||
# TODO: Should be `str | None`
|
||||
reveal_type(Derived().defined_in_init) # revealed: @Todo(implicit instance attribute)
|
||||
```
|
||||
|
||||
## Union of attributes
|
||||
|
||||
```py
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
This document demonstrates how type-inference and diagnostics work for *public* uses of a symbol,
|
||||
that is, a use of a symbol from another scope. If a symbol has a declared type in its local scope
|
||||
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective
|
||||
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`).
|
||||
of other scopes). If there is an inferred type in addition (i.e. if we also see bindings for this
|
||||
symbol, not just declarations), we use `T_decl | T_decl & T_inf` as the public type, which
|
||||
simplifies to `T_decl` for `T_inf = Unknown` (the unbound case).
|
||||
|
||||
[TODO: more explanation]
|
||||
|
||||
If a symbol has no declared type, we use the union of `Unknown` with the inferred type as the public
|
||||
type. If there is no declaration, then the symbol can be reassigned to any type from another scope;
|
||||
@@ -17,11 +21,11 @@ this behavior is questionable and might change in the future. See the TODOs in `
|
||||
In particular, we should raise errors in the "possibly-undeclared-and-unbound" as well as the
|
||||
"undeclared-and-possibly-unbound" cases (marked with a "?").
|
||||
|
||||
| **Public type** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | ------------ | -------------------------- | ----------------------- |
|
||||
| bound | `T_declared` | `T_declared \| T_inferred` | `Unknown \| T_inferred` |
|
||||
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `Unknown \| T_inferred` |
|
||||
| unbound | `T_declared` | `T_declared` | `Unknown` |
|
||||
| **Public type** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | -------------------------- | ------------------- | ------------------ |
|
||||
| bound | `T_decl \| T_decl & T_inf` | `T_decl \| T_inf` | `Unknown \| T_inf` |
|
||||
| possibly-unbound | `T_decl \| T_decl & T_inf` | `T_decl \| T_inf` | `Unknown \| T_inf` |
|
||||
| unbound | `T_decl` | `T_decl` | `Unknown` |
|
||||
|
||||
| **Diagnostic** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | -------- | ------------------------- | ------------------- |
|
||||
@@ -34,7 +38,7 @@ In particular, we should raise errors in the "possibly-undeclared-and-unbound" a
|
||||
### Declared and bound
|
||||
|
||||
If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type
|
||||
(`Literal[1]`), or a conflicting inferred type (`str` vs. `Literal[2]` below):
|
||||
(`Literal[1]`), or a conflicting inferred type (`Literal[2]`):
|
||||
|
||||
```py path=mod.py
|
||||
from typing import Any
|
||||
@@ -42,8 +46,12 @@ from typing import Any
|
||||
def any() -> Any: ...
|
||||
|
||||
a: int = 1
|
||||
b: str = 2 # error: [invalid-assignment]
|
||||
|
||||
# error: [invalid-assignment]
|
||||
b: str = 2
|
||||
|
||||
c: Any = 3
|
||||
|
||||
d: int = any()
|
||||
```
|
||||
|
||||
@@ -52,7 +60,7 @@ from mod import a, b, c, d
|
||||
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
reveal_type(c) # revealed: Any
|
||||
reveal_type(c) # revealed: Any | Literal[3]
|
||||
reveal_type(d) # revealed: int
|
||||
```
|
||||
|
||||
@@ -74,7 +82,8 @@ d: int
|
||||
|
||||
if flag:
|
||||
a = 1
|
||||
b = 2 # error: [invalid-assignment]
|
||||
# error: [invalid-assignment]
|
||||
b = 2
|
||||
c = 3
|
||||
d = any()
|
||||
```
|
||||
@@ -84,7 +93,7 @@ from mod import a, b, c, d
|
||||
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
reveal_type(c) # revealed: Any
|
||||
reveal_type(c) # revealed: Any | Literal[3]
|
||||
reveal_type(d) # revealed: int
|
||||
```
|
||||
|
||||
@@ -117,27 +126,24 @@ inferred types:
|
||||
```py path=mod.py
|
||||
from typing import Any
|
||||
|
||||
def any() -> Any: ...
|
||||
def flag() -> bool: ...
|
||||
|
||||
a = 1
|
||||
b = 2
|
||||
c = 3
|
||||
d = any()
|
||||
if flag():
|
||||
a: int
|
||||
b: Any
|
||||
c: str # error: [invalid-declaration]
|
||||
d: int
|
||||
# error: [invalid-declaration]
|
||||
c: str
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import a, b, c, d
|
||||
from mod import a, b, c
|
||||
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: Literal[2] | Any
|
||||
reveal_type(c) # revealed: Literal[3] | Unknown
|
||||
reveal_type(d) # revealed: Any | int
|
||||
|
||||
# External modifications of `a` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
@@ -202,23 +208,14 @@ a = None
|
||||
|
||||
### Undeclared but bound
|
||||
|
||||
If a symbol is *undeclared*, we use the union of `Unknown` with the inferred type. Note that we
|
||||
treat this case differently from the case where a symbol is implicitly declared with `Unknown`,
|
||||
possibly due to the usage of an unknown name in the annotation:
|
||||
|
||||
```py path=mod.py
|
||||
# Undeclared:
|
||||
a = 1
|
||||
|
||||
# Implicitly declared with `Unknown`, due to the usage of an unknown name in the annotation:
|
||||
b: SomeUnknownName = 1 # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import a, b
|
||||
from mod import a
|
||||
|
||||
reveal_type(a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
# All external modifications of `a` are allowed:
|
||||
a = None
|
||||
@@ -234,16 +231,14 @@ def flag() -> bool: ...
|
||||
|
||||
if flag:
|
||||
a = 1
|
||||
b: SomeUnknownName = 1 # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO: this should raise an error. Once we fix this, update the section description and the table
|
||||
# on top of this document.
|
||||
from mod import a, b
|
||||
from mod import a
|
||||
|
||||
reveal_type(a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
# All external modifications of `a` are allowed:
|
||||
a = None
|
||||
|
||||
@@ -180,11 +180,3 @@ a = 4 / 0 # error: [division-by-zero]
|
||||
# error: [unknown-rule] "Unknown rule `is-equal-14`"
|
||||
a = 10 + 4 # knot: ignore[is-equal-14]
|
||||
```
|
||||
|
||||
## Code with `lint:` prefix
|
||||
|
||||
```py
|
||||
# error:[unknown-rule] "Unknown rule `lint:division-by-zero`. Did you mean `division-by-zero`?"
|
||||
# error: [division-by-zero]
|
||||
a = 10 / 0 # knot: ignore[lint:division-by-zero]
|
||||
```
|
||||
|
||||
@@ -151,7 +151,7 @@ b = a / 0
|
||||
```py
|
||||
"""
|
||||
File level suppressions must come before any non-trivia token,
|
||||
including module docstrings.
|
||||
including module docstrings.
|
||||
"""
|
||||
|
||||
# error: [unused-ignore-comment] "Unused blanket `type: ignore` directive"
|
||||
|
||||
@@ -1,640 +0,0 @@
|
||||
# Terminal statements
|
||||
|
||||
## Introduction
|
||||
|
||||
Terminal statements complicate a naive control-flow analysis.
|
||||
|
||||
As a simple example:
|
||||
|
||||
```py
|
||||
def f(cond: bool) -> str:
|
||||
if cond:
|
||||
x = "test"
|
||||
else:
|
||||
raise ValueError
|
||||
return x
|
||||
|
||||
def g(cond: bool):
|
||||
if cond:
|
||||
x = "test"
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
else:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
raise ValueError
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
```
|
||||
|
||||
In `f`, we should be able to determine that the `else` branch ends in a terminal statement, and that
|
||||
the `return` statement can only be executed when the condition is true. We should therefore consider
|
||||
the reference always bound, even though `x` is only bound in the true branch.
|
||||
|
||||
Similarly, in `g`, we should see that the assignment of the value `"terminal"` can never be seen by
|
||||
the final `reveal_type`.
|
||||
|
||||
## `return`
|
||||
|
||||
A `return` statement is terminal; bindings that occur before it are not visible after it.
|
||||
|
||||
```py
|
||||
def resolved_reference(cond: bool) -> str:
|
||||
if cond:
|
||||
x = "test"
|
||||
else:
|
||||
return "early"
|
||||
return x # no possibly-unresolved-reference diagnostic!
|
||||
|
||||
def return_in_then_branch(cond: bool):
|
||||
if cond:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
return
|
||||
else:
|
||||
x = "test"
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
|
||||
def return_in_else_branch(cond: bool):
|
||||
if cond:
|
||||
x = "test"
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
else:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
return
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
|
||||
def return_in_both_branches(cond: bool):
|
||||
if cond:
|
||||
x = "terminal1"
|
||||
reveal_type(x) # revealed: Literal["terminal1"]
|
||||
return
|
||||
else:
|
||||
x = "terminal2"
|
||||
reveal_type(x) # revealed: Literal["terminal2"]
|
||||
return
|
||||
|
||||
def return_in_try(cond: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond:
|
||||
x = "test"
|
||||
return
|
||||
except:
|
||||
# TODO: Literal["before"]
|
||||
reveal_type(x) # revealed: Literal["before", "test"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["before"]
|
||||
finally:
|
||||
reveal_type(x) # revealed: Literal["before", "test"]
|
||||
reveal_type(x) # revealed: Literal["before", "test"]
|
||||
|
||||
def return_in_nested_then_branch(cond1: bool, cond2: bool):
|
||||
if cond1:
|
||||
x = "test1"
|
||||
reveal_type(x) # revealed: Literal["test1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
return
|
||||
else:
|
||||
x = "test2"
|
||||
reveal_type(x) # revealed: Literal["test2"]
|
||||
reveal_type(x) # revealed: Literal["test2"]
|
||||
reveal_type(x) # revealed: Literal["test1", "test2"]
|
||||
|
||||
def return_in_nested_else_branch(cond1: bool, cond2: bool):
|
||||
if cond1:
|
||||
x = "test1"
|
||||
reveal_type(x) # revealed: Literal["test1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "test2"
|
||||
reveal_type(x) # revealed: Literal["test2"]
|
||||
else:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
return
|
||||
reveal_type(x) # revealed: Literal["test2"]
|
||||
reveal_type(x) # revealed: Literal["test1", "test2"]
|
||||
|
||||
def return_in_both_nested_branches(cond1: bool, cond2: bool):
|
||||
if cond1:
|
||||
x = "test"
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
else:
|
||||
x = "terminal0"
|
||||
if cond2:
|
||||
x = "terminal1"
|
||||
reveal_type(x) # revealed: Literal["terminal1"]
|
||||
return
|
||||
else:
|
||||
x = "terminal2"
|
||||
reveal_type(x) # revealed: Literal["terminal2"]
|
||||
return
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
```
|
||||
|
||||
## `continue`
|
||||
|
||||
A `continue` statement jumps back to the top of the innermost loop. This makes it terminal within
|
||||
the loop body: definitions before it are not visible after it within the rest of the loop body. They
|
||||
are likely visible after the loop body, since loops do not introduce new scopes. (Statically known
|
||||
infinite loops are one exception — if control never leaves the loop body, bindings inside of the
|
||||
loop are not visible outside of it.)
|
||||
|
||||
TODO: We are not currently modeling the cyclic control flow for loops, pending fixpoint support in
|
||||
Salsa. The false positives in this section are because of that, and not our terminal statement
|
||||
support. See [ruff#14160](https://github.com/astral-sh/ruff/issues/14160) for more details.
|
||||
|
||||
```py
|
||||
def resolved_reference(cond: bool) -> str:
|
||||
while True:
|
||||
if cond:
|
||||
x = "test"
|
||||
else:
|
||||
continue
|
||||
return x
|
||||
|
||||
def continue_in_then_branch(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "continue"
|
||||
reveal_type(x) # revealed: Literal["continue"]
|
||||
continue
|
||||
else:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
# TODO: Should be Literal["before", "loop", "continue"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||
|
||||
def continue_in_else_branch(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
else:
|
||||
x = "continue"
|
||||
reveal_type(x) # revealed: Literal["continue"]
|
||||
continue
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
# TODO: Should be Literal["before", "loop", "continue"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||
|
||||
def continue_in_both_branches(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "continue1"
|
||||
reveal_type(x) # revealed: Literal["continue1"]
|
||||
continue
|
||||
else:
|
||||
x = "continue2"
|
||||
reveal_type(x) # revealed: Literal["continue2"]
|
||||
continue
|
||||
# TODO: Should be Literal["before", "continue1", "continue2"]
|
||||
reveal_type(x) # revealed: Literal["before"]
|
||||
|
||||
def continue_in_nested_then_branch(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop1"
|
||||
reveal_type(x) # revealed: Literal["loop1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "continue"
|
||||
reveal_type(x) # revealed: Literal["continue"]
|
||||
continue
|
||||
else:
|
||||
x = "loop2"
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||
# TODO: Should be Literal["before", "loop1", "loop2", "continue"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop1", "loop2"]
|
||||
|
||||
def continue_in_nested_else_branch(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop1"
|
||||
reveal_type(x) # revealed: Literal["loop1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "loop2"
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
else:
|
||||
x = "continue"
|
||||
reveal_type(x) # revealed: Literal["continue"]
|
||||
continue
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||
# TODO: Should be Literal["before", "loop1", "loop2", "continue"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop1", "loop2"]
|
||||
|
||||
def continue_in_both_nested_branches(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "continue1"
|
||||
reveal_type(x) # revealed: Literal["continue1"]
|
||||
continue
|
||||
else:
|
||||
x = "continue2"
|
||||
reveal_type(x) # revealed: Literal["continue2"]
|
||||
continue
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
# TODO: Should be Literal["before", "loop", "continue1", "continue2"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||
```
|
||||
|
||||
## `break`
|
||||
|
||||
A `break` statement jumps to the end of the innermost loop. This makes it terminal within the loop
|
||||
body: definitions before it are not visible after it within the rest of the loop body. They are
|
||||
likely visible after the loop body, since loops do not introduce new scopes. (Statically known
|
||||
infinite loops are one exception — if control never leaves the loop body, bindings inside of the
|
||||
loop are not visible outside of it.)
|
||||
|
||||
```py
|
||||
def resolved_reference(cond: bool) -> str:
|
||||
while True:
|
||||
if cond:
|
||||
x = "test"
|
||||
else:
|
||||
break
|
||||
return x
|
||||
return x # error: [unresolved-reference]
|
||||
|
||||
def break_in_then_branch(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "break"
|
||||
reveal_type(x) # revealed: Literal["break"]
|
||||
break
|
||||
else:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["before", "break", "loop"]
|
||||
|
||||
def break_in_else_branch(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
else:
|
||||
x = "break"
|
||||
reveal_type(x) # revealed: Literal["break"]
|
||||
break
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop", "break"]
|
||||
|
||||
def break_in_both_branches(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "break1"
|
||||
reveal_type(x) # revealed: Literal["break1"]
|
||||
break
|
||||
else:
|
||||
x = "break2"
|
||||
reveal_type(x) # revealed: Literal["break2"]
|
||||
break
|
||||
reveal_type(x) # revealed: Literal["before", "break1", "break2"]
|
||||
|
||||
def break_in_nested_then_branch(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop1"
|
||||
reveal_type(x) # revealed: Literal["loop1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "break"
|
||||
reveal_type(x) # revealed: Literal["break"]
|
||||
break
|
||||
else:
|
||||
x = "loop2"
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop1", "break", "loop2"]
|
||||
|
||||
def break_in_nested_else_branch(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop1"
|
||||
reveal_type(x) # revealed: Literal["loop1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "loop2"
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
else:
|
||||
x = "break"
|
||||
reveal_type(x) # revealed: Literal["break"]
|
||||
break
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop1", "loop2", "break"]
|
||||
|
||||
def break_in_both_nested_branches(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "break1"
|
||||
reveal_type(x) # revealed: Literal["break1"]
|
||||
break
|
||||
else:
|
||||
x = "break2"
|
||||
reveal_type(x) # revealed: Literal["break2"]
|
||||
break
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop", "break1", "break2"]
|
||||
```
|
||||
|
||||
## `raise`
|
||||
|
||||
A `raise` statement is terminal. If it occurs in a lexically containing `try` statement, it will
|
||||
jump to one of the `except` clauses (if it matches the value being raised), or to the `else` clause
|
||||
(if none match). Currently, we assume definitions from before the `raise` are visible in all
|
||||
`except` and `else` clauses. (In the future, we might analyze the `except` clauses to see which ones
|
||||
match the value being raised, and limit visibility to those clauses.) Definitions from before the
|
||||
`raise` are not visible in any `else` clause, but are visible in `except` clauses or after the
|
||||
containing `try` statement (since control flow may have passed through an `except`).
|
||||
|
||||
Currently we assume that an exception could be raised anywhere within a `try` block. We may want to
|
||||
implement a more precise understanding of where exceptions (barring `KeyboardInterrupt` and
|
||||
`MemoryError`) can and cannot actually be raised.
|
||||
|
||||
```py
|
||||
def raise_in_then_branch(cond: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond:
|
||||
x = "raise"
|
||||
reveal_type(x) # revealed: Literal["raise"]
|
||||
raise ValueError
|
||||
else:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||
|
||||
def raise_in_else_branch(cond: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
else:
|
||||
x = "raise"
|
||||
reveal_type(x) # revealed: Literal["raise"]
|
||||
raise ValueError
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||
|
||||
def raise_in_both_branches(cond: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond:
|
||||
x = "raise1"
|
||||
reveal_type(x) # revealed: Literal["raise1"]
|
||||
raise ValueError
|
||||
else:
|
||||
x = "raise2"
|
||||
reveal_type(x) # revealed: Literal["raise2"]
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
else:
|
||||
x = "unreachable"
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
|
||||
def raise_in_nested_then_branch(cond1: bool, cond2: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond1:
|
||||
x = "else1"
|
||||
reveal_type(x) # revealed: Literal["else1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "raise"
|
||||
reveal_type(x) # revealed: Literal["raise"]
|
||||
raise ValueError
|
||||
else:
|
||||
x = "else2"
|
||||
reveal_type(x) # revealed: Literal["else2"]
|
||||
reveal_type(x) # revealed: Literal["else2"]
|
||||
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||
|
||||
def raise_in_nested_else_branch(cond1: bool, cond2: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond1:
|
||||
x = "else1"
|
||||
reveal_type(x) # revealed: Literal["else1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "else2"
|
||||
reveal_type(x) # revealed: Literal["else2"]
|
||||
else:
|
||||
x = "raise"
|
||||
reveal_type(x) # revealed: Literal["raise"]
|
||||
raise ValueError
|
||||
reveal_type(x) # revealed: Literal["else2"]
|
||||
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||
|
||||
def raise_in_both_nested_branches(cond1: bool, cond2: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond1:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "raise1"
|
||||
reveal_type(x) # revealed: Literal["raise1"]
|
||||
raise ValueError
|
||||
else:
|
||||
x = "raise2"
|
||||
reveal_type(x) # revealed: Literal["raise2"]
|
||||
raise ValueError
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||
```
|
||||
|
||||
## Terminal in `try` with `finally` clause
|
||||
|
||||
TODO: we don't yet model that a `break` or `continue` in a `try` block will jump to a `finally`
|
||||
clause before it jumps to end/start of the loop.
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
while True:
|
||||
try:
|
||||
break
|
||||
finally:
|
||||
x = 2
|
||||
# TODO: should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Nested functions
|
||||
|
||||
Free references inside of a function body refer to variables defined in the containing scope.
|
||||
Function bodies are _lazy scopes_: at runtime, these references are not resolved immediately at the
|
||||
point of the function definition. Instead, they are resolved _at the time of the call_, which means
|
||||
that their values (and types) can be different for different invocations. For simplicity, we instead
|
||||
resolve free references _at the end of the containing scope_. That means that in the examples below,
|
||||
all of the `x` bindings should be visible to the `reveal_type`, regardless of where we place the
|
||||
`return` statements.
|
||||
|
||||
TODO: These currently produce the wrong results, but not because of our terminal statement support.
|
||||
See [ruff#15777](https://github.com/astral-sh/ruff/issues/15777) for more details.
|
||||
|
||||
```py
|
||||
def top_level_return(cond1: bool, cond2: bool):
|
||||
x = 1
|
||||
|
||||
def g():
|
||||
# TODO eliminate Unknown
|
||||
reveal_type(x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
if cond1:
|
||||
if cond2:
|
||||
x = 2
|
||||
else:
|
||||
x = 3
|
||||
return
|
||||
|
||||
def return_from_if(cond1: bool, cond2: bool):
|
||||
x = 1
|
||||
|
||||
def g():
|
||||
# TODO: Literal[1, 2, 3]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
if cond1:
|
||||
if cond2:
|
||||
x = 2
|
||||
else:
|
||||
x = 3
|
||||
return
|
||||
|
||||
def return_from_nested_if(cond1: bool, cond2: bool):
|
||||
x = 1
|
||||
|
||||
def g():
|
||||
# TODO: Literal[1, 2, 3]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1, 3]
|
||||
if cond1:
|
||||
if cond2:
|
||||
x = 2
|
||||
return
|
||||
else:
|
||||
x = 3
|
||||
```
|
||||
|
||||
## Statically known terminal statements
|
||||
|
||||
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):
|
||||
x = "a"
|
||||
if cond:
|
||||
x = "b"
|
||||
if True:
|
||||
return
|
||||
|
||||
# TODO: Literal["a"]
|
||||
reveal_type(x) # revealed: Literal["a", "b"]
|
||||
```
|
||||
@@ -84,38 +84,4 @@ static_assert(
|
||||
)
|
||||
```
|
||||
|
||||
## Unions containing tuples containing tuples containing unions (etc.)
|
||||
|
||||
```py
|
||||
from knot_extensions import is_equivalent_to, static_assert, Intersection
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
static_assert(
|
||||
is_equivalent_to(
|
||||
tuple[tuple[tuple[P | Q]]] | P,
|
||||
tuple[tuple[tuple[Q | P]]] | P,
|
||||
)
|
||||
)
|
||||
static_assert(
|
||||
is_equivalent_to(
|
||||
tuple[tuple[tuple[tuple[tuple[Intersection[P, Q]]]]]],
|
||||
tuple[tuple[tuple[tuple[tuple[Intersection[Q, P]]]]]],
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Intersections containing tuples containing unions
|
||||
|
||||
```py
|
||||
from knot_extensions import is_equivalent_to, static_assert, Intersection
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
static_assert(is_equivalent_to(Intersection[tuple[P | Q], R], Intersection[tuple[Q | P], R]))
|
||||
```
|
||||
|
||||
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent
|
||||
|
||||
@@ -21,8 +21,7 @@ class C:
|
||||
reveal_type(C.a) # revealed: int
|
||||
reveal_type(C.b) # revealed: int
|
||||
reveal_type(C.c) # revealed: int
|
||||
# TODO: should be Unknown | Literal[1]
|
||||
reveal_type(C.d) # revealed: Unknown
|
||||
reveal_type(C.d) # revealed: Unknown | Literal[1]
|
||||
reveal_type(C.e) # revealed: int
|
||||
|
||||
c = C()
|
||||
|
||||
@@ -282,7 +282,7 @@ reveal_type(b) # revealed: Unknown
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "\u9e6c"
|
||||
(a, b) = "\u9E6C"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
@@ -292,7 +292,7 @@ reveal_type(b) # revealed: Unknown
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "\U0010ffff"
|
||||
(a, b) = "\U0010FFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
@@ -301,7 +301,7 @@ reveal_type(b) # revealed: Unknown
|
||||
### Surrogates
|
||||
|
||||
```py
|
||||
(a, b) = "\ud800\udfff"
|
||||
(a, b) = "\uD800\uDFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
|
||||
use ruff_db::diagnostic::{LintName, Severity};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::hash::Hasher;
|
||||
use thiserror::Error;
|
||||
@@ -345,18 +345,7 @@ impl LintRegistry {
|
||||
}
|
||||
}
|
||||
Some(LintEntry::Removed(lint)) => Err(GetLintError::Removed(lint.name())),
|
||||
None => {
|
||||
if let Some(without_prefix) = DiagnosticId::strip_category(code) {
|
||||
if let Some(entry) = self.by_name.get(without_prefix) {
|
||||
return Err(GetLintError::PrefixedWithCategory {
|
||||
prefixed: code.to_string(),
|
||||
suggestion: entry.id().name.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(GetLintError::Unknown(code.to_string()))
|
||||
}
|
||||
None => Err(GetLintError::Unknown(code.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,20 +382,12 @@ impl LintRegistry {
|
||||
#[derive(Error, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GetLintError {
|
||||
/// The name maps to this removed lint.
|
||||
#[error("lint `{0}` has been removed")]
|
||||
#[error("lint {0} has been removed")]
|
||||
Removed(LintName),
|
||||
|
||||
/// No lint with the given name is known.
|
||||
#[error("unknown lint `{0}`")]
|
||||
#[error("unknown lint {0}")]
|
||||
Unknown(String),
|
||||
|
||||
/// The name uses the full qualified diagnostic id `lint:<rule>` instead of just `rule`.
|
||||
/// The String is the name without the `lint:` category prefix.
|
||||
#[error("unknown lint `{prefixed}`. Did you mean `{suggestion}`?")]
|
||||
PrefixedWithCategory {
|
||||
prefixed: String,
|
||||
suggestion: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
@@ -418,16 +399,6 @@ pub enum LintEntry {
|
||||
Alias(LintId),
|
||||
}
|
||||
|
||||
impl LintEntry {
|
||||
fn id(self) -> LintId {
|
||||
match self {
|
||||
LintEntry::Lint(id) => id,
|
||||
LintEntry::Removed(id) => id,
|
||||
LintEntry::Alias(id) => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static LintMetadata> for LintEntry {
|
||||
fn from(metadata: &'static LintMetadata) -> Self {
|
||||
if metadata.status.is_removed() {
|
||||
|
||||
@@ -368,12 +368,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
.record_visibility_constraint(VisibilityConstraint::VisibleIf(constraint))
|
||||
}
|
||||
|
||||
/// Records that all remaining statements in the current block are unreachable, and therefore
|
||||
/// not visible.
|
||||
fn mark_unreachable(&mut self) {
|
||||
self.current_use_def_map_mut().mark_unreachable();
|
||||
}
|
||||
|
||||
/// Records a [`VisibilityConstraint::Ambiguous`] constraint.
|
||||
fn record_ambiguous_visibility(&mut self) -> ScopedVisibilityConstraintId {
|
||||
self.current_use_def_map_mut()
|
||||
@@ -1025,6 +1019,11 @@ where
|
||||
}
|
||||
self.visit_body(body);
|
||||
}
|
||||
ast::Stmt::Break(_) => {
|
||||
if self.loop_state().is_inside() {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::For(
|
||||
for_stmt @ ast::StmtFor {
|
||||
@@ -1271,21 +1270,6 @@ where
|
||||
// - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702
|
||||
self.visit_body(finalbody);
|
||||
}
|
||||
|
||||
ast::Stmt::Raise(_) | ast::Stmt::Return(_) | ast::Stmt::Continue(_) => {
|
||||
walk_stmt(self, stmt);
|
||||
// Everything in the current block after a terminal statement is unreachable.
|
||||
self.mark_unreachable();
|
||||
}
|
||||
|
||||
ast::Stmt::Break(_) => {
|
||||
if self.loop_state().is_inside() {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
}
|
||||
// Everything in the current block after a terminal statement is unreachable.
|
||||
self.mark_unreachable();
|
||||
}
|
||||
|
||||
_ => {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
@@ -476,7 +476,6 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
|
||||
pub(super) struct FlowSnapshot {
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
scope_start_visibility: ScopedVisibilityConstraintId,
|
||||
reachable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -504,8 +503,6 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
|
||||
/// Currently live bindings and declarations for each symbol.
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
|
||||
reachable: bool,
|
||||
}
|
||||
|
||||
impl Default for UseDefMapBuilder<'_> {
|
||||
@@ -518,16 +515,11 @@ 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.reachable = false;
|
||||
}
|
||||
|
||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||
let new_symbol = self
|
||||
.symbol_states
|
||||
@@ -664,7 +656,6 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
FlowSnapshot {
|
||||
symbol_states: self.symbol_states.clone(),
|
||||
scope_start_visibility: self.scope_start_visibility,
|
||||
reachable: self.reachable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,25 +678,12 @@ 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) {
|
||||
// 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.reachable {
|
||||
self.restore(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
|
||||
// IDs must line up), so the current number of known symbols must always be equal to or
|
||||
// greater than the number of known symbols in a previously-taken snapshot.
|
||||
@@ -727,9 +705,6 @@ 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> {
|
||||
|
||||
@@ -43,14 +43,12 @@
|
||||
//!
|
||||
//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very
|
||||
//! similar to tracking live bindings.
|
||||
use crate::semantic_index::use_def::VisibilityConstraints;
|
||||
|
||||
use itertools::{EitherOrBoth, Itertools};
|
||||
use super::bitset::{BitSet, BitSetIterator};
|
||||
use ruff_index::newtype_index;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::semantic_index::use_def::bitset::{BitSet, BitSetIterator};
|
||||
use crate::semantic_index::use_def::VisibilityConstraints;
|
||||
|
||||
/// A newtype-index for a definition in a particular scope.
|
||||
#[newtype_index]
|
||||
pub(super) struct ScopedDefinitionId;
|
||||
@@ -98,6 +96,7 @@ type ConstraintsPerBinding = SmallVec<InlineConstraintArray>;
|
||||
|
||||
/// Iterate over all constraints for a single binding.
|
||||
type ConstraintsIterator<'a> = std::slice::Iter<'a, Constraints>;
|
||||
type ConstraintsIntoIterator = smallvec::IntoIter<InlineConstraintArray>;
|
||||
|
||||
/// A newtype-index for a visibility constraint in a particular scope.
|
||||
#[newtype_index]
|
||||
@@ -124,18 +123,13 @@ type VisibilityConstraintPerBinding = SmallVec<InlineVisibilityConstraintsArray>
|
||||
/// Iterator over the visibility constraints for all live bindings/declarations.
|
||||
type VisibilityConstraintsIterator<'a> = std::slice::Iter<'a, ScopedVisibilityConstraintId>;
|
||||
|
||||
type VisibilityConstraintsIntoIterator = smallvec::IntoIter<InlineVisibilityConstraintsArray>;
|
||||
|
||||
/// Live declarations for a single symbol at some point in control flow, with their
|
||||
/// corresponding visibility constraints.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct SymbolDeclarations {
|
||||
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
|
||||
///
|
||||
/// Invariant: Because this is a `BitSet`, it can be viewed as a _sorted_ set of definition
|
||||
/// IDs. The `visibility_constraints` field stores constraints for each definition. Therefore
|
||||
/// those fields must always have the same `len()` as `live_declarations`, and the elements
|
||||
/// must appear in the same order. Effectively, this means that elements must always be added
|
||||
/// in sorted order, or via a binary search that determines the correct place to insert new
|
||||
/// constraints.
|
||||
pub(crate) live_declarations: Declarations,
|
||||
|
||||
/// For each live declaration, which visibility constraint applies to it?
|
||||
@@ -179,51 +173,13 @@ impl SymbolDeclarations {
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraints) {
|
||||
let a = std::mem::take(self);
|
||||
self.live_declarations = a.live_declarations.clone();
|
||||
self.live_declarations.union(&b.live_declarations);
|
||||
|
||||
// Invariant: These zips are well-formed since we maintain an invariant that all of our
|
||||
// fields are sets/vecs with the same length.
|
||||
let a = (a.live_declarations.iter()).zip(a.visibility_constraints);
|
||||
let b = (b.live_declarations.iter()).zip(b.visibility_constraints);
|
||||
|
||||
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
|
||||
// the definition IDs and constraints line up correctly in the merged result. If a
|
||||
// definition is found in both `a` and `b`, we compose the constraints from the two paths
|
||||
// in an appropriate way (intersection for narrowing constraints; ternary OR for visibility
|
||||
// constraints). If a definition is found in only one path, it is used as-is.
|
||||
for zipped in a.merge_join_by(b, |(a_decl, _), (b_decl, _)| a_decl.cmp(b_decl)) {
|
||||
match zipped {
|
||||
EitherOrBoth::Both((_, a_vis_constraint), (_, b_vis_constraint)) => {
|
||||
let vis_constraint = visibility_constraints
|
||||
.add_or_constraint(a_vis_constraint, b_vis_constraint);
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
}
|
||||
|
||||
EitherOrBoth::Left((_, vis_constraint))
|
||||
| EitherOrBoth::Right((_, vis_constraint)) => {
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Live bindings for a single symbol at some point in control flow. Each live binding comes
|
||||
/// with a set of narrowing constraints and a visibility constraint.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct SymbolBindings {
|
||||
/// [`BitSet`]: which bindings (as [`ScopedDefinitionId`]) can reach the current location?
|
||||
///
|
||||
/// Invariant: Because this is a `BitSet`, it can be viewed as a _sorted_ set of definition
|
||||
/// IDs. The `constraints` and `visibility_constraints` field stores constraints for each
|
||||
/// definition. Therefore those fields must always have the same `len()` as
|
||||
/// `live_bindings`, and the elements must appear in the same order. Effectively, this means
|
||||
/// that elements must always be added in sorted order, or via a binary search that determines
|
||||
/// the correct place to insert new constraints.
|
||||
live_bindings: Bindings,
|
||||
|
||||
/// For each live binding, which [`ScopedConstraintId`] apply?
|
||||
@@ -286,62 +242,6 @@ impl SymbolBindings {
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, mut b: Self, visibility_constraints: &mut VisibilityConstraints) {
|
||||
let mut a = std::mem::take(self);
|
||||
self.live_bindings = a.live_bindings.clone();
|
||||
self.live_bindings.union(&b.live_bindings);
|
||||
|
||||
// Invariant: These zips are well-formed since we maintain an invariant that all of our
|
||||
// fields are sets/vecs with the same length.
|
||||
//
|
||||
// Performance: We iterate over the `constraints` smallvecs via mut reference, because the
|
||||
// individual elements are `BitSet`s (currently 24 bytes in size), and we don't want to
|
||||
// move them by value multiple times during iteration. By iterating by reference, we only
|
||||
// have to copy single pointers around. In the loop below, the `std::mem::take` calls
|
||||
// specify precisely where we want to move them into the merged `constraints` smallvec.
|
||||
//
|
||||
// We don't need a similar optimization for `visibility_constraints`, since those elements
|
||||
// are 32-bit IndexVec IDs, and so are already cheap to move/copy.
|
||||
let a = (a.live_bindings.iter())
|
||||
.zip(a.constraints.iter_mut())
|
||||
.zip(a.visibility_constraints);
|
||||
let b = (b.live_bindings.iter())
|
||||
.zip(b.constraints.iter_mut())
|
||||
.zip(b.visibility_constraints);
|
||||
|
||||
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
|
||||
// the definition IDs and constraints line up correctly in the merged result. If a
|
||||
// definition is found in both `a` and `b`, we compose the constraints from the two paths
|
||||
// in an appropriate way (intersection for narrowing constraints; ternary OR for visibility
|
||||
// constraints). If a definition is found in only one path, it is used as-is.
|
||||
for zipped in a.merge_join_by(b, |((a_def, _), _), ((b_def, _), _)| a_def.cmp(b_def)) {
|
||||
match zipped {
|
||||
EitherOrBoth::Both(
|
||||
((_, a_constraints), a_vis_constraint),
|
||||
((_, b_constraints), b_vis_constraint),
|
||||
) => {
|
||||
// If the same definition is visible through both paths, any constraint
|
||||
// that applies on only one path is irrelevant to the resulting type from
|
||||
// unioning the two paths, so we intersect the constraints.
|
||||
let constraints = a_constraints;
|
||||
constraints.intersect(b_constraints);
|
||||
self.constraints.push(std::mem::take(constraints));
|
||||
|
||||
// For visibility constraints, we merge them using a ternary OR operation:
|
||||
let vis_constraint = visibility_constraints
|
||||
.add_or_constraint(a_vis_constraint, b_vis_constraint);
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
}
|
||||
|
||||
EitherOrBoth::Left(((_, constraints), vis_constraint))
|
||||
| EitherOrBoth::Right(((_, constraints), vis_constraint)) => {
|
||||
self.constraints.push(std::mem::take(constraints));
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -403,9 +303,202 @@ impl SymbolState {
|
||||
b: SymbolState,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
) {
|
||||
self.bindings.merge(b.bindings, visibility_constraints);
|
||||
let mut a = Self {
|
||||
bindings: SymbolBindings {
|
||||
live_bindings: Bindings::default(),
|
||||
constraints: ConstraintsPerBinding::default(),
|
||||
visibility_constraints: VisibilityConstraintPerBinding::default(),
|
||||
},
|
||||
declarations: SymbolDeclarations {
|
||||
live_declarations: self.declarations.live_declarations.clone(),
|
||||
visibility_constraints: VisibilityConstraintPerDeclaration::default(),
|
||||
},
|
||||
};
|
||||
|
||||
std::mem::swap(&mut a, self);
|
||||
self.declarations
|
||||
.merge(b.declarations, visibility_constraints);
|
||||
.live_declarations
|
||||
.union(&b.declarations.live_declarations);
|
||||
|
||||
let mut a_defs_iter = a.bindings.live_bindings.iter();
|
||||
let mut b_defs_iter = b.bindings.live_bindings.iter();
|
||||
let mut a_constraints_iter = a.bindings.constraints.into_iter();
|
||||
let mut b_constraints_iter = b.bindings.constraints.into_iter();
|
||||
let mut a_vis_constraints_iter = a.bindings.visibility_constraints.into_iter();
|
||||
let mut b_vis_constraints_iter = b.bindings.visibility_constraints.into_iter();
|
||||
|
||||
let mut opt_a_def: Option<u32> = a_defs_iter.next();
|
||||
let mut opt_b_def: Option<u32> = b_defs_iter.next();
|
||||
|
||||
// Iterate through the definitions from `a` and `b`, always processing the lower definition
|
||||
// ID first, and pushing each definition onto the merged `SymbolState` with its
|
||||
// constraints. If a definition is found in both `a` and `b`, push it with the intersection
|
||||
// of the constraints from the two paths; a constraint that applies from only one possible
|
||||
// path is irrelevant.
|
||||
|
||||
// Helper to push `def`, with constraints in `constraints_iter`, onto `self`.
|
||||
let push = |def,
|
||||
constraints_iter: &mut ConstraintsIntoIterator,
|
||||
visibility_constraints_iter: &mut VisibilityConstraintsIntoIterator,
|
||||
merged: &mut Self| {
|
||||
merged.bindings.live_bindings.insert(def);
|
||||
// SAFETY: we only ever create SymbolState using [`SymbolState::undefined`], which adds
|
||||
// one "unbound" definition with corresponding narrowing and visibility constraints, or
|
||||
// using [`SymbolState::record_binding`] or [`SymbolState::record_declaration`], which
|
||||
// similarly add one definition with corresponding constraints. [`SymbolState::merge`]
|
||||
// always pushes one definition and one constraint bitset and one visibility constraint
|
||||
// together (just below), so the number of definitions and the number of constraints can
|
||||
// never get out of sync.
|
||||
// get out of sync.
|
||||
let constraints = constraints_iter
|
||||
.next()
|
||||
.expect("definitions and constraints length mismatch");
|
||||
let visibility_constraints = visibility_constraints_iter
|
||||
.next()
|
||||
.expect("definitions and visibility_constraints length mismatch");
|
||||
merged.bindings.constraints.push(constraints);
|
||||
merged
|
||||
.bindings
|
||||
.visibility_constraints
|
||||
.push(visibility_constraints);
|
||||
};
|
||||
|
||||
loop {
|
||||
match (opt_a_def, opt_b_def) {
|
||||
(Some(a_def), Some(b_def)) => match a_def.cmp(&b_def) {
|
||||
std::cmp::Ordering::Less => {
|
||||
// Next definition ID is only in `a`, push it to `self` and advance `a`.
|
||||
push(
|
||||
a_def,
|
||||
&mut a_constraints_iter,
|
||||
&mut a_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
opt_a_def = a_defs_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// Next definition ID is only in `b`, push it to `self` and advance `b`.
|
||||
push(
|
||||
b_def,
|
||||
&mut b_constraints_iter,
|
||||
&mut b_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
// Next definition is in both; push to `self` and intersect constraints.
|
||||
push(
|
||||
a_def,
|
||||
&mut b_constraints_iter,
|
||||
&mut b_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
|
||||
// SAFETY: see comment in `push` above.
|
||||
let a_constraints = a_constraints_iter
|
||||
.next()
|
||||
.expect("definitions and constraints length mismatch");
|
||||
let current_constraints = self.bindings.constraints.last_mut().unwrap();
|
||||
|
||||
// If the same definition is visible through both paths, any constraint
|
||||
// that applies on only one path is irrelevant to the resulting type from
|
||||
// unioning the two paths, so we intersect the constraints.
|
||||
current_constraints.intersect(&a_constraints);
|
||||
|
||||
// For visibility constraints, we merge them using a ternary OR operation:
|
||||
let a_vis_constraint = a_vis_constraints_iter
|
||||
.next()
|
||||
.expect("visibility_constraints length mismatch");
|
||||
let current_vis_constraint =
|
||||
self.bindings.visibility_constraints.last_mut().unwrap();
|
||||
*current_vis_constraint = visibility_constraints
|
||||
.add_or_constraint(*current_vis_constraint, a_vis_constraint);
|
||||
|
||||
opt_a_def = a_defs_iter.next();
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
},
|
||||
(Some(a_def), None) => {
|
||||
// We've exhausted `b`, just push the def from `a` and move on to the next.
|
||||
push(
|
||||
a_def,
|
||||
&mut a_constraints_iter,
|
||||
&mut a_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
opt_a_def = a_defs_iter.next();
|
||||
}
|
||||
(None, Some(b_def)) => {
|
||||
// We've exhausted `a`, just push the def from `b` and move on to the next.
|
||||
push(
|
||||
b_def,
|
||||
&mut b_constraints_iter,
|
||||
&mut b_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Same as above, but for declarations.
|
||||
let mut a_decls_iter = a.declarations.live_declarations.iter();
|
||||
let mut b_decls_iter = b.declarations.live_declarations.iter();
|
||||
let mut a_vis_constraints_iter = a.declarations.visibility_constraints.into_iter();
|
||||
let mut b_vis_constraints_iter = b.declarations.visibility_constraints.into_iter();
|
||||
|
||||
let mut opt_a_decl: Option<u32> = a_decls_iter.next();
|
||||
let mut opt_b_decl: Option<u32> = b_decls_iter.next();
|
||||
|
||||
let push = |vis_constraints_iter: &mut VisibilityConstraintsIntoIterator,
|
||||
merged: &mut Self| {
|
||||
let vis_constraints = vis_constraints_iter
|
||||
.next()
|
||||
.expect("declarations and visibility_constraints length mismatch");
|
||||
merged
|
||||
.declarations
|
||||
.visibility_constraints
|
||||
.push(vis_constraints);
|
||||
};
|
||||
|
||||
loop {
|
||||
match (opt_a_decl, opt_b_decl) {
|
||||
(Some(a_decl), Some(b_decl)) => match a_decl.cmp(&b_decl) {
|
||||
std::cmp::Ordering::Less => {
|
||||
push(&mut a_vis_constraints_iter, self);
|
||||
opt_a_decl = a_decls_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
push(&mut b_vis_constraints_iter, self);
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
push(&mut b_vis_constraints_iter, self);
|
||||
|
||||
let a_vis_constraint = a_vis_constraints_iter
|
||||
.next()
|
||||
.expect("declarations and visibility_constraints length mismatch");
|
||||
let current = self.declarations.visibility_constraints.last_mut().unwrap();
|
||||
*current =
|
||||
visibility_constraints.add_or_constraint(*current, a_vis_constraint);
|
||||
|
||||
opt_a_decl = a_decls_iter.next();
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
},
|
||||
(Some(_), None) => {
|
||||
push(&mut a_vis_constraints_iter, self);
|
||||
opt_a_decl = a_decls_iter.next();
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
push(&mut b_vis_constraints_iter, self);
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn bindings(&self) -> &SymbolBindings {
|
||||
|
||||
@@ -163,17 +163,6 @@ fn check_unknown_rule(context: &mut CheckSuppressionsContext) {
|
||||
format_args!("Unknown rule `{rule}`"),
|
||||
);
|
||||
}
|
||||
|
||||
GetLintError::PrefixedWithCategory {
|
||||
prefixed,
|
||||
suggestion,
|
||||
} => {
|
||||
context.report_lint(
|
||||
&UNKNOWN_RULE,
|
||||
unknown.range,
|
||||
format_args!("Unknown rule `{prefixed}`. Did you mean `{suggestion}`?"),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -776,9 +765,8 @@ impl<'src> SuppressionParser<'src> {
|
||||
|
||||
fn eat_word(&mut self) -> bool {
|
||||
if self.cursor.eat_if(char::is_alphabetic) {
|
||||
// Allow `:` for better error recovery when someone uses `lint:code` instead of just `code`.
|
||||
self.cursor
|
||||
.eat_while(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | ':'));
|
||||
.eat_while(|c| c.is_alphanumeric() || matches!(c, '_' | '-'));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -103,6 +103,54 @@ fn widen_type_for_undeclared_public_symbol<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a possibly more precise public type for a (possibly) declared symbol where
|
||||
/// we also have an inferred type from visible bindings.
|
||||
///
|
||||
/// Below, we build the type `declared_ty | declared_ty & inferred_ty`. This represents
|
||||
/// the fact that we want to return a type that is no larger than and no smaller than
|
||||
/// `declared_ty`. If `declared_ty` is a fully static type, this means that we will
|
||||
/// simply return `declared_ty`. But if `declared_ty` is an arbitrary gradual type, this
|
||||
/// can make the public type more specific by including information about `inferred_ty`.
|
||||
///
|
||||
/// We have a special handling for the following cases, both for performance reasons
|
||||
/// and to construct the easiest possible representation of a type:
|
||||
///
|
||||
/// ```txt
|
||||
/// inferred_ty = Any/Unknown
|
||||
///
|
||||
/// => declared_ty | declared_ty & Any = declared_ty
|
||||
///
|
||||
/// declared_ty = Any/Unknown
|
||||
///
|
||||
/// => Any | Any & inferred_ty = Any | inferred_ty
|
||||
/// ```
|
||||
fn widen_type_for_declared_public_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
declared_ty: Type<'db>,
|
||||
inferred: &Symbol<'db>,
|
||||
) -> Type<'db> {
|
||||
match inferred.ignore_possibly_unbound() {
|
||||
Some(inferred_ty) => {
|
||||
if inferred_ty.is_dynamic() {
|
||||
declared_ty
|
||||
} else if declared_ty.is_dynamic() {
|
||||
UnionType::from_elements(db, [declared_ty, inferred_ty])
|
||||
} else if declared_ty.is_fully_static(db) {
|
||||
declared_ty
|
||||
} else {
|
||||
IntersectionBuilder::new(db)
|
||||
.add_positive(UnionType::from_elements(
|
||||
db,
|
||||
[declared_ty, inferred_ty].iter().copied(),
|
||||
))
|
||||
.add_positive(declared_ty)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
None => declared_ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope).
|
||||
fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
#[salsa::tracked]
|
||||
@@ -122,14 +170,17 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final);
|
||||
let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol);
|
||||
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
match declared {
|
||||
// Symbol is declared, trust the declared type
|
||||
Ok(symbol @ Symbol::Type(_, Boundness::Bound)) => symbol,
|
||||
// Symbol is declared
|
||||
Ok(Symbol::Type(declared_ty, Boundness::Bound)) => Symbol::Type(
|
||||
widen_type_for_declared_public_symbol(db, declared_ty, &inferred),
|
||||
Boundness::Bound,
|
||||
),
|
||||
// Symbol is possibly declared
|
||||
Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
match inferred {
|
||||
// Symbol is possibly undeclared and definitely unbound
|
||||
Symbol::Unbound => {
|
||||
@@ -646,6 +697,10 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
|
||||
}
|
||||
|
||||
pub const fn is_dynamic(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(_))
|
||||
}
|
||||
|
||||
pub const fn class_literal(class: Class<'db>) -> Self {
|
||||
Self::ClassLiteral(ClassLiteralType { class })
|
||||
}
|
||||
@@ -811,35 +866,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a normalized version of `self` in which all unions and intersections are sorted
|
||||
/// according to a canonical order, no matter how "deeply" a union/intersection may be nested.
|
||||
#[must_use]
|
||||
pub fn with_sorted_unions(self, db: &'db dyn Db) -> Self {
|
||||
match self {
|
||||
Type::Union(union) => Type::Union(union.to_sorted_union(db)),
|
||||
Type::Intersection(intersection) => {
|
||||
Type::Intersection(intersection.to_sorted_intersection(db))
|
||||
}
|
||||
Type::Tuple(tuple) => Type::Tuple(tuple.with_sorted_unions(db)),
|
||||
Type::LiteralString
|
||||
| Type::Instance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::SubclassOf(_) => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is a [subtype of] type `target`.
|
||||
///
|
||||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
@@ -1183,7 +1209,7 @@ impl<'db> Type<'db> {
|
||||
left.is_equivalent_to(db, right)
|
||||
}
|
||||
(Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
|
||||
_ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
|
||||
_ => self.is_fully_static(db) && other.is_fully_static(db) && self == other,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4154,8 +4180,12 @@ impl<'db> Class<'db> {
|
||||
let use_def = use_def_map(db, body_scope);
|
||||
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
let declared = symbol_from_declarations(db, declarations);
|
||||
|
||||
match symbol_from_declarations(db, declarations) {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
match declared {
|
||||
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => {
|
||||
if let Some(function) = declared_ty.into_function_literal() {
|
||||
// TODO: Eventually, we are going to process all decorators correctly. This is
|
||||
@@ -4167,13 +4197,16 @@ impl<'db> Class<'db> {
|
||||
todo_type!("bound method").into()
|
||||
}
|
||||
} else {
|
||||
SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::Bound), qualifiers)
|
||||
SymbolAndQualifiers(
|
||||
Symbol::Type(
|
||||
widen_type_for_declared_public_symbol(db, declared_ty, &inferred),
|
||||
Boundness::Bound,
|
||||
),
|
||||
qualifiers,
|
||||
)
|
||||
}
|
||||
}
|
||||
Ok(symbol @ SymbolAndQualifiers(Symbol::Unbound, qualifiers)) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
SymbolAndQualifiers(
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, symbol.is_final()),
|
||||
qualifiers,
|
||||
@@ -4381,11 +4414,12 @@ impl<'db> UnionType<'db> {
|
||||
/// Create a new union type with the elements sorted according to a canonical ordering.
|
||||
#[must_use]
|
||||
pub fn to_sorted_union(self, db: &'db dyn Db) -> Self {
|
||||
let mut new_elements: Vec<Type<'db>> = self
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|element| element.with_sorted_unions(db))
|
||||
.collect();
|
||||
let mut new_elements = self.elements(db).to_vec();
|
||||
for element in &mut new_elements {
|
||||
if let Type::Intersection(intersection) = element {
|
||||
intersection.sort(db);
|
||||
}
|
||||
}
|
||||
new_elements.sort_unstable_by(union_elements_ordering);
|
||||
UnionType::new(db, new_elements.into_boxed_slice())
|
||||
}
|
||||
@@ -4481,24 +4515,19 @@ impl<'db> IntersectionType<'db> {
|
||||
/// according to a canonical ordering.
|
||||
#[must_use]
|
||||
pub fn to_sorted_intersection(self, db: &'db dyn Db) -> Self {
|
||||
fn normalized_set<'db>(
|
||||
db: &'db dyn Db,
|
||||
elements: &FxOrderSet<Type<'db>>,
|
||||
) -> FxOrderSet<Type<'db>> {
|
||||
let mut elements: FxOrderSet<Type<'db>> = elements
|
||||
.iter()
|
||||
.map(|ty| ty.with_sorted_unions(db))
|
||||
.collect();
|
||||
let mut positive = self.positive(db).clone();
|
||||
positive.sort_unstable_by(union_elements_ordering);
|
||||
|
||||
elements.sort_unstable_by(union_elements_ordering);
|
||||
elements
|
||||
}
|
||||
let mut negative = self.negative(db).clone();
|
||||
negative.sort_unstable_by(union_elements_ordering);
|
||||
|
||||
IntersectionType::new(
|
||||
db,
|
||||
normalized_set(db, self.positive(db)),
|
||||
normalized_set(db, self.negative(db)),
|
||||
)
|
||||
IntersectionType::new(db, positive, negative)
|
||||
}
|
||||
|
||||
/// Perform an in-place sort of this [`IntersectionType`] instance
|
||||
/// according to a canonical ordering.
|
||||
fn sort(&mut self, db: &'db dyn Db) {
|
||||
*self = self.to_sorted_intersection(db);
|
||||
}
|
||||
|
||||
pub fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
@@ -4516,33 +4545,21 @@ impl<'db> IntersectionType<'db> {
|
||||
}
|
||||
|
||||
let self_positive = self.positive(db);
|
||||
|
||||
if !all_fully_static(db, self_positive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let other_positive = other.positive(db);
|
||||
|
||||
if self_positive.len() != other_positive.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !all_fully_static(db, other_positive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let self_negative = self.negative(db);
|
||||
|
||||
if !all_fully_static(db, self_negative) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let other_negative = other.negative(db);
|
||||
|
||||
if self_negative.len() != other_negative.len() {
|
||||
let other_positive = other.positive(db);
|
||||
if !all_fully_static(db, other_positive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let other_negative = other.negative(db);
|
||||
if !all_fully_static(db, other_negative) {
|
||||
return false;
|
||||
}
|
||||
@@ -4551,13 +4568,7 @@ impl<'db> IntersectionType<'db> {
|
||||
return true;
|
||||
}
|
||||
|
||||
let sorted_self = self.to_sorted_intersection(db);
|
||||
|
||||
if sorted_self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
sorted_self == other.to_sorted_intersection(db)
|
||||
self_positive.set_eq(other_positive) && self_negative.set_eq(other_negative)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` has exactly the same set of possible static materializations as `other`
|
||||
@@ -4659,18 +4670,6 @@ impl<'db> TupleType<'db> {
|
||||
Type::Tuple(Self::new(db, elements.into_boxed_slice()))
|
||||
}
|
||||
|
||||
/// Return a normalized version of `self` in which all unions and intersections are sorted
|
||||
/// according to a canonical order, no matter how "deeply" a union/intersection may be nested.
|
||||
#[must_use]
|
||||
pub fn with_sorted_unions(self, db: &'db dyn Db) -> Self {
|
||||
let elements: Box<[Type<'db>]> = self
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| ty.with_sorted_unions(db))
|
||||
.collect();
|
||||
TupleType::new(db, elements)
|
||||
}
|
||||
|
||||
pub fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
let self_elements = self.elements(db);
|
||||
let other_elements = other.elements(db);
|
||||
|
||||
@@ -467,13 +467,6 @@ mod stable {
|
||||
assignable_to_is_reflexive, db,
|
||||
forall types t. t.is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// For *any* pair of types, whether fully static or not,
|
||||
// each of the pair should be assignable to the union of the two.
|
||||
type_property_test!(
|
||||
all_type_pairs_are_assignable_to_their_union, db,
|
||||
forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t]))
|
||||
);
|
||||
}
|
||||
|
||||
/// This module contains property tests that currently lead to many false positives.
|
||||
@@ -522,6 +515,13 @@ mod flaky {
|
||||
forall types s, t. intersection(db, [s, t]).is_assignable_to(db, s) && intersection(db, [s, t]).is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// For *any* pair of types, whether fully static or not,
|
||||
// each of the pair should be assignable to the union of the two.
|
||||
type_property_test!(
|
||||
all_type_pairs_are_assignable_to_their_union, db,
|
||||
forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t]))
|
||||
);
|
||||
|
||||
// Equal element sets of intersections implies equivalence
|
||||
// flaky at least in part because of https://github.com/astral-sh/ruff/issues/15513
|
||||
type_property_test!(
|
||||
|
||||
@@ -283,7 +283,7 @@ cargo test -p red_knot_python_semantic -- mdtest__
|
||||
Alternatively, you can use the `mdtest.py` runner which has a watch mode that will re-run corresponding tests when Markdown files change, and recompile automatically when Rust code changes:
|
||||
|
||||
```bash
|
||||
uv run crates/red_knot_python_semantic/mdtest.py
|
||||
uv -q run crates/red_knot_python_semantic/mdtest.py
|
||||
```
|
||||
|
||||
## Planned features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.9.4"
|
||||
version = "0.9.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -30,8 +30,6 @@ use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
use rustc_hash::FxHashMap;
|
||||
use toml;
|
||||
|
||||
use crate::commands::completions::config::{OptionString, OptionStringParser};
|
||||
|
||||
/// All configuration options that can be passed "globally",
|
||||
/// i.e., can be passed to all subcommands
|
||||
#[derive(Debug, Default, Clone, clap::Args)]
|
||||
@@ -116,11 +114,7 @@ pub enum Command {
|
||||
/// List or describe the available configuration options.
|
||||
Config {
|
||||
/// Config key to show
|
||||
#[arg(
|
||||
value_parser = OptionStringParser,
|
||||
hide_possible_values = true
|
||||
)]
|
||||
option: Option<OptionString>,
|
||||
option: Option<String>,
|
||||
/// Output format
|
||||
#[arg(long, value_enum, default_value = "text")]
|
||||
output_format: HelpFormat,
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
|
||||
use itertools::Itertools;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_workspace::{
|
||||
options::Options,
|
||||
options_base::{OptionField, OptionSet, OptionsMetadata, Visit},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct CollectOptionsVisitor {
|
||||
values: Vec<(String, String)>,
|
||||
parents: Vec<String>,
|
||||
}
|
||||
|
||||
impl IntoIterator for CollectOptionsVisitor {
|
||||
type Item = (String, String);
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.values.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for CollectOptionsVisitor {
|
||||
fn record_set(&mut self, name: &str, group: OptionSet) {
|
||||
let fully_qualified_name = self
|
||||
.parents
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.chain(std::iter::once(name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
|
||||
// Only add the set to completion list if it has it's own documentation.
|
||||
self.values.push((
|
||||
fully_qualified_name,
|
||||
group.documentation().unwrap_or("").to_owned(),
|
||||
));
|
||||
|
||||
self.parents.push(name.to_owned());
|
||||
group.record(self);
|
||||
self.parents.pop();
|
||||
}
|
||||
|
||||
fn record_field(&mut self, name: &str, field: OptionField) {
|
||||
let fqn = self
|
||||
.parents
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.chain(std::iter::once(name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
|
||||
self.values.push((fqn, field.doc.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque type used solely to enable tab completions
|
||||
/// for `ruff option [OPTION]` command.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OptionString(String);
|
||||
|
||||
impl From<String> for OptionString {
|
||||
fn from(s: String) -> Self {
|
||||
OptionString(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OptionString> for String {
|
||||
fn from(value: OptionString) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for OptionString {
|
||||
fn from(s: &str) -> Self {
|
||||
OptionString(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for OptionString {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for OptionString {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Options::metadata()
|
||||
.has(s)
|
||||
.then(|| OptionString(s.to_owned()))
|
||||
.ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OptionStringParser;
|
||||
|
||||
impl ValueParserFactory for OptionString {
|
||||
type Parser = OptionStringParser;
|
||||
|
||||
fn value_parser() -> Self::Parser {
|
||||
OptionStringParser
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedValueParser for OptionStringParser {
|
||||
type Value = OptionString;
|
||||
|
||||
fn parse_ref(
|
||||
&self,
|
||||
cmd: &clap::Command,
|
||||
arg: Option<&clap::Arg>,
|
||||
value: &std::ffi::OsStr,
|
||||
) -> Result<Self::Value, clap::Error> {
|
||||
let value = value
|
||||
.to_str()
|
||||
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
|
||||
|
||||
value.parse().map_err(|()| {
|
||||
let mut error = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
|
||||
if let Some(arg) = arg {
|
||||
error.insert(
|
||||
clap::error::ContextKind::InvalidArg,
|
||||
clap::error::ContextValue::String(arg.to_string()),
|
||||
);
|
||||
}
|
||||
error.insert(
|
||||
clap::error::ContextKind::InvalidValue,
|
||||
clap::error::ContextValue::String(value.to_string()),
|
||||
);
|
||||
error
|
||||
})
|
||||
}
|
||||
|
||||
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
|
||||
let mut visitor = CollectOptionsVisitor::default();
|
||||
Options::metadata().record(&mut visitor);
|
||||
|
||||
Some(Box::new(visitor.into_iter().map(|(name, doc)| {
|
||||
let first_paragraph = doc
|
||||
.lines()
|
||||
.take_while(|line| !line.trim_end().is_empty())
|
||||
// Replace double quotes with single quotes,to avoid clap's lack of escaping
|
||||
// when creating zsh completions. This has no security implications, as it only
|
||||
// affects the help string, which is never executed
|
||||
.map(|s| s.replace('"', "'"))
|
||||
.join(" ");
|
||||
|
||||
PossibleValue::new(name).help(first_paragraph)
|
||||
})))
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod config;
|
||||
@@ -893,7 +893,7 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
|
||||
QuoteStyle::Single | QuoteStyle::Double
|
||||
)
|
||||
{
|
||||
warn_user_once!("The `flake8-quotes.docstring-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
|
||||
warn_user_once!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
// Validate all isort settings.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
pub(crate) mod completions;
|
||||
|
||||
pub(crate) mod add_noqa;
|
||||
pub(crate) mod analyze_graph;
|
||||
pub(crate) mod check;
|
||||
|
||||
@@ -924,7 +924,7 @@ def say_hy(name: str):
|
||||
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
||||
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
||||
warning: The `flake8-quotes.docstring-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
||||
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
|
||||
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
|
||||
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
|
||||
@@ -983,7 +983,7 @@ def say_hy(name: str):
|
||||
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
||||
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
||||
warning: The `flake8-quotes.docstring-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
||||
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
|
||||
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
|
||||
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
|
||||
|
||||
@@ -100,7 +100,7 @@ impl Renderer {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the terminal width
|
||||
// Set the terminal width
|
||||
pub const fn term_width(mut self, term_width: usize) -> Self {
|
||||
self.term_width = term_width;
|
||||
self
|
||||
|
||||
@@ -26,8 +26,25 @@ const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/
|
||||
static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
// We don't support `*` imports yet:
|
||||
"error[lint:unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
|
||||
// We don't support terminal statements in control flow yet:
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
|
||||
// We don't handle intersections in `is_assignable_to` yet
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:639:52 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`",
|
||||
"warning[lint:unused-ignore-comment] /src/tomllib/_parser.py:682:31 Unused blanket `type: ignore` directive",
|
||||
|
||||
@@ -94,10 +94,6 @@ impl DiagnosticId {
|
||||
matches!(self, DiagnosticId::Lint(self_name) if self_name == name)
|
||||
}
|
||||
|
||||
pub fn strip_category(code: &str) -> Option<&str> {
|
||||
code.split_once(':').map(|(_, rest)| rest)
|
||||
}
|
||||
|
||||
/// Returns `true` if this `DiagnosticId` matches the given name.
|
||||
///
|
||||
/// ## Examples
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.4"
|
||||
version = "0.9.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pendulum
|
||||
|
||||
from airflow.decorators import dag, task
|
||||
from airflow.models import DAG
|
||||
from airflow.models.baseoperator import BaseOperator
|
||||
@@ -14,22 +13,30 @@ from airflow.utils.context import get_current_context
|
||||
|
||||
def access_invalid_key_in_context(**context):
|
||||
print("access invalid key", context["conf"])
|
||||
print("access invalid key", context.get("conf"))
|
||||
|
||||
|
||||
@task
|
||||
def access_invalid_key_task_out_of_dag(**context):
|
||||
print("access invalid key", context["conf"])
|
||||
print("access invalid key", context.get("conf"))
|
||||
|
||||
@dag(
|
||||
schedule=None,
|
||||
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
|
||||
catchup=False,
|
||||
tags=[""],
|
||||
)
|
||||
def invalid_dag():
|
||||
@task()
|
||||
def access_invalid_key_task(**context):
|
||||
print("access invalid key", context.get("conf"))
|
||||
|
||||
@task
|
||||
def access_invalid_argument_task_out_of_dag(
|
||||
execution_date, tomorrow_ds, logical_date, **context
|
||||
):
|
||||
print("execution date", execution_date)
|
||||
print("access invalid key", context.get("conf"))
|
||||
task1 = PythonOperator(
|
||||
task_id="task1",
|
||||
python_callable=access_invalid_key_in_context,
|
||||
)
|
||||
access_invalid_key_task() >> task1
|
||||
access_invalid_key_task_out_of_dag()
|
||||
|
||||
invalid_dag()
|
||||
|
||||
@task
|
||||
def print_config(**context):
|
||||
@@ -49,9 +56,31 @@ def print_config(**context):
|
||||
yesterday_ds = context["yesterday_ds"]
|
||||
yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
||||
with DAG(
|
||||
dag_id="example_dag",
|
||||
schedule_interval="@daily",
|
||||
start_date=datetime(2023, 1, 1),
|
||||
template_searchpath=["/templates"],
|
||||
) as dag:
|
||||
task1 = DummyOperator(
|
||||
task_id="task1",
|
||||
params={
|
||||
# Removed variables in template
|
||||
"execution_date": "{{ execution_date }}",
|
||||
"next_ds": "{{ next_ds }}",
|
||||
"prev_ds": "{{ prev_ds }}"
|
||||
},
|
||||
)
|
||||
|
||||
class CustomMacrosPlugin(AirflowPlugin):
|
||||
name = "custom_macros"
|
||||
macros = {
|
||||
"execution_date_macro": lambda context: context["execution_date"],
|
||||
"next_ds_macro": lambda context: context["next_ds"]
|
||||
}
|
||||
|
||||
@task
|
||||
def print_config_with_get_current_context():
|
||||
def print_config():
|
||||
context = get_current_context()
|
||||
execution_date = context["execution_date"]
|
||||
next_ds = context["next_ds"]
|
||||
@@ -65,74 +94,8 @@ def print_config_with_get_current_context():
|
||||
yesterday_ds = context["yesterday_ds"]
|
||||
yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
||||
|
||||
@task(task_id="print_the_context")
|
||||
def print_context(ds=None, **kwargs):
|
||||
"""Print the Airflow context and ds variable from the context."""
|
||||
print(ds)
|
||||
print(kwargs.get("tomorrow_ds"))
|
||||
c = get_current_context()
|
||||
c.get("execution_date")
|
||||
|
||||
|
||||
@dag(
|
||||
schedule=None,
|
||||
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
|
||||
catchup=False,
|
||||
tags=[""],
|
||||
)
|
||||
def invalid_dag():
|
||||
@task()
|
||||
def access_invalid_key_task(**context):
|
||||
print("access invalid key", context.get("conf"))
|
||||
|
||||
@task()
|
||||
def access_invalid_key_explicit_task(execution_date):
|
||||
print(execution_date)
|
||||
|
||||
task1 = PythonOperator(
|
||||
task_id="task1",
|
||||
python_callable=access_invalid_key_in_context,
|
||||
)
|
||||
|
||||
access_invalid_key_task() >> task1
|
||||
access_invalid_key_explicit_task()
|
||||
access_invalid_argument_task_out_of_dag()
|
||||
access_invalid_key_task_out_of_dag()
|
||||
print_config()
|
||||
print_config_with_get_current_context()
|
||||
print_context()
|
||||
|
||||
|
||||
invalid_dag()
|
||||
|
||||
with DAG(
|
||||
dag_id="example_dag",
|
||||
schedule_interval="@daily",
|
||||
start_date=datetime(2023, 1, 1),
|
||||
template_searchpath=["/templates"],
|
||||
) as dag:
|
||||
task1 = DummyOperator(
|
||||
task_id="task1",
|
||||
params={
|
||||
# Removed variables in template
|
||||
"execution_date": "{{ execution_date }}",
|
||||
"next_ds": "{{ next_ds }}",
|
||||
"prev_ds": "{{ prev_ds }}",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CustomMacrosPlugin(AirflowPlugin):
|
||||
name = "custom_macros"
|
||||
macros = {
|
||||
"execution_date_macro": lambda context: context["execution_date"],
|
||||
"next_ds_macro": lambda context: context["next_ds"],
|
||||
}
|
||||
|
||||
|
||||
class CustomOperator(BaseOperator):
|
||||
def execute(self, next_ds, context):
|
||||
def execute(self, context):
|
||||
execution_date = context["execution_date"]
|
||||
next_ds = context["next_ds"]
|
||||
next_ds_nodash = context["next_ds_nodash"]
|
||||
@@ -145,6 +108,18 @@ class CustomOperator(BaseOperator):
|
||||
yesterday_ds = context["yesterday_ds"]
|
||||
yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
||||
@task
|
||||
def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context):
|
||||
print("execution date", execution_date)
|
||||
print("access invalid key", context.get("conf"))
|
||||
|
||||
@task(task_id="print_the_context")
|
||||
def print_context(ds=None, **kwargs):
|
||||
"""Print the Airflow context and ds variable from the context."""
|
||||
print(ds)
|
||||
print(kwargs.get("tomorrow_ds"))
|
||||
c = get_current_context()
|
||||
c.get("execution_date")
|
||||
|
||||
class CustomOperatorNew(BaseOperator):
|
||||
def execute(self, context):
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import telnetlib
|
||||
from telnetlib import Telnet
|
||||
@@ -1,2 +0,0 @@
|
||||
import ftplib
|
||||
from ftplib import FTP
|
||||
@@ -1,8 +0,0 @@
|
||||
import dill
|
||||
from dill import objects
|
||||
import shelve
|
||||
from shelve import open
|
||||
import cPickle
|
||||
from cPickle import load
|
||||
import pickle
|
||||
from pickle import load
|
||||
@@ -1,3 +0,0 @@
|
||||
import subprocess
|
||||
from subprocess import Popen
|
||||
from subprocess import Popen as pop
|
||||
@@ -1,4 +0,0 @@
|
||||
import xml.etree.cElementTree
|
||||
from xml.etree import cElementTree
|
||||
import xml.etree.ElementTree
|
||||
from xml.etree import ElementTree
|
||||
@@ -1,3 +0,0 @@
|
||||
from xml import sax
|
||||
import xml.sax as xmls
|
||||
import xml.sax
|
||||
@@ -1,2 +0,0 @@
|
||||
from xml.dom import expatbuilder
|
||||
import xml.dom.expatbuilder
|
||||
@@ -1,2 +0,0 @@
|
||||
from xml.dom.minidom import parseString
|
||||
import xml.dom.minidom
|
||||
@@ -1,2 +0,0 @@
|
||||
from xml.dom.pulldom import parseString
|
||||
import xml.dom.pulldom
|
||||
@@ -1,2 +0,0 @@
|
||||
import lxml
|
||||
from lxml import etree
|
||||
@@ -1,2 +0,0 @@
|
||||
import xmlrpc
|
||||
from xmlrpc import server
|
||||
@@ -1 +0,0 @@
|
||||
from twisted.web.twcgi import CGIScript
|
||||
@@ -1,4 +0,0 @@
|
||||
import Crypto.Hash
|
||||
from Crypto.Hash import MD2
|
||||
import Crypto.PublicKey
|
||||
from Crypto.PublicKey import RSA
|
||||
@@ -1,2 +0,0 @@
|
||||
import pyghmi
|
||||
from pyghmi import foo
|
||||
@@ -35,14 +35,3 @@ def okay(obj=Class()):
|
||||
|
||||
def error(obj=OtherClass()):
|
||||
...
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/12717
|
||||
|
||||
from typing import NewType
|
||||
|
||||
N = NewType("N", int)
|
||||
L = NewType("L", list[str])
|
||||
|
||||
def okay(obj = N()): ...
|
||||
def error(obj = L()): ...
|
||||
|
||||
@@ -17,13 +17,3 @@ reversed(sorted((i for i in range(42)), reverse=True))
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/10335
|
||||
reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
|
||||
# These fixes need to be parenthesized to avoid syntax errors and behavior
|
||||
# changes.
|
||||
# See https://github.com/astral-sh/ruff/issues/15789
|
||||
reversed(sorted
|
||||
(""))
|
||||
list(sorted
|
||||
(""))
|
||||
list(sorted
|
||||
("xy"))
|
||||
|
||||
@@ -6,12 +6,12 @@ list(map(lambda x: x * 2, nums))
|
||||
set(map(lambda x: x % 2 == 0, nums))
|
||||
dict(map(lambda v: (v, v**2), nums))
|
||||
dict(map(lambda v: [v, v**2], nums))
|
||||
|
||||
map(lambda: "const", nums)
|
||||
map(lambda _: 3.0, nums)
|
||||
_ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
|
||||
all(map(lambda v: isinstance(v, dict), nums))
|
||||
filter(func, map(lambda v: v, nums))
|
||||
|
||||
list(map(lambda x, y: x * y, nums))
|
||||
|
||||
# When inside f-string, then the fix should be surrounded by whitespace
|
||||
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
|
||||
@@ -49,7 +49,7 @@ map(lambda x: x, (y if y else z))
|
||||
map(lambda x: x, (x, y, z))
|
||||
|
||||
# See https://github.com/astral-sh/ruff/issues/14808
|
||||
# The following should be Ok since
|
||||
# The following should be Ok since
|
||||
# named expressions are a syntax error inside comprehensions
|
||||
a = [1, 2, 3]
|
||||
b = map(lambda x: x, c := a)
|
||||
@@ -57,16 +57,3 @@ print(c)
|
||||
|
||||
# Check nested as well
|
||||
map(lambda x:x, [c:=a])
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/15796
|
||||
map(lambda: "const", nums)
|
||||
list(map(lambda x, y: x * y, nums))
|
||||
|
||||
|
||||
map(lambda: 1, "xyz")
|
||||
map(lambda x, y: x, [(1, 2), (3, 4)])
|
||||
list(map(lambda: 1, "xyz"))
|
||||
list(map(lambda x, y: x, [(1, 2), (3, 4)]))
|
||||
list(map(lambda: 1, "xyz"))
|
||||
list(map(lambda x, y: x, [(1, 2), (3, 4)]))
|
||||
|
||||
@@ -194,17 +194,3 @@ if foo():
|
||||
z = not foo()
|
||||
else:
|
||||
z = other
|
||||
|
||||
|
||||
# These two cases double as tests for f-string quote preservation. The first
|
||||
# f-string should preserve its double quotes, and the second should preserve
|
||||
# single quotes
|
||||
if cond:
|
||||
var = "str"
|
||||
else:
|
||||
var = f"{first}-{second}"
|
||||
|
||||
if cond:
|
||||
var = "str"
|
||||
else:
|
||||
var = f'{first}-{second}'
|
||||
|
||||
@@ -29,47 +29,47 @@ no_sep = None
|
||||
' 1 2 3 '.split()
|
||||
'1<>2<>3<4'.split('<>')
|
||||
|
||||
" a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
||||
" a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
|
||||
"".split() # []
|
||||
"""
|
||||
""".split() # []
|
||||
" ".split() # []
|
||||
"/abc/".split() # ["/abc/"]
|
||||
"/abc/".split() # ['/abc/']
|
||||
("a,b,c"
|
||||
# comment
|
||||
.split()
|
||||
) # ["a,b,c"]
|
||||
) # ['a,b,c']
|
||||
("a,b,c"
|
||||
# comment1
|
||||
.split(",")
|
||||
) # ["a", "b", "c"]
|
||||
) # ['a', 'b', 'c']
|
||||
("a,"
|
||||
# comment
|
||||
"b,"
|
||||
"c"
|
||||
.split(",")
|
||||
) # ["a", "b", "c"]
|
||||
) # ['a', 'b', 'c']
|
||||
|
||||
"hello "\
|
||||
"world".split()
|
||||
# ["hello", "world"]
|
||||
# ['hello', 'world']
|
||||
|
||||
# prefixes and isc
|
||||
u"a b".split() # [u"a", u"b"]
|
||||
r"a \n b".split() # [r"a", r"\n", r"b"]
|
||||
("a " "b").split() # ["a", "b"]
|
||||
"a " "b".split() # ["a", "b"]
|
||||
u"a " "b".split() # [u"a", u"b"]
|
||||
"a " u"b".split() # ["a", "b"]
|
||||
u"a " r"\n".split() # [u"a", u"\\n"]
|
||||
r"\n " u"\n".split() # [r"\n"]
|
||||
r"\n " "\n".split() # [r"\n"]
|
||||
"a " r"\n".split() # ["a", "\\n"]
|
||||
u"a b".split() # ['a', 'b']
|
||||
r"a \n b".split() # ['a', '\\n', 'b']
|
||||
("a " "b").split() # ['a', 'b']
|
||||
"a " "b".split() # ['a', 'b']
|
||||
u"a " "b".split() # ['a', 'b']
|
||||
"a " u"b".split() # ['a', 'b']
|
||||
u"a " r"\n".split() # ['a', '\\n']
|
||||
r"\n " u"\n".split() # ['\\n']
|
||||
r"\n " "\n".split() # ['\\n']
|
||||
"a " r"\n".split() # ['a', '\\n']
|
||||
|
||||
"a,b,c".split(',', maxsplit=0) # ["a,b,c"]
|
||||
"a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
|
||||
"a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
|
||||
"a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
|
||||
"a,b,c".split(',', maxsplit=0) # ['a,b,c']
|
||||
"a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
|
||||
"a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
|
||||
"a,b,c".split(',', maxsplit=-0) # ['a,b,c']
|
||||
|
||||
# negatives
|
||||
|
||||
|
||||
@@ -13,5 +13,3 @@ fruits in [[1, 2, 3], [4, 5, 6]]
|
||||
fruits in [1, 2, 3]
|
||||
1 in [[1, 2, 3], [4, 5, 6]]
|
||||
_ = {key: value for key, value in {"a": 1, "b": 2}.items() if key in (["a", "b"], ["c", "d"])}
|
||||
1 in []
|
||||
1 in ()
|
||||
|
||||
@@ -82,11 +82,3 @@ class Collection(Protocol[*_B0]):
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/8609
|
||||
def f(x: Union[int, str, bytes]) -> None:
|
||||
...
|
||||
|
||||
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/14132
|
||||
class AClass:
|
||||
...
|
||||
|
||||
def myfunc(param: "tuple[Union[int, 'AClass', None], str]"):
|
||||
print(param)
|
||||
|
||||
@@ -109,9 +109,3 @@ from typing_extensions import CapsuleType
|
||||
|
||||
# UP035 on py313+ only
|
||||
from typing_extensions import deprecated
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/15780
|
||||
from typing_extensions import is_typeddict
|
||||
# https://github.com/astral-sh/ruff/pull/15800#pullrequestreview-2580704217
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
@@ -106,17 +106,3 @@ x = TypeVar("x", "str", "int")
|
||||
x = cast("str", x)
|
||||
|
||||
X = List["MyClass"]
|
||||
|
||||
# Handle end of line comment in string annotation
|
||||
# See https://github.com/astral-sh/ruff/issues/15816
|
||||
def f() -> "Literal[0]#":
|
||||
return 0
|
||||
|
||||
def g(x: "Literal['abc']#") -> None:
|
||||
return
|
||||
|
||||
def f() -> """Literal[0]
|
||||
#
|
||||
|
||||
""":
|
||||
return 0
|
||||
|
||||
@@ -2,101 +2,57 @@ from pathlib import Path
|
||||
|
||||
lines = ["line 1", "line 2", "line 3"]
|
||||
|
||||
|
||||
# Errors
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
with open("file", "w") as f:
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
|
||||
other_line = "other line"
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write(other_line)
|
||||
|
||||
def _():
|
||||
other_line = "other line"
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write(other_line)
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
|
||||
with Path("file").open("wb") as f:
|
||||
for line in lines:
|
||||
f.write(line.encode())
|
||||
|
||||
def _():
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write(line.upper())
|
||||
|
||||
with Path("file").open("w") as f:
|
||||
pass
|
||||
|
||||
def _():
|
||||
with Path("file").open("wb") as f:
|
||||
for line in lines:
|
||||
f.write(line.encode())
|
||||
|
||||
|
||||
def _():
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write(line.upper())
|
||||
|
||||
|
||||
def _():
|
||||
with Path("file").open("w") as f:
|
||||
pass
|
||||
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
|
||||
|
||||
def _():
|
||||
# Offer unsafe fix if it would delete comments
|
||||
with open("file","w") as f:
|
||||
for line in lines:
|
||||
# a really important comment
|
||||
f.write(line)
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
for () in a:
|
||||
f.write(())
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
for a, b, c in d:
|
||||
f.write((a, b))
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
for [(), [a.b], (c,)] in d:
|
||||
f.write(())
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
for [([([a[b]],)],), [], (c[d],)] in e:
|
||||
f.write(())
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
|
||||
# Offer unsafe fix if it would delete comments
|
||||
with open("file","w") as f:
|
||||
for line in lines:
|
||||
# a really important comment
|
||||
f.write(line)
|
||||
|
||||
# OK
|
||||
|
||||
def _():
|
||||
for line in lines:
|
||||
Path("file").open("w").write(line)
|
||||
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
Path("file").open("w").write(line)
|
||||
pass
|
||||
|
||||
f.write(line)
|
||||
|
||||
def _():
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
pass
|
||||
|
||||
f.write(line)
|
||||
|
||||
|
||||
def _():
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
else:
|
||||
pass
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
async def func():
|
||||
@@ -105,51 +61,6 @@ async def func():
|
||||
f.write(line)
|
||||
|
||||
|
||||
def _():
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write() # type: ignore
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
global CURRENT_LINE
|
||||
for CURRENT_LINE in lines:
|
||||
f.write(CURRENT_LINE)
|
||||
|
||||
|
||||
def _():
|
||||
foo = 1
|
||||
def __():
|
||||
with open("file", "w") as f:
|
||||
nonlocal foo
|
||||
for foo in lines:
|
||||
f.write(foo)
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
line = ''
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
for a, b, c in d:
|
||||
f.write((a, b))
|
||||
print(a)
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
for [*[*a]], b, [[c]] in d:
|
||||
f.write((a, b))
|
||||
print(c)
|
||||
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
global global_foo
|
||||
for [a, b, (global_foo, c)] in d:
|
||||
f.write((a, b))
|
||||
with Path("file").open("w") as f:
|
||||
for line in lines:
|
||||
f.write() # type: ignore
|
||||
|
||||
@@ -2,7 +2,7 @@ import re
|
||||
|
||||
s = "str"
|
||||
|
||||
# this should be replaced with `s.replace("abc", "")`
|
||||
# this should be replaced with s.replace("abc", "")
|
||||
re.sub("abc", "", s)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def dashrepl(matchobj):
|
||||
|
||||
re.sub("-", dashrepl, "pro----gram-files")
|
||||
|
||||
# this one should be replaced with `s.startswith("abc")` because the Match is
|
||||
# this one should be replaced with s.startswith("abc") because the Match is
|
||||
# used in an if context for its truth value
|
||||
if re.match("abc", s):
|
||||
pass
|
||||
@@ -25,17 +25,17 @@ if m := re.match("abc", s): # this should *not* be replaced
|
||||
pass
|
||||
re.match("abc", s) # this should not be replaced because match returns a Match
|
||||
|
||||
# this should be replaced with `"abc" in s`
|
||||
# this should be replaced with "abc" in s
|
||||
if re.search("abc", s):
|
||||
pass
|
||||
re.search("abc", s) # this should not be replaced
|
||||
|
||||
# this should be replaced with `"abc" == s`
|
||||
# this should be replaced with "abc" == s
|
||||
if re.fullmatch("abc", s):
|
||||
pass
|
||||
re.fullmatch("abc", s) # this should not be replaced
|
||||
|
||||
# this should be replaced with `s.split("abc")`
|
||||
# this should be replaced with s.split("abc")
|
||||
re.split("abc", s)
|
||||
|
||||
# these currently should not be modified because the patterns contain regex
|
||||
@@ -93,8 +93,3 @@ re.sub(r"a", r"\a", "a")
|
||||
|
||||
re.sub(r"a", "\?", "a")
|
||||
re.sub(r"a", r"\?", "a")
|
||||
|
||||
# these double as tests for preserving raw string quoting style
|
||||
re.sub(r'abc', "", s)
|
||||
re.sub(r"""abc""", "", s)
|
||||
re.sub(r'''abc''', "", s)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
"""Patterns that don't just involve the call, but rather the parent expression"""
|
||||
import re
|
||||
|
||||
s = "str"
|
||||
|
||||
# this should be replaced with `"abc" not in s`
|
||||
re.search("abc", s) is None
|
||||
|
||||
|
||||
# this should be replaced with `"abc" in s`
|
||||
re.search("abc", s) is not None
|
||||
|
||||
|
||||
# this should be replaced with `not s.startswith("abc")`
|
||||
re.match("abc", s) is None
|
||||
|
||||
|
||||
# this should be replaced with `s.startswith("abc")`
|
||||
re.match("abc", s) is not None
|
||||
|
||||
|
||||
# this should be replaced with `s != "abc"`
|
||||
re.fullmatch("abc", s) is None
|
||||
|
||||
|
||||
# this should be replaced with `s == "abc"`
|
||||
re.fullmatch("abc", s) is not None
|
||||
|
||||
|
||||
# this should trigger an unsafe fix because of the presence of a comment within the
|
||||
# expression being replaced (which we'd lose)
|
||||
if (
|
||||
re.fullmatch(
|
||||
"a really really really really long string",
|
||||
s,
|
||||
)
|
||||
# with a comment here
|
||||
is None
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
# this should trigger a safe fix (comments are preserved given they're outside the
|
||||
# expression)
|
||||
if ( # leading
|
||||
re.fullmatch(
|
||||
"a really really really really long string",
|
||||
s,
|
||||
)
|
||||
is None # trailing
|
||||
):
|
||||
pass
|
||||
@@ -6,7 +6,7 @@ import itertools
|
||||
|
||||
starmap(func, zip())
|
||||
starmap(func, zip([]))
|
||||
|
||||
starmap(func, zip(*args))
|
||||
|
||||
starmap(func, zip(a, b, c,),)
|
||||
|
||||
@@ -71,7 +71,3 @@ starmap(func, zip(a, b, c), lorem=ipsum)
|
||||
starmap(func, zip(a, b, c, strict=True))
|
||||
starmap(func, zip(a, b, c, strict=False))
|
||||
starmap(func, zip(a, b, c, strict=strict))
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/15742
|
||||
starmap(func, zip(*a))
|
||||
starmap(func, zip(*a, *b))
|
||||
@@ -1,11 +0,0 @@
|
||||
"""Regression test for https://github.com/astral-sh/ruff/issues/15786. This
|
||||
should be separate from other tests because it shadows the `map` builtin.
|
||||
|
||||
This should still get a diagnostic but not a fix that would lead to an error.
|
||||
"""
|
||||
|
||||
from itertools import starmap
|
||||
|
||||
map = {}
|
||||
for _ in starmap(print, zip("A", "12")):
|
||||
pass
|
||||
@@ -5,7 +5,7 @@ use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{
|
||||
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
|
||||
pylint, refurb, ruff,
|
||||
pylint, ruff,
|
||||
};
|
||||
|
||||
/// Run lint rules over the [`Binding`]s.
|
||||
@@ -22,7 +22,6 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
Rule::UnquotedTypeAlias,
|
||||
Rule::UsedDummyVariable,
|
||||
Rule::PytestUnittestRaisesAssertion,
|
||||
Rule::ForLoopWrites,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
@@ -110,10 +109,5 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::ForLoopWrites) {
|
||||
if let Some(diagnostic) = refurb::rules::for_loop_writes_binding(checker, binding) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pyupgrade::rules::use_pep646_unpack(checker, subscript);
|
||||
}
|
||||
if checker.enabled(Rule::Airflow3Removal) {
|
||||
airflow::rules::airflow_3_removal_expr(checker, expr);
|
||||
airflow::rules::removed_in_3(checker, expr);
|
||||
}
|
||||
pandas_vet::rules::subscript(checker, value, expr);
|
||||
}
|
||||
@@ -227,7 +227,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
refurb::rules::regex_flag_alias(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::Airflow3Removal) {
|
||||
airflow::rules::airflow_3_removal_expr(checker, expr);
|
||||
airflow::rules::removed_in_3(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::Airflow3MovedToProvider) {
|
||||
airflow::rules::moved_to_provider_in_3(checker, expr);
|
||||
@@ -311,7 +311,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::Airflow3Removal) {
|
||||
airflow::rules::airflow_3_removal_expr(checker, expr);
|
||||
airflow::rules::removed_in_3(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::MixedCaseVariableInGlobalScope) {
|
||||
if matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
|
||||
@@ -449,7 +449,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
flake8_pyi::rules::bytestring_attribute(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::Airflow3Removal) {
|
||||
airflow::rules::airflow_3_removal_expr(checker, expr);
|
||||
airflow::rules::removed_in_3(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::Call(
|
||||
@@ -506,7 +506,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
checker,
|
||||
attr,
|
||||
call,
|
||||
string_value,
|
||||
string_value.to_str(),
|
||||
);
|
||||
}
|
||||
} else if attr == "format" {
|
||||
@@ -1150,7 +1150,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
ruff::rules::unnecessary_regular_expression(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::Airflow3Removal) {
|
||||
airflow::rules::airflow_3_removal_expr(checker, expr);
|
||||
airflow::rules::removed_in_3(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryCastToInt) {
|
||||
ruff::rules::unnecessary_cast_to_int(checker, call);
|
||||
|
||||
@@ -377,7 +377,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
flake8_pytest_style::rules::parameter_with_default_argument(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::Airflow3Removal) {
|
||||
airflow::rules::airflow_3_removal_function_def(checker, function_def);
|
||||
airflow::rules::removed_in_3_function_def(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::NonPEP695GenericFunction) {
|
||||
pyupgrade::rules::non_pep695_generic_function(checker, function_def);
|
||||
@@ -1415,7 +1415,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
refurb::rules::for_loop_set_mutations(checker, for_stmt);
|
||||
}
|
||||
if checker.enabled(Rule::ForLoopWrites) {
|
||||
refurb::rules::for_loop_writes_stmt(checker, for_stmt);
|
||||
refurb::rules::for_loop_writes(checker, for_stmt);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::NeedlessElse) {
|
||||
|
||||
@@ -294,31 +294,11 @@ impl<'a> Checker<'a> {
|
||||
|
||||
/// Create a [`Generator`] to generate source code based on the current AST state.
|
||||
pub(crate) fn generator(&self) -> Generator {
|
||||
Generator::new(self.stylist.indentation(), self.stylist.line_ending())
|
||||
}
|
||||
|
||||
/// Return the preferred quote for a generated `StringLiteral` node, given where we are in the
|
||||
/// AST.
|
||||
fn preferred_quote(&self) -> Quote {
|
||||
self.f_string_quote_style().unwrap_or(self.stylist.quote())
|
||||
}
|
||||
|
||||
/// Return the default string flags a generated `StringLiteral` node should use, given where we
|
||||
/// are in the AST.
|
||||
pub(crate) fn default_string_flags(&self) -> ast::StringLiteralFlags {
|
||||
ast::StringLiteralFlags::empty().with_quote_style(self.preferred_quote())
|
||||
}
|
||||
|
||||
/// Return the default bytestring flags a generated `ByteStringLiteral` node should use, given
|
||||
/// where we are in the AST.
|
||||
pub(crate) fn default_bytes_flags(&self) -> ast::BytesLiteralFlags {
|
||||
ast::BytesLiteralFlags::empty().with_quote_style(self.preferred_quote())
|
||||
}
|
||||
|
||||
/// Return the default f-string flags a generated `FString` node should use, given where we are
|
||||
/// in the AST.
|
||||
pub(crate) fn default_fstring_flags(&self) -> ast::FStringFlags {
|
||||
ast::FStringFlags::empty().with_quote_style(self.preferred_quote())
|
||||
Generator::new(
|
||||
self.stylist.indentation(),
|
||||
self.f_string_quote_style().unwrap_or(self.stylist.quote()),
|
||||
self.stylist.line_ending(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the appropriate quoting for f-string by reversing the one used outside of
|
||||
|
||||
@@ -80,7 +80,7 @@ enum Replacement {
|
||||
}
|
||||
|
||||
/// AIR302
|
||||
pub(crate) fn airflow_3_removal_expr(checker: &mut Checker, expr: &Expr) {
|
||||
pub(crate) fn removed_in_3(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::AIRFLOW) {
|
||||
return;
|
||||
}
|
||||
@@ -117,10 +117,7 @@ pub(crate) fn airflow_3_removal_expr(checker: &mut Checker, expr: &Expr) {
|
||||
}
|
||||
|
||||
/// AIR302
|
||||
pub(crate) fn airflow_3_removal_function_def(
|
||||
checker: &mut Checker,
|
||||
function_def: &StmtFunctionDef,
|
||||
) {
|
||||
pub(crate) fn removed_in_3_function_def(checker: &mut Checker, function_def: &StmtFunctionDef) {
|
||||
if !checker.semantic().seen_module(Modules::AIRFLOW) {
|
||||
return;
|
||||
}
|
||||
@@ -157,9 +154,7 @@ const REMOVED_CONTEXT_KEYS: [&str; 12] = [
|
||||
/// pass
|
||||
/// ```
|
||||
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())
|
||||
{
|
||||
if !is_airflow_task(function_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1081,35 +1076,3 @@ fn is_airflow_task(function_def: &StmtFunctionDef, semantic: &SemanticModel) ->
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Check it's "execute" method inherits from Airflow base operator
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```python
|
||||
/// from airflow.models.baseoperator import BaseOperator
|
||||
///
|
||||
/// class CustomOperator(BaseOperator):
|
||||
/// def execute(self):
|
||||
/// pass
|
||||
/// ```
|
||||
fn is_execute_method_inherits_from_airflow_operator(
|
||||
function_def: &StmtFunctionDef,
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
if function_def.name.as_str() != "execute" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ScopeKind::Class(class_def) = semantic.current_scope().kind else {
|
||||
return false;
|
||||
};
|
||||
|
||||
class_def.bases().iter().any(|class_base| {
|
||||
semantic
|
||||
.resolve_qualified_name(class_base)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["airflow", .., "BaseOperator"])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,338 +1,319 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
AIR302_context.py:22:41: AIR302 `conf` is removed in Airflow 3.0
|
||||
AIR302_context.py:19:45: AIR302 `conf` is removed in Airflow 3.0
|
||||
|
|
||||
20 | @task
|
||||
21 | def access_invalid_key_task_out_of_dag(**context):
|
||||
22 | print("access invalid key", context["conf"])
|
||||
| ^^^^^^ AIR302
|
||||
23 | print("access invalid key", context.get("conf"))
|
||||
|
|
||||
|
||||
AIR302_context.py:23:45: AIR302 `conf` is removed in Airflow 3.0
|
||||
|
|
||||
21 | def access_invalid_key_task_out_of_dag(**context):
|
||||
22 | print("access invalid key", context["conf"])
|
||||
23 | print("access invalid key", context.get("conf"))
|
||||
17 | @task
|
||||
18 | def access_invalid_key_task_out_of_dag(**context):
|
||||
19 | print("access invalid key", context.get("conf"))
|
||||
| ^^^^^^ AIR302
|
||||
20 |
|
||||
21 | @dag(
|
||||
|
|
||||
|
||||
AIR302_context.py:28:5: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
AIR302_context.py:30:49: AIR302 `conf` is removed in Airflow 3.0
|
||||
|
|
||||
26 | @task
|
||||
27 | def access_invalid_argument_task_out_of_dag(
|
||||
28 | execution_date, tomorrow_ds, logical_date, **context
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
29 | ):
|
||||
30 | print("execution date", execution_date)
|
||||
|
|
||||
|
||||
AIR302_context.py:28:21: AIR302 `tomorrow_ds` is removed in Airflow 3.0
|
||||
|
|
||||
26 | @task
|
||||
27 | def access_invalid_argument_task_out_of_dag(
|
||||
28 | execution_date, tomorrow_ds, logical_date, **context
|
||||
| ^^^^^^^^^^^ AIR302
|
||||
29 | ):
|
||||
30 | print("execution date", execution_date)
|
||||
|
|
||||
|
||||
AIR302_context.py:31:45: AIR302 `conf` is removed in Airflow 3.0
|
||||
|
|
||||
29 | ):
|
||||
30 | print("execution date", execution_date)
|
||||
31 | print("access invalid key", context.get("conf"))
|
||||
| ^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_context.py:40:30: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
39 | # Removed usage - should trigger violations
|
||||
40 | execution_date = context["execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
41 | next_ds = context["next_ds"]
|
||||
42 | next_ds_nodash = context["next_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:41:23: AIR302 `next_ds` is removed in Airflow 3.0
|
||||
|
|
||||
39 | # Removed usage - should trigger violations
|
||||
40 | execution_date = context["execution_date"]
|
||||
41 | next_ds = context["next_ds"]
|
||||
| ^^^^^^^^^ AIR302
|
||||
42 | next_ds_nodash = context["next_ds_nodash"]
|
||||
43 | next_execution_date = context["next_execution_date"]
|
||||
|
|
||||
|
||||
AIR302_context.py:42:30: AIR302 `next_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
40 | execution_date = context["execution_date"]
|
||||
41 | next_ds = context["next_ds"]
|
||||
42 | next_ds_nodash = context["next_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
43 | next_execution_date = context["next_execution_date"]
|
||||
44 | prev_ds = context["prev_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:43:35: AIR302 `next_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
41 | next_ds = context["next_ds"]
|
||||
42 | next_ds_nodash = context["next_ds_nodash"]
|
||||
43 | next_execution_date = context["next_execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
44 | prev_ds = context["prev_ds"]
|
||||
45 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:44:23: AIR302 `prev_ds` is removed in Airflow 3.0
|
||||
|
|
||||
42 | next_ds_nodash = context["next_ds_nodash"]
|
||||
43 | next_execution_date = context["next_execution_date"]
|
||||
44 | prev_ds = context["prev_ds"]
|
||||
| ^^^^^^^^^ AIR302
|
||||
45 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
46 | prev_execution_date = context["prev_execution_date"]
|
||||
|
|
||||
|
||||
AIR302_context.py:45:30: AIR302 `prev_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
43 | next_execution_date = context["next_execution_date"]
|
||||
44 | prev_ds = context["prev_ds"]
|
||||
45 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
46 | prev_execution_date = context["prev_execution_date"]
|
||||
47 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
|
|
||||
|
||||
AIR302_context.py:46:35: AIR302 `prev_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
44 | prev_ds = context["prev_ds"]
|
||||
45 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
46 | prev_execution_date = context["prev_execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
47 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
48 | tomorrow_ds = context["tomorrow_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:47:43: AIR302 `prev_execution_date_success` is removed in Airflow 3.0
|
||||
|
|
||||
45 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
46 | prev_execution_date = context["prev_execution_date"]
|
||||
47 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
48 | tomorrow_ds = context["tomorrow_ds"]
|
||||
49 | yesterday_ds = context["yesterday_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:48:27: AIR302 `tomorrow_ds` is removed in Airflow 3.0
|
||||
|
|
||||
46 | prev_execution_date = context["prev_execution_date"]
|
||||
47 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
48 | tomorrow_ds = context["tomorrow_ds"]
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
49 | yesterday_ds = context["yesterday_ds"]
|
||||
50 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:49:28: AIR302 `yesterday_ds` is removed in Airflow 3.0
|
||||
|
|
||||
47 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
48 | tomorrow_ds = context["tomorrow_ds"]
|
||||
49 | yesterday_ds = context["yesterday_ds"]
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
50 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:50:35: AIR302 `yesterday_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
48 | tomorrow_ds = context["tomorrow_ds"]
|
||||
49 | yesterday_ds = context["yesterday_ds"]
|
||||
50 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_context.py:56:30: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
54 | def print_config_with_get_current_context():
|
||||
55 | context = get_current_context()
|
||||
56 | execution_date = context["execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
57 | next_ds = context["next_ds"]
|
||||
58 | next_ds_nodash = context["next_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:57:23: AIR302 `next_ds` is removed in Airflow 3.0
|
||||
|
|
||||
55 | context = get_current_context()
|
||||
56 | execution_date = context["execution_date"]
|
||||
57 | next_ds = context["next_ds"]
|
||||
| ^^^^^^^^^ AIR302
|
||||
58 | next_ds_nodash = context["next_ds_nodash"]
|
||||
59 | next_execution_date = context["next_execution_date"]
|
||||
|
|
||||
|
||||
AIR302_context.py:58:30: AIR302 `next_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
56 | execution_date = context["execution_date"]
|
||||
57 | next_ds = context["next_ds"]
|
||||
58 | next_ds_nodash = context["next_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
59 | next_execution_date = context["next_execution_date"]
|
||||
60 | prev_ds = context["prev_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:59:35: AIR302 `next_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
57 | next_ds = context["next_ds"]
|
||||
58 | next_ds_nodash = context["next_ds_nodash"]
|
||||
59 | next_execution_date = context["next_execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
60 | prev_ds = context["prev_ds"]
|
||||
61 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:60:23: AIR302 `prev_ds` is removed in Airflow 3.0
|
||||
|
|
||||
58 | next_ds_nodash = context["next_ds_nodash"]
|
||||
59 | next_execution_date = context["next_execution_date"]
|
||||
60 | prev_ds = context["prev_ds"]
|
||||
| ^^^^^^^^^ AIR302
|
||||
61 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
62 | prev_execution_date = context["prev_execution_date"]
|
||||
|
|
||||
|
||||
AIR302_context.py:61:30: AIR302 `prev_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
59 | next_execution_date = context["next_execution_date"]
|
||||
60 | prev_ds = context["prev_ds"]
|
||||
61 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
62 | prev_execution_date = context["prev_execution_date"]
|
||||
63 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
|
|
||||
|
||||
AIR302_context.py:62:35: AIR302 `prev_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
60 | prev_ds = context["prev_ds"]
|
||||
61 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
62 | prev_execution_date = context["prev_execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
63 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
64 | tomorrow_ds = context["tomorrow_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:63:43: AIR302 `prev_execution_date_success` is removed in Airflow 3.0
|
||||
|
|
||||
61 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
62 | prev_execution_date = context["prev_execution_date"]
|
||||
63 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
64 | tomorrow_ds = context["tomorrow_ds"]
|
||||
65 | yesterday_ds = context["yesterday_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:64:27: AIR302 `tomorrow_ds` is removed in Airflow 3.0
|
||||
|
|
||||
62 | prev_execution_date = context["prev_execution_date"]
|
||||
63 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
64 | tomorrow_ds = context["tomorrow_ds"]
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
65 | yesterday_ds = context["yesterday_ds"]
|
||||
66 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:65:28: AIR302 `yesterday_ds` is removed in Airflow 3.0
|
||||
|
|
||||
63 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
64 | tomorrow_ds = context["tomorrow_ds"]
|
||||
65 | yesterday_ds = context["yesterday_ds"]
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
66 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:66:35: AIR302 `yesterday_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
64 | tomorrow_ds = context["tomorrow_ds"]
|
||||
65 | yesterday_ds = context["yesterday_ds"]
|
||||
66 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_context.py:73:22: AIR302 `tomorrow_ds` is removed in Airflow 3.0
|
||||
|
|
||||
71 | """Print the Airflow context and ds variable from the context."""
|
||||
72 | print(ds)
|
||||
73 | print(kwargs.get("tomorrow_ds"))
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
74 | c = get_current_context()
|
||||
75 | c.get("execution_date")
|
||||
|
|
||||
|
||||
AIR302_context.py:75:11: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
73 | print(kwargs.get("tomorrow_ds"))
|
||||
74 | c = get_current_context()
|
||||
75 | c.get("execution_date")
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
|
|
||||
|
||||
AIR302_context.py:87:49: AIR302 `conf` is removed in Airflow 3.0
|
||||
|
|
||||
85 | @task()
|
||||
86 | def access_invalid_key_task(**context):
|
||||
87 | print("access invalid key", context.get("conf"))
|
||||
28 | @task()
|
||||
29 | def access_invalid_key_task(**context):
|
||||
30 | print("access invalid key", context.get("conf"))
|
||||
| ^^^^^^ AIR302
|
||||
88 |
|
||||
89 | @task()
|
||||
31 |
|
||||
32 | task1 = PythonOperator(
|
||||
|
|
||||
|
||||
AIR302_context.py:90:42: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
AIR302_context.py:47:30: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
89 | @task()
|
||||
90 | def access_invalid_key_explicit_task(execution_date):
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
91 | print(execution_date)
|
||||
46 | # Removed usage - should trigger violations
|
||||
47 | execution_date = context["execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
48 | next_ds = context["next_ds"]
|
||||
49 | next_ds_nodash = context["next_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:111:5: AIR302 [*] `schedule_interval` is removed in Airflow 3.0
|
||||
|
|
||||
109 | with DAG(
|
||||
110 | dag_id="example_dag",
|
||||
111 | schedule_interval="@daily",
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
112 | start_date=datetime(2023, 1, 1),
|
||||
113 | template_searchpath=["/templates"],
|
||||
|
|
||||
= help: Use `schedule` instead
|
||||
AIR302_context.py:48:23: AIR302 `next_ds` is removed in Airflow 3.0
|
||||
|
|
||||
46 | # Removed usage - should trigger violations
|
||||
47 | execution_date = context["execution_date"]
|
||||
48 | next_ds = context["next_ds"]
|
||||
| ^^^^^^^^^ AIR302
|
||||
49 | next_ds_nodash = context["next_ds_nodash"]
|
||||
50 | next_execution_date = context["next_execution_date"]
|
||||
|
|
||||
|
||||
AIR302_context.py:49:30: AIR302 `next_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
47 | execution_date = context["execution_date"]
|
||||
48 | next_ds = context["next_ds"]
|
||||
49 | next_ds_nodash = context["next_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
50 | next_execution_date = context["next_execution_date"]
|
||||
51 | prev_ds = context["prev_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:50:35: AIR302 `next_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
48 | next_ds = context["next_ds"]
|
||||
49 | next_ds_nodash = context["next_ds_nodash"]
|
||||
50 | next_execution_date = context["next_execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
51 | prev_ds = context["prev_ds"]
|
||||
52 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:51:23: AIR302 `prev_ds` is removed in Airflow 3.0
|
||||
|
|
||||
49 | next_ds_nodash = context["next_ds_nodash"]
|
||||
50 | next_execution_date = context["next_execution_date"]
|
||||
51 | prev_ds = context["prev_ds"]
|
||||
| ^^^^^^^^^ AIR302
|
||||
52 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
53 | prev_execution_date = context["prev_execution_date"]
|
||||
|
|
||||
|
||||
AIR302_context.py:52:30: AIR302 `prev_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
50 | next_execution_date = context["next_execution_date"]
|
||||
51 | prev_ds = context["prev_ds"]
|
||||
52 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
53 | prev_execution_date = context["prev_execution_date"]
|
||||
54 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
|
|
||||
|
||||
AIR302_context.py:53:35: AIR302 `prev_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
51 | prev_ds = context["prev_ds"]
|
||||
52 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
53 | prev_execution_date = context["prev_execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
54 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
55 | tomorrow_ds = context["tomorrow_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:54:43: AIR302 `prev_execution_date_success` is removed in Airflow 3.0
|
||||
|
|
||||
52 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
53 | prev_execution_date = context["prev_execution_date"]
|
||||
54 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
55 | tomorrow_ds = context["tomorrow_ds"]
|
||||
56 | yesterday_ds = context["yesterday_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:55:27: AIR302 `tomorrow_ds` is removed in Airflow 3.0
|
||||
|
|
||||
53 | prev_execution_date = context["prev_execution_date"]
|
||||
54 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
55 | tomorrow_ds = context["tomorrow_ds"]
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
56 | yesterday_ds = context["yesterday_ds"]
|
||||
57 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:56:28: AIR302 `yesterday_ds` is removed in Airflow 3.0
|
||||
|
|
||||
54 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
55 | tomorrow_ds = context["tomorrow_ds"]
|
||||
56 | yesterday_ds = context["yesterday_ds"]
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
57 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:57:35: AIR302 `yesterday_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
55 | tomorrow_ds = context["tomorrow_ds"]
|
||||
56 | yesterday_ds = context["yesterday_ds"]
|
||||
57 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
58 |
|
||||
59 | with DAG(
|
||||
|
|
||||
|
||||
AIR302_context.py:61:5: AIR302 [*] `schedule_interval` is removed in Airflow 3.0
|
||||
|
|
||||
59 | with DAG(
|
||||
60 | dag_id="example_dag",
|
||||
61 | schedule_interval="@daily",
|
||||
| ^^^^^^^^^^^^^^^^^ AIR302
|
||||
62 | start_date=datetime(2023, 1, 1),
|
||||
63 | template_searchpath=["/templates"],
|
||||
|
|
||||
= help: Use `schedule` instead
|
||||
|
||||
ℹ Safe fix
|
||||
108 108 |
|
||||
109 109 | with DAG(
|
||||
110 110 | dag_id="example_dag",
|
||||
111 |- schedule_interval="@daily",
|
||||
111 |+ schedule="@daily",
|
||||
112 112 | start_date=datetime(2023, 1, 1),
|
||||
113 113 | template_searchpath=["/templates"],
|
||||
114 114 | ) as dag:
|
||||
58 58 |
|
||||
59 59 | with DAG(
|
||||
60 60 | dag_id="example_dag",
|
||||
61 |- schedule_interval="@daily",
|
||||
61 |+ schedule="@daily",
|
||||
62 62 | start_date=datetime(2023, 1, 1),
|
||||
63 63 | template_searchpath=["/templates"],
|
||||
64 64 | ) as dag:
|
||||
|
||||
AIR302_context.py:115:13: AIR302 `airflow.operators.dummy.DummyOperator` is removed in Airflow 3.0
|
||||
|
|
||||
113 | template_searchpath=["/templates"],
|
||||
114 | ) as dag:
|
||||
115 | task1 = DummyOperator(
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
116 | task_id="task1",
|
||||
117 | params={
|
||||
|
|
||||
= help: Use `airflow.operators.empty.EmptyOperator` instead
|
||||
AIR302_context.py:65:13: AIR302 `airflow.operators.dummy.DummyOperator` is removed in Airflow 3.0
|
||||
|
|
||||
63 | template_searchpath=["/templates"],
|
||||
64 | ) as dag:
|
||||
65 | task1 = DummyOperator(
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
66 | task_id="task1",
|
||||
67 | params={
|
||||
|
|
||||
= help: Use `airflow.operators.empty.EmptyOperator` instead
|
||||
|
||||
AIR302_context.py:135:23: AIR302 `next_ds` is removed in Airflow 3.0
|
||||
AIR302_context.py:85:30: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
83 | def print_config():
|
||||
84 | context = get_current_context()
|
||||
85 | execution_date = context["execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
86 | next_ds = context["next_ds"]
|
||||
87 | next_ds_nodash = context["next_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:86:23: AIR302 `next_ds` is removed in Airflow 3.0
|
||||
|
|
||||
84 | context = get_current_context()
|
||||
85 | execution_date = context["execution_date"]
|
||||
86 | next_ds = context["next_ds"]
|
||||
| ^^^^^^^^^ AIR302
|
||||
87 | next_ds_nodash = context["next_ds_nodash"]
|
||||
88 | next_execution_date = context["next_execution_date"]
|
||||
|
|
||||
|
||||
AIR302_context.py:87:30: AIR302 `next_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
85 | execution_date = context["execution_date"]
|
||||
86 | next_ds = context["next_ds"]
|
||||
87 | next_ds_nodash = context["next_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
88 | next_execution_date = context["next_execution_date"]
|
||||
89 | prev_ds = context["prev_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:88:35: AIR302 `next_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
86 | next_ds = context["next_ds"]
|
||||
87 | next_ds_nodash = context["next_ds_nodash"]
|
||||
88 | next_execution_date = context["next_execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
89 | prev_ds = context["prev_ds"]
|
||||
90 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:89:23: AIR302 `prev_ds` is removed in Airflow 3.0
|
||||
|
|
||||
87 | next_ds_nodash = context["next_ds_nodash"]
|
||||
88 | next_execution_date = context["next_execution_date"]
|
||||
89 | prev_ds = context["prev_ds"]
|
||||
| ^^^^^^^^^ AIR302
|
||||
90 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
91 | prev_execution_date = context["prev_execution_date"]
|
||||
|
|
||||
|
||||
AIR302_context.py:90:30: AIR302 `prev_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
88 | next_execution_date = context["next_execution_date"]
|
||||
89 | prev_ds = context["prev_ds"]
|
||||
90 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
91 | prev_execution_date = context["prev_execution_date"]
|
||||
92 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
|
|
||||
|
||||
AIR302_context.py:91:35: AIR302 `prev_execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
89 | prev_ds = context["prev_ds"]
|
||||
90 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
91 | prev_execution_date = context["prev_execution_date"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
92 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
93 | tomorrow_ds = context["tomorrow_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:92:43: AIR302 `prev_execution_date_success` is removed in Airflow 3.0
|
||||
|
|
||||
90 | prev_ds_nodash = context["prev_ds_nodash"]
|
||||
91 | prev_execution_date = context["prev_execution_date"]
|
||||
92 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
93 | tomorrow_ds = context["tomorrow_ds"]
|
||||
94 | yesterday_ds = context["yesterday_ds"]
|
||||
|
|
||||
|
||||
AIR302_context.py:93:27: AIR302 `tomorrow_ds` is removed in Airflow 3.0
|
||||
|
|
||||
91 | prev_execution_date = context["prev_execution_date"]
|
||||
92 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
93 | tomorrow_ds = context["tomorrow_ds"]
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
94 | yesterday_ds = context["yesterday_ds"]
|
||||
95 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:94:28: AIR302 `yesterday_ds` is removed in Airflow 3.0
|
||||
|
|
||||
92 | prev_execution_date_success = context["prev_execution_date_success"]
|
||||
93 | tomorrow_ds = context["tomorrow_ds"]
|
||||
94 | yesterday_ds = context["yesterday_ds"]
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
95 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
|
||||
|
||||
AIR302_context.py:95:35: AIR302 `yesterday_ds_nodash` is removed in Airflow 3.0
|
||||
|
|
||||
93 | tomorrow_ds = context["tomorrow_ds"]
|
||||
94 | yesterday_ds = context["yesterday_ds"]
|
||||
95 | yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
|
||||
96 |
|
||||
97 | class CustomOperator(BaseOperator):
|
||||
|
|
||||
|
||||
AIR302_context.py:112:45: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
134 | class CustomOperator(BaseOperator):
|
||||
135 | def execute(self, next_ds, context):
|
||||
| ^^^^^^^ AIR302
|
||||
136 | execution_date = context["execution_date"]
|
||||
137 | next_ds = context["next_ds"]
|
||||
111 | @task
|
||||
112 | def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context):
|
||||
| ^^^^^^^^^^^^^^ AIR302
|
||||
113 | print("execution date", execution_date)
|
||||
114 | print("access invalid key", context.get("conf"))
|
||||
|
|
||||
|
||||
AIR302_context.py:112:61: AIR302 `tomorrow_ds` is removed in Airflow 3.0
|
||||
|
|
||||
111 | @task
|
||||
112 | def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context):
|
||||
| ^^^^^^^^^^^ AIR302
|
||||
113 | print("execution date", execution_date)
|
||||
114 | print("access invalid key", context.get("conf"))
|
||||
|
|
||||
|
||||
AIR302_context.py:114:45: AIR302 `conf` is removed in Airflow 3.0
|
||||
|
|
||||
112 | def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context):
|
||||
113 | print("execution date", execution_date)
|
||||
114 | print("access invalid key", context.get("conf"))
|
||||
| ^^^^^^ AIR302
|
||||
115 |
|
||||
116 | @task(task_id="print_the_context")
|
||||
|
|
||||
|
||||
AIR302_context.py:120:22: AIR302 `tomorrow_ds` is removed in Airflow 3.0
|
||||
|
|
||||
118 | """Print the Airflow context and ds variable from the context."""
|
||||
119 | print(ds)
|
||||
120 | print(kwargs.get("tomorrow_ds"))
|
||||
| ^^^^^^^^^^^^^ AIR302
|
||||
121 | c = get_current_context()
|
||||
122 | c.get("execution_date")
|
||||
|
|
||||
|
||||
AIR302_context.py:122:11: AIR302 `execution_date` is removed in Airflow 3.0
|
||||
|
|
||||
120 | print(kwargs.get("tomorrow_ds"))
|
||||
121 | c = get_current_context()
|
||||
122 | c.get("execution_date")
|
||||
| ^^^^^^^^^^^^^^^^ AIR302
|
||||
123 |
|
||||
124 | class CustomOperatorNew(BaseOperator):
|
||||
|
|
||||
|
||||
@@ -17,8 +17,7 @@ use crate::registry::AsRule;
|
||||
/// call to complete, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking call, use an equivalent asynchronous library
|
||||
/// or function, like [`trio.run_process()`](https://trio.readthedocs.io/en/stable/reference-io.html#trio.run_process)
|
||||
/// or [`anyio.run_process()`](https://anyio.readthedocs.io/en/latest/api.html#anyio.run_process).
|
||||
/// or function.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -50,8 +49,7 @@ impl Violation for CreateSubprocessInAsyncFunction {
|
||||
/// call to complete, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking call, use an equivalent asynchronous library
|
||||
/// or function, like [`trio.run_process()`](https://trio.readthedocs.io/en/stable/reference-io.html#trio.run_process)
|
||||
/// or [`anyio.run_process()`](https://anyio.readthedocs.io/en/latest/api.html#anyio.run_process).
|
||||
/// or function.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -83,8 +81,7 @@ impl Violation for RunProcessInAsyncFunction {
|
||||
/// call to complete, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking call, use an equivalent asynchronous library
|
||||
/// or function, like [`trio.to_thread.run_sync()`](https://trio.readthedocs.io/en/latest/reference-core.html#trio.to_thread.run_sync)
|
||||
/// or [`anyio.to_thread.run_sync()`](https://anyio.readthedocs.io/en/latest/api.html#anyio.to_thread.run_sync).
|
||||
/// or function.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
|
||||
@@ -52,33 +52,19 @@ mod tests {
|
||||
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.pyi"))]
|
||||
#[test_case(Rule::SuspiciousFtplibImport, Path::new("S402.py"))]
|
||||
#[test_case(Rule::SuspiciousFtplibImport, Path::new("S402.pyi"))]
|
||||
#[test_case(Rule::SuspiciousPickleImport, Path::new("S403.py"))]
|
||||
#[test_case(Rule::SuspiciousPickleImport, Path::new("S403.pyi"))]
|
||||
#[test_case(Rule::SuspiciousSubprocessImport, Path::new("S404.py"))]
|
||||
#[test_case(Rule::SuspiciousSubprocessImport, Path::new("S404.pyi"))]
|
||||
#[test_case(Rule::SuspiciousXmlEtreeImport, Path::new("S405.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlEtreeImport, Path::new("S405.pyi"))]
|
||||
#[test_case(Rule::SuspiciousXmlSaxImport, Path::new("S406.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlSaxImport, Path::new("S406.pyi"))]
|
||||
#[test_case(Rule::SuspiciousXmlExpatImport, Path::new("S407.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlExpatImport, Path::new("S407.pyi"))]
|
||||
#[test_case(Rule::SuspiciousXmlMinidomImport, Path::new("S408.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlMinidomImport, Path::new("S408.pyi"))]
|
||||
#[test_case(Rule::SuspiciousXmlPulldomImport, Path::new("S409.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlPulldomImport, Path::new("S409.pyi"))]
|
||||
#[test_case(Rule::SuspiciousLxmlImport, Path::new("S410.py"))]
|
||||
#[test_case(Rule::SuspiciousLxmlImport, Path::new("S410.pyi"))]
|
||||
#[test_case(Rule::SuspiciousXmlrpcImport, Path::new("S411.py"))]
|
||||
#[test_case(Rule::SuspiciousXmlrpcImport, Path::new("S411.pyi"))]
|
||||
#[test_case(Rule::SuspiciousHttpoxyImport, Path::new("S412.py"))]
|
||||
#[test_case(Rule::SuspiciousHttpoxyImport, Path::new("S412.pyi"))]
|
||||
#[test_case(Rule::SuspiciousPycryptoImport, Path::new("S413.py"))]
|
||||
#[test_case(Rule::SuspiciousPycryptoImport, Path::new("S413.pyi"))]
|
||||
#[test_case(Rule::SuspiciousPyghmiImport, Path::new("S415.py"))]
|
||||
#[test_case(Rule::SuspiciousPyghmiImport, Path::new("S415.pyi"))]
|
||||
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"))]
|
||||
#[test_case(Rule::TryExceptPass, Path::new("S110.py"))]
|
||||
#[test_case(Rule::UnixCommandWildcardInjection, Path::new("S609.py"))]
|
||||
|
||||
@@ -352,11 +352,6 @@ impl Violation for SuspiciousPyghmiImport {
|
||||
|
||||
/// S401, S402, S403, S404, S405, S406, S407, S408, S409, S410, S411, S412, S413, S415
|
||||
pub(crate) fn suspicious_imports(checker: &mut Checker, stmt: &Stmt) {
|
||||
// Skip stub files.
|
||||
if checker.source_type.is_stub() {
|
||||
return;
|
||||
}
|
||||
|
||||
match stmt {
|
||||
Stmt::Import(ast::StmtImport { names, .. }) => {
|
||||
for name in names {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
@@ -8,7 +8,7 @@ use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
|
||||
use ruff_python_ast::visitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_semantic::analyze::typing::{
|
||||
is_immutable_annotation, is_immutable_func, is_immutable_newtype_call, is_mutable_func,
|
||||
is_immutable_annotation, is_immutable_func, is_mutable_func,
|
||||
};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
@@ -22,12 +22,12 @@ use crate::checkers::ast::Checker;
|
||||
/// once, at definition time. The returned value will then be reused by all
|
||||
/// calls to the function, which can lead to unexpected behaviour.
|
||||
///
|
||||
/// Parameters with immutable type annotations will be ignored by this rule.
|
||||
/// Those whose default arguments are `NewType` calls where the original type
|
||||
/// is immutable are also ignored.
|
||||
/// Calls can be marked as an exception to this rule with the
|
||||
/// [`lint.flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
///
|
||||
/// Calls and types outside of the standard library can be marked as an exception
|
||||
/// to this rule with the [`lint.flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
/// Arguments with immutable type annotations will be ignored by this rule.
|
||||
/// Types outside of the standard library can be marked as immutable with the
|
||||
/// [`lint.flake8-bugbear.extend-immutable-calls`] configuration option as well.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
@@ -105,9 +105,6 @@ impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> {
|
||||
Expr::Call(ast::ExprCall { func, .. }) => {
|
||||
if !is_mutable_func(func, self.semantic)
|
||||
&& !is_immutable_func(func, self.semantic, self.extend_immutable_calls)
|
||||
&& !func.as_name_expr().is_some_and(|name| {
|
||||
is_immutable_newtype_call(name, self.semantic, self.extend_immutable_calls)
|
||||
})
|
||||
{
|
||||
self.diagnostics.push((
|
||||
FunctionCallInDefaultArgument {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
B008_extended.py:24:51: B008 Do not perform function call `Depends` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
||||
|
|
||||
@@ -14,10 +15,3 @@ B008_extended.py:36:15: B008 Do not perform function call `OtherClass` in argume
|
||||
| ^^^^^^^^^^^^ B008
|
||||
37 | ...
|
||||
|
|
||||
|
||||
B008_extended.py:48:17: B008 Do not perform function call `L` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
||||
|
|
||||
47 | def okay(obj = N()): ...
|
||||
48 | def error(obj = L()): ...
|
||||
| ^^^ B008
|
||||
|
|
||||
|
||||
@@ -416,17 +416,9 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
|
||||
}
|
||||
};
|
||||
|
||||
let inner_needs_parens = matches!(
|
||||
inner_call.whitespace_after_func,
|
||||
ParenthesizableWhitespace::ParenthesizedWhitespace(_)
|
||||
);
|
||||
|
||||
if let Expression::Name(outer_name) = &*outer_call.func {
|
||||
if outer_name.value == "list" {
|
||||
tree = Expression::Call(Box::new((*inner_call).clone()));
|
||||
if inner_needs_parens {
|
||||
tree = tree.with_parens(LeftParen::default(), RightParen::default());
|
||||
};
|
||||
} else {
|
||||
// If the `reverse` argument is used...
|
||||
let args = if inner_call.args.iter().any(|arg| {
|
||||
@@ -511,9 +503,6 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
|
||||
whitespace_after_func: inner_call.whitespace_after_func.clone(),
|
||||
whitespace_before_args: inner_call.whitespace_before_args.clone(),
|
||||
}));
|
||||
if inner_needs_parens {
|
||||
tree = tree.with_parens(LeftParen::default(), RightParen::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,14 @@ pub(crate) fn unnecessary_map(
|
||||
return;
|
||||
}
|
||||
|
||||
if !lambda_has_expected_arity(parameters.as_deref(), body) {
|
||||
if parameters.as_ref().is_some_and(|parameters| {
|
||||
late_binding(parameters, body)
|
||||
|| parameters
|
||||
.iter_non_variadic_params()
|
||||
.any(|param| param.default.is_some())
|
||||
|| parameters.vararg.is_some()
|
||||
|| parameters.kwarg.is_some()
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +153,14 @@ pub(crate) fn unnecessary_map(
|
||||
return;
|
||||
};
|
||||
|
||||
if !lambda_has_expected_arity(parameters.as_deref(), body) {
|
||||
if parameters.as_ref().is_some_and(|parameters| {
|
||||
late_binding(parameters, body)
|
||||
|| parameters
|
||||
.iter_non_variadic_params()
|
||||
.any(|param| param.default.is_some())
|
||||
|| parameters.vararg.is_some()
|
||||
|| parameters.kwarg.is_some()
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -191,7 +205,14 @@ pub(crate) fn unnecessary_map(
|
||||
return;
|
||||
}
|
||||
|
||||
if !lambda_has_expected_arity(parameters.as_deref(), body) {
|
||||
if parameters.as_ref().is_some_and(|parameters| {
|
||||
late_binding(parameters, body)
|
||||
|| parameters
|
||||
.iter_non_variadic_params()
|
||||
.any(|param| param.default.is_some())
|
||||
|| parameters.vararg.is_some()
|
||||
|| parameters.kwarg.is_some()
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -211,35 +232,6 @@ pub(crate) fn unnecessary_map(
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// A lambda as the first argument to `map()` has the "expected" arity when:
|
||||
///
|
||||
/// * It has exactly one parameter
|
||||
/// * That parameter is not variadic
|
||||
/// * That parameter does not have a default value
|
||||
fn lambda_has_expected_arity(parameters: Option<&Parameters>, body: &Expr) -> bool {
|
||||
let Some(parameters) = parameters else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let [parameter] = ¶meters.args[..] else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if parameter.default.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if parameters.vararg.is_some() || parameters.kwarg.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if late_binding(parameters, body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum ObjectType {
|
||||
Generator,
|
||||
|
||||
@@ -244,8 +244,6 @@ C413.py:18:1: C413 [*] Unnecessary `reversed()` call around `sorted()`
|
||||
18 |-reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
18 |+sorted([1, 2, 3], reverse=not (False or True))
|
||||
19 19 | reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
20 20 |
|
||||
21 21 | # These fixes need to be parenthesized to avoid syntax errors and behavior
|
||||
|
||||
C413.py:19:1: C413 [*] Unnecessary `reversed()` call around `sorted()`
|
||||
|
|
||||
@@ -253,8 +251,6 @@ C413.py:19:1: C413 [*] Unnecessary `reversed()` call around `sorted()`
|
||||
18 | reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
19 | reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
20 |
|
||||
21 | # These fixes need to be parenthesized to avoid syntax errors and behavior
|
||||
|
|
||||
= help: Remove unnecessary `reversed()` call
|
||||
|
||||
@@ -264,70 +260,3 @@ C413.py:19:1: C413 [*] Unnecessary `reversed()` call around `sorted()`
|
||||
18 18 | reversed(sorted([1, 2, 3], reverse=False or True))
|
||||
19 |-reversed(sorted([1, 2, 3], reverse=(False or True)))
|
||||
19 |+sorted([1, 2, 3], reverse=not (False or True))
|
||||
20 20 |
|
||||
21 21 | # These fixes need to be parenthesized to avoid syntax errors and behavior
|
||||
22 22 | # changes.
|
||||
|
||||
C413.py:24:1: C413 [*] Unnecessary `reversed()` call around `sorted()`
|
||||
|
|
||||
22 | # changes.
|
||||
23 | # See https://github.com/astral-sh/ruff/issues/15789
|
||||
24 | / reversed(sorted
|
||||
25 | | (""))
|
||||
| |_____^ C413
|
||||
26 | list(sorted
|
||||
27 | (""))
|
||||
|
|
||||
= help: Remove unnecessary `reversed()` call
|
||||
|
||||
ℹ Unsafe fix
|
||||
21 21 | # These fixes need to be parenthesized to avoid syntax errors and behavior
|
||||
22 22 | # changes.
|
||||
23 23 | # See https://github.com/astral-sh/ruff/issues/15789
|
||||
24 |-reversed(sorted
|
||||
25 |-(""))
|
||||
24 |+(sorted
|
||||
25 |+("", reverse=True))
|
||||
26 26 | list(sorted
|
||||
27 27 | (""))
|
||||
28 28 | list(sorted
|
||||
|
||||
C413.py:26:1: C413 [*] Unnecessary `list()` call around `sorted()`
|
||||
|
|
||||
24 | reversed(sorted
|
||||
25 | (""))
|
||||
26 | / list(sorted
|
||||
27 | | (""))
|
||||
| |_________^ C413
|
||||
28 | list(sorted
|
||||
29 | ("xy"))
|
||||
|
|
||||
= help: Remove unnecessary `list()` call
|
||||
|
||||
ℹ Safe fix
|
||||
23 23 | # See https://github.com/astral-sh/ruff/issues/15789
|
||||
24 24 | reversed(sorted
|
||||
25 25 | (""))
|
||||
26 |-list(sorted
|
||||
26 |+(sorted
|
||||
27 27 | (""))
|
||||
28 28 | list(sorted
|
||||
29 29 | ("xy"))
|
||||
|
||||
C413.py:28:1: C413 [*] Unnecessary `list()` call around `sorted()`
|
||||
|
|
||||
26 | list(sorted
|
||||
27 | (""))
|
||||
28 | / list(sorted
|
||||
29 | | ("xy"))
|
||||
| |_______^ C413
|
||||
|
|
||||
= help: Remove unnecessary `list()` call
|
||||
|
||||
ℹ Safe fix
|
||||
25 25 | (""))
|
||||
26 26 | list(sorted
|
||||
27 27 | (""))
|
||||
28 |-list(sorted
|
||||
28 |+(sorted
|
||||
29 29 | ("xy"))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user