Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
9220598fc8 [ty] Experiment: half-baked typing.TypeAlias support 2025-05-20 11:33:59 +02:00
164 changed files with 1387 additions and 4982 deletions

View File

@@ -5,4 +5,3 @@
[rules]
possibly-unresolved-reference = "warn"
unused-ignore-comment = "warn"
division-by-zero = "warn"

View File

@@ -11,7 +11,6 @@ on:
- "crates/ruff_python_parser"
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"
- "Cargo.lock"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}

View File

@@ -1,30 +1,5 @@
# Changelog
## 0.11.11
### Preview features
- \[`airflow`\] Add autofixes for `AIR302` and `AIR312` ([#17942](https://github.com/astral-sh/ruff/pull/17942))
- \[`airflow`\] Move rules from `AIR312` to `AIR302` ([#17940](https://github.com/astral-sh/ruff/pull/17940))
- \[`airflow`\] Update `AIR301` and `AIR311` with the latest Airflow implementations ([#17985](https://github.com/astral-sh/ruff/pull/17985))
- \[`flake8-simplify`\] Enable fix in preview mode (`SIM117`) ([#18208](https://github.com/astral-sh/ruff/pull/18208))
### Bug fixes
- Fix inconsistent formatting of match-case on `[]` and `_` ([#18147](https://github.com/astral-sh/ruff/pull/18147))
- \[`pylint`\] Fix `PLW1514` not recognizing the `encoding` positional argument of `codecs.open` ([#18109](https://github.com/astral-sh/ruff/pull/18109))
### CLI
- Add full option name in formatter warning ([#18217](https://github.com/astral-sh/ruff/pull/18217))
### Documentation
- Fix rendering of admonition in docs ([#18163](https://github.com/astral-sh/ruff/pull/18163))
- \[`flake8-print`\] Improve print/pprint docs for `T201` and `T203` ([#18130](https://github.com/astral-sh/ruff/pull/18130))
- \[`flake8-simplify`\] Add fix safety section (`SIM110`,`SIM210`) ([#18114](https://github.com/astral-sh/ruff/pull/18114),[#18100](https://github.com/astral-sh/ruff/pull/18100))
- \[`pylint`\] Fix docs example that produced different output (`PLW0603`) ([#18216](https://github.com/astral-sh/ruff/pull/18216))
## 0.11.10
### Preview features

14
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
version = 3
[[package]]
name = "adler2"
@@ -2485,7 +2485,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.11"
version = "0.11.10"
dependencies = [
"anyhow",
"argfile",
@@ -2725,7 +2725,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.11"
version = "0.11.10"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3061,7 +3061,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.11"
version = "0.11.10"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3180,7 +3180,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=4818b15f3b7516555d39f5a41cb75970448bee4c#4818b15f3b7516555d39f5a41cb75970448bee4c"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
dependencies = [
"boxcar",
"compact_str",
@@ -3203,12 +3203,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=4818b15f3b7516555d39f5a41cb75970448bee4c#4818b15f3b7516555d39f5a41cb75970448bee4c"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
[[package]]
name = "salsa-macros"
version = "0.21.1"
source = "git+https://github.com/salsa-rs/salsa.git?rev=4818b15f3b7516555d39f5a41cb75970448bee4c#4818b15f3b7516555d39f5a41cb75970448bee4c"
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
dependencies = [
"heck",
"proc-macro2",

View File

@@ -129,7 +129,7 @@ regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "4818b15f3b7516555d39f5a41cb75970448bee4c" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "7edce6e248f35c8114b4b021cdb474a3fb2813b3" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

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.11.11/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.11/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.11.10/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.10/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.11.11
rev: v0.11.10
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,7 +1,6 @@
doc-valid-idents = [
"..",
"CodeQL",
"CPython",
"FastAPI",
"IPython",
"LangChain",
@@ -15,7 +14,7 @@ doc-valid-idents = [
"SNMPv1",
"SNMPv2",
"SNMPv3",
"PyFlakes",
"PyFlakes"
]
ignore-interior-mutability = [

View File

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

View File

@@ -822,11 +822,11 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
rule_names.sort();
if let [rule] = rule_names.as_slice() {
warn_user_once!(
"The following rule may cause conflicts when used with the formatter: {rule}. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration."
"The following rule may cause conflicts when used with the formatter: {rule}. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration."
);
} else {
warn_user_once!(
"The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `lint.select` or `lint.extend-select` configuration, or adding them to the `lint.ignore` configuration.",
"The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.",
rule_names.join(", ")
);
}

View File

@@ -862,7 +862,7 @@ if condition:
print('Should change quotes')
----- stderr -----
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
"#);
Ok(())
}
@@ -999,7 +999,7 @@ def say_hy(name: str):
1 file reformatted
----- stderr -----
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. 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 `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`.
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"`.
@@ -1059,7 +1059,7 @@ def say_hy(name: str):
print(f"Hy {name}")
----- stderr -----
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. 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 `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"`.
@@ -1199,7 +1199,7 @@ def say_hy(name: str):
----- stderr -----
warning: `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `incorrect-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration.
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
");
Ok(())
}

View File

@@ -275,12 +275,7 @@ impl fmt::Debug for Files {
impl std::panic::RefUnwindSafe for Files {}
/// A file that's either stored on the host system's file system or in the vendored file system.
///
/// # Ordering
/// Ordering is based on the file's salsa-assigned id and not on its values.
/// The id may change between runs.
#[salsa::input]
#[derive(PartialOrd, Ord)]
pub struct File {
/// The path of the file (immutable).
#[returns(ref)]

View File

@@ -80,7 +80,6 @@ fn generate() -> String {
let mut parents = Vec::new();
output.push_str("<!-- WARNING: This file is auto-generated (cargo dev generate-all). Edit the doc comments in 'crates/ty/src/args.rs' if you want to change anything here. -->\n\n");
output.push_str("# CLI Reference\n\n");
generate_command(&mut output, &ty, &mut parents);

View File

@@ -25,10 +25,6 @@ pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
let file_name = "crates/ty/docs/configuration.md";
let markdown_path = PathBuf::from(ROOT_DIR).join(file_name);
output.push_str(
"<!-- WARNING: This file is auto-generated (cargo dev generate-all). Update the doc comments on the 'Options' struct in 'crates/ty_project/src/metadata/options.rs' if you want to change anything here. -->\n\n",
);
generate_set(
&mut output,
Set::Toplevel(Options::metadata()),

View File

@@ -56,10 +56,6 @@ fn generate_markdown() -> String {
let mut output = String::new();
let _ = writeln!(
&mut output,
"<!-- WARNING: This file is auto-generated (cargo dev generate-all). Edit the lint-declarations in 'crates/ty_python_semantic/src/types/diagnostic.rs' if you want to change anything here. -->\n"
);
let _ = writeln!(&mut output, "# Rules\n");
let mut lints: Vec<_> = registry.lints().iter().collect();

View File

@@ -9,8 +9,8 @@ use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::PythonVersion;
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{
Db, Program, ProgramSettings, PythonPath, PythonPlatform, PythonVersionSource,
PythonVersionWithSource, SearchPathSettings, default_lint_registry,
Db, Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
default_lint_registry,
};
static EMPTY_VENDORED: std::sync::LazyLock<VendoredFileSystem> = std::sync::LazyLock::new(|| {
@@ -44,10 +44,7 @@ impl ModuleDb {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource {
version: python_version,
source: PythonVersionSource::default(),
},
python_version,
python_platform: PythonPlatform::default(),
search_paths,
},

View File

@@ -19,20 +19,19 @@ impl<'a> Resolver<'a> {
pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
match import {
CollectedImport::Import(import) => {
let module = resolve_module(self.db, &import)?;
Some(module.file()?.path(self.db))
resolve_module(self.db, &import).map(|module| module.file().path(self.db))
}
CollectedImport::ImportFrom(import) => {
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
let parent = import.parent();
let module = resolve_module(self.db, &import).or_else(|| {
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
resolve_module(self.db, &import)
.map(|module| module.file().path(self.db))
.or_else(|| {
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
resolve_module(self.db, &parent?)
})?;
Some(module.file()?.path(self.db))
resolve_module(self.db, &parent?).map(|module| module.file().path(self.db))
})
}
}
}

View File

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

View File

@@ -9,11 +9,3 @@ class Foo:
yield 3
yield from 3
await f()
def _():
# Invalid yield scopes; but not outside a function
type X[T: (yield 1)] = int
type Y = (yield 2)
# Valid yield scope
yield 3

View File

@@ -83,7 +83,11 @@ pub(crate) fn bindings(checker: &Checker) {
}
}
if !checker.source_type.is_stub() && checker.enabled(Rule::UnquotedTypeAlias) {
flake8_type_checking::rules::unquoted_type_alias(checker, binding);
if let Some(diagnostics) =
flake8_type_checking::rules::unquoted_type_alias(checker, binding)
{
checker.report_diagnostics(diagnostics);
}
}
if checker.enabled(Rule::UnsortedDunderSlots) {
if let Some(diagnostic) = ruff::rules::sort_dunder_slots(checker, binding) {

View File

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

View File

@@ -385,6 +385,15 @@ impl<'a> Checker<'a> {
diagnostics.push(diagnostic);
}
/// Extend the collection of [`Diagnostic`] objects in the [`Checker`]
pub(crate) fn report_diagnostics<I>(&self, diagnostics: I)
where
I: IntoIterator<Item = Diagnostic>,
{
let mut checker_diagnostics = self.diagnostics.borrow_mut();
checker_diagnostics.extend(diagnostics);
}
/// Adds a [`TextRange`] to the set of ranges of variable names
/// flagged in `flake8-bugbear` violations so far.
///
@@ -681,17 +690,6 @@ impl SemanticSyntaxContext for Checker<'_> {
false
}
fn in_yield_allowed_context(&self) -> bool {
for scope in self.semantic.current_scopes() {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Generator { .. } => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Module | ScopeKind::Type => {}
}
}
false
}
fn in_sync_comprehension(&self) -> bool {
for scope in self.semantic.current_scopes() {
if let ScopeKind::Generator {

View File

@@ -127,10 +127,6 @@ pub(crate) const fn is_check_file_level_directives_enabled(settings: &LinterSett
}
// https://github.com/astral-sh/ruff/pull/17644
pub(crate) const fn is_readlines_in_for_fix_safe_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
pub(crate) const fn multiple_with_statements_fix_safe_enabled(settings: &LinterSettings) -> bool {
pub(crate) const fn is_readlines_in_for_fix_safe(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -190,12 +190,24 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
match qualified_name.segments() {
["airflow", .., "DAG" | "dag"] => {
// with replacement
diagnostic_for_argument(checker, arguments, "fail_stop", Some("fail_fast"));
diagnostic_for_argument(checker, arguments, "schedule_interval", Some("schedule"));
diagnostic_for_argument(checker, arguments, "timetable", Some("schedule"));
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"fail_stop",
Some("fail_fast"),
));
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"schedule_interval",
Some("schedule"),
));
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"timetable",
Some("schedule"),
));
// without replacement
diagnostic_for_argument(checker, arguments, "default_view", None);
diagnostic_for_argument(checker, arguments, "orientation", None);
checker.report_diagnostics(diagnostic_for_argument(arguments, "default_view", None));
checker.report_diagnostics(diagnostic_for_argument(arguments, "orientation", None));
}
segments => {
if is_airflow_auth_manager(segments) {
@@ -211,14 +223,17 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
));
}
} else if is_airflow_task_handler(segments) {
diagnostic_for_argument(checker, arguments, "filename_template", None);
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"filename_template",
None,
));
} else if is_airflow_builtin_or_provider(segments, "operators", "Operator") {
diagnostic_for_argument(
checker,
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"task_concurrency",
Some("max_active_tis_per_dag"),
);
));
match segments {
[
"airflow",
@@ -227,12 +242,11 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
"trigger_dagrun",
"TriggerDagRunOperator",
] => {
diagnostic_for_argument(
checker,
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"execution_date",
Some("logical_date"),
);
));
}
[
"airflow",
@@ -249,12 +263,11 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
"BranchDayOfWeekOperator",
]
| ["airflow", .., "sensors", "weekday", "DayOfWeekSensor"] => {
diagnostic_for_argument(
checker,
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
);
));
}
_ => {}
}
@@ -1044,14 +1057,11 @@ fn check_airflow_plugin_extension(
/// Check if the `deprecated` keyword argument is being used and create a diagnostic if so along
/// with a possible `replacement`.
fn diagnostic_for_argument(
checker: &Checker,
arguments: &Arguments,
deprecated: &str,
replacement: Option<&'static str>,
) {
let Some(keyword) = arguments.find_keyword(deprecated) else {
return;
};
) -> Option<Diagnostic> {
let keyword = arguments.find_keyword(deprecated)?;
let mut diagnostic = Diagnostic::new(
Airflow3Removal {
deprecated: deprecated.to_string(),
@@ -1073,7 +1083,7 @@ fn diagnostic_for_argument(
)));
}
checker.report_diagnostic(diagnostic);
Some(diagnostic)
}
/// Check whether the symbol is coming from the `secrets` builtin or provider module which ends

View File

@@ -112,14 +112,11 @@ pub(crate) fn airflow_3_0_suggested_update_expr(checker: &Checker, expr: &Expr)
/// Check if the `deprecated` keyword argument is being used and create a diagnostic if so along
/// with a possible `replacement`.
fn diagnostic_for_argument(
checker: &Checker,
arguments: &Arguments,
deprecated: &str,
replacement: Option<&'static str>,
) {
let Some(keyword) = arguments.find_keyword(deprecated) else {
return;
};
) -> Option<Diagnostic> {
let keyword = arguments.find_keyword(deprecated)?;
let mut diagnostic = Diagnostic::new(
Airflow3SuggestedUpdate {
deprecated: deprecated.to_string(),
@@ -141,7 +138,7 @@ fn diagnostic_for_argument(
)));
}
checker.report_diagnostic(diagnostic);
Some(diagnostic)
}
/// Check whether a removed Airflow argument is passed.
///
@@ -155,11 +152,15 @@ fn diagnostic_for_argument(
fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, arguments: &Arguments) {
match qualified_name.segments() {
["airflow", .., "DAG" | "dag"] => {
diagnostic_for_argument(checker, arguments, "sla_miss_callback", None);
checker.report_diagnostics(diagnostic_for_argument(
arguments,
"sla_miss_callback",
None,
));
}
segments => {
if is_airflow_builtin_or_provider(segments, "operators", "Operator") {
diagnostic_for_argument(checker, arguments, "sla", None);
checker.report_diagnostics(diagnostic_for_argument(arguments, "sla", None));
}
}
}

View File

@@ -604,9 +604,9 @@ pub(crate) fn definition(
checker: &Checker,
definition: &Definition,
visibility: visibility::Visibility,
) {
) -> Vec<Diagnostic> {
let Some(function) = definition.as_function_def() else {
return;
return vec![];
};
let ast::StmtFunctionDef {
@@ -899,10 +899,13 @@ pub(crate) fn definition(
}
}
if !checker.settings.flake8_annotations.ignore_fully_untyped {
return diagnostics;
}
// If settings say so, don't report any of the
// diagnostics gathered here if there were no type annotations at all.
if !checker.settings.flake8_annotations.ignore_fully_untyped
|| has_any_typed_arg
if has_any_typed_arg
|| has_typed_return
|| (is_method
&& !visibility::is_staticmethod(decorator_list, checker.semantic())
@@ -912,8 +915,8 @@ pub(crate) fn definition(
.or_else(|| parameters.args.first())
.is_some_and(|first_param| first_param.annotation().is_some()))
{
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
diagnostics
} else {
vec![]
}
}

View File

@@ -52,21 +52,17 @@ impl Violation for HardcodedPasswordFuncArg {
/// S106
pub(crate) fn hardcoded_password_func_arg(checker: &Checker, keywords: &[Keyword]) {
for keyword in keywords {
if string_literal(&keyword.value).is_none_or(str::is_empty) {
continue;
}
let Some(arg) = &keyword.arg else {
continue;
};
checker.report_diagnostics(keywords.iter().filter_map(|keyword| {
string_literal(&keyword.value).filter(|string| !string.is_empty())?;
let arg = keyword.arg.as_ref()?;
if !matches_password_name(arg) {
continue;
return None;
}
checker.report_diagnostic(Diagnostic::new(
Some(Diagnostic::new(
HardcodedPasswordFuncArg {
name: arg.to_string(),
},
keyword.range(),
));
}
))
}));
}

View File

@@ -76,20 +76,16 @@ pub(crate) fn compare_to_hardcoded_password_string(
left: &Expr,
comparators: &[Expr],
) {
for comp in comparators {
if string_literal(comp).is_none_or(str::is_empty) {
continue;
}
let Some(name) = password_target(left) else {
continue;
};
checker.report_diagnostic(Diagnostic::new(
checker.report_diagnostics(comparators.iter().filter_map(|comp| {
string_literal(comp).filter(|string| !string.is_empty())?;
let name = password_target(left)?;
Some(Diagnostic::new(
HardcodedPasswordString {
name: name.to_string(),
},
comp.range(),
));
}
))
}));
}
/// S105

View File

@@ -7,6 +7,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing::{
is_immutable_annotation, is_immutable_func, is_immutable_newtype_call, is_mutable_func,
};
@@ -82,15 +83,20 @@ impl Violation for FunctionCallInDefaultArgument {
}
struct ArgumentDefaultVisitor<'a, 'b> {
checker: &'a Checker<'b>,
semantic: &'a SemanticModel<'b>,
extend_immutable_calls: &'a [QualifiedName<'b>],
diagnostics: Vec<Diagnostic>,
}
impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> {
fn new(checker: &'a Checker<'b>, extend_immutable_calls: &'a [QualifiedName<'b>]) -> Self {
fn new(
semantic: &'a SemanticModel<'b>,
extend_immutable_calls: &'a [QualifiedName<'b>],
) -> Self {
Self {
checker,
semantic,
extend_immutable_calls,
diagnostics: Vec::new(),
}
}
}
@@ -99,21 +105,13 @@ impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> {
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Call(ast::ExprCall { func, .. }) => {
if !is_mutable_func(func, self.checker.semantic())
&& !is_immutable_func(
func,
self.checker.semantic(),
self.extend_immutable_calls,
)
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.checker.semantic(),
self.extend_immutable_calls,
)
is_immutable_newtype_call(name, self.semantic, self.extend_immutable_calls)
})
{
self.checker.report_diagnostic(Diagnostic::new(
self.diagnostics.push(Diagnostic::new(
FunctionCallInDefaultArgument {
name: UnqualifiedName::from_expr(func).map(|name| name.to_string()),
},
@@ -141,7 +139,7 @@ pub(crate) fn function_call_in_argument_default(checker: &Checker, parameters: &
.map(|target| QualifiedName::from_dotted_name(target))
.collect();
let mut visitor = ArgumentDefaultVisitor::new(checker, &extend_immutable_calls);
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), &extend_immutable_calls);
for parameter in parameters.iter_non_variadic_params() {
if let Some(default) = parameter.default() {
if !parameter.annotation().is_some_and(|expr| {
@@ -151,4 +149,6 @@ pub(crate) fn function_call_in_argument_default(checker: &Checker, parameters: &
}
}
}
checker.report_diagnostics(visitor.diagnostics);
}

View File

@@ -109,7 +109,5 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &Checker, expr: &'a Expr) {
}
}
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
checker.report_diagnostics(diagnostics);
}

View File

@@ -139,9 +139,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
}
// Add all diagnostics to the checker
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
checker.report_diagnostics(diagnostics);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@@ -210,23 +210,23 @@ impl Violation for PytestUnittestAssertion {
/// Visitor that tracks assert statements and checks if they reference
/// the exception name.
struct ExceptionHandlerVisitor<'a, 'b> {
struct ExceptionHandlerVisitor<'a> {
exception_name: &'a str,
current_assert: Option<&'a Stmt>,
checker: &'a Checker<'b>,
errors: Vec<Diagnostic>,
}
impl<'a, 'b> ExceptionHandlerVisitor<'a, 'b> {
const fn new(checker: &'a Checker<'b>, exception_name: &'a str) -> Self {
impl<'a> ExceptionHandlerVisitor<'a> {
const fn new(exception_name: &'a str) -> Self {
Self {
exception_name,
current_assert: None,
checker,
errors: Vec::new(),
}
}
}
impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a, '_> {
impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::Assert(_) => {
@@ -243,7 +243,7 @@ impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a, '_> {
Expr::Name(ast::ExprName { id, .. }) => {
if let Some(current_assert) = self.current_assert {
if id.as_str() == self.exception_name {
self.checker.report_diagnostic(Diagnostic::new(
self.errors.push(Diagnostic::new(
PytestAssertInExcept {
name: id.to_string(),
},
@@ -257,12 +257,13 @@ impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a, '_> {
}
}
fn check_assert_in_except(checker: &Checker, name: &str, body: &[Stmt]) {
fn check_assert_in_except(name: &str, body: &[Stmt]) -> Vec<Diagnostic> {
// Walk body to find assert statements that reference the exception name
let mut visitor = ExceptionHandlerVisitor::new(checker, name);
let mut visitor = ExceptionHandlerVisitor::new(name);
for stmt in body {
visitor.visit_stmt(stmt);
}
visitor.errors
}
/// PT009
@@ -595,13 +596,15 @@ pub(crate) fn assert_falsy(checker: &Checker, stmt: &Stmt, test: &Expr) {
/// PT017
pub(crate) fn assert_in_exception_handler(checker: &Checker, handlers: &[ExceptHandler]) {
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) =
handler;
if let Some(name) = name {
check_assert_in_except(checker, name, body);
checker.report_diagnostics(handlers.iter().flat_map(|handler| match handler {
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) => {
if let Some(name) = name {
check_assert_in_except(name, body)
} else {
Vec::new()
}
}
}
}));
}
#[derive(Copy, Clone)]

View File

@@ -9,6 +9,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::Locator;
use crate::checkers::ast::Checker;
use crate::rules::flake8_quotes;
use crate::settings::LinterSettings;
/// ## What it does
/// Checks for strings that include escaped quotes, and suggests changing
@@ -60,7 +61,11 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
return;
}
let mut rule_checker = AvoidableEscapedQuoteChecker::new(checker, checker.target_version());
let mut rule_checker = AvoidableEscapedQuoteChecker::new(
checker.locator(),
checker.settings,
checker.target_version(),
);
for part in string_like.parts() {
match part {
@@ -73,45 +78,59 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
ast::StringLikePart::FString(f_string) => rule_checker.visit_f_string(f_string),
}
}
checker.report_diagnostics(rule_checker.into_diagnostics());
}
/// Checks for `Q003` violations using the [`Visitor`] implementation.
struct AvoidableEscapedQuoteChecker<'a, 'b> {
checker: &'a Checker<'b>,
#[derive(Debug)]
struct AvoidableEscapedQuoteChecker<'a> {
locator: &'a Locator<'a>,
quotes_settings: &'a flake8_quotes::settings::Settings,
supports_pep701: bool,
diagnostics: Vec<Diagnostic>,
}
impl<'a, 'b> AvoidableEscapedQuoteChecker<'a, 'b> {
fn new(checker: &'a Checker<'b>, target_version: PythonVersion) -> Self {
impl<'a> AvoidableEscapedQuoteChecker<'a> {
fn new(
locator: &'a Locator<'a>,
settings: &'a LinterSettings,
target_version: PythonVersion,
) -> Self {
Self {
checker,
quotes_settings: &checker.settings.flake8_quotes,
locator,
quotes_settings: &settings.flake8_quotes,
supports_pep701: target_version.supports_pep_701(),
diagnostics: vec![],
}
}
/// Consumes the checker and returns a vector of [`Diagnostic`] found during the visit.
fn into_diagnostics(self) -> Vec<Diagnostic> {
self.diagnostics
}
}
impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_, '_> {
fn visit_string_literal(&mut self, string_literal: &ast::StringLiteral) {
impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_> {
fn visit_string_literal(&mut self, string_literal: &'_ ast::StringLiteral) {
if let Some(diagnostic) = check_string_or_bytes(
self.checker.locator(),
self.locator,
self.quotes_settings,
string_literal.range(),
AnyStringFlags::from(string_literal.flags),
) {
self.checker.report_diagnostic(diagnostic);
self.diagnostics.push(diagnostic);
}
}
fn visit_bytes_literal(&mut self, bytes_literal: &ast::BytesLiteral) {
fn visit_bytes_literal(&mut self, bytes_literal: &'_ ast::BytesLiteral) {
if let Some(diagnostic) = check_string_or_bytes(
self.checker.locator(),
self.locator,
self.quotes_settings,
bytes_literal.range(),
AnyStringFlags::from(bytes_literal.flags),
) {
self.checker.report_diagnostic(diagnostic);
self.diagnostics.push(diagnostic);
}
}
@@ -184,10 +203,8 @@ impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_, '_> {
.literals()
.any(|literal| contains_quote(literal, opposite_quote_char))
{
if let Some(diagnostic) =
check_f_string(self.checker.locator(), self.quotes_settings, f_string)
{
self.checker.report_diagnostic(diagnostic);
if let Some(diagnostic) = check_f_string(self.locator, self.quotes_settings, f_string) {
self.diagnostics.push(diagnostic);
}
}

View File

@@ -59,7 +59,6 @@ mod tests {
}
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))]
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -8,10 +8,10 @@ use ruff_python_ast::{self as ast, Stmt, WithItem};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange};
use super::fix_with;
use crate::checkers::ast::Checker;
use crate::fix::edits::fits;
use crate::preview::multiple_with_statements_fix_safe_enabled;
use super::fix_with;
/// ## What it does
/// Checks for the unnecessary nesting of multiple consecutive context
@@ -45,15 +45,8 @@ use crate::preview::multiple_with_statements_fix_safe_enabled;
/// pass
/// ```
///
/// # Fix safety
///
/// This fix is marked as always unsafe unless [preview] mode is enabled, in which case it is always
/// marked as safe. Note that the fix is unavailable if it would remove comments (in either case).
///
/// ## References
/// - [Python documentation: The `with` statement](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct MultipleWithStatements;
@@ -195,11 +188,7 @@ pub(crate) fn multiple_with_statements(
checker.settings.tab_size,
)
}) {
if multiple_with_statements_fix_safe_enabled(checker.settings) {
Ok(Some(Fix::safe_edit(edit)))
} else {
Ok(Some(Fix::unsafe_edit(edit)))
}
Ok(Some(Fix::unsafe_edit(edit)))
} else {
Ok(None)
}

View File

@@ -1,339 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM117.py:2:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
1 | # SIM117
2 | / with A() as a:
3 | | with B() as b:
| |__________________^ SIM117
4 | print("hello")
|
= help: Combine `with` statements
Safe fix
1 1 | # SIM117
2 |-with A() as a:
3 |- with B() as b:
4 |- print("hello")
2 |+with A() as a, B() as b:
3 |+ print("hello")
5 4 |
6 5 | # SIM117
7 6 | with A():
SIM117.py:7:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
6 | # SIM117
7 | / with A():
8 | | with B():
| |_____________^ SIM117
9 | with C():
10 | print("hello")
|
= help: Combine `with` statements
Safe fix
4 4 | print("hello")
5 5 |
6 6 | # SIM117
7 |-with A():
8 |- with B():
9 |- with C():
10 |- print("hello")
7 |+with A(), B():
8 |+ with C():
9 |+ print("hello")
11 10 |
12 11 | # SIM117
13 12 | with A() as a:
SIM117.py:13:1: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
12 | # SIM117
13 | / with A() as a:
14 | | # Unfixable due to placement of this comment.
15 | | with B() as b:
| |__________________^ SIM117
16 | print("hello")
|
= help: Combine `with` statements
SIM117.py:19:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
18 | # SIM117
19 | / with A() as a:
20 | | with B() as b:
| |__________________^ SIM117
21 | # Fixable due to placement of this comment.
22 | print("hello")
|
= help: Combine `with` statements
Safe fix
16 16 | print("hello")
17 17 |
18 18 | # SIM117
19 |-with A() as a:
20 |- with B() as b:
21 |- # Fixable due to placement of this comment.
22 |- print("hello")
19 |+with A() as a, B() as b:
20 |+ # Fixable due to placement of this comment.
21 |+ print("hello")
23 22 |
24 23 | # OK
25 24 | with A() as a:
SIM117.py:47:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
46 | # SIM117
47 | / async with A() as a:
48 | | async with B() as b:
| |________________________^ SIM117
49 | print("hello")
|
= help: Combine `with` statements
Safe fix
44 44 | print("hello")
45 45 |
46 46 | # SIM117
47 |-async with A() as a:
48 |- async with B() as b:
49 |- print("hello")
47 |+async with A() as a, B() as b:
48 |+ print("hello")
50 49 |
51 50 | while True:
52 51 | # SIM117
SIM117.py:53:5: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
51 | while True:
52 | # SIM117
53 | / with A() as a:
54 | | with B() as b:
| |______________________^ SIM117
55 | """this
56 | is valid"""
|
= help: Combine `with` statements
Safe fix
50 50 |
51 51 | while True:
52 52 | # SIM117
53 |- with A() as a:
54 |- with B() as b:
55 |- """this
53 |+ with A() as a, B() as b:
54 |+ """this
56 55 | is valid"""
57 56 |
58 |- """the indentation on
57 |+ """the indentation on
59 58 | this line is significant"""
60 59 |
61 |- "this is" \
60 |+ "this is" \
62 61 | "allowed too"
63 62 |
64 |- ("so is"
63 |+ ("so is"
65 64 | "this for some reason")
66 65 |
67 66 | # SIM117
SIM117.py:68:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
67 | # SIM117
68 | / with (
69 | | A() as a,
70 | | B() as b,
71 | | ):
72 | | with C() as c:
| |__________________^ SIM117
73 | print("hello")
|
= help: Combine `with` statements
Safe fix
67 67 | # SIM117
68 68 | with (
69 69 | A() as a,
70 |- B() as b,
70 |+ B() as b,C() as c
71 71 | ):
72 |- with C() as c:
73 |- print("hello")
72 |+ print("hello")
74 73 |
75 74 | # SIM117
76 75 | with A() as a:
SIM117.py:76:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
75 | # SIM117
76 | / with A() as a:
77 | | with (
78 | | B() as b,
79 | | C() as c,
80 | | ):
| |______^ SIM117
81 | print("hello")
|
= help: Combine `with` statements
Safe fix
73 73 | print("hello")
74 74 |
75 75 | # SIM117
76 |-with A() as a:
77 |- with (
78 |- B() as b,
79 |- C() as c,
80 |- ):
81 |- print("hello")
76 |+with (
77 |+ A() as a, B() as b,
78 |+ C() as c,
79 |+):
80 |+ print("hello")
82 81 |
83 82 | # SIM117
84 83 | with (
SIM117.py:84:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
83 | # SIM117
84 | / with (
85 | | A() as a,
86 | | B() as b,
87 | | ):
88 | | with (
89 | | C() as c,
90 | | D() as d,
91 | | ):
| |______^ SIM117
92 | print("hello")
|
= help: Combine `with` statements
Safe fix
83 83 | # SIM117
84 84 | with (
85 85 | A() as a,
86 |- B() as b,
86 |+ B() as b,C() as c,
87 |+ D() as d,
87 88 | ):
88 |- with (
89 |- C() as c,
90 |- D() as d,
91 |- ):
92 |- print("hello")
89 |+ print("hello")
93 90 |
94 91 | # SIM117 (auto-fixable)
95 92 | with A("01ß9💣28901ß9💣28901ß9💣289") as a:
SIM117.py:95:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
94 | # SIM117 (auto-fixable)
95 | / with A("01ß9💣28901ß9💣28901ß9💣289") as a:
96 | | with B("01ß9💣28901ß9💣28901ß9💣289") as b:
| |__________________________________________________^ SIM117
97 | print("hello")
|
= help: Combine `with` statements
Safe fix
92 92 | print("hello")
93 93 |
94 94 | # SIM117 (auto-fixable)
95 |-with A("01ß9💣28901ß9💣28901ß9💣289") as a:
96 |- with B("01ß9💣28901ß9💣28901ß9💣289") as b:
97 |- print("hello")
95 |+with A("01ß9💣28901ß9💣28901ß9💣289") as a, B("01ß9💣28901ß9💣28901ß9💣289") as b:
96 |+ print("hello")
98 97 |
99 98 | # SIM117 (not auto-fixable too long)
100 99 | with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
SIM117.py:100:1: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
99 | # SIM117 (not auto-fixable too long)
100 | / with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
101 | | with B("01ß9💣28901ß9💣28901ß9💣289") as b:
| |__________________________________________________^ SIM117
102 | print("hello")
|
= help: Combine `with` statements
SIM117.py:106:5: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
104 | # From issue #3025.
105 | async def main():
106 | / async with A() as a: # SIM117.
107 | | async with B() as b:
| |____________________________^ SIM117
108 | print("async-inside!")
|
= help: Combine `with` statements
SIM117.py:126:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
125 | # SIM117
126 | / with A() as a:
127 | | with B() as b:
| |__________________^ SIM117
128 | type ListOrSet[T] = list[T] | set[T]
|
= help: Combine `with` statements
Safe fix
123 123 | f(b2, c2, d2)
124 124 |
125 125 | # SIM117
126 |-with A() as a:
127 |- with B() as b:
128 |- type ListOrSet[T] = list[T] | set[T]
126 |+with A() as a, B() as b:
127 |+ type ListOrSet[T] = list[T] | set[T]
129 128 |
130 |- class ClassA[T: str]:
131 |- def method1(self) -> T:
132 |- ...
129 |+ class ClassA[T: str]:
130 |+ def method1(self) -> T:
131 |+ ...
133 132 |
134 |- f" something { my_dict["key"] } something else "
133 |+ f" something { my_dict["key"] } something else "
135 134 |
136 |- f"foo {f"bar {x}"} baz"
135 |+ f"foo {f"bar {x}"} baz"
137 136 |
138 137 | # Allow cascading for some statements.
139 138 | import anyio
SIM117.py:163:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
162 | # Do not suppress combination, if a context manager is already combined with another.
163 | / async with asyncio.timeout(1), A():
164 | | async with B():
| |___________________^ SIM117
165 | pass
|
= help: Combine `with` statements
Safe fix
160 160 | pass
161 161 |
162 162 | # Do not suppress combination, if a context manager is already combined with another.
163 |-async with asyncio.timeout(1), A():
164 |- async with B():
165 |- pass
163 |+async with asyncio.timeout(1), A(), B():
164 |+ pass

View File

@@ -142,26 +142,26 @@ impl AlwaysFixableViolation for QuotedTypeAlias {
}
/// TC007
pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) {
pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) -> Option<Vec<Diagnostic>> {
if binding.context.is_typing() {
return;
return None;
}
if !binding.is_annotated_type_alias() {
return;
return None;
}
let Some(Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(expr), ..
})) = binding.statement(checker.semantic())
else {
return;
return None;
};
let mut names = Vec::new();
collect_typing_references(checker, expr, &mut names);
if names.is_empty() {
return;
return None;
}
// We generate a diagnostic for every name that needs to be quoted
@@ -178,13 +178,14 @@ pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) {
checker.locator(),
checker.default_string_flags(),
);
let mut diagnostics = Vec::with_capacity(names.len());
for name in names {
checker.report_diagnostic(
Diagnostic::new(UnquotedTypeAlias, name.range())
.with_parent(parent)
.with_fix(Fix::unsafe_edit(edit.clone())),
);
let mut diagnostic = Diagnostic::new(UnquotedTypeAlias, name.range());
diagnostic.set_parent(parent);
diagnostic.set_fix(Fix::unsafe_edit(edit.clone()));
diagnostics.push(diagnostic);
}
Some(diagnostics)
}
/// Traverses the type expression and collects `[Expr::Name]` nodes that are

View File

@@ -304,21 +304,19 @@ fn call<'a>(
) {
let semantic = checker.semantic();
let dummy_variable_rgx = &checker.settings.dummy_variable_rgx;
for arg in parameters {
let Some(binding) = scope
checker.report_diagnostics(parameters.filter_map(|arg| {
let binding = scope
.get(arg.name())
.map(|binding_id| semantic.binding(binding_id))
else {
continue;
};
.map(|binding_id| semantic.binding(binding_id))?;
if binding.kind.is_argument()
&& binding.is_unused()
&& !dummy_variable_rgx.is_match(arg.name())
{
checker
.report_diagnostic(argumentable.check_for(arg.name.to_string(), binding.range()));
Some(argumentable.check_for(arg.name.to_string(), binding.range()))
} else {
None
}
}
}));
}
/// Returns `true` if a function appears to be a base class stub. In other

View File

@@ -431,7 +431,5 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
}
}
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
checker.report_diagnostics(diagnostics);
}

View File

@@ -873,6 +873,8 @@ pub(crate) fn check_docstring(
section_contexts: &SectionContexts,
convention: Option<Convention>,
) {
let mut diagnostics = Vec::new();
// Only check function docstrings.
let Some(function_def) = definition.as_function_def() else {
return;
@@ -925,7 +927,7 @@ pub(crate) fn check_docstring(
semantic,
)
{
checker.report_diagnostic(Diagnostic::new(
diagnostics.push(Diagnostic::new(
DocstringMissingReturns,
docstring.range(),
));
@@ -936,10 +938,8 @@ pub(crate) fn check_docstring(
.iter()
.any(|entry| !entry.is_none_return()) =>
{
checker.report_diagnostic(Diagnostic::new(
DocstringMissingReturns,
docstring.range(),
));
diagnostics
.push(Diagnostic::new(DocstringMissingReturns, docstring.range()));
}
_ => {}
}
@@ -958,16 +958,12 @@ pub(crate) fn check_docstring(
|arguments| arguments.first().is_none_or(Expr::is_none_literal_expr),
) =>
{
checker.report_diagnostic(Diagnostic::new(
DocstringMissingYields,
docstring.range(),
));
diagnostics
.push(Diagnostic::new(DocstringMissingYields, docstring.range()));
}
None if body_entries.yields.iter().any(|entry| !entry.is_none_yield) => {
checker.report_diagnostic(Diagnostic::new(
DocstringMissingYields,
docstring.range(),
));
diagnostics
.push(Diagnostic::new(DocstringMissingYields, docstring.range()));
}
_ => {}
}
@@ -1000,7 +996,7 @@ pub(crate) fn check_docstring(
},
docstring.range(),
);
checker.report_diagnostic(diagnostic);
diagnostics.push(diagnostic);
}
}
}
@@ -1015,7 +1011,7 @@ pub(crate) fn check_docstring(
|| body_entries.returns.iter().all(ReturnEntry::is_implicit)
{
let diagnostic = Diagnostic::new(DocstringExtraneousReturns, docstring.range());
checker.report_diagnostic(diagnostic);
diagnostics.push(diagnostic);
}
}
}
@@ -1025,7 +1021,7 @@ pub(crate) fn check_docstring(
if docstring_sections.yields.is_some() {
if body_entries.yields.is_empty() {
let diagnostic = Diagnostic::new(DocstringExtraneousYields, docstring.range());
checker.report_diagnostic(diagnostic);
diagnostics.push(diagnostic);
}
}
}
@@ -1051,9 +1047,11 @@ pub(crate) fn check_docstring(
},
docstring.range(),
);
checker.report_diagnostic(diagnostic);
diagnostics.push(diagnostic);
}
}
}
}
checker.report_diagnostics(diagnostics);
}

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F704.py:6:5: F704 `yield` statement outside of a function
|
@@ -30,6 +31,4 @@ F704.py:11:1: F704 `await` statement outside of a function
10 | yield from 3
11 | await f()
| ^^^^^^^^^ F704
12 |
13 | def _():
|

View File

@@ -158,9 +158,7 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB
Some(diagnostic)
});
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
checker.report_diagnostics(diagnostics);
}
/// Checks whether two compare expressions are simplifiable

View File

@@ -31,9 +31,8 @@ use crate::checkers::ast::Checker;
///
///
/// def foo():
/// var = 10
/// print(var)
/// return var
/// return 10
///
///
/// var = foo()

View File

@@ -379,13 +379,15 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) {
_ => panic!("redefined_loop_name called on Statement that is not a `With` or `For`"),
};
let mut diagnostics = Vec::new();
for outer_assignment_target in &outer_assignment_targets {
for inner_assignment_target in &inner_assignment_targets {
// Compare the targets structurally.
if ComparableExpr::from(outer_assignment_target.expr)
.eq(&(ComparableExpr::from(inner_assignment_target.expr)))
{
checker.report_diagnostic(Diagnostic::new(
diagnostics.push(Diagnostic::new(
RedefinedLoopName {
name: checker.generator().expr(outer_assignment_target.expr),
outer_kind: outer_assignment_target.binding_kind,
@@ -396,4 +398,6 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) {
}
}
}
checker.report_diagnostics(diagnostics);
}

View File

@@ -1,6 +1,6 @@
use std::hash::Hash;
use ruff_python_semantic::analyze::class::iter_super_class;
use ruff_python_semantic::{SemanticModel, analyze::class::iter_super_class};
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
@@ -66,9 +66,11 @@ pub(crate) fn redefined_slots_in_subclass(checker: &Checker, class_def: &ast::St
return;
}
for slot in class_slots {
check_super_slots(checker, class_def, &slot);
}
let semantic = checker.semantic();
let diagnostics = class_slots
.iter()
.filter_map(|slot| check_super_slots(class_def, semantic, slot));
checker.report_diagnostics(diagnostics);
}
#[derive(Clone, Debug)]
@@ -101,18 +103,25 @@ impl Ranged for Slot<'_> {
}
}
fn check_super_slots(checker: &Checker, class_def: &ast::StmtClassDef, slot: &Slot) {
for super_class in iter_super_class(class_def, checker.semantic()).skip(1) {
if slots_members(&super_class.body).contains(slot) {
checker.report_diagnostic(Diagnostic::new(
RedefinedSlotsInSubclass {
base: super_class.name.to_string(),
slot_name: slot.name.to_string(),
},
slot.range(),
));
}
}
fn check_super_slots(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
slot: &Slot,
) -> Option<Diagnostic> {
iter_super_class(class_def, semantic)
.skip(1)
.find_map(&|super_class: &ast::StmtClassDef| {
if slots_members(&super_class.body).contains(slot) {
return Some(Diagnostic::new(
RedefinedSlotsInSubclass {
base: super_class.name.to_string(),
slot_name: slot.name.to_string(),
},
slot.range(),
));
}
None
})
}
fn slots_members(body: &[Stmt]) -> FxHashSet<Slot> {

View File

@@ -43,6 +43,7 @@ pub(crate) fn self_assignment(checker: &Checker, assign: &ast::StmtAssign) {
if checker.semantic().current_scope().kind.is_class() {
return;
}
let mut diagnostics = Vec::new();
for (left, right) in assign
.targets
@@ -50,8 +51,9 @@ pub(crate) fn self_assignment(checker: &Checker, assign: &ast::StmtAssign) {
.chain(std::iter::once(assign.value.as_ref()))
.tuple_combinations()
{
visit_assignments(checker, left, right);
visit_assignments(left, right, &mut diagnostics);
}
checker.report_diagnostics(diagnostics);
}
/// PLW0127
@@ -65,21 +67,23 @@ pub(crate) fn self_annotated_assignment(checker: &Checker, assign: &ast::StmtAnn
if checker.semantic().current_scope().kind.is_class() {
return;
}
let mut diagnostics = Vec::new();
visit_assignments(checker, &assign.target, value);
visit_assignments(&assign.target, value, &mut diagnostics);
checker.report_diagnostics(diagnostics);
}
fn visit_assignments(checker: &Checker, left: &Expr, right: &Expr) {
fn visit_assignments(left: &Expr, right: &Expr, diagnostics: &mut Vec<Diagnostic>) {
match (left, right) {
(Expr::Tuple(lhs), Expr::Tuple(rhs)) if lhs.len() == rhs.len() => lhs
.iter()
.zip(rhs)
.for_each(|(lhs_elem, rhs_elem)| visit_assignments(checker, lhs_elem, rhs_elem)),
.for_each(|(lhs_elem, rhs_elem)| visit_assignments(lhs_elem, rhs_elem, diagnostics)),
(
Expr::Name(ast::ExprName { id: lhs_name, .. }),
Expr::Name(ast::ExprName { id: rhs_name, .. }),
) if lhs_name == rhs_name => {
checker.report_diagnostic(Diagnostic::new(
diagnostics.push(Diagnostic::new(
SelfAssigningVariable {
name: lhs_name.to_string(),
},

View File

@@ -64,26 +64,49 @@ pub(crate) fn read_whole_file(checker: &Checker, with: &ast::StmtWith) {
}
// Then we need to match each `open` operation with exactly one `read` call.
let mut matcher = ReadMatcher::new(checker, candidates);
visitor::walk_body(&mut matcher, &with.body);
let matches = {
let mut matcher = ReadMatcher::new(candidates);
visitor::walk_body(&mut matcher, &with.body);
matcher.into_matches()
};
// All the matched operations should be reported.
let diagnostics: Vec<Diagnostic> = matches
.iter()
.map(|open| {
Diagnostic::new(
ReadWholeFile {
filename: SourceCodeSnippet::from_str(&checker.generator().expr(open.filename)),
suggestion: make_suggestion(open, checker.generator()),
},
open.item.range(),
)
})
.collect();
checker.report_diagnostics(diagnostics);
}
/// AST visitor that matches `open` operations with the corresponding `read` calls.
struct ReadMatcher<'a, 'b> {
checker: &'a Checker<'b>,
#[derive(Debug)]
struct ReadMatcher<'a> {
candidates: Vec<FileOpen<'a>>,
matches: Vec<FileOpen<'a>>,
}
impl<'a, 'b> ReadMatcher<'a, 'b> {
fn new(checker: &'a Checker<'b>, candidates: Vec<FileOpen<'a>>) -> Self {
impl<'a> ReadMatcher<'a> {
fn new(candidates: Vec<FileOpen<'a>>) -> Self {
Self {
checker,
candidates,
matches: vec![],
}
}
fn into_matches(self) -> Vec<FileOpen<'a>> {
self.matches
}
}
impl<'a> Visitor<'a> for ReadMatcher<'a, '_> {
impl<'a> Visitor<'a> for ReadMatcher<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
if let Some(read_from) = match_read_call(expr) {
if let Some(open) = self
@@ -91,16 +114,7 @@ impl<'a> Visitor<'a> for ReadMatcher<'a, '_> {
.iter()
.position(|open| open.is_ref(read_from))
{
let open = self.candidates.remove(open);
self.checker.report_diagnostic(Diagnostic::new(
ReadWholeFile {
filename: SourceCodeSnippet::from_str(
&self.checker.generator().expr(open.filename),
),
suggestion: make_suggestion(&open, self.checker.generator()),
},
open.item.range(),
));
self.matches.push(self.candidates.remove(open));
}
return;
}

View File

@@ -1,4 +1,4 @@
use crate::preview::is_readlines_in_for_fix_safe_enabled;
use crate::preview::is_readlines_in_for_fix_safe;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{Comprehension, Expr, StmtFor};
@@ -86,7 +86,7 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
}
let mut diagnostic = Diagnostic::new(ReadlinesInFor, expr_call.range());
diagnostic.set_fix(if is_readlines_in_for_fix_safe_enabled(checker.settings) {
diagnostic.set_fix(if is_readlines_in_for_fix_safe(checker.settings) {
Fix::safe_edit(Edit::range_deletion(
expr_call.range().add_start(expr_attr.value.range().len()),
))

View File

@@ -86,42 +86,48 @@ pub(crate) fn repeated_append(checker: &Checker, stmt: &Stmt) {
return;
}
for group in group_appends(appends) {
// Groups with just one element are fine, and shouldn't be replaced by `extend`.
if group.appends.len() <= 1 {
continue;
}
// group borrows from checker, so we can't directly push into checker.diagnostics
let diagnostics: Vec<Diagnostic> = group_appends(appends)
.iter()
.filter_map(|group| {
// Groups with just one element are fine, and shouldn't be replaced by `extend`.
if group.appends.len() <= 1 {
return None;
}
let replacement = make_suggestion(&group, checker.generator());
let replacement = make_suggestion(group, checker.generator());
let mut diagnostic = Diagnostic::new(
RepeatedAppend {
name: group.name().to_string(),
replacement: SourceCodeSnippet::new(replacement.clone()),
},
group.range(),
);
let mut diagnostic = Diagnostic::new(
RepeatedAppend {
name: group.name().to_string(),
replacement: SourceCodeSnippet::new(replacement.clone()),
},
group.range(),
);
// We only suggest a fix when all appends in a group are clumped together. If they're
// non-consecutive, fixing them is much more difficult.
//
// Avoid fixing if there are comments in between the appends:
//
// ```python
// a.append(1)
// # comment
// a.append(2)
// ```
if group.is_consecutive && !checker.comment_ranges().intersects(group.range()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::replacement(
replacement,
group.start(),
group.end(),
)));
}
// We only suggest a fix when all appends in a group are clumped together. If they're
// non-consecutive, fixing them is much more difficult.
//
// Avoid fixing if there are comments in between the appends:
//
// ```python
// a.append(1)
// # comment
// a.append(2)
// ```
if group.is_consecutive && !checker.comment_ranges().intersects(group.range()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::replacement(
replacement,
group.start(),
group.end(),
)));
}
checker.report_diagnostic(diagnostic);
}
Some(diagnostic)
})
.collect();
checker.report_diagnostics(diagnostics);
}
#[derive(Debug, Clone)]

View File

@@ -65,28 +65,54 @@ pub(crate) fn write_whole_file(checker: &Checker, with: &ast::StmtWith) {
}
// Then we need to match each `open` operation with exactly one `write` call.
let mut matcher = WriteMatcher::new(checker, candidates);
visitor::walk_body(&mut matcher, &with.body);
let (matches, contents) = {
let mut matcher = WriteMatcher::new(candidates);
visitor::walk_body(&mut matcher, &with.body);
matcher.finish()
};
// All the matched operations should be reported.
let diagnostics: Vec<Diagnostic> = matches
.iter()
.zip(contents)
.map(|(open, content)| {
Diagnostic::new(
WriteWholeFile {
filename: SourceCodeSnippet::from_str(&checker.generator().expr(open.filename)),
suggestion: make_suggestion(open, content, checker.generator()),
},
open.item.range(),
)
})
.collect();
checker.report_diagnostics(diagnostics);
}
/// AST visitor that matches `open` operations with the corresponding `write` calls.
struct WriteMatcher<'a, 'b> {
checker: &'a Checker<'b>,
#[derive(Debug)]
struct WriteMatcher<'a> {
candidates: Vec<FileOpen<'a>>,
matches: Vec<FileOpen<'a>>,
contents: Vec<&'a Expr>,
loop_counter: u32,
}
impl<'a, 'b> WriteMatcher<'a, 'b> {
fn new(checker: &'a Checker<'b>, candidates: Vec<FileOpen<'a>>) -> Self {
impl<'a> WriteMatcher<'a> {
fn new(candidates: Vec<FileOpen<'a>>) -> Self {
Self {
checker,
candidates,
matches: vec![],
contents: vec![],
loop_counter: 0,
}
}
fn finish(self) -> (Vec<FileOpen<'a>>, Vec<&'a Expr>) {
(self.matches, self.contents)
}
}
impl<'a> Visitor<'a> for WriteMatcher<'a, '_> {
impl<'a> Visitor<'a> for WriteMatcher<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
if matches!(stmt, ast::Stmt::While(_) | ast::Stmt::For(_)) {
self.loop_counter += 1;
@@ -105,16 +131,8 @@ impl<'a> Visitor<'a> for WriteMatcher<'a, '_> {
.position(|open| open.is_ref(write_to))
{
if self.loop_counter == 0 {
let open = self.candidates.remove(open);
self.checker.report_diagnostic(Diagnostic::new(
WriteWholeFile {
filename: SourceCodeSnippet::from_str(
&self.checker.generator().expr(open.filename),
),
suggestion: make_suggestion(&open, content, self.checker.generator()),
},
open.item.range(),
));
self.matches.push(self.candidates.remove(open));
self.contents.push(content);
} else {
self.candidates.remove(open);
}

View File

@@ -211,9 +211,7 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
context,
checker.settings,
);
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
checker.report_diagnostics(diagnostics);
}
ast::StringLikePart::Bytes(_) => {}
ast::StringLikePart::FString(f_string) => {
@@ -227,9 +225,7 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
context,
checker.settings,
);
for diagnostic in diagnostics {
checker.report_diagnostic(diagnostic);
}
checker.report_diagnostics(diagnostics);
}
}
}

View File

@@ -108,6 +108,7 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
}
let mut stopped_fixes = false;
let mut diagnostics = vec![];
for parameter in function_def.parameters.iter_non_variadic_params() {
let Some(default) = parameter.default() else {
@@ -131,8 +132,10 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
stopped_fixes |= diagnostic.fix.is_none();
}
checker.report_diagnostic(diagnostic);
diagnostics.push(diagnostic);
}
checker.report_diagnostics(diagnostics);
}
/// Generate a [`Fix`] to transform a `__post_init__` default argument into a

View File

@@ -40,30 +40,33 @@ impl Violation for UselessTryExcept {
/// TRY203 (previously TRY302)
pub(crate) fn useless_try_except(checker: &Checker, handlers: &[ExceptHandler]) {
if handlers.iter().all(|handler| {
let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) = handler;
let Some(Stmt::Raise(ast::StmtRaise {
exc, cause: None, ..
})) = &body.first()
else {
return false;
};
if let Some(expr) = exc {
// E.g., `except ... as e: raise e`
if let Expr::Name(ast::ExprName { id, .. }) = expr.as_ref() {
if name.as_ref().is_some_and(|name| name.as_str() == id) {
return true;
if let Some(diagnostics) = handlers
.iter()
.map(|handler| {
let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) =
handler;
let Some(Stmt::Raise(ast::StmtRaise {
exc, cause: None, ..
})) = &body.first()
else {
return None;
};
if let Some(expr) = exc {
// E.g., `except ... as e: raise e`
if let Expr::Name(ast::ExprName { id, .. }) = expr.as_ref() {
if name.as_ref().is_some_and(|name| name.as_str() == id) {
return Some(Diagnostic::new(UselessTryExcept, handler.range()));
}
}
None
} else {
// E.g., `except ...: raise`
Some(Diagnostic::new(UselessTryExcept, handler.range()))
}
false
} else {
// E.g., `except ...: raise`
true
}
}) {
})
.collect::<Option<Vec<_>>>()
{
// Require that all handlers are useless, but create one diagnostic per handler.
for handler in handlers {
checker.report_diagnostic(Diagnostic::new(UselessTryExcept, handler.range()));
}
checker.report_diagnostics(diagnostics);
}
}

View File

@@ -288,13 +288,5 @@ match x:
]:
pass
match a, b:
case [], []:
...
case [], _:
...
case _, []:
...
case _, _:
...

View File

@@ -1,8 +0,0 @@
# Ruff in some cases added brackets around tuples in some cases; deviating from Black.
# Ensure we don't revert already-applied formats with the fix.
# See https://github.com/astral-sh/ruff/pull/18147
match a, b:
case [[], []]:
...
case [[], _]:
...

View File

@@ -79,26 +79,9 @@ pub(crate) enum SequenceType {
impl SequenceType {
pub(crate) fn from_pattern(pattern: &PatternMatchSequence, source: &str) -> SequenceType {
let before_first_pattern = &source[TextRange::new(
pattern.start(),
pattern
.patterns
.first()
.map(Ranged::start)
.unwrap_or(pattern.end()),
)];
let after_last_patttern = &source[TextRange::new(
pattern.start(),
pattern
.patterns
.first()
.map(Ranged::end)
.unwrap_or(pattern.end()),
)];
if before_first_pattern.starts_with('[') && !after_last_patttern.ends_with(',') {
if source[pattern.range()].starts_with('[') {
SequenceType::List
} else if before_first_pattern.starts_with('(') {
} else if source[pattern.range()].starts_with('(') {
// If the pattern is empty, it must be a parenthesized tuple with no members. (This
// branch exists to differentiate between a tuple with and without its own parentheses,
// but a tuple without its own parentheses must have at least one member.)

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/pattern_matching_trailing_comma.py
snapshot_kind: text
---
## Input
@@ -26,7 +27,7 @@ match more := (than, one), indeed,:
```diff
--- Black
+++ Ruff
@@ -8,7 +8,10 @@
@@ -8,13 +8,16 @@
pass
@@ -37,7 +38,15 @@ match more := (than, one), indeed,:
+):
case _, (5, 6):
pass
case (
- case (
+ case [
[[5], (6)],
[7],
- ):
+ ]:
pass
case _:
pass
```
## Ruff Output
@@ -59,10 +68,10 @@ match (
):
case _, (5, 6):
pass
case (
case [
[[5], (6)],
[7],
):
]:
pass
case _:
pass

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py
snapshot_kind: text
---
## Input
```python
@@ -294,15 +295,7 @@ match x:
]:
pass
match a, b:
case [], []:
...
case [], _:
...
case _, []:
...
case _, _:
...
```
@@ -599,14 +592,4 @@ match x:
ccccccccccccccccccccccccccccccccc,
]:
pass
match a, b:
case [], []:
...
case [], _:
...
case _, []:
...
case _, _:
...
```

View File

@@ -1,27 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern_match_regression_brackets.py
---
## Input
```python
# Ruff in some cases added brackets around tuples in some cases; deviating from Black.
# Ensure we don't revert already-applied formats with the fix.
# See https://github.com/astral-sh/ruff/pull/18147
match a, b:
case [[], []]:
...
case [[], _]:
...
```
## Output
```python
# Ruff in some cases added brackets around tuples in some cases; deviating from Black.
# Ensure we don't revert already-applied formats with the fix.
# See https://github.com/astral-sh/ruff/pull/18147
match a, b:
case [[], []]:
...
case [[], _]:
...
```

View File

@@ -769,21 +769,16 @@ impl SemanticSyntaxChecker {
// We are intentionally not inspecting the async status of the scope for now to mimic F704.
// await-outside-async is PLE1142 instead, so we'll end up emitting both syntax errors for
// cases that trigger F704
if ctx.in_function_scope() {
return;
}
if kind.is_await() {
if ctx.in_await_allowed_context() {
return;
}
// `await` is allowed at the top level of a Jupyter notebook.
// See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html.
if ctx.in_module_scope() && ctx.in_notebook() {
return;
}
if ctx.in_await_allowed_context() {
return;
}
} else if ctx.in_yield_allowed_context() {
} else if ctx.in_function_scope() {
return;
}
@@ -1724,35 +1719,6 @@ pub trait SemanticSyntaxContext {
/// See the trait-level documentation for more details.
fn in_await_allowed_context(&self) -> bool;
/// Returns `true` if the visitor is currently in a context where `yield` and `yield from`
/// expressions are allowed.
///
/// Yield expressions are allowed only in:
/// 1. Function definitions
/// 2. Lambda expressions
///
/// Unlike `await`, yield is not allowed in:
/// - Comprehensions (list, set, dict)
/// - Generator expressions
/// - Class definitions
///
/// This method should traverse parent scopes to check if the closest relevant scope
/// is a function or lambda, and that no disallowed context (class, comprehension, generator)
/// intervenes. For example:
///
/// ```python
/// def f():
/// yield 1 # okay, in a function
/// lambda: (yield 1) # okay, in a lambda
///
/// [(yield 1) for x in range(3)] # error, in a comprehension
/// ((yield 1) for x in range(3)) # error, in a generator expression
/// class C:
/// yield 1 # error, in a class within a function
/// ```
///
fn in_yield_allowed_context(&self) -> bool;
/// Returns `true` if the visitor is currently inside of a synchronous comprehension.
///
/// This method is necessary because `in_async_context` only checks for the nearest, enclosing

View File

@@ -556,10 +556,6 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
true
}
fn in_yield_allowed_context(&self) -> bool {
true
}
fn in_generator_scope(&self) -> bool {
true
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.11.11"
version = "0.11.10"
publish = false
authors = { workspace = true }
edition = { workspace = true }

4
crates/ty/docs/cli.md generated
View File

@@ -1,5 +1,3 @@
<!-- WARNING: This file is auto-generated (cargo dev generate-all). Edit the doc comments in 'crates/ty/src/args.rs' if you want to change anything here. -->
# CLI Reference
## ty
@@ -53,7 +51,7 @@ ty check [OPTIONS] [PATH]...
</dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p>
<p>Possible values:</p>
<ul>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints [default]</li>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints</li>
<li><code>concise</code>: Print diagnostics concisely, one per line</li>
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>

View File

@@ -1,5 +1,3 @@
<!-- WARNING: This file is auto-generated (cargo dev generate-all). Update the doc comments on the 'Options' struct in 'crates/ty_project/src/metadata/options.rs' if you want to change anything here. -->
# Configuration
#### `respect-ignore-files`

250
crates/ty/docs/rules.md generated
View File

@@ -1,5 +1,3 @@
<!-- WARNING: This file is auto-generated (cargo dev generate-all). Edit the lint-declarations in 'crates/ty_python_semantic/src/types/diagnostic.rs' if you want to change anything here. -->
# Rules
## `byte-string-type-annotation`
@@ -52,7 +50,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L91)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L87)
</details>
## `conflicting-argument-forms`
@@ -83,7 +81,7 @@ f(int) # error
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L135)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L118)
</details>
## `conflicting-declarations`
@@ -113,7 +111,7 @@ a = 1
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L161)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144)
</details>
## `conflicting-metaclass`
@@ -144,7 +142,7 @@ class C(A, B): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L186)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
</details>
## `cyclic-class-definition`
@@ -175,7 +173,30 @@ class B(A): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L212)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L195)
</details>
## `division-by-zero`
**Default level**: error
<details>
<summary>detects division by zero</summary>
### What it does
It detects division by zero.
### Why is this bad?
Dividing by zero raises a `ZeroDivisionError` at runtime.
### Examples
```python
5 / 0
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221)
</details>
## `duplicate-base`
@@ -201,7 +222,7 @@ class B(A, A): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L239)
</details>
## `escape-character-in-forward-annotation`
@@ -338,7 +359,7 @@ TypeError: multiple bases have instance lay-out conflict
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L277)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260)
</details>
## `inconsistent-mro`
@@ -367,7 +388,7 @@ class C(A, B): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L363)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L346)
</details>
## `index-out-of-bounds`
@@ -392,7 +413,7 @@ t[3] # IndexError: tuple index out of range
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L387)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L370)
</details>
## `invalid-argument-type`
@@ -418,7 +439,7 @@ func("foo") # error: [invalid-argument-type]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L407)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L390)
</details>
## `invalid-assignment`
@@ -445,7 +466,7 @@ a: int = ''
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L447)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430)
</details>
## `invalid-attribute-access`
@@ -478,7 +499,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1395)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1336)
</details>
## `invalid-base`
@@ -486,22 +507,13 @@ C.instance_var = 3 # error: Cannot assign to instance variable
**Default level**: error
<details>
<summary>detects class bases that will cause the class definition to raise an exception at runtime</summary>
<summary>detects invalid bases in class definitions</summary>
### What it does
Checks for class definitions that have bases which are not instances of `type`.
### Why is this bad?
Class definitions with bases like this will lead to `TypeError` being raised at runtime.
### Examples
```python
class A(42): ... # error: [invalid-base]
```
TODO #14889
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L469)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L452)
</details>
## `invalid-context-manager`
@@ -527,7 +539,7 @@ with 1:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L520)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L461)
</details>
## `invalid-declaration`
@@ -555,7 +567,7 @@ a: str
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L482)
</details>
## `invalid-exception-caught`
@@ -596,7 +608,7 @@ except ZeroDivisionError:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L564)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L505)
</details>
## `invalid-generic-class`
@@ -627,7 +639,7 @@ class C[U](Generic[T]): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L600)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541)
</details>
## `invalid-legacy-type-variable`
@@ -660,7 +672,7 @@ def f(t: TypeVar("U")): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567)
</details>
## `invalid-metaclass`
@@ -692,7 +704,7 @@ class B(metaclass=f): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L675)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L616)
</details>
## `invalid-overload`
@@ -740,7 +752,7 @@ def foo(x: int) -> int: ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L643)
</details>
## `invalid-parameter-default`
@@ -765,7 +777,7 @@ def f(a: int = ''): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L745)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L686)
</details>
## `invalid-protocol`
@@ -798,7 +810,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L335)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L318)
</details>
## `invalid-raise`
@@ -846,7 +858,7 @@ def g():
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L706)
</details>
## `invalid-return-type`
@@ -870,7 +882,7 @@ def func() -> int:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L428)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L411)
</details>
## `invalid-super-argument`
@@ -914,7 +926,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)`
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L808)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L749)
</details>
## `invalid-syntax-in-forward-annotation`
@@ -954,7 +966,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L595)
</details>
## `invalid-type-checking-constant`
@@ -983,7 +995,7 @@ TYPE_CHECKING = ''
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L847)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788)
</details>
## `invalid-type-form`
@@ -1012,7 +1024,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L871)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L812)
</details>
## `invalid-type-variable-constraints`
@@ -1046,7 +1058,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L895)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L836)
</details>
## `missing-argument`
@@ -1070,7 +1082,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L924)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865)
</details>
## `no-matching-overload`
@@ -1098,7 +1110,7 @@ func("string") # error: [no-matching-overload]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L943)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L884)
</details>
## `non-subscriptable`
@@ -1121,7 +1133,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L966)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L907)
</details>
## `not-iterable`
@@ -1146,7 +1158,7 @@ for i in 34: # TypeError: 'int' object is not iterable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L984)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L925)
</details>
## `parameter-already-assigned`
@@ -1172,7 +1184,7 @@ f(1, x=2) # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1035)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L976)
</details>
## `raw-string-type-annotation`
@@ -1231,7 +1243,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
</details>
## `subclass-of-final-class`
@@ -1259,7 +1271,7 @@ class B(A): ... # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1126)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1067)
</details>
## `too-many-positional-arguments`
@@ -1285,7 +1297,7 @@ f("foo") # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1171)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1112)
</details>
## `type-assertion-failure`
@@ -1312,7 +1324,7 @@ def _(x: int):
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1149)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1090)
</details>
## `unavailable-implicit-super-arguments`
@@ -1356,7 +1368,7 @@ class A:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1192)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1133)
</details>
## `unknown-argument`
@@ -1382,7 +1394,7 @@ f(x=1, y=2) # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1249)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1190)
</details>
## `unresolved-attribute`
@@ -1409,7 +1421,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1270)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1211)
</details>
## `unresolved-import`
@@ -1433,7 +1445,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1292)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1233)
</details>
## `unresolved-reference`
@@ -1457,7 +1469,7 @@ print(x) # NameError: name 'x' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1311)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1252)
</details>
## `unsupported-bool-conversion`
@@ -1493,7 +1505,7 @@ b1 < b2 < b1 # exception raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1004)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L945)
</details>
## `unsupported-operator`
@@ -1520,7 +1532,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1330)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271)
</details>
## `zero-stepsize-in-slice`
@@ -1544,7 +1556,25 @@ l[1:10:0] # ValueError: slice step cannot be zero
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1352)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1293)
</details>
## `call-possibly-unbound-method`
**Default level**: warn
<details>
<summary>detects calls to possibly unbound methods</summary>
### What it does
Checks for calls to possibly unbound methods.
### Why is this bad?
Calling an unbound method will raise an `AttributeError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-possibly-unbound-method)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L105)
</details>
## `invalid-ignore-comment`
@@ -1600,38 +1630,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1056)
</details>
## `possibly-unbound-implicit-call`
**Default level**: warn
<details>
<summary>detects implicit calls to possibly unbound methods</summary>
### What it does
Checks for implicit calls to possibly unbound methods.
### Why is this bad?
Expressions such as `x[y]` and `x * y` call methods
under the hood (`__getitem__` and `__mul__` respectively).
Calling an unbound method will raise an `AttributeError` at runtime.
### Examples
```python
import datetime
class A:
if datetime.date.today().weekday() != 6:
def __getitem__(self, v): ...
A()[0] # TypeError: 'A' object is not subscriptable
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L109)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L997)
</details>
## `possibly-unbound-import`
@@ -1662,7 +1661,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1078)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1019)
</details>
## `redundant-cast`
@@ -1688,7 +1687,7 @@ cast(int, f()) # Redundant
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1423)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1364)
</details>
## `undefined-reveal`
@@ -1711,7 +1710,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1231)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1172)
</details>
## `unknown-rule`
@@ -1744,67 +1743,6 @@ a = 20 / 0 # ty: ignore[division-by-zero]
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L40)
</details>
## `unsupported-base`
**Default level**: warn
<details>
<summary>detects class bases that are unsupported as ty could not feasibly calculate the class's MRO</summary>
### What it does
Checks for class definitions that have bases which are unsupported by ty.
### Why is this bad?
If a class has a base that is an instance of a complex type such as a union type,
ty will not be able to resolve the [method resolution order] (MRO) for the class.
This will lead to an inferior understanding of your codebase and unpredictable
type-checking behavior.
### Examples
```python
import datetime
class A: ...
class B: ...
if datetime.date.today().weekday() != 6:
C = A
else:
C = B
class D(C): ... # error: [unsupported-base]
```
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L487)
</details>
## `division-by-zero`
**Default level**: ignore
<details>
<summary>detects division by zero</summary>
### What it does
It detects division by zero.
### Why is this bad?
Dividing by zero raises a `ZeroDivisionError` at runtime.
### Examples
```python
5 / 0
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L238)
</details>
## `possibly-unresolved-reference`
**Default level**: ignore
@@ -1829,7 +1767,7 @@ print(x) # NameError: name 'x' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1104)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1045)
</details>
## `unused-ignore-comment`

View File

@@ -283,7 +283,7 @@ impl clap::Args for RulesArg {
/// The diagnostic output format.
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum OutputFormat {
/// Print diagnostics verbosely, with context and helpful hints \[default\].
/// Print diagnostics verbosely, with context and helpful hints.
///
/// Diagnostic messages may include additional context and
/// annotations on the input to help understand the message.

View File

@@ -241,73 +241,6 @@ fn config_override_python_platform() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn config_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-version = "3.8"
"#,
),
(
"test.py",
r#"
aiter
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:2:1
|
2 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types
--> pyproject.toml:3:18
|
2 | [tool.ty.environment]
3 | python-version = "3.8"
| ^^^^^ Python 3.8 assumed due to this configuration setting
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:2:1
|
2 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.9 was assumed when resolving types because it was specified on the command line
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// Paths specified on the CLI are relative to the current working directory and not the project root.
///
/// We test this by adding an extra search path from the CLI to the libs directory when
@@ -454,11 +387,22 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error).
assert_cmd_snapshot!(case.command(), @r###"
// Assert that there's an `unresolved-reference` diagnostic (error)
// and a `division-by-zero` diagnostic (error).
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: rule `division-by-zero` is enabled by default
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:7:1
|
@@ -469,17 +413,17 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"###);
");
case.write_file(
"pyproject.toml",
r#"
[tool.ty.rules]
division-by-zero = "warn" # promote to warn
division-by-zero = "warn" # demote to warn
unresolved-reference = "ignore"
"#,
)?;
@@ -524,9 +468,9 @@ fn cli_rule_severity() -> anyhow::Result<()> {
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error)
// and an unresolved-import (error) diagnostic by default.
assert_cmd_snapshot!(case.command(), @r###"
// Assert that there's an `unresolved-reference` diagnostic (error),
// a `division-by-zero` (error) and a unresolved-import (error) diagnostic by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
@@ -541,6 +485,18 @@ fn cli_rule_severity() -> anyhow::Result<()> {
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
info: rule `division-by-zero` is enabled by default
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:9:1
|
@@ -551,11 +507,11 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
Found 3 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"###);
");
assert_cmd_snapshot!(
case
@@ -619,11 +575,22 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
"#,
)?;
// Assert that there's a `unresolved-reference` diagnostic (error) by default.
assert_cmd_snapshot!(case.command(), @r###"
// Assert that there's a `unresolved-reference` diagnostic (error)
// and a `division-by-zero` (error) by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: rule `division-by-zero` is enabled by default
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:7:1
|
@@ -634,11 +601,11 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"###);
");
assert_cmd_snapshot!(
case
@@ -647,6 +614,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
.arg("unresolved-reference")
.arg("--warn")
.arg("division-by-zero")
// Override the error severity with warning
.arg("--ignore")
.arg("unresolved-reference"),
@r"
@@ -1135,10 +1103,18 @@ fn check_specific_paths() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command(),
@r###"
@r"
success: false
exit_code: 1
----- stdout -----
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> project/main.py:2:5
|
2 | y = 4 / 0 # error: division-by-zero
| ^^^^^
|
info: rule `division-by-zero` is enabled by default
error[unresolved-import]: Cannot resolve imported module `main2`
--> project/other.py:2:6
|
@@ -1159,11 +1135,11 @@ fn check_specific_paths() -> anyhow::Result<()> {
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
Found 3 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"###
"
);
// Now check only the `tests` and `other.py` files.
@@ -1454,10 +1430,10 @@ fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
}
#[test]
fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> {
fn cli_config_args_overrides_knot_toml() -> anyhow::Result<()> {
let case = TestCase::with_files(vec![
(
"ty.toml",
"knot.toml",
r#"
[terminal]
error-on-warning = true
@@ -1465,27 +1441,6 @@ fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> {
),
("test.py", r"print(x) # [unresolved-reference]"),
])?;
// Exit code of 1 due to the setting in `ty.toml`
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Exit code of 0 because the `ty.toml` setting is overwritten by `--config`
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r"
success: true
exit_code: 0

View File

@@ -1444,11 +1444,13 @@ mod unix {
)
.expect("Expected bar.baz to exist in site-packages.");
let baz_project = case.project_path("bar/baz.py");
let baz_file = baz.file().unwrap();
assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
assert_eq!(
baz_file.path(case.db()).as_system_path(),
source_text(case.db(), baz.file()).as_str(),
"def baz(): ..."
);
assert_eq!(
baz.file().path(case.db()).as_system_path(),
Some(&*baz_project)
);
@@ -1463,7 +1465,7 @@ mod unix {
case.apply_changes(changes);
assert_eq!(
source_text(case.db(), baz_file).as_str(),
source_text(case.db(), baz.file()).as_str(),
"def baz(): print('Version 2')"
);
@@ -1476,7 +1478,7 @@ mod unix {
case.apply_changes(changes);
assert_eq!(
source_text(case.db(), baz_file).as_str(),
source_text(case.db(), baz.file()).as_str(),
"def baz(): print('Version 3')"
);
@@ -1522,7 +1524,6 @@ mod unix {
&ModuleName::new_static("bar.baz").unwrap(),
)
.expect("Expected bar.baz to exist in site-packages.");
let baz_file = baz.file().unwrap();
let bar_baz = case.project_path("bar/baz.py");
let patched_bar_baz = case.project_path("patched/bar/baz.py");
@@ -1533,8 +1534,11 @@ mod unix {
"def baz(): ..."
);
assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
assert_eq!(baz_file.path(case.db()).as_system_path(), Some(&*bar_baz));
assert_eq!(
source_text(case.db(), baz.file()).as_str(),
"def baz(): ..."
);
assert_eq!(baz.file().path(case.db()).as_system_path(), Some(&*bar_baz));
case.assert_indexed_project_files([patched_bar_baz_file]);
@@ -1563,7 +1567,7 @@ mod unix {
let patched_baz_text = source_text(case.db(), patched_bar_baz_file);
let did_update_patched_baz = patched_baz_text.as_str() == "def baz(): print('Version 2')";
let bar_baz_text = source_text(case.db(), baz_file);
let bar_baz_text = source_text(case.db(), baz.file());
let did_update_bar_baz = bar_baz_text.as_str() == "def baz(): print('Version 2')";
assert!(
@@ -1646,7 +1650,7 @@ mod unix {
"def baz(): ..."
);
assert_eq!(
baz.file().unwrap().path(case.db()).as_system_path(),
baz.file().path(case.db()).as_system_path(),
Some(&*baz_original)
);

View File

@@ -158,9 +158,9 @@ mod tests {
use crate::db::tests::TestDb;
use ruff_db::system::{DbWithWritableSystem, SystemPathBuf};
use ruff_python_ast::PythonVersion;
use ty_python_semantic::{
Program, ProgramSettings, PythonPath, PythonPlatform, PythonVersionWithSource,
SearchPathSettings,
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
};
pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest {
@@ -191,7 +191,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_version: PythonVersion::latest_ty(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],

View File

@@ -13,10 +13,11 @@ pub use hover::hover;
pub use inlay_hints::inlay_hints;
pub use markup::MarkupKind;
use ruff_db::files::{File, FileRange};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::ops::{Deref, DerefMut};
use ruff_db::files::{File, FileRange};
use ruff_text_size::{Ranged, TextRange};
use ty_python_semantic::types::{Type, TypeDefinition};
/// Information associated with a text range.
@@ -187,10 +188,7 @@ impl HasNavigationTargets for Type<'_> {
impl HasNavigationTargets for TypeDefinition<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
let Some(full_range) = self.full_range(db.upcast()) else {
return NavigationTargets::empty();
};
let full_range = self.full_range(db.upcast());
NavigationTargets::single(NavigationTarget {
file: full_range.file(),
focus_range: self.focus_range(db.upcast()).unwrap_or(full_range).range(),
@@ -207,10 +205,10 @@ mod tests {
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
use ruff_db::files::{File, system_path_to_file};
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion;
use ruff_text_size::TextSize;
use ty_python_semantic::{
Program, ProgramSettings, PythonPath, PythonPlatform, PythonVersionWithSource,
SearchPathSettings,
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
};
pub(super) fn cursor_test(source: &str) -> CursorTest {
@@ -230,7 +228,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_version: PythonVersion::latest_ty(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],

View File

@@ -25,17 +25,9 @@ pub trait Db: SemanticDb + Upcast<dyn SemanticDb> {
#[derive(Clone)]
pub struct ProjectDatabase {
project: Option<Project>,
files: Files,
// IMPORTANT: Never return clones of `system` outside `ProjectDatabase` (only return references)
// or the "trick" to get a mutable `Arc` in `Self::system_mut` is no longer guaranteed to work.
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
// IMPORTANT: This field must be the last because we use `zalsa_mut` (drops all other storage references)
// to drop all other references to the database, which gives us exclusive access to other `Arc`s stored on this db.
// However, for this to work it's important that the `storage` is dropped AFTER any `Arc` that
// we try to mutably borrow using `Arc::get_mut` (like `system`).
storage: salsa::Storage<ProjectDatabase>,
files: Files,
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
}
impl ProjectDatabase {

View File

@@ -663,11 +663,10 @@ mod tests {
use ruff_db::source::source_text;
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::PythonVersion;
use ruff_python_ast::name::Name;
use ty_python_semantic::types::check_types;
use ty_python_semantic::{
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
};
use ty_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
#[test]
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
@@ -678,7 +677,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
},

View File

@@ -10,10 +10,7 @@ use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use thiserror::Error;
use ty_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
use ty_python_semantic::{
ProgramSettings, PythonPath, PythonPlatform, PythonVersionSource, PythonVersionWithSource,
SearchPathSettings,
};
use ty_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
use super::settings::{Settings, TerminalSettings};
@@ -96,17 +93,8 @@ impl Options {
let python_version = self
.environment
.as_ref()
.and_then(|env| env.python_version.as_ref())
.map(|ranged_version| PythonVersionWithSource {
version: **ranged_version,
source: match ranged_version.source() {
ValueSource::Cli => PythonVersionSource::Cli,
ValueSource::File(path) => {
PythonVersionSource::File(path.clone(), ranged_version.range())
}
},
})
.unwrap_or_default();
.and_then(|env| env.python_version.as_deref().copied())
.unwrap_or(PythonVersion::latest_ty());
let python_platform = self
.environment
.as_ref()

View File

@@ -113,35 +113,3 @@ def _(
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
```
## Diagnostics for common errors
<!-- snapshot-diagnostics -->
### Module-literal used when you meant to use a class from that module
It's pretty common in Python to accidentally use a module-literal type in a type expression when you
*meant* to use a class by the same name that comes from that module. We emit a nice subdiagnostic
for this case:
`foo.py`:
```py
import datetime
def f(x: datetime): ... # error: [invalid-type-form]
```
`PIL/Image.py`:
```py
class Image: ...
```
`bar.py`:
```py
from PIL import Image
def g(x: Image): ... # error: [invalid-type-form]
```

