Compare commits

..

2 Commits

Author SHA1 Message Date
David Peter
c366fb642a Docs 2025-01-24 21:52:06 +01:00
David Peter
a3d730bc7c [red-knot] Use T_declared | T_inferred for declared public symbols 2025-01-24 21:33:20 +01:00
153 changed files with 3109 additions and 4944 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -30,7 +30,7 @@ tracing-flamechart.svg
tracing-flamegraph.svg
# insta
*.rs.pending-snap
.rs.pending-snap
###

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.9.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

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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:?}"))

View File

@@ -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)]

View File

@@ -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

View File

@@ -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
```

View File

@@ -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]: ...

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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]
```

View File

@@ -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"

View File

@@ -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"]
```

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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() {

View File

@@ -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);
}

View File

@@ -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> {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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);

View File

@@ -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!(

View File

@@ -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

View File

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

View File

@@ -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,

View File

@@ -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)
})))
}
}

View File

@@ -1 +0,0 @@
pub(crate) mod config;

View File

@@ -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.

View File

@@ -1,5 +1,3 @@
pub(crate) mod completions;
pub(crate) mod add_noqa;
pub(crate) mod analyze_graph;
pub(crate) mod check;

View File

@@ -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`.

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

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

View File

@@ -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):

View File

@@ -1,2 +0,0 @@
import telnetlib
from telnetlib import Telnet

View File

@@ -1,2 +0,0 @@
import ftplib
from ftplib import FTP

View File

@@ -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

View File

@@ -1,3 +0,0 @@
import subprocess
from subprocess import Popen
from subprocess import Popen as pop

View File

@@ -1,4 +0,0 @@
import xml.etree.cElementTree
from xml.etree import cElementTree
import xml.etree.ElementTree
from xml.etree import ElementTree

View File

@@ -1,3 +0,0 @@
from xml import sax
import xml.sax as xmls
import xml.sax

View File

@@ -1,2 +0,0 @@
from xml.dom import expatbuilder
import xml.dom.expatbuilder

View File

@@ -1,2 +0,0 @@
from xml.dom.minidom import parseString
import xml.dom.minidom

View File

@@ -1,2 +0,0 @@
from xml.dom.pulldom import parseString
import xml.dom.pulldom

View File

@@ -1,2 +0,0 @@
import lxml
from lxml import etree

View File

@@ -1,2 +0,0 @@
import xmlrpc
from xmlrpc import server

View File

@@ -1 +0,0 @@
from twisted.web.twcgi import CGIScript

View File

@@ -1,4 +0,0 @@
import Crypto.Hash
from Crypto.Hash import MD2
import Crypto.PublicKey
from Crypto.PublicKey import RSA

View File

@@ -1,2 +0,0 @@
import pyghmi
from pyghmi import foo

View File

@@ -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()): ...

View File

@@ -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"))

View File

@@ -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)]))

View File

@@ -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}'

View File

@@ -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

View File

@@ -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 ()

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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);
}
}
}
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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

View File

@@ -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"])
})
})
}

View File

@@ -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):
|

View File

@@ -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

View File

@@ -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"))]

View File

@@ -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 {

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---

View File

@@ -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 {

View File

@@ -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
|

View File

@@ -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());
}
}
}

View File

@@ -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] = &parameters.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,

View File

@@ -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