View File

@@ -81,22 +81,23 @@ import typing
class ListSubclass(typing.List): ...
# revealed: tuple[<class 'ListSubclass'>, <class 'list[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# revealed: tuple[<class 'ListSubclass'>, <class 'list[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# revealed: tuple[<class 'DictSubclass'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# TODO: should not have multiple `Generic[]` elements
# revealed: tuple[<class 'DictSubclass'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# revealed: tuple[<class 'SetSubclass'>, <class 'set[Unknown]'>, <class 'MutableSet[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# revealed: tuple[<class 'SetSubclass'>, <class 'set[Unknown]'>, <class 'MutableSet[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
reveal_type(FrozenSetSubclass.__mro__)
####################
@@ -105,26 +106,30 @@ reveal_type(FrozenSetSubclass.__mro__)
class ChainMapSubclass(typing.ChainMap): ...
# revealed: tuple[<class 'ChainMapSubclass'>, <class 'ChainMap[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# TODO: should not have multiple `Generic[]` elements
# revealed: tuple[<class 'ChainMapSubclass'>, <class 'ChainMap[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
# revealed: tuple[<class 'CounterSubclass'>, <class 'Counter[Unknown]'>, <class 'dict[Unknown, int]'>, <class 'MutableMapping[Unknown, int]'>, <class 'Mapping[Unknown, int]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# TODO: Should have one `Generic[]` element, not three(!)
# revealed: tuple[<class 'CounterSubclass'>, <class 'Counter[Unknown]'>, <class 'dict[Unknown, int]'>, <class 'MutableMapping[Unknown, int]'>, <class 'Mapping[Unknown, int]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], typing.Generic[_T], <class 'object'>]
reveal_type(CounterSubclass.__mro__)
class DefaultDictSubclass(typing.DefaultDict): ...
# revealed: tuple[<class 'DefaultDictSubclass'>, <class 'defaultdict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# TODO: Should not have multiple `Generic[]` elements
# revealed: tuple[<class 'DefaultDictSubclass'>, <class 'defaultdict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# revealed: tuple[<class 'DequeSubclass'>, <class 'deque[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# revealed: tuple[<class 'DequeSubclass'>, <class 'deque[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# revealed: tuple[<class 'OrderedDictSubclass'>, <class 'OrderedDict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
# TODO: Should not have multiple `Generic[]` elements
# revealed: tuple[<class 'OrderedDictSubclass'>, <class 'OrderedDict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
reveal_type(OrderedDictSubclass.__mro__)
```

View File

@@ -16,7 +16,8 @@ Alias: TypeAlias = int
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
# TODO: this is not correct. At runtime, this is `type` (`Alias` is just `<class 'int'>`).
reveal_type(Alias) # revealed: typing.TypeAliasType
def g() -> TypeGuard[int]: ...
def h() -> TypeIs[int]: ...

View File

@@ -777,30 +777,6 @@ reveal_type(C.variable_with_class_default1) # revealed: str
reveal_type(c_instance.variable_with_class_default1) # revealed: str
```
#### Descriptor attributes as class variables
Whether they are explicitly qualified as `ClassVar`, or just have a class level default, we treat
descriptor attributes as class variables. This test mainly makes sure that we do *not* treat them as
instance variables. This would lead to a different outcome, since the `__get__` method would not be
called (the descriptor protocol is not invoked for instance variables).
```py
from typing import ClassVar
class Descriptor:
def __get__(self, instance, owner) -> int:
return 42
class C:
a: ClassVar[Descriptor]
b: Descriptor = Descriptor()
c: ClassVar[Descriptor] = Descriptor()
reveal_type(C().a) # revealed: int
reveal_type(C().b) # revealed: int
reveal_type(C().c) # revealed: int
```
### Inheritance of class/instance attributes
#### Instance variable defined in a base class
@@ -1760,9 +1736,9 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
```py
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes
reveal_type(b"foo".join)
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
reveal_type(b"foo".endswith)
```

View File

@@ -313,8 +313,7 @@ reveal_type(A() + "foo") # revealed: A
reveal_type("foo" + A()) # revealed: A
reveal_type(A() + b"foo") # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(b"foo" + A()) # revealed: A
reveal_type(A() + ()) # revealed: A
reveal_type(() + A()) # revealed: A

View File

@@ -54,10 +54,8 @@ reveal_type(2**largest_u32) # revealed: int
def variable(x: int):
reveal_type(x**2) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(2**x) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(x**x) # revealed: int
reveal_type(2**x) # revealed: Any
reveal_type(x**x) # revealed: Any
```
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
@@ -72,34 +70,6 @@ reveal_type(2 ** (-1)) # revealed: float
reveal_type((-1) ** (-1)) # revealed: float
```
## Division and Modulus
Division works differently in Python than in Rust. If the result is negative and there is a
remainder, the division rounds down (instead of towards zero). The remainder needs to be adjusted to
compensate so that `(lhs // rhs) * rhs + (lhs % rhs) == lhs`:
```py
reveal_type(256 % 129) # revealed: Literal[127]
reveal_type(-256 % 129) # revealed: Literal[2]
reveal_type(256 % -129) # revealed: Literal[-2]
reveal_type(-256 % -129) # revealed: Literal[-127]
reveal_type(129 % 16) # revealed: Literal[1]
reveal_type(-129 % 16) # revealed: Literal[15]
reveal_type(129 % -16) # revealed: Literal[-15]
reveal_type(-129 % -16) # revealed: Literal[-1]
reveal_type(10 // 8) # revealed: Literal[1]
reveal_type(-10 // 8) # revealed: Literal[-2]
reveal_type(10 // -8) # revealed: Literal[-2]
reveal_type(-10 // -8) # revealed: Literal[1]
reveal_type(10 // 6) # revealed: Literal[1]
reveal_type(-10 // 6) # revealed: Literal[-2]
reveal_type(10 // -6) # revealed: Literal[-2]
reveal_type(-10 // -6) # revealed: Literal[1]
```
## Division by Zero
This error is really outside the current Python type system, because e.g. `int.__truediv__` and

View File

@@ -158,10 +158,10 @@ def _(flag: bool) -> None:
def __new__(cls):
return object.__new__(cls)
# error: [possibly-unbound-implicit-call]
# error: [call-possibly-unbound-method]
reveal_type(Foo()) # revealed: Foo
# error: [possibly-unbound-implicit-call]
# error: [call-possibly-unbound-method]
# error: [too-many-positional-arguments]
reveal_type(Foo(1)) # revealed: Foo
```

View File

@@ -112,7 +112,7 @@ def _(flag: bool):
this_fails = ThisFails()
# error: [possibly-unbound-implicit-call]
# error: [call-possibly-unbound-method]
reveal_type(this_fails[0]) # revealed: Unknown | str
```
@@ -236,40 +236,6 @@ def _(flag: bool):
return str(key)
c = C()
# error: [possibly-unbound-implicit-call]
# error: [call-possibly-unbound-method]
reveal_type(c[0]) # revealed: str
```
## Dunder methods cannot be looked up on instances
Class-level annotations with no value assigned are considered instance-only, and aren't available as
dunder methods:
```py
from typing import Callable
class C:
__call__: Callable[..., None]
# error: [call-non-callable]
C()()
# error: [invalid-assignment]
_: Callable[..., None] = C()
```
And of course the same is true if we have only an implicit assignment inside a method:
```py
from typing import Callable
class C:
def __init__(self):
self.__call__ = lambda *a, **kw: None
# error: [call-non-callable]
C()()
# error: [invalid-assignment]
_: Callable[..., None] = C()
```

View File

@@ -109,50 +109,23 @@ def _(o: object):
### Unsupported operators for positive contributions
Raise an error if the given operator is unsupported for all positive contributions to the
intersection type:
```py
class NonContainer1: ...
class NonContainer2: ...
def _(x: object):
if isinstance(x, NonContainer1):
if isinstance(x, NonContainer2):
reveal_type(x) # revealed: NonContainer1 & NonContainer2
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer1`"
reveal_type(2 in x) # revealed: bool
```
Do not raise an error if at least one of the positive contributions to the intersection type support
the operator:
Raise an error if any of the positive contributions to the intersection type are unsupported for the
given operator:
```py
class Container:
def __contains__(self, x) -> bool:
return False
class NonContainer: ...
def _(x: object):
if isinstance(x, NonContainer1):
if isinstance(x, Container):
if isinstance(x, NonContainer2):
reveal_type(x) # revealed: NonContainer1 & Container & NonContainer2
reveal_type(2 in x) # revealed: bool
```
if isinstance(x, Container):
if isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & NonContainer
Do also raise an error if the intersection has no positive contributions at all, unless the operator
is supported on `object`:
```py
def _(x: object):
if not isinstance(x, NonContainer1):
reveal_type(x) # revealed: ~NonContainer1
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `object`, in comparing `Literal[2]` with `~NonContainer1`"
reveal_type(2 in x) # revealed: bool
reveal_type(2 is x) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
reveal_type(2 in x) # revealed: bool
```
### Unsupported operators for negative contributions

View File

@@ -369,65 +369,7 @@ To do
### `frozen`
If true (the default is False), assigning to fields will generate a diagnostic.
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int
frozen_instance = MyFrozenClass(1)
frozen_instance.x = 2 # error: [invalid-assignment]
```
If `__setattr__()` or `__delattr__()` is defined in the class, we should emit a diagnostic.
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int
# TODO: Emit a diagnostic here
def __setattr__(self, name: str, value: object) -> None: ...
# TODO: Emit a diagnostic here
def __delattr__(self, name: str) -> None: ...
```
This also works for generic dataclasses:
```toml
[environment]
python-version = "3.12"
```
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenGeneric[T]:
x: T
frozen_instance = MyFrozenGeneric[int](1)
frozen_instance.x = 2 # error: [invalid-assignment]
```
When attempting to mutate an unresolved attribute on a frozen dataclass, only `unresolved-attribute`
is emitted:
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass: ...
frozen = MyFrozenClass()
frozen.x = 2 # error: [unresolved-attribute]
```
To do
### `match_args`

View File

@@ -298,25 +298,25 @@ python-version = "3.12"
```
```py
def _():
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound"
type X[T: (yield 1)] = int
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound"
# error: [invalid-syntax] "`yield` statement outside of a function"
type X[T: (yield 1)] = int
def _():
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a type alias"
type Y = (yield 1)
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a type alias"
# error: [invalid-syntax] "`yield` statement outside of a function"
type Y = (yield 1)
# error: [invalid-type-form] "Named expressions are not allowed in type expressions"
# error: [invalid-syntax] "named expression cannot be used within a generic definition"
def f[T](x: int) -> (y := 3):
return x
def _():
# error: [invalid-syntax] "yield expression cannot be used within a generic definition"
class C[T]((yield from [object])):
pass
# error: [invalid-syntax] "`yield from` statement outside of a function"
# error: [invalid-syntax] "yield expression cannot be used within a generic definition"
class C[T]((yield from [object])):
pass
```
## `await` outside async function

View File

@@ -397,19 +397,3 @@ async def i() -> typing.AsyncIterable:
async def j() -> str: # error: [invalid-return-type]
yield 42
```
## Diagnostics for `invalid-return-type` on non-protocol subclasses of protocol classes
<!-- snapshot-diagnostics -->
We emit a nice subdiagnostic in this situation explaining the probable error here:
```py
from typing_extensions import Protocol
class Abstract(Protocol):
def method(self) -> str: ...
class Concrete(Abstract):
def method(self) -> str: ... # error: [invalid-return-type]
```

View File

@@ -24,7 +24,9 @@ class:
```py
class Bad(Generic[T], Generic[T]): ... # error: [duplicate-base]
class AlsoBad(Generic[T], Generic[S]): ... # error: [duplicate-base]
# TODO: should emit an error (fails at runtime)
class AlsoBad(Generic[T], Generic[S]): ...
```
You cannot use the same typevar more than once.

View File

@@ -29,10 +29,8 @@ import parent.child.two
`from.py`
```py
from parent.child import one, two
reveal_type(one) # revealed: <module 'parent.child.one'>
reveal_type(two) # revealed: <module 'parent.child.two'>
# TODO: This should not be an error
from parent.child import one, two # error: [unresolved-import]
```
## Regular package in namespace package
@@ -107,42 +105,3 @@ reveal_type(x) # revealed: Unknown | Literal["module"]
import foo.bar # error: [unresolved-import]
```
## `from` import with namespace package
Regression test for <https://github.com/astral-sh/ty/issues/363>
`google/cloud/pubsub_v1/__init__.py`:
```py
class PublisherClient: ...
```
```py
from google.cloud import pubsub_v1
reveal_type(pubsub_v1.PublisherClient) # revealed: <class 'PublisherClient'>
```
## `from` root importing sub-packages
Regresssion test for <https://github.com/astral-sh/ty/issues/375>
`opentelemetry/trace/__init__.py`:
```py
class Trace: ...
```
`opentelemetry/metrics/__init__.py`:
```py
class Metric: ...
```
```py
from opentelemetry import trace, metrics
reveal_type(trace) # revealed: <module 'opentelemetry.trace'>
reveal_type(metrics) # revealed: <module 'opentelemetry.metrics'>
```

View File

@@ -173,7 +173,7 @@ if hasattr(DoesNotExist, "__mro__"):
if not isinstance(DoesNotExist, type):
reveal_type(DoesNotExist) # revealed: Unknown & ~type
class Foo(DoesNotExist): ... # error: [unsupported-base]
class Foo(DoesNotExist): ... # error: [invalid-base]
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
```
@@ -232,15 +232,11 @@ reveal_type(AA.__mro__) # revealed: tuple[<class 'AA'>, <class 'Z'>, Unknown, <
## `__bases__` includes a `Union`
<!-- snapshot-diagnostics -->
We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we
find a union type in a class's bases, we infer the class's `__mro__` as being
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
```py
from typing_extensions import reveal_type
def returns_bool() -> bool:
return True
@@ -254,7 +250,7 @@ else:
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Foo(x): ...
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
@@ -263,8 +259,8 @@ reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'obje
## `__bases__` is a union of a dynamic type and valid bases
If a dynamic type such as `Any` or `Unknown` is one of the elements in the union, and all other
types *would be* valid class bases, we do not emit an `invalid-base` or `unsupported-base`
diagnostic, and we use the dynamic type as a base to prevent further downstream errors.
types *would be* valid class bases, we do not emit an `invalid-base` diagnostic and use the dynamic
type as a base to prevent further downstream errors.
```py
from typing import Any
@@ -303,8 +299,8 @@ else:
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
reveal_type(y) # revealed: <class 'C'> | <class 'D'>
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
# error: 14 [unsupported-base] "Unsupported class base with type `<class 'C'> | <class 'D'>`"
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 14 [invalid-base] "Invalid class base with type `<class 'C'> | <class 'D'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Foo(x, y): ...
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
@@ -325,7 +321,7 @@ if returns_bool():
else:
foo = object
# error: 21 [unsupported-base] "Unsupported class base with type `<class 'Y'> | <class 'object'>`"
# error: 21 [invalid-base] "Invalid class base with type `<class 'Y'> | <class 'object'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class PossibleError(foo, X): ...
reveal_type(PossibleError.__mro__) # revealed: tuple[<class 'PossibleError'>, Unknown, <class 'object'>]
@@ -343,47 +339,12 @@ else:
# revealed: tuple[<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>] | tuple[<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>]
reveal_type(B.__mro__)
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'B'> | <class 'B'>`"
# error: 12 [invalid-base] "Invalid class base with type `<class 'B'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Z(A, B): ...
reveal_type(Z.__mro__) # revealed: tuple[<class 'Z'>, Unknown, <class 'object'>]
```
## `__bases__` lists that include objects that are not instances of `type`
<!-- snapshot-diagnostics -->
```py
class Foo(2): ... # error: [invalid-base]
```
A base that is not an instance of `type` but does have an `__mro_entries__` method will not raise an
exception at runtime, so we issue `unsupported-base` rather than `invalid-base`:
```py
class Foo:
def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
return ()
class Bar(Foo()): ... # error: [unsupported-base]
```
But for objects that have badly defined `__mro_entries__`, `invalid-base` is emitted rather than
`unsupported-base`:
```py
class Bad1:
def __mro_entries__(self, bases, extra_arg):
return ()
class Bad2:
def __mro_entries__(self, bases) -> int:
return 42
class BadSub1(Bad1()): ... # error: [invalid-base]
class BadSub2(Bad2()): ... # error: [invalid-base]
```
## `__bases__` lists with duplicate bases
<!-- snapshot-diagnostics -->
@@ -527,45 +488,6 @@ reveal_type(unknown_object) # revealed: Unknown
reveal_type(unknown_object.__mro__) # revealed: Unknown
```
## MROs of classes that use multiple inheritance with generic aliases and subscripted `Generic`
```py
from typing import Generic, TypeVar, Iterator
T = TypeVar("T")
class peekable(Generic[T], Iterator[T]): ...
# revealed: tuple[<class 'peekable[Unknown]'>, <class 'Iterator[T]'>, <class 'Iterable[T]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(peekable.__mro__)
class peekable2(Iterator[T], Generic[T]): ...
# revealed: tuple[<class 'peekable2[Unknown]'>, <class 'Iterator[T]'>, <class 'Iterable[T]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(peekable2.__mro__)
class Base: ...
class Intermediate(Base, Generic[T]): ...
class Sub(Intermediate[T], Base): ...
# revealed: tuple[<class 'Sub[Unknown]'>, <class 'Intermediate[T]'>, <class 'Base'>, typing.Generic, <class 'object'>]
reveal_type(Sub.__mro__)
```
## Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases
<!-- snapshot-diagnostics -->
```py
from typing_extensions import Protocol, TypeVar, Generic
T = TypeVar("T")
class Foo(Protocol): ...
class Bar(Protocol[T]): ...
class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
```
## Classes that inherit from themselves
These are invalid, but we need to be able to handle them gracefully without panicking.

View File

@@ -146,13 +146,11 @@ def _(flag: bool):
def _(flag: bool):
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic during
# type checking
# error: [invalid-argument-type]
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1, "a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
# error: [invalid-argument-type]
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1, "a"]
```

View File

@@ -214,8 +214,7 @@ def flag() -> bool:
t = int if flag() else str
# TODO: this should cause us to emit a diagnostic during
# type checking
# error: [invalid-argument-type]
if issubclass(t, "str"):
reveal_type(t) # revealed: <class 'int'> | <class 'str'>

View File

@@ -28,7 +28,7 @@ def f() -> None:
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`)
reveal_type(IntOrStr.__value__) # revealed: Any
```
## Invalid assignment

View File

@@ -67,10 +67,12 @@ It's an error to include both bare `Protocol` and subscripted `Protocol[]` in th
simultaneously:
```py
class DuplicateBases(Protocol, Protocol[T]): # error: [duplicate-base]
# TODO: should emit a `[duplicate-bases]` error here:
class DuplicateBases(Protocol, Protocol[T]):
x: T
# revealed: tuple[<class 'DuplicateBases[Unknown]'>, Unknown, <class 'object'>]
# TODO: should not have `Protocol` or `Generic` multiple times
# revealed: tuple[<class 'DuplicateBases[Unknown]'>, typing.Protocol, typing.Generic, typing.Protocol[T], typing.Generic[T], <class 'object'>]
reveal_type(DuplicateBases.__mro__)
```
@@ -241,7 +243,8 @@ def f(
Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime:
```py
# Could also be `Literal[True]`, but `bool` is fine:
# TODO: Should be `Literal[True]`, but `bool` is also fine
# error: [invalid-argument-type]
reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
```

View File

@@ -27,7 +27,8 @@ error[unsupported-operator]: Operator `|` is unsupported between objects of type
| ^^^^^^^^^
|
info: Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later
info: Python 3.9 was assumed when resolving types because it was specified on the command line
info: The inferred target version of your project is Python 3.9
info: If using a pyproject.toml file, consider adjusting the `project.requires-python` or `tool.ty.environment.python-version` field
info: rule `unsupported-operator` is enabled by default
```

View File

@@ -1,62 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Module-literal used when you meant to use a class from that module
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
---
# Python source files
## foo.py
```
1 | import datetime
2 |
3 | def f(x: datetime): ... # error: [invalid-type-form]
```
## PIL/Image.py
```
1 | class Image: ...
```
## bar.py
```
1 | from PIL import Image
2 |
3 | def g(x: Image): ... # error: [invalid-type-form]
```
# Diagnostics
```
error[invalid-type-form]: Variable of type `<module 'datetime'>` is not allowed in a type expression
--> src/foo.py:3:10
|
1 | import datetime
2 |
3 | def f(x: datetime): ... # error: [invalid-type-form]
| ^^^^^^^^
|
info: Did you mean to use the module's member `datetime.datetime` instead?
info: rule `invalid-type-form` is enabled by default
```
```
error[invalid-type-form]: Variable of type `<module 'PIL.Image'>` is not allowed in a type expression
--> src/bar.py:3:10
|
1 | from PIL import Image
2 |
3 | def g(x: Image): ... # error: [invalid-type-form]
| ^^^^^
|
info: Did you mean to use the module's member `Image.Image` instead?
info: rule `invalid-type-form` is enabled by default
```

View File

@@ -1,37 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol, TypeVar, Generic
2 |
3 | T = TypeVar("T")
4 |
5 | class Foo(Protocol): ...
6 | class Bar(Protocol[T]): ...
7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
```
# Diagnostics
```
error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO) for class `Baz` with bases list `[typing.Protocol[T], <class 'Foo'>, <class 'Bar[T]'>]`
--> src/mdtest_snippet.py:7:1
|
5 | class Foo(Protocol): ...
6 | class Bar(Protocol[T]): ...
7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info: rule `inconsistent-mro` is enabled by default
```

View File

@@ -1,78 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - `__bases__` includes a `Union`
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def returns_bool() -> bool:
4 | return True
5 |
6 | class A: ...
7 | class B: ...
8 |
9 | if returns_bool():
10 | x = A
11 | else:
12 | x = B
13 |
14 | reveal_type(x) # revealed: <class 'A'> | <class 'B'>
15 |
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
17 | class Foo(x): ...
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
```
# Diagnostics
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:14:13
|
12 | x = B
13 |
14 | reveal_type(x) # revealed: <class 'A'> | <class 'B'>
| ^ `<class 'A'> | <class 'B'>`
15 |
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
|
```
```
warning[unsupported-base]: Unsupported class base with type `<class 'A'> | <class 'B'>`
--> src/mdtest_snippet.py:17:11
|
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
17 | class Foo(x): ...
| ^
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
info: ty cannot resolve a consistent MRO for class `Foo` due to this base
info: Only class objects or `Any` are supported as class bases
info: rule `unsupported-base` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:19:13
|
17 | class Foo(x): ...
18 |
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
| ^^^^^^^^^^^ `tuple[<class 'Foo'>, Unknown, <class 'object'>]`
|
```

View File

@@ -1,97 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - `__bases__` lists that include objects that are not instances of `type`
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | class Foo(2): ... # error: [invalid-base]
2 | class Foo:
3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
4 | return ()
5 |
6 | class Bar(Foo()): ... # error: [unsupported-base]
7 | class Bad1:
8 | def __mro_entries__(self, bases, extra_arg):
9 | return ()
10 |
11 | class Bad2:
12 | def __mro_entries__(self, bases) -> int:
13 | return 42
14 |
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
```
# Diagnostics
```
error[invalid-base]: Invalid class base with type `Literal[2]`
--> src/mdtest_snippet.py:1:11
|
1 | class Foo(2): ... # error: [invalid-base]
| ^
2 | class Foo:
3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
|
info: Definition of class `Foo` will raise `TypeError` at runtime
info: rule `invalid-base` is enabled by default
```
```
warning[unsupported-base]: Unsupported class base with type `Foo`
--> src/mdtest_snippet.py:6:11
|
4 | return ()
5 |
6 | class Bar(Foo()): ... # error: [unsupported-base]
| ^^^^^
7 | class Bad1:
8 | def __mro_entries__(self, bases, extra_arg):
|
info: ty cannot resolve a consistent MRO for class `Bar` due to this base
info: Only class objects or `Any` are supported as class bases
info: rule `unsupported-base` is enabled by default
```
```
error[invalid-base]: Invalid class base with type `Bad1`
--> src/mdtest_snippet.py:15:15
|
13 | return 42
14 |
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
| ^^^^^^
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
|
info: Definition of class `BadSub1` will raise `TypeError` at runtime
info: An instance type is only a valid class base if it has a valid `__mro_entries__` method
info: Type `Bad1` has an `__mro_entries__` method, but it cannot be called with the expected arguments
info: Expected a signature at least as permissive as `def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]`
info: rule `invalid-base` is enabled by default
```
```
error[invalid-base]: Invalid class base with type `Bad2`
--> src/mdtest_snippet.py:16:15
|
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
| ^^^^^^
|
info: Definition of class `BadSub2` will raise `TypeError` at runtime
info: An instance type is only a valid class base if it has a valid `__mro_entries__` method
info: Type `Bad2` has an `__mro_entries__` method, but it does not return a tuple of types
info: rule `invalid-base` is enabled by default
```

View File

@@ -1,48 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: return_type.md - Function return type - Diagnostics for `invalid-return-type` on non-protocol subclasses of protocol classes
mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol
2 |
3 | class Abstract(Protocol):
4 | def method(self) -> str: ...
5 |
6 | class Concrete(Abstract):
7 | def method(self) -> str: ... # error: [invalid-return-type]
```
# Diagnostics
```
error[invalid-return-type]: Function can implicitly return `None`, which is not assignable to return type `str`
--> src/mdtest_snippet.py:7:25
|
6 | class Concrete(Abstract):
7 | def method(self) -> str: ... # error: [invalid-return-type]
| ^^^
|
info: Only functions in stub files, methods on protocol classes, or methods with `@abstractmethod` are permitted to have empty bodies
info: Class `Concrete` has `typing.Protocol` in its MRO, but it is not a protocol class
info: Only classes that directly inherit from `typing.Protocol` or `typing_extensions.Protocol` are considered protocol classes
--> src/mdtest_snippet.py:6:7
|
4 | def method(self) -> str: ...
5 |
6 | class Concrete(Abstract):
| ^^^^^^^^^^^^^^^^^^ `Protocol` not present in `Concrete`'s immediate bases
7 | def method(self) -> str: ... # error: [invalid-return-type]
|
info: See https://typing.python.org/en/latest/spec/protocol.html#
info: rule `invalid-return-type` is enabled by default
```

View File

@@ -25,7 +25,8 @@ error[unresolved-reference]: Name `aiter` used when not defined
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.9 was assumed when resolving types because it was specified on the command line
info: The inferred target version of your project is Python 3.9
info: If using a pyproject.toml file, consider adjusting the `project.requires-python` or `tool.ty.environment.python-version` field
info: rule `unresolved-reference` is enabled by default
```

View File

@@ -16,7 +16,7 @@ class Foo[T]: ...
class Bar(Foo[Bar]): ...
reveal_type(Bar) # revealed: <class 'Bar'>
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, <class 'Foo[Bar]'>, typing.Generic, <class 'object'>]
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, <class 'Foo[Bar]'>, <class 'object'>]
```
## Access to attributes declared in stubs

View File

@@ -5,16 +5,16 @@
```py
b = b"\x00abc\xff"
reveal_type(b[0]) # revealed: Literal[0]
reveal_type(b[1]) # revealed: Literal[97]
reveal_type(b[4]) # revealed: Literal[255]
reveal_type(b[0]) # revealed: Literal[b"\x00"]
reveal_type(b[1]) # revealed: Literal[b"a"]
reveal_type(b[4]) # revealed: Literal[b"\xff"]
reveal_type(b[-1]) # revealed: Literal[255]
reveal_type(b[-2]) # revealed: Literal[99]
reveal_type(b[-5]) # revealed: Literal[0]
reveal_type(b[-1]) # revealed: Literal[b"\xff"]
reveal_type(b[-2]) # revealed: Literal[b"c"]
reveal_type(b[-5]) # revealed: Literal[b"\x00"]
reveal_type(b[False]) # revealed: Literal[0]
reveal_type(b[True]) # revealed: Literal[97]
reveal_type(b[False]) # revealed: Literal[b"\x00"]
reveal_type(b[True]) # revealed: Literal[b"a"]
x = b[5] # error: [index-out-of-bounds] "Index 5 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
reveal_type(x) # revealed: Unknown

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