Compare commits
32 Commits
codex/stab
...
dylan/stab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8316b909d9 | ||
|
|
829acf498d | ||
|
|
e07f352f99 | ||
|
|
8d0b6882b7 | ||
|
|
65a2daea02 | ||
|
|
8baaa2f7f3 | ||
|
|
8b1ce32f04 | ||
|
|
eb5abda8ac | ||
|
|
9c4ecf77b6 | ||
|
|
0809d88ca0 | ||
|
|
5c59167686 | ||
|
|
e2ea301c74 | ||
|
|
62364ea47e | ||
|
|
331821244b | ||
|
|
1dc8f8f903 | ||
|
|
301b9f4135 | ||
|
|
86e5a311f0 | ||
|
|
0c20010bb9 | ||
|
|
72552f31e4 | ||
|
|
95497ffaab | ||
|
|
b3b900dc1e | ||
|
|
503427855d | ||
|
|
6e785867c3 | ||
|
|
1274521f9f | ||
|
|
8d24760643 | ||
|
|
db8db536f8 | ||
|
|
cb8246bc5f | ||
|
|
5faf72a4d9 | ||
|
|
28dbc5c51e | ||
|
|
ce216c79cc | ||
|
|
33468cc8cc | ||
|
|
8531f4b3ca |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## 0.11.13
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Add unsafe fix for module moved cases (`AIR301`,`AIR311`,`AIR312`,`AIR302`) ([#18367](https://github.com/astral-sh/ruff/pull/18367),[#18366](https://github.com/astral-sh/ruff/pull/18366),[#18363](https://github.com/astral-sh/ruff/pull/18363),[#18093](https://github.com/astral-sh/ruff/pull/18093))
|
||||
- \[`refurb`\] Add coverage of `set` and `frozenset` calls (`FURB171`) ([#18035](https://github.com/astral-sh/ruff/pull/18035))
|
||||
- \[`refurb`\] Mark `FURB180` fix unsafe when class has bases ([#18149](https://github.com/astral-sh/ruff/pull/18149))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`perflint`\] Fix missing parentheses for lambda and ternary conditions (`PERF401`, `PERF403`) ([#18412](https://github.com/astral-sh/ruff/pull/18412))
|
||||
- \[`pyupgrade`\] Apply `UP035` only on py313+ for `get_type_hints()` ([#18476](https://github.com/astral-sh/ruff/pull/18476))
|
||||
- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP004`,`UP050`) ([#18393](https://github.com/astral-sh/ruff/pull/18393), [#18390](https://github.com/astral-sh/ruff/pull/18390))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`fastapi`\] Avoid false positive for class dependencies (`FAST003`) ([#18271](https://github.com/astral-sh/ruff/pull/18271))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update editor setup docs for Neovim and Vim ([#18324](https://github.com/astral-sh/ruff/pull/18324))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Support Python 3.14 template strings (t-strings) in formatter and parser ([#17851](https://github.com/astral-sh/ruff/pull/17851))
|
||||
|
||||
## 0.11.12
|
||||
|
||||
### Preview features
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -2501,7 +2501,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2738,7 +2738,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3074,7 +3074,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3965,6 +3965,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"compact_str",
|
||||
"countme",
|
||||
"dir-test",
|
||||
@@ -3977,6 +3978,7 @@ dependencies = [
|
||||
"ordermap",
|
||||
"quickcheck",
|
||||
"quickcheck_macros",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
|
||||
@@ -148,8 +148,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.12/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.12/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.11.13/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.13/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -182,7 +182,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.12
|
||||
rev: v0.11.13
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -439,7 +439,10 @@ impl LintCacheData {
|
||||
|
||||
let messages = messages
|
||||
.iter()
|
||||
.filter_map(|msg| msg.to_rule().map(|rule| (rule, msg)))
|
||||
// Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
|
||||
// this also serves to filter them out, but we shouldn't be caching files with syntax
|
||||
// errors anyway.
|
||||
.filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
|
||||
.map(|(rule, msg)| {
|
||||
// Make sure that all message use the same source file.
|
||||
assert_eq!(
|
||||
|
||||
@@ -30,7 +30,7 @@ impl<'a> Explanation<'a> {
|
||||
let (linter, _) = Linter::parse_code(&code).unwrap();
|
||||
let fix = rule.fixable().to_string();
|
||||
Self {
|
||||
name: rule.as_ref(),
|
||||
name: rule.name().as_str(),
|
||||
code,
|
||||
linter: linter.name(),
|
||||
summary: rule.message_formats()[0],
|
||||
@@ -44,7 +44,7 @@ impl<'a> Explanation<'a> {
|
||||
|
||||
fn format_rule_text(rule: Rule) -> String {
|
||||
let mut output = String::new();
|
||||
let _ = write!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code());
|
||||
let _ = write!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
|
||||
|
||||
@@ -165,9 +165,9 @@ impl AddAssign for FixMap {
|
||||
continue;
|
||||
}
|
||||
let fixed_in_file = self.0.entry(filename).or_default();
|
||||
for (rule, count) in fixed {
|
||||
for (rule, name, count) in fixed.iter() {
|
||||
if count > 0 {
|
||||
*fixed_in_file.entry(rule).or_default() += count;
|
||||
*fixed_in_file.entry(rule).or_default(name) += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,7 +305,7 @@ pub(crate) fn lint_path(
|
||||
ParseSource::None,
|
||||
);
|
||||
let transformed = source_kind;
|
||||
let fixed = FxHashMap::default();
|
||||
let fixed = FixTable::default();
|
||||
(result, transformed, fixed)
|
||||
}
|
||||
} else {
|
||||
@@ -319,7 +319,7 @@ pub(crate) fn lint_path(
|
||||
ParseSource::None,
|
||||
);
|
||||
let transformed = source_kind;
|
||||
let fixed = FxHashMap::default();
|
||||
let fixed = FixTable::default();
|
||||
(result, transformed, fixed)
|
||||
};
|
||||
|
||||
@@ -473,7 +473,7 @@ pub(crate) fn lint_stdin(
|
||||
}
|
||||
|
||||
let transformed = source_kind;
|
||||
let fixed = FxHashMap::default();
|
||||
let fixed = FixTable::default();
|
||||
(result, transformed, fixed)
|
||||
}
|
||||
} else {
|
||||
@@ -487,7 +487,7 @@ pub(crate) fn lint_stdin(
|
||||
ParseSource::None,
|
||||
);
|
||||
let transformed = source_kind;
|
||||
let fixed = FxHashMap::default();
|
||||
let fixed = FixTable::default();
|
||||
(result, transformed, fixed)
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use itertools::{Itertools, iterate};
|
||||
use ruff_linter::codes::NoqaCode;
|
||||
use ruff_linter::linter::FixTable;
|
||||
use serde::Serialize;
|
||||
|
||||
use ruff_linter::fs::relativize_path;
|
||||
@@ -80,7 +81,7 @@ impl Printer {
|
||||
let fixed = diagnostics
|
||||
.fixed
|
||||
.values()
|
||||
.flat_map(std::collections::HashMap::values)
|
||||
.flat_map(FixTable::counts)
|
||||
.sum::<usize>();
|
||||
|
||||
if self.flags.intersects(Flags::SHOW_VIOLATIONS) {
|
||||
@@ -302,7 +303,7 @@ impl Printer {
|
||||
let statistics: Vec<ExpandedStatistics> = diagnostics
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| (message.to_noqa_code(), message))
|
||||
.map(|message| (message.noqa_code(), message))
|
||||
.sorted_by_key(|(code, message)| (*code, message.fixable()))
|
||||
.fold(
|
||||
vec![],
|
||||
@@ -472,13 +473,13 @@ fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableStatistics
|
||||
fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap) -> Result<()> {
|
||||
let total = fixed
|
||||
.values()
|
||||
.map(|table| table.values().sum::<usize>())
|
||||
.map(|table| table.counts().sum::<usize>())
|
||||
.sum::<usize>();
|
||||
assert!(total > 0);
|
||||
let num_digits = num_digits(
|
||||
*fixed
|
||||
fixed
|
||||
.values()
|
||||
.filter_map(|table| table.values().max())
|
||||
.filter_map(|table| table.counts().max())
|
||||
.max()
|
||||
.unwrap(),
|
||||
);
|
||||
@@ -498,12 +499,11 @@ fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap) -> Result<()> {
|
||||
relativize_path(filename).bold(),
|
||||
":".cyan()
|
||||
)?;
|
||||
for (rule, count) in table.iter().sorted_by_key(|(.., count)| Reverse(*count)) {
|
||||
for (code, name, count) in table.iter().sorted_by_key(|(.., count)| Reverse(*count)) {
|
||||
writeln!(
|
||||
writer,
|
||||
" {count:>num_digits$} × {} ({})",
|
||||
rule.noqa_code().to_string().red().bold(),
|
||||
rule.as_ref(),
|
||||
" {count:>num_digits$} × {code} ({name})",
|
||||
code = code.to_string().red().bold(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,7 +566,7 @@ fn venv() -> Result<()> {
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument: `none` does not point to a Python executable or a directory on disk
|
||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `none`: does not point to a Python executable or a directory on disk
|
||||
");
|
||||
});
|
||||
|
||||
|
||||
@@ -5436,14 +5436,15 @@ match 2:
|
||||
print("it's one")
|
||||
"#
|
||||
),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
test.py:2:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
"###
|
||||
);
|
||||
|
||||
// syntax error on 3.9 with preview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_python_ast::ModModule;
|
||||
@@ -18,7 +17,7 @@ use crate::source::source_text;
|
||||
/// The query is only cached when the [`source_text()`] hasn't changed. This is because
|
||||
/// comparing two ASTs is a non-trivial operation and every offset change is directly
|
||||
/// reflected in the changed AST offsets.
|
||||
/// The other reason is that Ruff's AST doesn't implement `Eq` which Sala requires
|
||||
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
|
||||
/// for determining if a query result is unchanged.
|
||||
#[salsa::tracked(returns(ref), no_eq)]
|
||||
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
|
||||
@@ -36,7 +35,10 @@ pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
|
||||
ParsedModule::new(parsed)
|
||||
}
|
||||
|
||||
/// Cheap cloneable wrapper around the parsed module.
|
||||
/// A wrapper around a parsed module.
|
||||
///
|
||||
/// This type manages instances of the module AST. A particular instance of the AST
|
||||
/// is represented with the [`ParsedModuleRef`] type.
|
||||
#[derive(Clone)]
|
||||
pub struct ParsedModule {
|
||||
inner: Arc<Parsed<ModModule>>,
|
||||
@@ -49,17 +51,11 @@ impl ParsedModule {
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes `self` and returns the Arc storing the parsed module.
|
||||
pub fn into_arc(self) -> Arc<Parsed<ModModule>> {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ParsedModule {
|
||||
type Target = Parsed<ModModule>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
/// Loads a reference to the parsed module.
|
||||
pub fn load(&self, _db: &dyn Db) -> ParsedModuleRef {
|
||||
ParsedModuleRef {
|
||||
module_ref: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +73,30 @@ impl PartialEq for ParsedModule {
|
||||
|
||||
impl Eq for ParsedModule {}
|
||||
|
||||
/// Cheap cloneable wrapper around an instance of a module AST.
|
||||
#[derive(Clone)]
|
||||
pub struct ParsedModuleRef {
|
||||
module_ref: Arc<Parsed<ModModule>>,
|
||||
}
|
||||
|
||||
impl ParsedModuleRef {
|
||||
pub fn as_arc(&self) -> &Arc<Parsed<ModModule>> {
|
||||
&self.module_ref
|
||||
}
|
||||
|
||||
pub fn into_arc(self) -> Arc<Parsed<ModModule>> {
|
||||
self.module_ref
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ParsedModuleRef {
|
||||
type Target = Parsed<ModModule>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.module_ref
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Db;
|
||||
@@ -98,7 +118,7 @@ mod tests {
|
||||
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
let parsed = parsed_module(&db, file);
|
||||
let parsed = parsed_module(&db, file).load(&db);
|
||||
|
||||
assert!(parsed.has_valid_syntax());
|
||||
|
||||
@@ -114,7 +134,7 @@ mod tests {
|
||||
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
let parsed = parsed_module(&db, file);
|
||||
let parsed = parsed_module(&db, file).load(&db);
|
||||
|
||||
assert!(parsed.has_valid_syntax());
|
||||
|
||||
@@ -130,7 +150,7 @@ mod tests {
|
||||
|
||||
let virtual_file = db.files().virtual_file(&db, path);
|
||||
|
||||
let parsed = parsed_module(&db, virtual_file.file());
|
||||
let parsed = parsed_module(&db, virtual_file.file()).load(&db);
|
||||
|
||||
assert!(parsed.has_valid_syntax());
|
||||
|
||||
@@ -146,7 +166,7 @@ mod tests {
|
||||
|
||||
let virtual_file = db.files().virtual_file(&db, path);
|
||||
|
||||
let parsed = parsed_module(&db, virtual_file.file());
|
||||
let parsed = parsed_module(&db, virtual_file.file()).load(&db);
|
||||
|
||||
assert!(parsed.has_valid_syntax());
|
||||
|
||||
@@ -177,7 +197,7 @@ else:
|
||||
|
||||
let file = vendored_path_to_file(&db, VendoredPath::new("path.pyi")).unwrap();
|
||||
|
||||
let parsed = parsed_module(&db, file);
|
||||
let parsed = parsed_module(&db, file).load(&db);
|
||||
|
||||
assert!(parsed.has_valid_syntax());
|
||||
}
|
||||
|
||||
@@ -171,6 +171,21 @@ pub trait System: Debug {
|
||||
PatternError,
|
||||
>;
|
||||
|
||||
/// Fetches the environment variable `key` from the current process.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`std::env::VarError::NotPresent`] if:
|
||||
/// - The variable is not set.
|
||||
/// - The variable's name contains an equal sign or NUL (`'='` or `'\0'`).
|
||||
///
|
||||
/// Returns [`std::env::VarError::NotUnicode`] if the variable's value is not valid
|
||||
/// Unicode.
|
||||
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
||||
let _ = name;
|
||||
Err(std::env::VarError::NotPresent)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
|
||||
|
||||
@@ -214,6 +214,10 @@ impl System for OsSystem {
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
||||
std::env::var(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl OsSystem {
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
if let Some(explanation) = rule.explanation() {
|
||||
let mut output = String::new();
|
||||
|
||||
let _ = writeln!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code());
|
||||
let _ = writeln!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());
|
||||
|
||||
let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap();
|
||||
if linter.url().is_some() {
|
||||
@@ -101,7 +101,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
let filename = PathBuf::from(ROOT_DIR)
|
||||
.join("docs")
|
||||
.join("rules")
|
||||
.join(rule.as_ref())
|
||||
.join(&*rule.name())
|
||||
.with_extension("md");
|
||||
|
||||
if args.dry_run {
|
||||
|
||||
@@ -55,7 +55,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
FixAvailability::None => format!("<span {SYMBOL_STYLE}></span>"),
|
||||
};
|
||||
|
||||
let rule_name = rule.as_ref();
|
||||
let rule_name = rule.name();
|
||||
|
||||
// If the message ends in a bracketed expression (like: "Use {replacement}"), escape the
|
||||
// brackets. Otherwise, it'll be interpreted as an HTML attribute via the `attr_list`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -110,6 +110,8 @@ from typing_extensions import CapsuleType
|
||||
# UP035 on py313+ only
|
||||
from typing_extensions import deprecated
|
||||
|
||||
# UP035 on py313+ only
|
||||
from typing_extensions import get_type_hints
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/15780
|
||||
from typing_extensions import is_typeddict
|
||||
|
||||
@@ -102,3 +102,6 @@ with open("furb129.py") as f:
|
||||
pass
|
||||
for line in(f).readlines():
|
||||
pass
|
||||
|
||||
# Test case for issue #17683 (missing space before keyword)
|
||||
print([line for line in f.readlines()if True])
|
||||
|
||||
@@ -65,7 +65,7 @@ use crate::docstrings::extraction::ExtractionTarget;
|
||||
use crate::importer::{ImportRequest, Importer, ResolutionError};
|
||||
use crate::noqa::NoqaMapping;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::{is_semantic_errors_enabled, is_undefined_export_in_dunder_init_enabled};
|
||||
use crate::preview::is_undefined_export_in_dunder_init_enabled;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pyflakes::rules::{
|
||||
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
|
||||
@@ -663,9 +663,7 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
|
||||
| SemanticSyntaxErrorKind::DuplicateParameter(_)
|
||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
||||
if is_semantic_errors_enabled(self.settings) {
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::fix::edits::delete_comment;
|
||||
use crate::noqa::{
|
||||
Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping,
|
||||
};
|
||||
use crate::preview::is_check_file_level_directives_enabled;
|
||||
use crate::registry::{AsRule, Rule, RuleSet};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
use crate::rules::pygrep_hooks;
|
||||
@@ -112,25 +111,16 @@ pub(crate) fn check_noqa(
|
||||
&& !exemption.includes(Rule::UnusedNOQA)
|
||||
&& !per_file_ignores.contains(Rule::UnusedNOQA)
|
||||
{
|
||||
let directives: Vec<_> = if is_check_file_level_directives_enabled(settings) {
|
||||
noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.directive, &line.matches, false))
|
||||
.chain(
|
||||
file_noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.parsed_file_exemption, &line.matches, true)),
|
||||
)
|
||||
.collect()
|
||||
} else {
|
||||
noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.directive, &line.matches, false))
|
||||
.collect()
|
||||
};
|
||||
let directives = noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.directive, &line.matches, false))
|
||||
.chain(
|
||||
file_noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.parsed_file_exemption, &line.matches, true)),
|
||||
);
|
||||
for (directive, matches, is_file_level) in directives {
|
||||
match directive {
|
||||
Directive::All(directive) => {
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
/// `--select`. For pylint this is e.g. C0414 and E0118 but also C and E01.
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use strum_macros::{AsRefStr, EnumIter};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use crate::registry::Linter;
|
||||
use crate::rule_selector::is_single_rule_selector;
|
||||
use crate::rules;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct NoqaCode(&'static str, &'static str);
|
||||
|
||||
impl NoqaCode {
|
||||
@@ -1019,13 +1019,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "049") => (RuleGroup::Preview, rules::ruff::rules::DataclassEnum),
|
||||
(Ruff, "051") => (RuleGroup::Stable, rules::ruff::rules::IfKeyInDictDel),
|
||||
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
|
||||
(Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
|
||||
(Ruff, "053") => (RuleGroup::Stable, rules::ruff::rules::ClassWithMixedTypeVars),
|
||||
(Ruff, "054") => (RuleGroup::Preview, rules::ruff::rules::IndentedFormFeed),
|
||||
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
|
||||
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
|
||||
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),
|
||||
(Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip),
|
||||
(Ruff, "059") => (RuleGroup::Stable, rules::ruff::rules::UnusedUnpackedVariable),
|
||||
(Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable),
|
||||
(Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
||||
@@ -1121,7 +1121,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "132") => (RuleGroup::Preview, rules::refurb::rules::CheckAndRemoveFromSet),
|
||||
(Refurb, "136") => (RuleGroup::Stable, rules::refurb::rules::IfExprMinMax),
|
||||
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
|
||||
(Refurb, "142") => (RuleGroup::Preview, rules::refurb::rules::ForLoopSetMutations),
|
||||
(Refurb, "142") => (RuleGroup::Stable, rules::refurb::rules::ForLoopSetMutations),
|
||||
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
|
||||
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
|
||||
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
|
||||
@@ -1129,7 +1129,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "156") => (RuleGroup::Preview, rules::refurb::rules::HardcodedStringCharset),
|
||||
(Refurb, "157") => (RuleGroup::Preview, rules::refurb::rules::VerboseDecimalConstructor),
|
||||
(Refurb, "161") => (RuleGroup::Stable, rules::refurb::rules::BitCount),
|
||||
(Refurb, "162") => (RuleGroup::Preview, rules::refurb::rules::FromisoformatReplaceZ),
|
||||
(Refurb, "162") => (RuleGroup::Stable, rules::refurb::rules::FromisoformatReplaceZ),
|
||||
(Refurb, "163") => (RuleGroup::Stable, rules::refurb::rules::RedundantLogBase),
|
||||
(Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat),
|
||||
(Refurb, "166") => (RuleGroup::Preview, rules::refurb::rules::IntOnSlicedStr),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::{IsolationLevel, SourceMap};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
@@ -59,13 +59,13 @@ fn apply_fixes<'a>(
|
||||
let mut last_pos: Option<TextSize> = None;
|
||||
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
|
||||
let mut isolated: FxHashSet<u32> = FxHashSet::default();
|
||||
let mut fixed = FxHashMap::default();
|
||||
let mut fixed = FixTable::default();
|
||||
let mut source_map = SourceMap::default();
|
||||
|
||||
for (rule, fix) in diagnostics
|
||||
.filter_map(|msg| msg.to_rule().map(|rule| (rule, msg)))
|
||||
.filter_map(|(rule, diagnostic)| diagnostic.fix().map(|fix| (rule, fix)))
|
||||
.sorted_by(|(rule1, fix1), (rule2, fix2)| cmp_fix(*rule1, *rule2, fix1, fix2))
|
||||
for (code, name, fix) in diagnostics
|
||||
.filter_map(|msg| msg.noqa_code().map(|code| (code, msg.name(), msg)))
|
||||
.filter_map(|(code, name, diagnostic)| diagnostic.fix().map(|fix| (code, name, fix)))
|
||||
.sorted_by(|(_, name1, fix1), (_, name2, fix2)| cmp_fix(name1, name2, fix1, fix2))
|
||||
{
|
||||
let mut edits = fix
|
||||
.edits()
|
||||
@@ -110,7 +110,7 @@ fn apply_fixes<'a>(
|
||||
}
|
||||
|
||||
applied.extend(applied_edits.drain(..));
|
||||
*fixed.entry(rule).or_default() += 1;
|
||||
*fixed.entry(code).or_default(name) += 1;
|
||||
}
|
||||
|
||||
// Add the remaining content.
|
||||
@@ -125,34 +125,44 @@ fn apply_fixes<'a>(
|
||||
}
|
||||
|
||||
/// Compare two fixes.
|
||||
fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering {
|
||||
fn cmp_fix(name1: &str, name2: &str, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering {
|
||||
// Always apply `RedefinedWhileUnused` before `UnusedImport`, as the latter can end up fixing
|
||||
// the former. But we can't apply this just for `RedefinedWhileUnused` and `UnusedImport` because it violates
|
||||
// `< is transitive: a < b and b < c implies a < c. The same must hold for both == and >.`
|
||||
// See https://github.com/astral-sh/ruff/issues/12469#issuecomment-2244392085
|
||||
match (rule1, rule2) {
|
||||
(Rule::RedefinedWhileUnused, Rule::RedefinedWhileUnused) => std::cmp::Ordering::Equal,
|
||||
(Rule::RedefinedWhileUnused, _) => std::cmp::Ordering::Less,
|
||||
(_, Rule::RedefinedWhileUnused) => std::cmp::Ordering::Greater,
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
let redefined_while_unused = Rule::RedefinedWhileUnused.name().as_str();
|
||||
if (name1, name2) == (redefined_while_unused, redefined_while_unused) {
|
||||
std::cmp::Ordering::Equal
|
||||
} else if name1 == redefined_while_unused {
|
||||
std::cmp::Ordering::Less
|
||||
} else if name2 == redefined_while_unused {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
// Apply fixes in order of their start position.
|
||||
.then_with(|| fix1.min_start().cmp(&fix2.min_start()))
|
||||
// Break ties in the event of overlapping rules, for some specific combinations.
|
||||
.then_with(|| match (&rule1, &rule2) {
|
||||
.then_with(|| {
|
||||
let rules = (name1, name2);
|
||||
// Apply `MissingTrailingPeriod` fixes before `NewLineAfterLastParagraph` fixes.
|
||||
(Rule::MissingTrailingPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less,
|
||||
(Rule::NewLineAfterLastParagraph, Rule::MissingTrailingPeriod) => {
|
||||
let missing_trailing_period = Rule::MissingTrailingPeriod.name().as_str();
|
||||
let newline_after_last_paragraph = Rule::NewLineAfterLastParagraph.name().as_str();
|
||||
let if_else_instead_of_dict_get = Rule::IfElseBlockInsteadOfDictGet.name().as_str();
|
||||
let if_else_instead_of_if_exp = Rule::IfElseBlockInsteadOfIfExp.name().as_str();
|
||||
if rules == (missing_trailing_period, newline_after_last_paragraph) {
|
||||
std::cmp::Ordering::Less
|
||||
} else if rules == (newline_after_last_paragraph, missing_trailing_period) {
|
||||
std::cmp::Ordering::Greater
|
||||
}
|
||||
// Apply `IfElseBlockInsteadOfDictGet` fixes before `IfElseBlockInsteadOfIfExp` fixes.
|
||||
(Rule::IfElseBlockInsteadOfDictGet, Rule::IfElseBlockInsteadOfIfExp) => {
|
||||
else if rules == (if_else_instead_of_dict_get, if_else_instead_of_if_exp) {
|
||||
std::cmp::Ordering::Less
|
||||
}
|
||||
(Rule::IfElseBlockInsteadOfIfExp, Rule::IfElseBlockInsteadOfDictGet) => {
|
||||
} else if rules == (if_else_instead_of_if_exp, if_else_instead_of_dict_get) {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,7 +207,7 @@ mod tests {
|
||||
source_map,
|
||||
} = apply_fixes(diagnostics.iter(), &locator);
|
||||
assert_eq!(code, "");
|
||||
assert_eq!(fixes.values().sum::<usize>(), 0);
|
||||
assert_eq!(fixes.counts().sum::<usize>(), 0);
|
||||
assert!(source_map.markers().is_empty());
|
||||
}
|
||||
|
||||
@@ -234,7 +244,7 @@ print("hello world")
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
assert_eq!(fixes.values().sum::<usize>(), 1);
|
||||
assert_eq!(fixes.counts().sum::<usize>(), 1);
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
@@ -275,7 +285,7 @@ class A(Bar):
|
||||
"
|
||||
.trim(),
|
||||
);
|
||||
assert_eq!(fixes.values().sum::<usize>(), 1);
|
||||
assert_eq!(fixes.counts().sum::<usize>(), 1);
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
@@ -312,7 +322,7 @@ class A:
|
||||
"
|
||||
.trim()
|
||||
);
|
||||
assert_eq!(fixes.values().sum::<usize>(), 1);
|
||||
assert_eq!(fixes.counts().sum::<usize>(), 1);
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
@@ -353,7 +363,7 @@ class A(object):
|
||||
"
|
||||
.trim()
|
||||
);
|
||||
assert_eq!(fixes.values().sum::<usize>(), 2);
|
||||
assert_eq!(fixes.counts().sum::<usize>(), 2);
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
@@ -395,7 +405,7 @@ class A:
|
||||
"
|
||||
.trim(),
|
||||
);
|
||||
assert_eq!(fixes.values().sum::<usize>(), 1);
|
||||
assert_eq!(fixes.counts().sum::<usize>(), 1);
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -22,13 +23,14 @@ use crate::checkers::imports::check_imports;
|
||||
use crate::checkers::noqa::check_noqa;
|
||||
use crate::checkers::physical_lines::check_physical_lines;
|
||||
use crate::checkers::tokens::check_tokens;
|
||||
use crate::codes::NoqaCode;
|
||||
use crate::directives::Directives;
|
||||
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
|
||||
use crate::fix::{FixResult, fix_file};
|
||||
use crate::message::Message;
|
||||
use crate::noqa::add_noqa;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::{is_py314_support_enabled, is_unsupported_syntax_enabled};
|
||||
use crate::preview::is_py314_support_enabled;
|
||||
use crate::registry::{AsRule, Rule, RuleSet};
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule};
|
||||
@@ -84,7 +86,53 @@ impl LinterResult {
|
||||
}
|
||||
}
|
||||
|
||||
pub type FixTable = FxHashMap<Rule, usize>;
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct FixCount {
|
||||
rule_name: &'static str,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
/// A mapping from a noqa code to the corresponding lint name and a count of applied fixes.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct FixTable(FxHashMap<NoqaCode, FixCount>);
|
||||
|
||||
impl FixTable {
|
||||
pub fn counts(&self) -> impl Iterator<Item = usize> {
|
||||
self.0.values().map(|fc| fc.count)
|
||||
}
|
||||
|
||||
pub fn entry(&mut self, code: NoqaCode) -> FixTableEntry {
|
||||
FixTableEntry(self.0.entry(code))
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (NoqaCode, &'static str, usize)> {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|(code, FixCount { rule_name, count })| (*code, *rule_name, *count))
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> impl Iterator<Item = NoqaCode> {
|
||||
self.0.keys().copied()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FixTableEntry<'a>(Entry<'a, NoqaCode, FixCount>);
|
||||
|
||||
impl<'a> FixTableEntry<'a> {
|
||||
pub fn or_default(self, rule_name: &'static str) -> &'a mut usize {
|
||||
&mut (self
|
||||
.0
|
||||
.or_insert(FixCount {
|
||||
rule_name,
|
||||
count: 0,
|
||||
})
|
||||
.count)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FixerResult<'a> {
|
||||
/// The result returned by the linter, after applying any fixes.
|
||||
@@ -399,11 +447,7 @@ pub fn check_path(
|
||||
}
|
||||
}
|
||||
|
||||
let syntax_errors = if is_unsupported_syntax_enabled(settings) {
|
||||
parsed.unsupported_syntax_errors()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
let syntax_errors = parsed.unsupported_syntax_errors();
|
||||
|
||||
diagnostics_to_messages(
|
||||
diagnostics,
|
||||
@@ -581,7 +625,7 @@ pub fn lint_fix<'a>(
|
||||
let mut transformed = Cow::Borrowed(source_kind);
|
||||
|
||||
// Track the number of fixed errors across iterations.
|
||||
let mut fixed = FxHashMap::default();
|
||||
let mut fixed = FixTable::default();
|
||||
|
||||
// As an escape hatch, bail after 100 iterations.
|
||||
let mut iterations = 0;
|
||||
@@ -650,12 +694,7 @@ pub fn lint_fix<'a>(
|
||||
// syntax error. Return the original code.
|
||||
if has_valid_syntax && has_no_syntax_errors {
|
||||
if let Some(error) = parsed.errors().first() {
|
||||
report_fix_syntax_error(
|
||||
path,
|
||||
transformed.source_code(),
|
||||
error,
|
||||
fixed.keys().copied(),
|
||||
);
|
||||
report_fix_syntax_error(path, transformed.source_code(), error, fixed.keys());
|
||||
return Err(anyhow!("Fix introduced a syntax error"));
|
||||
}
|
||||
}
|
||||
@@ -670,8 +709,8 @@ pub fn lint_fix<'a>(
|
||||
{
|
||||
if iterations < MAX_ITERATIONS {
|
||||
// Count the number of fixed errors.
|
||||
for (rule, count) in applied {
|
||||
*fixed.entry(rule).or_default() += count;
|
||||
for (rule, name, count) in applied.iter() {
|
||||
*fixed.entry(rule).or_default(name) += count;
|
||||
}
|
||||
|
||||
transformed = Cow::Owned(transformed.updated(fixed_contents, &source_map));
|
||||
@@ -698,10 +737,10 @@ pub fn lint_fix<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_rule_codes(rules: impl IntoIterator<Item = Rule>) -> String {
|
||||
fn collect_rule_codes(rules: impl IntoIterator<Item = NoqaCode>) -> String {
|
||||
rules
|
||||
.into_iter()
|
||||
.map(|rule| rule.noqa_code().to_string())
|
||||
.map(|rule| rule.to_string())
|
||||
.sorted_unstable()
|
||||
.dedup()
|
||||
.join(", ")
|
||||
@@ -709,7 +748,7 @@ fn collect_rule_codes(rules: impl IntoIterator<Item = Rule>) -> String {
|
||||
|
||||
#[expect(clippy::print_stderr)]
|
||||
fn report_failed_to_converge_error(path: &Path, transformed: &str, messages: &[Message]) {
|
||||
let codes = collect_rule_codes(messages.iter().filter_map(Message::to_rule));
|
||||
let codes = collect_rule_codes(messages.iter().filter_map(Message::noqa_code));
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!(
|
||||
"{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
|
||||
@@ -745,7 +784,7 @@ fn report_fix_syntax_error(
|
||||
path: &Path,
|
||||
transformed: &str,
|
||||
error: &ParseError,
|
||||
rules: impl IntoIterator<Item = Rule>,
|
||||
rules: impl IntoIterator<Item = NoqaCode>,
|
||||
) {
|
||||
let codes = collect_rule_codes(rules);
|
||||
if cfg!(debug_assertions) {
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Emitter for AzureEmitter {
|
||||
line = location.line,
|
||||
col = location.column,
|
||||
code = message
|
||||
.to_noqa_code()
|
||||
.noqa_code()
|
||||
.map_or_else(String::new, |code| format!("code={code};")),
|
||||
body = message.body(),
|
||||
)?;
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Emitter for GithubEmitter {
|
||||
writer,
|
||||
"::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
|
||||
code = message
|
||||
.to_noqa_code()
|
||||
.noqa_code()
|
||||
.map_or_else(String::new, |code| format!(" ({code})")),
|
||||
file = message.filename(),
|
||||
row = source_location.line,
|
||||
@@ -50,7 +50,7 @@ impl Emitter for GithubEmitter {
|
||||
column = location.column,
|
||||
)?;
|
||||
|
||||
if let Some(code) = message.to_noqa_code() {
|
||||
if let Some(code) = message.noqa_code() {
|
||||
write!(writer, " {code}")?;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ impl Serialize for SerializedMessages<'_> {
|
||||
}
|
||||
fingerprints.insert(message_fingerprint);
|
||||
|
||||
let (description, check_name) = if let Some(code) = message.to_noqa_code() {
|
||||
let (description, check_name) = if let Some(code) = message.noqa_code() {
|
||||
(message.body().to_string(), code.to_string())
|
||||
} else {
|
||||
let description = message.body();
|
||||
|
||||
@@ -81,8 +81,8 @@ pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext)
|
||||
}
|
||||
|
||||
json!({
|
||||
"code": message.to_noqa_code().map(|code| code.to_string()),
|
||||
"url": message.to_rule().and_then(|rule| rule.url()),
|
||||
"code": message.noqa_code().map(|code| code.to_string()),
|
||||
"url": message.to_url(),
|
||||
"message": message.body(),
|
||||
"fix": fix,
|
||||
"cell": notebook_cell_index,
|
||||
|
||||
@@ -59,7 +59,7 @@ impl Emitter for JunitEmitter {
|
||||
body = message.body()
|
||||
));
|
||||
let mut case = TestCase::new(
|
||||
if let Some(code) = message.to_noqa_code() {
|
||||
if let Some(code) = message.noqa_code() {
|
||||
format!("org.ruff.{code}")
|
||||
} else {
|
||||
"org.ruff".to_string()
|
||||
|
||||
@@ -224,30 +224,22 @@ impl Message {
|
||||
self.fix().is_some()
|
||||
}
|
||||
|
||||
/// Returns the [`Rule`] corresponding to the diagnostic message.
|
||||
pub fn to_rule(&self) -> Option<Rule> {
|
||||
if self.is_syntax_error() {
|
||||
None
|
||||
} else {
|
||||
Some(self.name().parse().expect("Expected a valid rule name"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`NoqaCode`] corresponding to the diagnostic message.
|
||||
pub fn to_noqa_code(&self) -> Option<NoqaCode> {
|
||||
pub fn noqa_code(&self) -> Option<NoqaCode> {
|
||||
self.noqa_code
|
||||
}
|
||||
|
||||
/// Returns the URL for the rule documentation, if it exists.
|
||||
pub fn to_url(&self) -> Option<String> {
|
||||
// TODO(brent) Rule::url calls Rule::explanation, which calls ViolationMetadata::explain,
|
||||
// which when derived (seems always to be the case?) is always `Some`, so I think it's
|
||||
// pretty safe to inline the Rule::url implementation here, using `self.name()`:
|
||||
//
|
||||
// format!("{}/rules/{}", env!("CARGO_PKG_HOMEPAGE"), self.name())
|
||||
//
|
||||
// at least in the case of diagnostics, I guess syntax errors will return None
|
||||
self.to_rule().and_then(|rule| rule.url())
|
||||
if self.is_syntax_error() {
|
||||
None
|
||||
} else {
|
||||
Some(format!(
|
||||
"{}/rules/{}",
|
||||
env!("CARGO_PKG_HOMEPAGE"),
|
||||
self.name()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the filename for the message.
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Emitter for PylintEmitter {
|
||||
message.compute_start_location().line
|
||||
};
|
||||
|
||||
let body = if let Some(code) = message.to_noqa_code() {
|
||||
let body = if let Some(code) = message.noqa_code() {
|
||||
format!("[{code}] {body}", body = message.body())
|
||||
} else {
|
||||
message.body().to_string()
|
||||
|
||||
@@ -71,7 +71,7 @@ fn message_to_rdjson_value(message: &Message) -> Value {
|
||||
"range": rdjson_range(start_location, end_location),
|
||||
},
|
||||
"code": {
|
||||
"value": message.to_noqa_code().map(|code| code.to_string()),
|
||||
"value": message.noqa_code().map(|code| code.to_string()),
|
||||
"url": message.to_url(),
|
||||
},
|
||||
"suggestions": rdjson_suggestions(fix.edits(), &source_code),
|
||||
@@ -84,7 +84,7 @@ fn message_to_rdjson_value(message: &Message) -> Value {
|
||||
"range": rdjson_range(start_location, end_location),
|
||||
},
|
||||
"code": {
|
||||
"value": message.to_noqa_code().map(|code| code.to_string()),
|
||||
"value": message.noqa_code().map(|code| code.to_string()),
|
||||
"url": message.to_url(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde_json::json;
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
use crate::VERSION;
|
||||
use crate::codes::Rule;
|
||||
use crate::codes::NoqaCode;
|
||||
use crate::fs::normalize_path;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::{Linter, RuleNamespace};
|
||||
@@ -27,7 +27,7 @@ impl Emitter for SarifEmitter {
|
||||
.map(SarifResult::from_message)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.rule).collect();
|
||||
let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.code).collect();
|
||||
let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
|
||||
rules.sort_by(|a, b| a.code.cmp(&b.code));
|
||||
|
||||
@@ -61,13 +61,19 @@ struct SarifRule<'a> {
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Rule> for SarifRule<'_> {
|
||||
fn from(rule: Rule) -> Self {
|
||||
let code = rule.noqa_code().to_string();
|
||||
let (linter, _) = Linter::parse_code(&code).unwrap();
|
||||
impl From<NoqaCode> for SarifRule<'_> {
|
||||
fn from(code: NoqaCode) -> Self {
|
||||
let code_str = code.to_string();
|
||||
// This is a manual re-implementation of Rule::from_code, but we also want the Linter. This
|
||||
// avoids calling Linter::parse_code twice.
|
||||
let (linter, suffix) = Linter::parse_code(&code_str).unwrap();
|
||||
let rule = linter
|
||||
.all_rules()
|
||||
.find(|rule| rule.noqa_code().suffix() == suffix)
|
||||
.expect("Expected a valid noqa code corresponding to a rule");
|
||||
Self {
|
||||
name: rule.into(),
|
||||
code,
|
||||
code: code_str,
|
||||
linter: linter.name(),
|
||||
summary: rule.message_formats()[0],
|
||||
explanation: rule.explanation(),
|
||||
@@ -106,7 +112,7 @@ impl Serialize for SarifRule<'_> {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SarifResult {
|
||||
rule: Option<Rule>,
|
||||
code: Option<NoqaCode>,
|
||||
level: String,
|
||||
message: String,
|
||||
uri: String,
|
||||
@@ -123,7 +129,7 @@ impl SarifResult {
|
||||
let end_location = message.compute_end_location();
|
||||
let path = normalize_path(&*message.filename());
|
||||
Ok(Self {
|
||||
rule: message.to_rule(),
|
||||
code: message.noqa_code(),
|
||||
level: "error".to_string(),
|
||||
message: message.body().to_string(),
|
||||
uri: url::Url::from_file_path(&path)
|
||||
@@ -143,7 +149,7 @@ impl SarifResult {
|
||||
let end_location = message.compute_end_location();
|
||||
let path = normalize_path(&*message.filename());
|
||||
Ok(Self {
|
||||
rule: message.to_rule(),
|
||||
code: message.noqa_code(),
|
||||
level: "error".to_string(),
|
||||
message: message.body().to_string(),
|
||||
uri: path.display().to_string(),
|
||||
@@ -178,7 +184,7 @@ impl Serialize for SarifResult {
|
||||
}
|
||||
}
|
||||
}],
|
||||
"ruleId": self.rule.map(|rule| rule.noqa_code().to_string()),
|
||||
"ruleId": self.code.map(|code| code.to_string()),
|
||||
})
|
||||
.serialize(serializer)
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ impl Display for RuleCodeAndBody<'_> {
|
||||
if let Some(fix) = self.message.fix() {
|
||||
// Do not display an indicator for inapplicable fixes
|
||||
if fix.applies(self.unsafe_fixes.required_applicability()) {
|
||||
if let Some(code) = self.message.to_noqa_code() {
|
||||
if let Some(code) = self.message.noqa_code() {
|
||||
write!(f, "{} ", code.to_string().red().bold())?;
|
||||
}
|
||||
return write!(
|
||||
@@ -164,7 +164,7 @@ impl Display for RuleCodeAndBody<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(code) = self.message.to_noqa_code() {
|
||||
if let Some(code) = self.message.noqa_code() {
|
||||
write!(
|
||||
f,
|
||||
"{code} {body}",
|
||||
@@ -254,7 +254,7 @@ impl Display for MessageCodeFrame<'_> {
|
||||
|
||||
let label = self
|
||||
.message
|
||||
.to_noqa_code()
|
||||
.noqa_code()
|
||||
.map_or_else(String::new, |code| code.to_string());
|
||||
|
||||
let line_start = self.notebook_index.map_or_else(
|
||||
|
||||
@@ -12,13 +12,14 @@ use log::warn;
|
||||
use ruff_python_trivia::{CommentRanges, Cursor, indentation_at_offset};
|
||||
use ruff_source_file::{LineEnding, LineRanges};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::Edit;
|
||||
use crate::Locator;
|
||||
use crate::codes::NoqaCode;
|
||||
use crate::fs::relativize_path;
|
||||
use crate::message::Message;
|
||||
use crate::registry::{Rule, RuleSet};
|
||||
use crate::registry::Rule;
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
|
||||
/// Generates an array of edits that matches the length of `messages`.
|
||||
@@ -780,7 +781,7 @@ fn build_noqa_edits_by_diagnostic(
|
||||
if let Some(noqa_edit) = generate_noqa_edit(
|
||||
comment.directive,
|
||||
comment.line,
|
||||
RuleSet::from_rule(comment.rule),
|
||||
FxHashSet::from_iter([comment.code]),
|
||||
locator,
|
||||
line_ending,
|
||||
) {
|
||||
@@ -816,7 +817,7 @@ fn build_noqa_edits_by_line<'a>(
|
||||
offset,
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|NoqaComment { rule, .. }| rule)
|
||||
.map(|NoqaComment { code, .. }| code)
|
||||
.collect(),
|
||||
locator,
|
||||
line_ending,
|
||||
@@ -829,7 +830,7 @@ fn build_noqa_edits_by_line<'a>(
|
||||
|
||||
struct NoqaComment<'a> {
|
||||
line: TextSize,
|
||||
rule: Rule,
|
||||
code: NoqaCode,
|
||||
directive: Option<&'a Directive<'a>>,
|
||||
}
|
||||
|
||||
@@ -845,13 +846,11 @@ fn find_noqa_comments<'a>(
|
||||
|
||||
// Mark any non-ignored diagnostics.
|
||||
for message in messages {
|
||||
let Some(rule) = message.to_rule() else {
|
||||
let Some(code) = message.noqa_code() else {
|
||||
comments_by_line.push(None);
|
||||
continue;
|
||||
};
|
||||
|
||||
let code = rule.noqa_code();
|
||||
|
||||
match &exemption {
|
||||
FileExemption::All(_) => {
|
||||
// If the file is exempted, don't add any noqa directives.
|
||||
@@ -900,7 +899,7 @@ fn find_noqa_comments<'a>(
|
||||
if !codes.includes(code) {
|
||||
comments_by_line.push(Some(NoqaComment {
|
||||
line: directive_line.start(),
|
||||
rule,
|
||||
code,
|
||||
directive: Some(directive),
|
||||
}));
|
||||
}
|
||||
@@ -912,7 +911,7 @@ fn find_noqa_comments<'a>(
|
||||
// There's no existing noqa directive that suppresses the diagnostic.
|
||||
comments_by_line.push(Some(NoqaComment {
|
||||
line: locator.line_start(noqa_offset),
|
||||
rule,
|
||||
code,
|
||||
directive: None,
|
||||
}));
|
||||
}
|
||||
@@ -922,7 +921,7 @@ fn find_noqa_comments<'a>(
|
||||
|
||||
struct NoqaEdit<'a> {
|
||||
edit_range: TextRange,
|
||||
rules: RuleSet,
|
||||
noqa_codes: FxHashSet<NoqaCode>,
|
||||
codes: Option<&'a Codes<'a>>,
|
||||
line_ending: LineEnding,
|
||||
}
|
||||
@@ -941,18 +940,15 @@ impl NoqaEdit<'_> {
|
||||
Some(codes) => {
|
||||
push_codes(
|
||||
writer,
|
||||
self.rules
|
||||
self.noqa_codes
|
||||
.iter()
|
||||
.map(|rule| rule.noqa_code().to_string())
|
||||
.map(ToString::to_string)
|
||||
.chain(codes.iter().map(ToString::to_string))
|
||||
.sorted_unstable(),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
push_codes(
|
||||
writer,
|
||||
self.rules.iter().map(|rule| rule.noqa_code().to_string()),
|
||||
);
|
||||
push_codes(writer, self.noqa_codes.iter().map(ToString::to_string));
|
||||
}
|
||||
}
|
||||
write!(writer, "{}", self.line_ending.as_str()).unwrap();
|
||||
@@ -968,7 +964,7 @@ impl Ranged for NoqaEdit<'_> {
|
||||
fn generate_noqa_edit<'a>(
|
||||
directive: Option<&'a Directive>,
|
||||
offset: TextSize,
|
||||
rules: RuleSet,
|
||||
noqa_codes: FxHashSet<NoqaCode>,
|
||||
locator: &Locator,
|
||||
line_ending: LineEnding,
|
||||
) -> Option<NoqaEdit<'a>> {
|
||||
@@ -997,7 +993,7 @@ fn generate_noqa_edit<'a>(
|
||||
|
||||
Some(NoqaEdit {
|
||||
edit_range,
|
||||
rules,
|
||||
noqa_codes,
|
||||
codes,
|
||||
line_ending,
|
||||
})
|
||||
|
||||
@@ -7,17 +7,6 @@
|
||||
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
// https://github.com/astral-sh/ruff/issues/17412
|
||||
// https://github.com/astral-sh/ruff/issues/11934
|
||||
pub(crate) const fn is_semantic_errors_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/16429
|
||||
pub(crate) const fn is_unsupported_syntax_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
@@ -29,23 +18,11 @@ pub(crate) const fn is_full_path_match_source_strategy_enabled(settings: &Linter
|
||||
|
||||
// Rule-specific behavior
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/17136
|
||||
pub(crate) const fn is_shell_injection_only_trusted_input_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/15541
|
||||
pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/7501
|
||||
pub(crate) const fn is_bool_subtype_of_annotation_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/10759
|
||||
pub(crate) const fn is_comprehension_with_min_max_sum_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
@@ -63,21 +40,11 @@ pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSet
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/12676
|
||||
pub(crate) const fn is_fix_future_annotations_in_stub_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/11074
|
||||
pub(crate) const fn is_only_add_return_none_at_end_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/12796
|
||||
pub(crate) const fn is_simplify_ternary_to_binary_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/16719
|
||||
pub(crate) const fn is_fix_manual_dict_comprehension_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
@@ -104,13 +71,6 @@ pub(crate) const fn is_unicode_to_unicode_confusables_enabled(settings: &LinterS
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/17078
|
||||
pub(crate) const fn is_support_slices_in_literal_concatenation_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/11370
|
||||
pub(crate) const fn is_undefined_export_in_dunder_init_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
@@ -121,16 +81,9 @@ pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) ->
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/17061
|
||||
pub(crate) const fn is_check_file_level_directives_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// https://github.com/astral-sh/ruff/pull/18208
|
||||
pub(crate) const fn is_multiple_with_statements_fix_safe_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Remnant of the registry of all [`Rule`] implementations, now it's reexporting from codes.rs
|
||||
//! with some helper symbols
|
||||
|
||||
use ruff_db::diagnostic::LintName;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
pub use codes::Rule;
|
||||
@@ -348,9 +349,18 @@ impl Rule {
|
||||
|
||||
/// Return the URL for the rule documentation, if it exists.
|
||||
pub fn url(&self) -> Option<String> {
|
||||
self.explanation()
|
||||
.is_some()
|
||||
.then(|| format!("{}/rules/{}", env!("CARGO_PKG_HOMEPAGE"), self.as_ref()))
|
||||
self.explanation().is_some().then(|| {
|
||||
format!(
|
||||
"{}/rules/{name}",
|
||||
env!("CARGO_PKG_HOMEPAGE"),
|
||||
name = self.name()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn name(&self) -> LintName {
|
||||
let name: &'static str = self.into();
|
||||
LintName::of(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,7 +431,7 @@ pub mod clap_completion {
|
||||
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
|
||||
Some(Box::new(Rule::iter().map(|rule| {
|
||||
let name = rule.noqa_code().to_string();
|
||||
let help = rule.as_ref().to_string();
|
||||
let help = rule.name().as_str();
|
||||
PossibleValue::new(name).help(help)
|
||||
})))
|
||||
}
|
||||
@@ -443,7 +453,7 @@ mod tests {
|
||||
assert!(
|
||||
rule.explanation().is_some(),
|
||||
"Rule {} is missing documentation",
|
||||
rule.as_ref()
|
||||
rule.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -460,10 +470,10 @@ mod tests {
|
||||
.collect();
|
||||
|
||||
for rule in Rule::iter() {
|
||||
let rule_name = rule.as_ref();
|
||||
let rule_name = rule.name();
|
||||
for pattern in &patterns {
|
||||
assert!(
|
||||
!pattern.matches(rule_name),
|
||||
!pattern.matches(&rule_name),
|
||||
"{rule_name} does not match naming convention, see CONTRIBUTING.md"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -302,9 +302,8 @@ impl Display for RuleSet {
|
||||
} else {
|
||||
writeln!(f, "[")?;
|
||||
for rule in self {
|
||||
let name = rule.as_ref();
|
||||
let code = rule.noqa_code();
|
||||
writeln!(f, "\t{name} ({code}),")?;
|
||||
writeln!(f, "\t{name} ({code}),", name = rule.name())?;
|
||||
}
|
||||
write!(f, "]")?;
|
||||
}
|
||||
|
||||
@@ -485,8 +485,7 @@ pub mod clap_completion {
|
||||
prefix.linter().common_prefix(),
|
||||
prefix.short_code()
|
||||
);
|
||||
let name: &'static str = rule.into();
|
||||
return Some(PossibleValue::new(code).help(name));
|
||||
return Some(PossibleValue::new(code).help(rule.name().as_str()));
|
||||
}
|
||||
|
||||
None
|
||||
|
||||
@@ -3,7 +3,6 @@ pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -18,7 +17,7 @@ mod tests {
|
||||
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
|
||||
#[test_case(Rule::FastApiUnusedPathParameter, Path::new("FAST003.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("fastapi").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
@@ -32,7 +31,7 @@ mod tests {
|
||||
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))]
|
||||
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
|
||||
fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}_py38", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("fastapi").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
|
||||
@@ -104,7 +104,6 @@ mod tests {
|
||||
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
|
||||
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
|
||||
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -7,7 +7,6 @@ use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::preview::is_shell_injection_only_trusted_input_enabled;
|
||||
use crate::{
|
||||
checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal,
|
||||
};
|
||||
@@ -325,9 +324,7 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) {
|
||||
}
|
||||
// S603
|
||||
_ => {
|
||||
if !is_trusted_input(arg)
|
||||
|| !is_shell_injection_only_trusted_input_enabled(checker.settings)
|
||||
{
|
||||
if !is_trusted_input(arg) {
|
||||
if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) {
|
||||
checker.report_diagnostic(
|
||||
SubprocessWithoutShellEqualsTrue,
|
||||
|
||||
@@ -106,74 +106,6 @@ S603.py:21:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
23 | # Literals are fine, they're trusted.
|
||||
|
|
||||
|
||||
S603.py:24:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
23 | # Literals are fine, they're trusted.
|
||||
24 | run("true")
|
||||
| ^^^ S603
|
||||
25 | Popen(["true"])
|
||||
26 | Popen("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:25:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
23 | # Literals are fine, they're trusted.
|
||||
24 | run("true")
|
||||
25 | Popen(["true"])
|
||||
| ^^^^^ S603
|
||||
26 | Popen("true", shell=False)
|
||||
27 | call("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:26:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
24 | run("true")
|
||||
25 | Popen(["true"])
|
||||
26 | Popen("true", shell=False)
|
||||
| ^^^^^ S603
|
||||
27 | call("true", shell=False)
|
||||
28 | check_call("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:27:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
25 | Popen(["true"])
|
||||
26 | Popen("true", shell=False)
|
||||
27 | call("true", shell=False)
|
||||
| ^^^^ S603
|
||||
28 | check_call("true", shell=False)
|
||||
29 | check_output("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:28:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
26 | Popen("true", shell=False)
|
||||
27 | call("true", shell=False)
|
||||
28 | check_call("true", shell=False)
|
||||
| ^^^^^^^^^^ S603
|
||||
29 | check_output("true", shell=False)
|
||||
30 | run("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:29:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
27 | call("true", shell=False)
|
||||
28 | check_call("true", shell=False)
|
||||
29 | check_output("true", shell=False)
|
||||
| ^^^^^^^^^^^^ S603
|
||||
30 | run("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:30:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
28 | check_call("true", shell=False)
|
||||
29 | check_output("true", shell=False)
|
||||
30 | run("true", shell=False)
|
||||
| ^^^ S603
|
||||
31 |
|
||||
32 | # Not through assignments though.
|
||||
|
|
||||
|
||||
S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
32 | # Not through assignments though.
|
||||
@@ -184,15 +116,6 @@ S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
36 | # Instant named expressions are fine.
|
||||
|
|
||||
|
||||
S603.py:37:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
36 | # Instant named expressions are fine.
|
||||
37 | run(c := "true")
|
||||
| ^^^ S603
|
||||
38 |
|
||||
39 | # But non-instant are not.
|
||||
|
|
||||
|
||||
S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
39 | # But non-instant are not.
|
||||
@@ -200,20 +123,3 @@ S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
41 | run(e)
|
||||
| ^^^ S603
|
||||
|
|
||||
|
||||
S603.py:46:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
44 | # https://github.com/astral-sh/ruff/issues/17798
|
||||
45 | # Tuple literals are trusted
|
||||
46 | check_output(("literal", "cmd", "using", "tuple"), text=True)
|
||||
| ^^^^^^^^^^^^ S603
|
||||
47 | Popen(("literal", "cmd", "using", "tuple"))
|
||||
|
|
||||
|
||||
S603.py:47:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
45 | # Tuple literals are trusted
|
||||
46 | check_output(("literal", "cmd", "using", "tuple"), text=True)
|
||||
47 | Popen(("literal", "cmd", "using", "tuple"))
|
||||
| ^^^^^ S603
|
||||
|
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S603.py:5:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
3 | # Different Popen wrappers are checked.
|
||||
4 | a = input()
|
||||
5 | Popen(a, shell=False)
|
||||
| ^^^^^ S603
|
||||
6 | call(a, shell=False)
|
||||
7 | check_call(a, shell=False)
|
||||
|
|
||||
|
||||
S603.py:6:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
4 | a = input()
|
||||
5 | Popen(a, shell=False)
|
||||
6 | call(a, shell=False)
|
||||
| ^^^^ S603
|
||||
7 | check_call(a, shell=False)
|
||||
8 | check_output(a, shell=False)
|
||||
|
|
||||
|
||||
S603.py:7:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
5 | Popen(a, shell=False)
|
||||
6 | call(a, shell=False)
|
||||
7 | check_call(a, shell=False)
|
||||
| ^^^^^^^^^^ S603
|
||||
8 | check_output(a, shell=False)
|
||||
9 | run(a, shell=False)
|
||||
|
|
||||
|
||||
S603.py:8:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
6 | call(a, shell=False)
|
||||
7 | check_call(a, shell=False)
|
||||
8 | check_output(a, shell=False)
|
||||
| ^^^^^^^^^^^^ S603
|
||||
9 | run(a, shell=False)
|
||||
|
|
||||
|
||||
S603.py:9:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
7 | check_call(a, shell=False)
|
||||
8 | check_output(a, shell=False)
|
||||
9 | run(a, shell=False)
|
||||
| ^^^ S603
|
||||
10 |
|
||||
11 | # Falsey values are treated as false.
|
||||
|
|
||||
|
||||
S603.py:12:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
11 | # Falsey values are treated as false.
|
||||
12 | Popen(a, shell=0)
|
||||
| ^^^^^ S603
|
||||
13 | Popen(a, shell=[])
|
||||
14 | Popen(a, shell={})
|
||||
|
|
||||
|
||||
S603.py:13:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
11 | # Falsey values are treated as false.
|
||||
12 | Popen(a, shell=0)
|
||||
13 | Popen(a, shell=[])
|
||||
| ^^^^^ S603
|
||||
14 | Popen(a, shell={})
|
||||
15 | Popen(a, shell=None)
|
||||
|
|
||||
|
||||
S603.py:14:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
12 | Popen(a, shell=0)
|
||||
13 | Popen(a, shell=[])
|
||||
14 | Popen(a, shell={})
|
||||
| ^^^^^ S603
|
||||
15 | Popen(a, shell=None)
|
||||
|
|
||||
|
||||
S603.py:15:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
13 | Popen(a, shell=[])
|
||||
14 | Popen(a, shell={})
|
||||
15 | Popen(a, shell=None)
|
||||
| ^^^^^ S603
|
||||
16 |
|
||||
17 | # Unknown values are treated as falsey.
|
||||
|
|
||||
|
||||
S603.py:18:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
17 | # Unknown values are treated as falsey.
|
||||
18 | Popen(a, shell=True if True else False)
|
||||
| ^^^^^ S603
|
||||
19 |
|
||||
20 | # No value is also caught.
|
||||
|
|
||||
|
||||
S603.py:21:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
20 | # No value is also caught.
|
||||
21 | Popen(a)
|
||||
| ^^^^^ S603
|
||||
22 |
|
||||
23 | # Literals are fine, they're trusted.
|
||||
|
|
||||
|
||||
S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
32 | # Not through assignments though.
|
||||
33 | cmd = ["true"]
|
||||
34 | run(cmd)
|
||||
| ^^^ S603
|
||||
35 |
|
||||
36 | # Instant named expressions are fine.
|
||||
|
|
||||
|
||||
S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
39 | # But non-instant are not.
|
||||
40 | (e := "echo")
|
||||
41 | run(e)
|
||||
| ^^^ S603
|
||||
|
|
||||
@@ -12,7 +12,6 @@ mod tests {
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -29,24 +28,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_boolean_trap").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_allowed_callable() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -7,12 +7,12 @@ use ruff_python_semantic::analyze::visibility;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_bool_subtype_of_annotation_enabled;
|
||||
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of boolean positional arguments in function definitions,
|
||||
/// as determined by the presence of a `bool` type hint.
|
||||
/// as determined by the presence of a type hint containing `bool` as an
|
||||
/// evident subtype - e.g. `bool`, `bool | int`, `typing.Optional[bool]`, etc.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean positional arguments is confusing as the
|
||||
@@ -30,9 +30,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// Dunder methods that define operators are exempt from this rule, as are
|
||||
/// setters and `@override` definitions.
|
||||
///
|
||||
/// In [preview], this rule will also flag annotations that include boolean
|
||||
/// variants, like `bool | int`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
@@ -96,8 +93,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// ## References
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct BooleanTypeHintPositionalArgument;
|
||||
|
||||
@@ -128,14 +123,8 @@ pub(crate) fn boolean_type_hint_positional_argument(
|
||||
let Some(annotation) = parameter.annotation() else {
|
||||
continue;
|
||||
};
|
||||
if is_bool_subtype_of_annotation_enabled(checker.settings) {
|
||||
if !match_annotation_to_complex_bool(annotation, checker.semantic()) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if !match_annotation_to_literal_bool(annotation) {
|
||||
continue;
|
||||
}
|
||||
if !match_annotation_to_complex_bool(annotation, checker.semantic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow Boolean type hints in setters.
|
||||
@@ -161,17 +150,6 @@ pub(crate) fn boolean_type_hint_positional_argument(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the annotation is a boolean type hint (e.g., `bool`).
|
||||
fn match_annotation_to_literal_bool(annotation: &Expr) -> bool {
|
||||
match annotation {
|
||||
// Ex) `True`
|
||||
Expr::Name(name) => &name.id == "bool",
|
||||
// Ex) `"True"`
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "bool",
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the annotation is a boolean type hint (e.g., `bool`), or a type hint that
|
||||
/// includes boolean as a variant (e.g., `bool | int`).
|
||||
fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
@@ -89,3 +88,17 @@ FBT.py:90:19: FBT001 Boolean-typed positional argument in function definition
|
||||
| ^^^^^ FBT001
|
||||
91 | pass
|
||||
|
|
||||
|
||||
FBT.py:100:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
100 | def func(x: Union[list, Optional[int | str | float | bool]]):
|
||||
| ^ FBT001
|
||||
101 | pass
|
||||
|
|
||||
|
||||
FBT.py:104:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
104 | def func(x: bool | str):
|
||||
| ^ FBT001
|
||||
105 | pass
|
||||
|
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
2 | posonly_nohint,
|
||||
3 | posonly_nonboolhint: int,
|
||||
4 | posonly_boolhint: bool,
|
||||
| ^^^^^^^^^^^^^^^^ FBT001
|
||||
5 | posonly_boolstrhint: "bool",
|
||||
6 | /,
|
||||
|
|
||||
|
||||
FBT.py:5:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
3 | posonly_nonboolhint: int,
|
||||
4 | posonly_boolhint: bool,
|
||||
5 | posonly_boolstrhint: "bool",
|
||||
| ^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
6 | /,
|
||||
7 | offset,
|
||||
|
|
||||
|
||||
FBT.py:10:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
8 | posorkw_nonvalued_nohint,
|
||||
9 | posorkw_nonvalued_nonboolhint: int,
|
||||
10 | posorkw_nonvalued_boolhint: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
11 | posorkw_nonvalued_boolstrhint: "bool",
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
|
|
||||
|
||||
FBT.py:11:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
9 | posorkw_nonvalued_nonboolhint: int,
|
||||
10 | posorkw_nonvalued_boolhint: bool,
|
||||
11 | posorkw_nonvalued_boolstrhint: "bool",
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
|
|
||||
|
||||
FBT.py:14:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
|
|
||||
|
||||
FBT.py:15:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
|
|
||||
|
||||
FBT.py:18:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
18 | posorkw_nonboolvalued_boolhint: bool = 3,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
|
||||
20 | *,
|
||||
|
|
||||
|
||||
FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
18 | posorkw_nonboolvalued_boolhint: bool = 3,
|
||||
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
20 | *,
|
||||
21 | kwonly_nonvalued_nohint,
|
||||
|
|
||||
|
||||
FBT.py:90:19: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
89 | # FBT001: Boolean positional arg in function definition
|
||||
90 | def foo(self, value: bool) -> None:
|
||||
| ^^^^^ FBT001
|
||||
91 | pass
|
||||
|
|
||||
|
||||
FBT.py:100:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
100 | def func(x: Union[list, Optional[int | str | float | bool]]):
|
||||
| ^ FBT001
|
||||
101 | pass
|
||||
|
|
||||
|
||||
FBT.py:104:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
104 | def func(x: bool | str):
|
||||
| ^ FBT001
|
||||
105 | pass
|
||||
|
|
||||
@@ -16,7 +16,7 @@ mod tests {
|
||||
#[test_case(Rule::LineContainsTodo; "T003")]
|
||||
#[test_case(Rule::LineContainsXxx; "T004")]
|
||||
fn rules(rule_code: Rule) -> Result<()> {
|
||||
let snapshot = format!("{}_T00.py", rule_code.as_ref());
|
||||
let snapshot = format!("{}_T00.py", rule_code.name());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_fixme/T00.py"),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
|
||||
@@ -29,7 +29,7 @@ mod tests {
|
||||
#[test_case(Rule::FormatInGetTextFuncCall, Path::new("INT002.py"))]
|
||||
#[test_case(Rule::PrintfInGetTextFuncCall, Path::new("INT003.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_gettext").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
|
||||
@@ -11,7 +11,6 @@ mod tests {
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pep8_naming;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -172,22 +171,4 @@ mod tests {
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_pyi").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use ruff_python_ast::StmtImportFrom;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
|
||||
use crate::{Fix, FixAvailability, Violation};
|
||||
use crate::{checkers::ast::Checker, fix, preview::is_fix_future_annotations_in_stub_enabled};
|
||||
use crate::{checkers::ast::Checker, fix};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of the `from __future__ import annotations` import
|
||||
@@ -55,20 +55,18 @@ pub(crate) fn from_future_import(checker: &Checker, target: &StmtImportFrom) {
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(FutureAnnotationsInStub, *range);
|
||||
|
||||
if is_fix_future_annotations_in_stub_enabled(checker.settings) {
|
||||
let stmt = checker.semantic().current_statement();
|
||||
let stmt = checker.semantic().current_statement();
|
||||
|
||||
diagnostic.try_set_fix(|| {
|
||||
let edit = fix::edits::remove_unused_imports(
|
||||
std::iter::once("annotations"),
|
||||
stmt,
|
||||
None,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
)?;
|
||||
diagnostic.try_set_fix(|| {
|
||||
let edit = fix::edits::remove_unused_imports(
|
||||
std::iter::once("annotations"),
|
||||
stmt,
|
||||
None,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
)?;
|
||||
|
||||
Ok(Fix::safe_edit(edit))
|
||||
});
|
||||
}
|
||||
Ok(Fix::safe_edit(edit))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
PYI044.pyi:2:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
|
|
||||
1 | # Bad import.
|
||||
2 | from __future__ import annotations # PYI044.
|
||||
@@ -10,7 +10,14 @@ PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stu
|
||||
|
|
||||
= help: Remove `from __future__ import annotations`
|
||||
|
||||
PYI044.pyi:3:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
ℹ Safe fix
|
||||
1 1 | # Bad import.
|
||||
2 |-from __future__ import annotations # PYI044.
|
||||
3 2 | from __future__ import annotations, with_statement # PYI044.
|
||||
4 3 |
|
||||
5 4 | # Good imports.
|
||||
|
||||
PYI044.pyi:3:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
|
|
||||
1 | # Bad import.
|
||||
2 | from __future__ import annotations # PYI044.
|
||||
@@ -20,3 +27,12 @@ PYI044.pyi:3:1: PYI044 `from __future__ import annotations` has no effect in stu
|
||||
5 | # Good imports.
|
||||
|
|
||||
= help: Remove `from __future__ import annotations`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Bad import.
|
||||
2 2 | from __future__ import annotations # PYI044.
|
||||
3 |-from __future__ import annotations, with_statement # PYI044.
|
||||
3 |+from __future__ import with_statement # PYI044.
|
||||
4 4 |
|
||||
5 5 | # Good imports.
|
||||
6 6 | from __future__ import with_statement
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI044.pyi:2:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
|
|
||||
1 | # Bad import.
|
||||
2 | from __future__ import annotations # PYI044.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044
|
||||
3 | from __future__ import annotations, with_statement # PYI044.
|
||||
|
|
||||
= help: Remove `from __future__ import annotations`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Bad import.
|
||||
2 |-from __future__ import annotations # PYI044.
|
||||
3 2 | from __future__ import annotations, with_statement # PYI044.
|
||||
4 3 |
|
||||
5 4 | # Good imports.
|
||||
|
||||
PYI044.pyi:3:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
|
|
||||
1 | # Bad import.
|
||||
2 | from __future__ import annotations # PYI044.
|
||||
3 | from __future__ import annotations, with_statement # PYI044.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044
|
||||
4 |
|
||||
5 | # Good imports.
|
||||
|
|
||||
= help: Remove `from __future__ import annotations`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Bad import.
|
||||
2 2 | from __future__ import annotations # PYI044.
|
||||
3 |-from __future__ import annotations, with_statement # PYI044.
|
||||
3 |+from __future__ import with_statement # PYI044.
|
||||
4 4 |
|
||||
5 5 | # Good imports.
|
||||
6 6 | from __future__ import with_statement
|
||||
@@ -3,7 +3,6 @@ pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -15,7 +14,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::UnnecessaryParenOnRaiseException, Path::new("RSE102.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_raise").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
|
||||
@@ -4,7 +4,6 @@ pub mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::registry::Rule;
|
||||
@@ -18,7 +17,7 @@ mod tests {
|
||||
#[test_case(Rule::PrivateMemberAccess, Path::new("SLF001.py"))]
|
||||
#[test_case(Rule::PrivateMemberAccess, Path::new("SLF001_1.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_self").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
|
||||
@@ -58,7 +58,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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!(
|
||||
|
||||
@@ -10,7 +10,7 @@ use super::fix_with;
|
||||
use crate::Fix;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::fits;
|
||||
use crate::preview::multiple_with_statements_fix_safe_enabled;
|
||||
use crate::preview::is_multiple_with_statements_fix_safe_enabled;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -195,7 +195,7 @@ pub(crate) fn multiple_with_statements(
|
||||
checker.settings.tab_size,
|
||||
)
|
||||
}) {
|
||||
if multiple_with_statements_fix_safe_enabled(checker.settings) {
|
||||
if is_multiple_with_statements_fix_safe_enabled(checker.settings) {
|
||||
Ok(Some(Fix::safe_edit(edit)))
|
||||
} else {
|
||||
Ok(Some(Fix::unsafe_edit(edit)))
|
||||
|
||||
@@ -7,13 +7,14 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::fits;
|
||||
use crate::preview::is_simplify_ternary_to_binary_enabled;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Check for `if`-`else`-blocks that can be replaced with a ternary operator.
|
||||
/// Moreover, in [preview], check if these ternary expressions can be
|
||||
/// further simplified to binary expressions.
|
||||
/// Check for `if`-`else`-blocks that can be replaced with a ternary
|
||||
/// or binary operator.
|
||||
///
|
||||
/// The lint is suppressed if the suggested replacement would exceed
|
||||
/// the maximum line length configured in [pycodestyle.max-line-length].
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `if`-`else`-blocks that assign a value to a variable in both branches can
|
||||
@@ -33,7 +34,7 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// bar = x if foo else y
|
||||
/// ```
|
||||
///
|
||||
/// Or, in [preview]:
|
||||
/// Or:
|
||||
///
|
||||
/// ```python
|
||||
/// if cond:
|
||||
@@ -57,8 +58,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// ## References
|
||||
/// - [Python documentation: Conditional expressions](https://docs.python.org/3/reference/expressions.html#conditional-expressions)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
/// [code coverage]: https://github.com/nedbat/coveragepy/issues/509
|
||||
/// [pycodestyle.max-line-length]: https://docs.astral.sh/ruff/settings/#lint_pycodestyle_max-line-length
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct IfElseBlockInsteadOfIfExp {
|
||||
/// The ternary or binary expression to replace the `if`-`else`-block.
|
||||
@@ -183,16 +184,12 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast::
|
||||
//
|
||||
// The match statement below implements the following
|
||||
// logic:
|
||||
// - If `test == body_value` and preview enabled, replace with `target_var = test or else_value`
|
||||
// - If `test == not body_value` and preview enabled, replace with `target_var = body_value and else_value`
|
||||
// - If `not test == body_value` and preview enabled, replace with `target_var = body_value and else_value`
|
||||
// - If `test == body_value`, replace with `target_var = test or else_value`
|
||||
// - If `test == not body_value`, replace with `target_var = body_value and else_value`
|
||||
// - If `not test == body_value`, replace with `target_var = body_value and else_value`
|
||||
// - Otherwise, replace with `target_var = body_value if test else else_value`
|
||||
let (contents, assignment_kind) = match (
|
||||
is_simplify_ternary_to_binary_enabled(checker.settings),
|
||||
test,
|
||||
body_value,
|
||||
) {
|
||||
(true, test_node, body_node)
|
||||
let (contents, assignment_kind) = match (test, body_value) {
|
||||
(test_node, body_node)
|
||||
if ComparableExpr::from(test_node) == ComparableExpr::from(body_node)
|
||||
&& !contains_effect(test_node, |id| checker.semantic().has_builtin_binding(id)) =>
|
||||
{
|
||||
@@ -200,7 +197,7 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast::
|
||||
let binary = assignment_binary_or(target_var, body_value, else_value);
|
||||
(checker.generator().stmt(&binary), AssignmentKind::Binary)
|
||||
}
|
||||
(true, test_node, body_node)
|
||||
(test_node, body_node)
|
||||
if (test_node.as_unary_op_expr().is_some_and(|op_expr| {
|
||||
op_expr.op.is_not()
|
||||
&& ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(body_node)
|
||||
|
||||
@@ -118,7 +118,7 @@ SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_cond` instead of `if`-`else`-block
|
||||
SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
139 | # SIM108 - should suggest
|
||||
140 | # z = cond or other_cond
|
||||
@@ -130,7 +130,7 @@ SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_co
|
||||
145 |
|
||||
146 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond if cond else other_cond`
|
||||
= help: Replace `if`-`else`-block with `z = cond or other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
138 138 |
|
||||
@@ -140,12 +140,12 @@ SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_co
|
||||
142 |- z = cond
|
||||
143 |-else:
|
||||
144 |- z = other_cond
|
||||
141 |+z = cond if cond else other_cond
|
||||
141 |+z = cond or other_cond
|
||||
145 142 |
|
||||
146 143 | # SIM108 - should suggest
|
||||
147 144 | # z = cond and other_cond
|
||||
|
||||
SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else other_cond` instead of `if`-`else`-block
|
||||
SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
146 | # SIM108 - should suggest
|
||||
147 | # z = cond and other_cond
|
||||
@@ -157,7 +157,7 @@ SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else othe
|
||||
152 |
|
||||
153 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond if not cond else other_cond`
|
||||
= help: Replace `if`-`else`-block with `z = cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
145 145 |
|
||||
@@ -167,12 +167,12 @@ SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else othe
|
||||
149 |- z = cond
|
||||
150 |-else:
|
||||
151 |- z = other_cond
|
||||
148 |+z = cond if not cond else other_cond
|
||||
148 |+z = cond and other_cond
|
||||
152 149 |
|
||||
153 150 | # SIM108 - should suggest
|
||||
154 151 | # z = not cond and other_cond
|
||||
|
||||
SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else other_cond` instead of `if`-`else`-block
|
||||
SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
153 | # SIM108 - should suggest
|
||||
154 | # z = not cond and other_cond
|
||||
@@ -184,7 +184,7 @@ SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else othe
|
||||
159 |
|
||||
160 | # SIM108 does not suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not cond if cond else other_cond`
|
||||
= help: Replace `if`-`else`-block with `z = not cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
152 152 |
|
||||
@@ -194,7 +194,7 @@ SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else othe
|
||||
156 |- z = not cond
|
||||
157 |-else:
|
||||
158 |- z = other_cond
|
||||
155 |+z = not cond if cond else other_cond
|
||||
155 |+z = not cond and other_cond
|
||||
159 156 |
|
||||
160 157 | # SIM108 does not suggest
|
||||
161 158 | # a binary option in these cases,
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM108.py:2:1: SIM108 [*] Use ternary operator `b = c if a else d` instead of `if`-`else`-block
|
||||
|
|
||||
1 | # SIM108
|
||||
2 | / if a:
|
||||
3 | | b = c
|
||||
4 | | else:
|
||||
5 | | b = d
|
||||
| |_________^ SIM108
|
||||
6 |
|
||||
7 | # OK
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = c if a else d`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 1 | # SIM108
|
||||
2 |-if a:
|
||||
3 |- b = c
|
||||
4 |-else:
|
||||
5 |- b = d
|
||||
2 |+b = c if a else d
|
||||
6 3 |
|
||||
7 4 | # OK
|
||||
8 5 | b = c if a else d
|
||||
|
||||
SIM108.py:30:5: SIM108 [*] Use ternary operator `b = 1 if a else 2` instead of `if`-`else`-block
|
||||
|
|
||||
28 | pass
|
||||
29 | else:
|
||||
30 | / if a:
|
||||
31 | | b = 1
|
||||
32 | | else:
|
||||
33 | | b = 2
|
||||
| |_____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = 1 if a else 2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
27 27 | if True:
|
||||
28 28 | pass
|
||||
29 29 | else:
|
||||
30 |- if a:
|
||||
31 |- b = 1
|
||||
32 |- else:
|
||||
33 |- b = 2
|
||||
30 |+ b = 1 if a else 2
|
||||
34 31 |
|
||||
35 32 |
|
||||
36 33 | import sys
|
||||
|
||||
SIM108.py:58:1: SIM108 Use ternary operator `abc = x if x > 0 else -x` instead of `if`-`else`-block
|
||||
|
|
||||
57 | # SIM108 (without fix due to comments)
|
||||
58 | / if x > 0:
|
||||
59 | | # test test
|
||||
60 | | abc = x
|
||||
61 | | else:
|
||||
62 | | # test test test
|
||||
63 | | abc = -x
|
||||
| |____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `abc = x if x > 0 else -x`
|
||||
|
||||
SIM108.py:82:1: SIM108 [*] Use ternary operator `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"` instead of `if`-`else`-block
|
||||
|
|
||||
81 | # SIM108
|
||||
82 | / if a:
|
||||
83 | | b = "cccccccccccccccccccccccccccccccccß"
|
||||
84 | | else:
|
||||
85 | | b = "ddddddddddddddddddddddddddddddddd💣"
|
||||
| |_____________________________________________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"`
|
||||
|
||||
ℹ Unsafe fix
|
||||
79 79 |
|
||||
80 80 |
|
||||
81 81 | # SIM108
|
||||
82 |-if a:
|
||||
83 |- b = "cccccccccccccccccccccccccccccccccß"
|
||||
84 |-else:
|
||||
85 |- b = "ddddddddddddddddddddddddddddddddd💣"
|
||||
82 |+b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"
|
||||
86 83 |
|
||||
87 84 |
|
||||
88 85 | # OK (too long)
|
||||
|
||||
SIM108.py:105:1: SIM108 Use ternary operator `exitcode = 0 if True else 1` instead of `if`-`else`-block
|
||||
|
|
||||
104 | # SIM108 (without fix due to trailing comment)
|
||||
105 | / if True:
|
||||
106 | | exitcode = 0
|
||||
107 | | else:
|
||||
108 | | exitcode = 1 # Trailing comment
|
||||
| |________________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `exitcode = 0 if True else 1`
|
||||
|
||||
SIM108.py:112:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block
|
||||
|
|
||||
111 | # SIM108
|
||||
112 | / if True: x = 3 # Foo
|
||||
113 | | else: x = 5
|
||||
| |___________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block
|
||||
|
|
||||
116 | # SIM108
|
||||
117 | / if True: # Foo
|
||||
118 | | x = 3
|
||||
119 | | else:
|
||||
120 | | x = 5
|
||||
| |_________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
139 | # SIM108 - should suggest
|
||||
140 | # z = cond or other_cond
|
||||
141 | / if cond:
|
||||
142 | | z = cond
|
||||
143 | | else:
|
||||
144 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
145 |
|
||||
146 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond or other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
138 138 |
|
||||
139 139 | # SIM108 - should suggest
|
||||
140 140 | # z = cond or other_cond
|
||||
141 |-if cond:
|
||||
142 |- z = cond
|
||||
143 |-else:
|
||||
144 |- z = other_cond
|
||||
141 |+z = cond or other_cond
|
||||
145 142 |
|
||||
146 143 | # SIM108 - should suggest
|
||||
147 144 | # z = cond and other_cond
|
||||
|
||||
SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
146 | # SIM108 - should suggest
|
||||
147 | # z = cond and other_cond
|
||||
148 | / if not cond:
|
||||
149 | | z = cond
|
||||
150 | | else:
|
||||
151 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
152 |
|
||||
153 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
145 145 |
|
||||
146 146 | # SIM108 - should suggest
|
||||
147 147 | # z = cond and other_cond
|
||||
148 |-if not cond:
|
||||
149 |- z = cond
|
||||
150 |-else:
|
||||
151 |- z = other_cond
|
||||
148 |+z = cond and other_cond
|
||||
152 149 |
|
||||
153 150 | # SIM108 - should suggest
|
||||
154 151 | # z = not cond and other_cond
|
||||
|
||||
SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
153 | # SIM108 - should suggest
|
||||
154 | # z = not cond and other_cond
|
||||
155 | / if cond:
|
||||
156 | | z = not cond
|
||||
157 | | else:
|
||||
158 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
159 |
|
||||
160 | # SIM108 does not suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
152 152 |
|
||||
153 153 | # SIM108 - should suggest
|
||||
154 154 | # z = not cond and other_cond
|
||||
155 |-if cond:
|
||||
156 |- z = not cond
|
||||
157 |-else:
|
||||
158 |- z = other_cond
|
||||
155 |+z = not cond and other_cond
|
||||
159 156 |
|
||||
160 157 | # SIM108 does not suggest
|
||||
161 158 | # a binary option in these cases,
|
||||
|
||||
SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` instead of `if`-`else`-block
|
||||
|
|
||||
165 | # (Of course, these specific expressions
|
||||
166 | # should be simplified for other reasons...)
|
||||
167 | / if True:
|
||||
168 | | z = 1
|
||||
169 | | else:
|
||||
170 | | z = other
|
||||
| |_____________^ SIM108
|
||||
171 |
|
||||
172 | if False:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = 1 if True else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
164 164 | # so, e.g. `True == 1`.
|
||||
165 165 | # (Of course, these specific expressions
|
||||
166 166 | # should be simplified for other reasons...)
|
||||
167 |-if True:
|
||||
168 |- z = 1
|
||||
169 |-else:
|
||||
170 |- z = other
|
||||
167 |+z = 1 if True else other
|
||||
171 168 |
|
||||
172 169 | if False:
|
||||
173 170 | z = 1
|
||||
|
||||
SIM108.py:172:1: SIM108 [*] Use ternary operator `z = 1 if False else other` instead of `if`-`else`-block
|
||||
|
|
||||
170 | z = other
|
||||
171 |
|
||||
172 | / if False:
|
||||
173 | | z = 1
|
||||
174 | | else:
|
||||
175 | | z = other
|
||||
| |_____________^ SIM108
|
||||
176 |
|
||||
177 | if 1:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = 1 if False else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
169 169 | else:
|
||||
170 170 | z = other
|
||||
171 171 |
|
||||
172 |-if False:
|
||||
173 |- z = 1
|
||||
174 |-else:
|
||||
175 |- z = other
|
||||
172 |+z = 1 if False else other
|
||||
176 173 |
|
||||
177 174 | if 1:
|
||||
178 175 | z = True
|
||||
|
||||
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
|
||||
|
|
||||
175 | z = other
|
||||
176 |
|
||||
177 | / if 1:
|
||||
178 | | z = True
|
||||
179 | | else:
|
||||
180 | | z = other
|
||||
| |_____________^ SIM108
|
||||
181 |
|
||||
182 | # SIM108 does not suggest a binary option in this
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = True if 1 else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
174 174 | else:
|
||||
175 175 | z = other
|
||||
176 176 |
|
||||
177 |-if 1:
|
||||
178 |- z = True
|
||||
179 |-else:
|
||||
180 |- z = other
|
||||
177 |+z = True if 1 else other
|
||||
181 178 |
|
||||
182 179 | # SIM108 does not suggest a binary option in this
|
||||
183 180 | # case, since we'd be reducing the number of calls
|
||||
|
||||
SIM108.py:185:1: SIM108 [*] Use ternary operator `z = foo() if foo() else other` instead of `if`-`else`-block
|
||||
|
|
||||
183 | # case, since we'd be reducing the number of calls
|
||||
184 | # from Two to one.
|
||||
185 | / if foo():
|
||||
186 | | z = foo()
|
||||
187 | | else:
|
||||
188 | | z = other
|
||||
| |_____________^ SIM108
|
||||
189 |
|
||||
190 | # SIM108 does not suggest a binary option in this
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = foo() if foo() else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
182 182 | # SIM108 does not suggest a binary option in this
|
||||
183 183 | # case, since we'd be reducing the number of calls
|
||||
184 184 | # from Two to one.
|
||||
185 |-if foo():
|
||||
186 |- z = foo()
|
||||
187 |-else:
|
||||
188 |- z = other
|
||||
185 |+z = foo() if foo() else other
|
||||
189 186 |
|
||||
190 187 | # SIM108 does not suggest a binary option in this
|
||||
191 188 | # case, since we'd be reducing the number of calls
|
||||
|
||||
SIM108.py:193:1: SIM108 [*] Use ternary operator `z = not foo() if foo() else other` instead of `if`-`else`-block
|
||||
|
|
||||
191 | # case, since we'd be reducing the number of calls
|
||||
192 | # from Two to one.
|
||||
193 | / if foo():
|
||||
194 | | z = not foo()
|
||||
195 | | else:
|
||||
196 | | z = other
|
||||
| |_____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not foo() if foo() else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
190 190 | # SIM108 does not suggest a binary option in this
|
||||
191 191 | # case, since we'd be reducing the number of calls
|
||||
192 192 | # from Two to one.
|
||||
193 |-if foo():
|
||||
194 |- z = not foo()
|
||||
195 |-else:
|
||||
196 |- z = other
|
||||
193 |+z = not foo() if foo() else other
|
||||
197 194 |
|
||||
198 195 |
|
||||
199 196 | # These two cases double as tests for f-string quote preservation. The first
|
||||
|
||||
SIM108.py:202:1: SIM108 [*] Use ternary operator `var = "str" if cond else f"{first}-{second}"` instead of `if`-`else`-block
|
||||
|
|
||||
200 | # f-string should preserve its double quotes, and the second should preserve
|
||||
201 | # single quotes
|
||||
202 | / if cond:
|
||||
203 | | var = "str"
|
||||
204 | | else:
|
||||
205 | | var = f"{first}-{second}"
|
||||
| |_____________________________^ SIM108
|
||||
206 |
|
||||
207 | if cond:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `var = "str" if cond else f"{first}-{second}"`
|
||||
|
||||
ℹ Unsafe fix
|
||||
199 199 | # These two cases double as tests for f-string quote preservation. The first
|
||||
200 200 | # f-string should preserve its double quotes, and the second should preserve
|
||||
201 201 | # single quotes
|
||||
202 |-if cond:
|
||||
203 |- var = "str"
|
||||
204 |-else:
|
||||
205 |- var = f"{first}-{second}"
|
||||
202 |+var = "str" if cond else f"{first}-{second}"
|
||||
206 203 |
|
||||
207 204 | if cond:
|
||||
208 205 | var = "str"
|
||||
|
||||
SIM108.py:207:1: SIM108 [*] Use ternary operator `var = "str" if cond else f'{first}-{second}'` instead of `if`-`else`-block
|
||||
|
|
||||
205 | var = f"{first}-{second}"
|
||||
206 |
|
||||
207 | / if cond:
|
||||
208 | | var = "str"
|
||||
209 | | else:
|
||||
210 | | var = f'{first}-{second}'
|
||||
| |_____________________________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `var = "str" if cond else f'{first}-{second}'`
|
||||
|
||||
ℹ Unsafe fix
|
||||
204 204 | else:
|
||||
205 205 | var = f"{first}-{second}"
|
||||
206 206 |
|
||||
207 |-if cond:
|
||||
208 |- var = "str"
|
||||
209 |-else:
|
||||
210 |- var = f'{first}-{second}'
|
||||
207 |+var = "str" if cond else f'{first}-{second}'
|
||||
@@ -2,7 +2,6 @@ pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -20,7 +19,7 @@ mod tests {
|
||||
#[test_case(Rule::InvalidTodoCapitalization, Path::new("TD006.py"))]
|
||||
#[test_case(Rule::MissingSpaceAfterTodoColon, Path::new("TD007.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_todos").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
|
||||
@@ -6,7 +6,6 @@ pub mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -55,7 +54,7 @@ mod tests {
|
||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_1.py"))]
|
||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_2.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
@@ -70,7 +69,7 @@ mod tests {
|
||||
#[test_case(Rule::QuotedTypeAlias, Path::new("TC008.py"))]
|
||||
#[test_case(Rule::QuotedTypeAlias, Path::new("TC008_typing_execution_context.py"))]
|
||||
fn type_alias_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rules(vec![
|
||||
@@ -84,11 +83,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::QuotedTypeAlias, Path::new("TC008_union_syntax_pre_py310.py"))]
|
||||
fn type_alias_rules_pre_py310(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"pre_py310_{}_{}",
|
||||
rule_code.as_ref(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let snapshot = format!("pre_py310_{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -107,7 +102,7 @@ mod tests {
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote3.py"))]
|
||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote3.py"))]
|
||||
fn quote(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("quote_{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("quote_{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -126,7 +121,7 @@ mod tests {
|
||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))]
|
||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("kw_only.py"))]
|
||||
fn strict(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("strict_{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("strict_{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -170,7 +165,7 @@ mod tests {
|
||||
Path::new("exempt_type_checking_3.py")
|
||||
)]
|
||||
fn exempt_type_checking(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -207,7 +202,7 @@ mod tests {
|
||||
Path::new("runtime_evaluated_base_classes_5.py")
|
||||
)]
|
||||
fn runtime_evaluated_base_classes(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -238,7 +233,7 @@ mod tests {
|
||||
Path::new("runtime_evaluated_decorators_3.py")
|
||||
)]
|
||||
fn runtime_evaluated_decorators(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -264,7 +259,7 @@ mod tests {
|
||||
Path::new("module/undefined.py")
|
||||
)]
|
||||
fn base_class_same_file(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -282,7 +277,7 @@ mod tests {
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("module/app.py"))]
|
||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("module/routes.py"))]
|
||||
fn decorator_same_file(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
|
||||
@@ -4,7 +4,6 @@ pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -22,7 +21,7 @@ mod tests {
|
||||
#[test_case(Rule::Numpy2Deprecation, Path::new("NPY201_2.py"))]
|
||||
#[test_case(Rule::Numpy2Deprecation, Path::new("NPY201_3.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("numpy").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
|
||||
@@ -4,7 +4,6 @@ pub mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -20,7 +19,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::DocstringMissingException, Path::new("DOC501.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("pydoclint").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
@@ -36,7 +35,7 @@ mod tests {
|
||||
#[test_case(Rule::DocstringMissingException, Path::new("DOC501_google.py"))]
|
||||
#[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_google.py"))]
|
||||
fn rules_google_style(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("pydoclint").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -58,7 +57,7 @@ mod tests {
|
||||
#[test_case(Rule::DocstringMissingException, Path::new("DOC501_numpy.py"))]
|
||||
#[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_numpy.py"))]
|
||||
fn rules_numpy_style(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("pydoclint").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -79,7 +78,7 @@ mod tests {
|
||||
fn rules_google_style_ignore_one_line(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"{}_{}_ignore_one_line",
|
||||
rule_code.as_ref(),
|
||||
rule_code.name(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -776,8 +776,10 @@ mod tests {
|
||||
messages.sort_by_key(Ranged::start);
|
||||
let actual = messages
|
||||
.iter()
|
||||
.filter_map(Message::to_rule)
|
||||
.filter(|msg| !msg.is_syntax_error())
|
||||
.map(Message::name)
|
||||
.collect::<Vec<_>>();
|
||||
let expected: Vec<_> = expected.iter().map(|rule| rule.name().as_str()).collect();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ const TYPING_TO_RE_39: &[&str] = &["Match", "Pattern"];
|
||||
const TYPING_RE_TO_RE_39: &[&str] = &["Match", "Pattern"];
|
||||
|
||||
// Members of `typing_extensions` that were moved to `typing`.
|
||||
const TYPING_EXTENSIONS_TO_TYPING_39: &[&str] = &["Annotated", "get_type_hints"];
|
||||
const TYPING_EXTENSIONS_TO_TYPING_39: &[&str] = &["Annotated"];
|
||||
|
||||
// Members of `typing` that were moved _and_ renamed (and thus cannot be
|
||||
// automatically fixed).
|
||||
@@ -373,6 +373,9 @@ const TYPING_EXTENSIONS_TO_TYPING_313: &[&str] = &[
|
||||
"NoDefault",
|
||||
"ReadOnly",
|
||||
"TypeIs",
|
||||
// Introduced in Python 3.5,
|
||||
// but typing_extensions backports features from py313:
|
||||
"get_type_hints",
|
||||
// Introduced in Python 3.6,
|
||||
// but typing_extensions backports features from py313:
|
||||
"ContextManager",
|
||||
|
||||
@@ -1179,6 +1179,8 @@ UP035.py:111:1: UP035 [*] Import from `warnings` instead: `deprecated`
|
||||
110 | # UP035 on py313+ only
|
||||
111 | from typing_extensions import deprecated
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
112 |
|
||||
113 | # UP035 on py313+ only
|
||||
|
|
||||
= help: Import from `warnings`
|
||||
|
||||
@@ -1189,5 +1191,25 @@ UP035.py:111:1: UP035 [*] Import from `warnings` instead: `deprecated`
|
||||
111 |-from typing_extensions import deprecated
|
||||
111 |+from warnings import deprecated
|
||||
112 112 |
|
||||
113 113 |
|
||||
114 114 | # https://github.com/astral-sh/ruff/issues/15780
|
||||
113 113 | # UP035 on py313+ only
|
||||
114 114 | from typing_extensions import get_type_hints
|
||||
|
||||
UP035.py:114:1: UP035 [*] Import from `typing` instead: `get_type_hints`
|
||||
|
|
||||
113 | # UP035 on py313+ only
|
||||
114 | from typing_extensions import get_type_hints
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
115 |
|
||||
116 | # https://github.com/astral-sh/ruff/issues/15780
|
||||
|
|
||||
= help: Import from `typing`
|
||||
|
||||
ℹ Safe fix
|
||||
111 111 | from typing_extensions import deprecated
|
||||
112 112 |
|
||||
113 113 | # UP035 on py313+ only
|
||||
114 |-from typing_extensions import get_type_hints
|
||||
114 |+from typing import get_type_hints
|
||||
115 115 |
|
||||
116 116 | # https://github.com/astral-sh/ruff/issues/15780
|
||||
117 117 | from typing_extensions import is_typeddict
|
||||
|
||||
@@ -62,24 +62,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::ReadlinesInFor, Path::new("FURB129.py"))]
|
||||
fn preview(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("refurb").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: settings::types::PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_whole_file_python_39() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_python_semantic::analyze::typing::is_io_base_expr;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_readlines_in_for_fix_safe_enabled;
|
||||
use crate::fix::edits::pad_end;
|
||||
use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
|
||||
/// ## What it does
|
||||
@@ -85,21 +85,25 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let edit = if let Some(parenthesized_range) = parenthesized_range(
|
||||
|
||||
let deletion_range = if let Some(parenthesized_range) = parenthesized_range(
|
||||
expr_attr.value.as_ref().into(),
|
||||
expr_attr.into(),
|
||||
checker.comment_ranges(),
|
||||
checker.source(),
|
||||
) {
|
||||
Edit::range_deletion(expr_call.range().add_start(parenthesized_range.len()))
|
||||
expr_call.range().add_start(parenthesized_range.len())
|
||||
} else {
|
||||
Edit::range_deletion(expr_call.range().add_start(expr_attr.value.range().len()))
|
||||
expr_call.range().add_start(expr_attr.value.range().len())
|
||||
};
|
||||
|
||||
let padded = pad_end(String::new(), deletion_range.end(), checker.locator());
|
||||
let edit = if padded.is_empty() {
|
||||
Edit::range_deletion(deletion_range)
|
||||
} else {
|
||||
Edit::range_replacement(padded, deletion_range)
|
||||
};
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(ReadlinesInFor, expr_call.range());
|
||||
diagnostic.set_fix(if is_readlines_in_for_fix_safe_enabled(checker.settings) {
|
||||
Fix::safe_edit(edit)
|
||||
} else {
|
||||
Fix::unsafe_edit(edit)
|
||||
});
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 | # Errors
|
||||
6 6 | with open("FURB129.py") as f:
|
||||
@@ -33,7 +33,7 @@ FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
6 6 | with open("FURB129.py") as f:
|
||||
7 7 | for _line in f.readlines():
|
||||
8 8 | pass
|
||||
@@ -53,7 +53,7 @@ FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
7 7 | for _line in f.readlines():
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
@@ -74,7 +74,7 @@ FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
10 10 | b = {line.upper() for line in f.readlines()}
|
||||
@@ -93,7 +93,7 @@ FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
12 12 |
|
||||
13 13 | with Path("FURB129.py").open() as f:
|
||||
@@ -113,7 +113,7 @@ FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
14 14 | for _line in f.readlines():
|
||||
15 15 | pass
|
||||
16 16 |
|
||||
@@ -133,7 +133,7 @@ FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
17 17 | for _line in open("FURB129.py").readlines():
|
||||
18 18 | pass
|
||||
19 19 |
|
||||
@@ -154,7 +154,7 @@ FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
23 23 |
|
||||
24 24 | def func():
|
||||
25 25 | f = Path("FURB129.py").open()
|
||||
@@ -173,7 +173,7 @@ FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
29 29 |
|
||||
30 30 |
|
||||
31 31 | def func(f: io.BytesIO):
|
||||
@@ -194,7 +194,7 @@ FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
35 35 |
|
||||
36 36 | def func():
|
||||
37 37 | with (open("FURB129.py") as f, foo as bar):
|
||||
@@ -213,7 +213,7 @@ FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
44 44 | import builtins
|
||||
45 45 |
|
||||
46 46 | with builtins.open("FURB129.py") as f:
|
||||
@@ -232,7 +232,7 @@ FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
51 51 | from builtins import open as o
|
||||
52 52 |
|
||||
53 53 | with o("FURB129.py") as f:
|
||||
@@ -252,7 +252,7 @@ FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
90 90 |
|
||||
91 91 | # https://github.com/astral-sh/ruff/issues/18231
|
||||
92 92 | with open("furb129.py") as f:
|
||||
@@ -270,7 +270,7 @@ FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
94 94 | pass
|
||||
95 95 |
|
||||
96 96 | with open("furb129.py") as f:
|
||||
@@ -290,7 +290,7 @@ FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over fi
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
98 98 |
|
||||
99 99 |
|
||||
100 100 | with open("furb129.py") as f:
|
||||
@@ -310,10 +310,27 @@ FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over fi
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
100 100 | with open("furb129.py") as f:
|
||||
101 101 | for line in (((f))).readlines():
|
||||
102 102 | pass
|
||||
103 |- for line in(f).readlines():
|
||||
103 |+ for line in(f):
|
||||
104 104 | pass
|
||||
105 105 |
|
||||
106 106 | # Test case for issue #17683 (missing space before keyword)
|
||||
|
||||
FURB129.py:107:29: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
106 | # Test case for issue #17683 (missing space before keyword)
|
||||
107 | print([line for line in f.readlines()if True])
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
104 104 | pass
|
||||
105 105 |
|
||||
106 106 | # Test case for issue #17683 (missing space before keyword)
|
||||
107 |- print([line for line in f.readlines()if True])
|
||||
107 |+ print([line for line in f if True])
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
5 | # Errors
|
||||
6 | with open("FURB129.py") as f:
|
||||
7 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
8 | pass
|
||||
9 | a = [line.lower() for line in f.readlines()]
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 | # Errors
|
||||
6 6 | with open("FURB129.py") as f:
|
||||
7 |- for _line in f.readlines():
|
||||
7 |+ for _line in f:
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
10 10 | b = {line.upper() for line in f.readlines()}
|
||||
|
||||
FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
7 | for _line in f.readlines():
|
||||
8 | pass
|
||||
9 | a = [line.lower() for line in f.readlines()]
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
10 | b = {line.upper() for line in f.readlines()}
|
||||
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | with open("FURB129.py") as f:
|
||||
7 7 | for _line in f.readlines():
|
||||
8 8 | pass
|
||||
9 |- a = [line.lower() for line in f.readlines()]
|
||||
9 |+ a = [line.lower() for line in f]
|
||||
10 10 | b = {line.upper() for line in f.readlines()}
|
||||
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
12 12 |
|
||||
|
||||
FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
8 | pass
|
||||
9 | a = [line.lower() for line in f.readlines()]
|
||||
10 | b = {line.upper() for line in f.readlines()}
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | for _line in f.readlines():
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
10 |- b = {line.upper() for line in f.readlines()}
|
||||
10 |+ b = {line.upper() for line in f}
|
||||
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
12 12 |
|
||||
13 13 | with Path("FURB129.py").open() as f:
|
||||
|
||||
FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
9 | a = [line.lower() for line in f.readlines()]
|
||||
10 | b = {line.upper() for line in f.readlines()}
|
||||
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
12 |
|
||||
13 | with Path("FURB129.py").open() as f:
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
10 10 | b = {line.upper() for line in f.readlines()}
|
||||
11 |- c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
11 |+ c = {line.lower(): line.upper() for line in f}
|
||||
12 12 |
|
||||
13 13 | with Path("FURB129.py").open() as f:
|
||||
14 14 | for _line in f.readlines():
|
||||
|
||||
FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
13 | with Path("FURB129.py").open() as f:
|
||||
14 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
15 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
12 12 |
|
||||
13 13 | with Path("FURB129.py").open() as f:
|
||||
14 |- for _line in f.readlines():
|
||||
14 |+ for _line in f:
|
||||
15 15 | pass
|
||||
16 16 |
|
||||
17 17 | for _line in open("FURB129.py").readlines():
|
||||
|
||||
FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
15 | pass
|
||||
16 |
|
||||
17 | for _line in open("FURB129.py").readlines():
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129
|
||||
18 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 | for _line in f.readlines():
|
||||
15 15 | pass
|
||||
16 16 |
|
||||
17 |-for _line in open("FURB129.py").readlines():
|
||||
17 |+for _line in open("FURB129.py"):
|
||||
18 18 | pass
|
||||
19 19 |
|
||||
20 20 | for _line in Path("FURB129.py").open().readlines():
|
||||
|
||||
FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
18 | pass
|
||||
19 |
|
||||
20 | for _line in Path("FURB129.py").open().readlines():
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129
|
||||
21 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
17 17 | for _line in open("FURB129.py").readlines():
|
||||
18 18 | pass
|
||||
19 19 |
|
||||
20 |-for _line in Path("FURB129.py").open().readlines():
|
||||
20 |+for _line in Path("FURB129.py").open():
|
||||
21 21 | pass
|
||||
22 22 |
|
||||
23 23 |
|
||||
|
||||
FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
24 | def func():
|
||||
25 | f = Path("FURB129.py").open()
|
||||
26 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
27 | pass
|
||||
28 | f.close()
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
23 23 |
|
||||
24 24 | def func():
|
||||
25 25 | f = Path("FURB129.py").open()
|
||||
26 |- for _line in f.readlines():
|
||||
26 |+ for _line in f:
|
||||
27 27 | pass
|
||||
28 28 | f.close()
|
||||
29 29 |
|
||||
|
||||
FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
31 | def func(f: io.BytesIO):
|
||||
32 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
33 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
29 29 |
|
||||
30 30 |
|
||||
31 31 | def func(f: io.BytesIO):
|
||||
32 |- for _line in f.readlines():
|
||||
32 |+ for _line in f:
|
||||
33 33 | pass
|
||||
34 34 |
|
||||
35 35 |
|
||||
|
||||
FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
36 | def func():
|
||||
37 | with (open("FURB129.py") as f, foo as bar):
|
||||
38 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
39 | pass
|
||||
40 | for _line in bar.readlines():
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
35 35 |
|
||||
36 36 | def func():
|
||||
37 37 | with (open("FURB129.py") as f, foo as bar):
|
||||
38 |- for _line in f.readlines():
|
||||
38 |+ for _line in f:
|
||||
39 39 | pass
|
||||
40 40 | for _line in bar.readlines():
|
||||
41 41 | pass
|
||||
|
||||
FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
46 | with builtins.open("FURB129.py") as f:
|
||||
47 | for line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
48 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
44 44 | import builtins
|
||||
45 45 |
|
||||
46 46 | with builtins.open("FURB129.py") as f:
|
||||
47 |- for line in f.readlines():
|
||||
47 |+ for line in f:
|
||||
48 48 | pass
|
||||
49 49 |
|
||||
50 50 |
|
||||
|
||||
FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
53 | with o("FURB129.py") as f:
|
||||
54 | for line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
55 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
51 51 | from builtins import open as o
|
||||
52 52 |
|
||||
53 53 | with o("FURB129.py") as f:
|
||||
54 |- for line in f.readlines():
|
||||
54 |+ for line in f:
|
||||
55 55 | pass
|
||||
56 56 |
|
||||
57 57 |
|
||||
|
||||
FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
91 | # https://github.com/astral-sh/ruff/issues/18231
|
||||
92 | with open("furb129.py") as f:
|
||||
93 | for line in (f).readlines():
|
||||
| ^^^^^^^^^^^^^^^ FURB129
|
||||
94 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
90 90 |
|
||||
91 91 | # https://github.com/astral-sh/ruff/issues/18231
|
||||
92 92 | with open("furb129.py") as f:
|
||||
93 |- for line in (f).readlines():
|
||||
93 |+ for line in (f):
|
||||
94 94 | pass
|
||||
95 95 |
|
||||
96 96 | with open("furb129.py") as f:
|
||||
|
||||
FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
96 | with open("furb129.py") as f:
|
||||
97 | [line for line in (f).readlines()]
|
||||
| ^^^^^^^^^^^^^^^ FURB129
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
94 94 | pass
|
||||
95 95 |
|
||||
96 96 | with open("furb129.py") as f:
|
||||
97 |- [line for line in (f).readlines()]
|
||||
97 |+ [line for line in (f)]
|
||||
98 98 |
|
||||
99 99 |
|
||||
100 100 | with open("furb129.py") as f:
|
||||
|
||||
FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
100 | with open("furb129.py") as f:
|
||||
101 | for line in (((f))).readlines():
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB129
|
||||
102 | pass
|
||||
103 | for line in(f).readlines():
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
98 98 |
|
||||
99 99 |
|
||||
100 100 | with open("furb129.py") as f:
|
||||
101 |- for line in (((f))).readlines():
|
||||
101 |+ for line in (((f))):
|
||||
102 102 | pass
|
||||
103 103 | for line in(f).readlines():
|
||||
104 104 | pass
|
||||
|
||||
FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
101 | for line in (((f))).readlines():
|
||||
102 | pass
|
||||
103 | for line in(f).readlines():
|
||||
| ^^^^^^^^^^^^^^^ FURB129
|
||||
104 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
100 100 | with open("furb129.py") as f:
|
||||
101 101 | for line in (((f))).readlines():
|
||||
102 102 | pass
|
||||
103 |- for line in(f).readlines():
|
||||
103 |+ for line in(f):
|
||||
104 104 | pass
|
||||
@@ -24,6 +24,7 @@ mod tests {
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))]
|
||||
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005_slices.py"))]
|
||||
#[test_case(Rule::AsyncioDanglingTask, Path::new("RUF006.py"))]
|
||||
#[test_case(Rule::ZipInsteadOfPairwise, Path::new("RUF007.py"))]
|
||||
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))]
|
||||
@@ -94,6 +95,7 @@ mod tests {
|
||||
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
|
||||
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))]
|
||||
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
|
||||
#[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))]
|
||||
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_0.py"))]
|
||||
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_1.py"))]
|
||||
@@ -322,10 +324,7 @@ mod tests {
|
||||
fn ruff_noqa_filedirective_unused() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff/RUF100_6.py"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA])
|
||||
},
|
||||
&settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA]),
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
@@ -335,15 +334,12 @@ mod tests {
|
||||
fn ruff_noqa_filedirective_unused_last_of_many() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff/RUF100_7.py"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(vec![
|
||||
Rule::UnusedNOQA,
|
||||
Rule::FStringMissingPlaceholders,
|
||||
Rule::LineTooLong,
|
||||
Rule::UnusedVariable,
|
||||
])
|
||||
},
|
||||
&settings::LinterSettings::for_rules(vec![
|
||||
Rule::UnusedNOQA,
|
||||
Rule::FStringMissingPlaceholders,
|
||||
Rule::LineTooLong,
|
||||
Rule::UnusedVariable,
|
||||
]),
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
@@ -480,10 +476,8 @@ mod tests {
|
||||
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
|
||||
#[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))]
|
||||
#[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))]
|
||||
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
|
||||
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
|
||||
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]
|
||||
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005_slices.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -4,7 +4,6 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
use crate::preview::is_support_slices_in_literal_concatenation_enabled;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -96,7 +95,7 @@ enum Type {
|
||||
}
|
||||
|
||||
/// Recursively merge all the tuples and lists in the expression.
|
||||
fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(Expr, Type)> {
|
||||
fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> {
|
||||
let Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op: Operator::Add,
|
||||
@@ -108,22 +107,18 @@ fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(
|
||||
};
|
||||
|
||||
let new_left = match left.as_ref() {
|
||||
Expr::BinOp(ast::ExprBinOp { .. }) => {
|
||||
match concatenate_expressions(left, should_support_slices) {
|
||||
Some((new_left, _)) => new_left,
|
||||
None => *left.clone(),
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp { .. }) => match concatenate_expressions(left) {
|
||||
Some((new_left, _)) => new_left,
|
||||
None => *left.clone(),
|
||||
},
|
||||
_ => *left.clone(),
|
||||
};
|
||||
|
||||
let new_right = match right.as_ref() {
|
||||
Expr::BinOp(ast::ExprBinOp { .. }) => {
|
||||
match concatenate_expressions(right, should_support_slices) {
|
||||
Some((new_right, _)) => new_right,
|
||||
None => *right.clone(),
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp { .. }) => match concatenate_expressions(right) {
|
||||
Some((new_right, _)) => new_right,
|
||||
None => *right.clone(),
|
||||
},
|
||||
_ => *right.clone(),
|
||||
};
|
||||
|
||||
@@ -151,9 +146,7 @@ fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(
|
||||
make_splat_elts(splat_element, other_elements, splat_at_left)
|
||||
}
|
||||
// Subscripts are also considered safe-ish to splat if the indexer is a slice.
|
||||
Expr::Subscript(ast::ExprSubscript { slice, .. })
|
||||
if should_support_slices && matches!(&**slice, Expr::Slice(_)) =>
|
||||
{
|
||||
Expr::Subscript(ast::ExprSubscript { slice, .. }) if matches!(&**slice, Expr::Slice(_)) => {
|
||||
make_splat_elts(splat_element, other_elements, splat_at_left)
|
||||
}
|
||||
// If the splat element is itself a list/tuple, insert them in the other list/tuple.
|
||||
@@ -198,10 +191,7 @@ pub(crate) fn collection_literal_concatenation(checker: &Checker, expr: &Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
let should_support_slices =
|
||||
is_support_slices_in_literal_concatenation_enabled(checker.settings);
|
||||
|
||||
let Some((new_expr, type_)) = concatenate_expressions(expr, should_support_slices) else {
|
||||
let Some((new_expr, type_)) = concatenate_expressions(expr) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use ruff_python_semantic::analyze::function_type::is_stub;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::fastapi::rules::is_fastapi_route;
|
||||
|
||||
/// ## What it does
|
||||
|
||||
@@ -4,7 +4,6 @@ pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -25,7 +24,7 @@ mod tests {
|
||||
#[test_case(Rule::ErrorInsteadOfException, Path::new("TRY400.py"))]
|
||||
#[test_case(Rule::VerboseLogMessage, Path::new("TRY401.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("tryceratops").join(path).as_path(),
|
||||
&settings::LinterSettings::for_rule(rule_code),
|
||||
|
||||
@@ -20,6 +20,7 @@ use ruff_python_parser::{ParseError, ParseOptions};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
|
||||
use crate::codes::Rule;
|
||||
use crate::fix::{FixResult, fix_file};
|
||||
use crate::linter::check_path;
|
||||
use crate::message::{Emitter, EmitterContext, Message, TextEmitter};
|
||||
@@ -233,8 +234,9 @@ Source with applied fixes:
|
||||
|
||||
let messages = messages
|
||||
.into_iter()
|
||||
.filter_map(|msg| Some((msg.to_rule()?, msg)))
|
||||
.map(|(rule, mut diagnostic)| {
|
||||
.filter_map(|msg| Some((msg.noqa_code()?, msg)))
|
||||
.map(|(code, mut diagnostic)| {
|
||||
let rule = Rule::from_code(&code.to_string()).unwrap();
|
||||
let fixable = diagnostic.fix().is_some_and(|fix| {
|
||||
matches!(
|
||||
fix.applicability(),
|
||||
|
||||
@@ -174,7 +174,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
|
||||
|
||||
output.extend(quote! {
|
||||
impl #linter {
|
||||
pub fn rules(&self) -> ::std::vec::IntoIter<Rule> {
|
||||
pub(crate) fn rules(&self) -> ::std::vec::IntoIter<Rule> {
|
||||
match self { #prefix_into_iter_match_arms }
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
|
||||
}
|
||||
output.extend(quote! {
|
||||
impl RuleCodePrefix {
|
||||
pub fn parse(linter: &Linter, code: &str) -> Result<Self, crate::registry::FromCodeError> {
|
||||
pub(crate) fn parse(linter: &Linter, code: &str) -> Result<Self, crate::registry::FromCodeError> {
|
||||
use std::str::FromStr;
|
||||
|
||||
Ok(match linter {
|
||||
@@ -190,7 +190,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rules(&self) -> ::std::vec::IntoIter<Rule> {
|
||||
pub(crate) fn rules(&self) -> ::std::vec::IntoIter<Rule> {
|
||||
match self {
|
||||
#(RuleCodePrefix::#linter_idents(prefix) => prefix.clone().rules(),)*
|
||||
}
|
||||
@@ -319,7 +319,7 @@ See also https://github.com/astral-sh/ruff/issues/2186.
|
||||
matches!(self.group(), RuleGroup::Preview)
|
||||
}
|
||||
|
||||
pub fn is_stable(&self) -> bool {
|
||||
pub(crate) fn is_stable(&self) -> bool {
|
||||
matches!(self.group(), RuleGroup::Stable)
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ fn generate_iter_impl(
|
||||
quote! {
|
||||
impl Linter {
|
||||
/// Rules not in the preview.
|
||||
pub fn rules(self: &Linter) -> ::std::vec::IntoIter<Rule> {
|
||||
pub(crate) fn rules(self: &Linter) -> ::std::vec::IntoIter<Rule> {
|
||||
match self {
|
||||
#linter_rules_match_arms
|
||||
}
|
||||
@@ -385,7 +385,7 @@ fn generate_iter_impl(
|
||||
}
|
||||
|
||||
impl RuleCodePrefix {
|
||||
pub fn iter() -> impl Iterator<Item = RuleCodePrefix> {
|
||||
pub(crate) fn iter() -> impl Iterator<Item = RuleCodePrefix> {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
let mut prefixes = Vec::new();
|
||||
@@ -436,7 +436,6 @@ fn register_rules<'a>(input: impl Iterator<Item = &'a Rule>) -> TokenStream {
|
||||
PartialOrd,
|
||||
Ord,
|
||||
::ruff_macros::CacheKey,
|
||||
AsRefStr,
|
||||
::strum_macros::IntoStaticStr,
|
||||
::strum_macros::EnumString,
|
||||
::serde::Serialize,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
|
||||
@@ -165,7 +165,7 @@ where
|
||||
pub fn formatted_file(db: &dyn Db, file: File) -> Result<Option<String>, FormatModuleError> {
|
||||
let options = db.format_options(file);
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let parsed = parsed_module(db.upcast(), file).load(db.upcast());
|
||||
|
||||
if let Some(first) = parsed.errors().first() {
|
||||
return Err(FormatModuleError::ParseError(first.clone()));
|
||||
@@ -174,7 +174,7 @@ pub fn formatted_file(db: &dyn Db, file: File) -> Result<Option<String>, FormatM
|
||||
let comment_ranges = CommentRanges::from(parsed.tokens());
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
let formatted = format_node(parsed, &comment_ranges, &source, options)?;
|
||||
let formatted = format_node(&parsed, &comment_ranges, &source, options)?;
|
||||
let printed = formatted.print()?;
|
||||
|
||||
if printed.as_code() == &*source {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_comments.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
@@ -9,6 +8,7 @@ snapshot_kind: text
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
@@ -44,6 +44,7 @@ i = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
|
||||
@@ -26,5 +26,5 @@ pub fn is_pragma_comment(comment: &str) -> bool {
|
||||
// Case-sensitive match against a variety of pragmas that _do_ require a trailing colon.
|
||||
trimmed
|
||||
.split_once(':')
|
||||
.is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pylint" | "flake8" | "ruff"))
|
||||
.is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pylint" | "flake8" | "ruff" | "ty"))
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ fn to_lsp_diagnostic(
|
||||
let body = diagnostic.body().to_string();
|
||||
let fix = diagnostic.fix();
|
||||
let suggestion = diagnostic.suggestion();
|
||||
let code = diagnostic.to_noqa_code();
|
||||
let code = diagnostic.noqa_code();
|
||||
|
||||
let fix = fix.and_then(|fix| fix.applies(Applicability::Unsafe).then_some(fix));
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ pub(crate) fn hover(
|
||||
|
||||
fn format_rule_text(rule: Rule) -> String {
|
||||
let mut output = String::new();
|
||||
let _ = write!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code());
|
||||
let _ = write!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -208,7 +208,7 @@ impl Workspace {
|
||||
let messages: Vec<ExpandedMessage> = messages
|
||||
.into_iter()
|
||||
.map(|msg| ExpandedMessage {
|
||||
code: msg.to_noqa_code().map(|code| code.to_string()),
|
||||
code: msg.noqa_code().map(|code| code.to_string()),
|
||||
message: msg.body().to_string(),
|
||||
start_location: source_code.line_column(msg.start()).into(),
|
||||
end_location: source_code.line_column(msg.end()).into(),
|
||||
|
||||
@@ -65,7 +65,7 @@ fn syntax_error() {
|
||||
fn unsupported_syntax_error() {
|
||||
check!(
|
||||
"match 2:\n case 1: ...",
|
||||
r#"{"preview": true, "target-version": "py39"}"#,
|
||||
r#"{"target-version": "py39"}"#,
|
||||
[ExpandedMessage {
|
||||
code: None,
|
||||
message: "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)".to_string(),
|
||||
|
||||
@@ -1098,7 +1098,7 @@ impl LintConfiguration {
|
||||
// approach to give each pair it's own `warn_user_once`.
|
||||
for (preferred, expendable, message) in INCOMPATIBLE_CODES {
|
||||
if rules.enabled(*preferred) && rules.enabled(*expendable) {
|
||||
warn_user_once_by_id!(expendable.as_ref(), "{}", message);
|
||||
warn_user_once_by_id!(expendable.name().as_str(), "{}", message);
|
||||
rules.disable(*expendable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,9 @@ use clap::{CommandFactory, Parser};
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use ruff_db::Upcast;
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::{Upcast, max_parallelism};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use ty_project::metadata::options::ProjectOptionsOverrides;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
206
crates/ty/tests/cli/config_option.rs
Normal file
206
crates/ty/tests/cli/config_option.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
// Long flag
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @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.
|
||||
");
|
||||
|
||||
// Short flag
|
||||
assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files(vec![
|
||||
(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[terminal]
|
||||
error-on-warning = true
|
||||
"#,
|
||||
),
|
||||
("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
|
||||
----- 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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true").arg("--config").arg("terminal.error-on-warning=false"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- 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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(1)")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: TOML parse error at line 1, column 1
|
||||
|
|
||||
1 | bad-option=true
|
||||
| ^^^^^^^^^^
|
||||
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`
|
||||
|
||||
|
||||
Usage: ty <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_override() -> anyhow::Result<()> {
|
||||
// Set `error-on-warning` to true in the configuration file
|
||||
// Explicitly set `--warn unresolved-reference` to ensure the rule warns instead of errors
|
||||
let case = CliTest::with_files(vec![
|
||||
("test.py", r"print(x) # [unresolved-reference]"),
|
||||
(
|
||||
"ty-override.toml",
|
||||
r#"
|
||||
[terminal]
|
||||
error-on-warning = true
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Ensure flag works via CLI arg
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config-file").arg("ty-override.toml"), @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.
|
||||
");
|
||||
|
||||
// Ensure the flag works via an environment variable
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").env("TY_CONFIG_FILE", "ty-override.toml"), @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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
272
crates/ty/tests/cli/exit_code.rs
Normal file
272
crates/ty/tests/cli/exit_code.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
#[test]
|
||||
fn only_warnings() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- 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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_info() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
reveal_type(1)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:3:13
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| ^ `Literal[1]`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
reveal_type(1)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:3:13
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| ^ `Literal[1]`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning").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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
("test.py", r"print(x) # [unresolved-reference]"),
|
||||
(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[terminal]
|
||||
error-on-warning = true
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
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:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r###"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"###,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_zero_is_true() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
690
crates/ty/tests/cli/main.rs
Normal file
690
crates/ty/tests/cli/main.rs
Normal file
@@ -0,0 +1,690 @@
|
||||
mod config_option;
|
||||
mod exit_code;
|
||||
mod python_environment;
|
||||
mod rule_selection;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_run_in_sub_directory() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?;
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> <temp_dir>/test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_include_hidden_files_by_default() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([(".test.py", "~")])?;
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> .test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_respect_ignore_files() -> anyhow::Result<()> {
|
||||
// First test that the default option works correctly (the file is skipped)
|
||||
let case = CliTest::with_files([(".ignore", "test.py"), ("test.py", "~")])?;
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
WARN No python files found under the given path(s)
|
||||
");
|
||||
|
||||
// Test that we can set to false via CLI
|
||||
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
// Test that we can set to false via config file
|
||||
case.write_file("ty.toml", "src.respect-ignore-files = false")?;
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
// Ensure CLI takes precedence
|
||||
case.write_file("ty.toml", "src.respect-ignore-files = true")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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
|
||||
/// running the CLI from the child directory (using relative paths).
|
||||
///
|
||||
/// Project layout:
|
||||
/// ```
|
||||
/// - libs
|
||||
/// |- utils.py
|
||||
/// - child
|
||||
/// | - test.py
|
||||
/// - pyproject.toml
|
||||
/// ```
|
||||
///
|
||||
/// And the command is run in the `child` directory.
|
||||
#[test]
|
||||
fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-version = "3.11"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"libs/utils.py",
|
||||
r#"
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"child/test.py",
|
||||
r#"
|
||||
from utils import add
|
||||
|
||||
stat = add(10, 15)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Make sure that the CLI fails when the `libs` directory is not in the search path.
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `utils`
|
||||
--> test.py:2:6
|
||||
|
|
||||
2 | from utils import add
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | stat = add(10, 15)
|
||||
|
|
||||
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 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().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- 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 in a configuration file are relative to the project root.
|
||||
///
|
||||
/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run
|
||||
/// the CLI from a subdirectory.
|
||||
///
|
||||
/// Project layout:
|
||||
/// ```
|
||||
/// - libs
|
||||
/// |- utils.py
|
||||
/// - child
|
||||
/// | - test.py
|
||||
/// - pyproject.toml
|
||||
/// ```
|
||||
#[test]
|
||||
fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-version = "3.11"
|
||||
extra-paths = ["libs"]
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"libs/utils.py",
|
||||
r#"
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"child/test.py",
|
||||
r#"
|
||||
from utils import add
|
||||
|
||||
stat = add(10, 15)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_configuration() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/ty.toml",
|
||||
r#"
|
||||
[rules]
|
||||
division-by-zero = "warn"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/main.py",
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
prin(x)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
let config_directory = case.root().join("home/.config");
|
||||
let config_env_var = if cfg!(windows) {
|
||||
"APPDATA"
|
||||
} else {
|
||||
"XDG_CONFIG_HOME"
|
||||
};
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: rule `division-by-zero` was selected in the configuration file
|
||||
|
||||
error[unresolved-reference]: Name `prin` used when not defined
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x)
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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.
|
||||
"
|
||||
);
|
||||
|
||||
// The user-level configuration sets the severity for `unresolved-reference` to warn.
|
||||
// Changing the level for `division-by-zero` has no effect, because the project-level configuration
|
||||
// has higher precedence.
|
||||
case.write_file(
|
||||
config_directory.join("ty/ty.toml"),
|
||||
r#"
|
||||
[rules]
|
||||
division-by-zero = "error"
|
||||
unresolved-reference = "warn"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: rule `division-by-zero` was selected in the configuration file
|
||||
|
||||
warning[unresolved-reference]: Name `prin` used when not defined
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x)
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` was selected in the configuration file
|
||||
|
||||
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.
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_specific_paths() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/main.py",
|
||||
r#"
|
||||
y = 4 / 0 # error: division-by-zero
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/tests/test_main.py",
|
||||
r#"
|
||||
import does_not_exist # error: unresolved-import
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/other.py",
|
||||
r#"
|
||||
from main2 import z # error: unresolved-import
|
||||
|
||||
print(z)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command(),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `main2`
|
||||
--> project/other.py:2:6
|
||||
|
|
||||
2 | from main2 import z # error: unresolved-import
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | print(z)
|
||||
|
|
||||
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[unresolved-import]: Cannot resolve imported module `does_not_exist`
|
||||
--> project/tests/test_main.py:2:8
|
||||
|
|
||||
2 | import does_not_exist # error: unresolved-import
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
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
|
||||
|
||||
----- 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.
|
||||
// We should no longer see any diagnostics related to `main.py`.
|
||||
assert_cmd_snapshot!(
|
||||
case.command().arg("project/tests").arg("project/other.py"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `main2`
|
||||
--> project/other.py:2:6
|
||||
|
|
||||
2 | from main2 import z # error: unresolved-import
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | print(z)
|
||||
|
|
||||
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[unresolved-import]: Cannot resolve imported module `does_not_exist`
|
||||
--> project/tests/test_main.py:2:8
|
||||
|
|
||||
2 | import does_not_exist # error: unresolved-import
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
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
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_non_existing_path() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([])?;
|
||||
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(
|
||||
®ex::escape("The system cannot find the path specified. (os error 3)"),
|
||||
"No such file or directory (os error 2)",
|
||||
);
|
||||
let _s = settings.bind_to_scope();
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().arg("project/main.py").arg("project/tests"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[io]: `<temp_dir>/project/main.py`: No such file or directory (os error 2)
|
||||
|
||||
error[io]: `<temp_dir>/project/tests`: No such file or directory (os error 2)
|
||||
|
||||
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.
|
||||
WARN No python files found under the given path(s)
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concise_diagnostics() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=concise").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference] test.py:2:7: Name `x` used when not defined
|
||||
error[non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This tests the diagnostic format for revealed type.
|
||||
///
|
||||
/// This test was introduced because changes were made to
|
||||
/// how the revealed type diagnostic was constructed and
|
||||
/// formatted in "verbose" mode. But it required extra
|
||||
/// logic to ensure the concise version didn't regress on
|
||||
/// information content. So this test was introduced to
|
||||
/// capture that.
|
||||
#[test]
|
||||
fn concise_revealed_type() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
x = "hello"
|
||||
reveal_type(x)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]`
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
|
||||
let mut content = String::new();
|
||||
writeln!(
|
||||
&mut content,
|
||||
"
|
||||
from typing_extensions import reveal_type
|
||||
total = 1{plus_one_repeated}
|
||||
reveal_type(total)
|
||||
",
|
||||
plus_one_repeated = " + 1".repeat(2000 - 1)
|
||||
)?;
|
||||
|
||||
let case = CliTest::with_file("test.py", &ruff_python_trivia::textwrap::dedent(&content))?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:4:13
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1...
|
||||
4 | reveal_type(total)
|
||||
| ^^^^^ `Literal[2000]`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
pub(crate) struct CliTest {
|
||||
_temp_dir: TempDir,
|
||||
_settings_scope: SettingsBindDropGuard,
|
||||
project_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl CliTest {
|
||||
pub(crate) fn new() -> anyhow::Result<Self> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
// Canonicalize the tempdir path because macos uses symlinks for tempdirs
|
||||
// and that doesn't play well with our snapshot filtering.
|
||||
// Simplify with dunce because otherwise we get UNC paths on Windows.
|
||||
let project_dir = dunce::simplified(
|
||||
&temp_dir
|
||||
.path()
|
||||
.canonicalize()
|
||||
.context("Failed to canonicalize project path")?,
|
||||
)
|
||||
.to_path_buf();
|
||||
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(&tempdir_filter(&project_dir), "<temp_dir>/");
|
||||
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||
|
||||
let settings_scope = settings.bind_to_scope();
|
||||
|
||||
Ok(Self {
|
||||
project_dir,
|
||||
_temp_dir: temp_dir,
|
||||
_settings_scope: settings_scope,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn with_files<'a>(
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
case.write_files(files)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
pub(crate) fn with_file(path: impl AsRef<Path>, content: &str) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
case.write_file(path, content)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
pub(crate) fn write_files<'a>(
|
||||
&self,
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> anyhow::Result<()> {
|
||||
for (path, content) in files {
|
||||
self.write_file(path, content)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_file(&self, path: impl AsRef<Path>, content: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let path = self.project_dir.join(path);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
|
||||
}
|
||||
std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content))
|
||||
.with_context(|| format!("Failed to write file `{path}`", path = path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn root(&self) -> &Path {
|
||||
&self.project_dir
|
||||
}
|
||||
|
||||
pub(crate) fn command(&self) -> Command {
|
||||
let mut command = Command::new(get_cargo_bin("ty"));
|
||||
command.current_dir(&self.project_dir).arg("check");
|
||||
|
||||
// Unset environment variables that can affect test behavior
|
||||
command.env_remove("VIRTUAL_ENV");
|
||||
command.env_remove("CONDA_PREFIX");
|
||||
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
fn tempdir_filter(path: &Path) -> String {
|
||||
format!(r"{}\\?/?", regex::escape(path.to_str().unwrap()))
|
||||
}
|
||||
774
crates/ty/tests/cli/python_environment.rs
Normal file
774
crates/ty/tests/cli/python_environment.rs
Normal file
@@ -0,0 +1,774 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
/// Specifying an option on the CLI should take precedence over the same setting in the
|
||||
/// project's configuration. Here, this is tested for the Python version.
|
||||
#[test]
|
||||
fn config_override_python_version() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-version = "3.11"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
import sys
|
||||
|
||||
# Access `sys.last_exc` that was only added in Python 3.12
|
||||
print(sys.last_exc)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-attribute]: Type `<module 'sys'>` has no attribute `last_exc`
|
||||
--> test.py:5:7
|
||||
|
|
||||
4 | # Access `sys.last_exc` that was only added in Python 3.12
|
||||
5 | print(sys.last_exc)
|
||||
| ^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-attribute` 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").arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as above, but for the Python platform.
|
||||
#[test]
|
||||
fn config_override_python_platform() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-platform = "linux"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
import sys
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
reveal_type(sys.platform)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:5:13
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^ `Literal["linux"]`
|
||||
|
|
||||
|
||||
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-platform").arg("all"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:5:13
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^ `LiteralString`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> {
|
||||
let case = CliTest::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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python = "venv"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"venv/pyvenv.cfg",
|
||||
r#"
|
||||
version = 3.8
|
||||
home = foo/bar/bin
|
||||
"#,
|
||||
),
|
||||
if cfg!(target_os = "windows") {
|
||||
("foo/bar/bin/python.exe", "")
|
||||
} else {
|
||||
("foo/bar/bin/python", "")
|
||||
},
|
||||
if cfg!(target_os = "windows") {
|
||||
("venv/Lib/site-packages/foo.py", "")
|
||||
} else {
|
||||
("venv/lib/python3.8/site-packages/foo.py", "")
|
||||
},
|
||||
("test.py", "aiter"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `aiter` used when not defined
|
||||
--> test.py:1:1
|
||||
|
|
||||
1 | aiter
|
||||
| ^^^^^
|
||||
|
|
||||
info: `aiter` was added as a builtin in Python 3.10
|
||||
info: Python 3.8 was assumed when resolving types because of your virtual environment
|
||||
--> venv/pyvenv.cfg:2:11
|
||||
|
|
||||
2 | version = 3.8
|
||||
| ^^^ Python version inferred from virtual environment metadata file
|
||||
3 | home = foo/bar/bin
|
||||
|
|
||||
info: No Python version was specified on the command line or in a configuration file
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python = "venv"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"venv/pyvenv.cfg",
|
||||
r#"home = foo/bar/bin
|
||||
|
||||
|
||||
version = 3.8"#,
|
||||
),
|
||||
if cfg!(target_os = "windows") {
|
||||
("foo/bar/bin/python.exe", "")
|
||||
} else {
|
||||
("foo/bar/bin/python", "")
|
||||
},
|
||||
if cfg!(target_os = "windows") {
|
||||
("venv/Lib/site-packages/foo.py", "")
|
||||
} else {
|
||||
("venv/lib/python3.8/site-packages/foo.py", "")
|
||||
},
|
||||
("test.py", "aiter"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `aiter` used when not defined
|
||||
--> test.py:1:1
|
||||
|
|
||||
1 | aiter
|
||||
| ^^^^^
|
||||
|
|
||||
info: `aiter` was added as a builtin in Python 3.10
|
||||
info: Python 3.8 was assumed when resolving types because of your virtual environment
|
||||
--> venv/pyvenv.cfg:4:23
|
||||
|
|
||||
4 | version = 3.8
|
||||
| ^^^ Python version inferred from virtual environment metadata file
|
||||
|
|
||||
info: No Python version was specified on the command line or in a configuration file
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
requires-python = ">=3.8"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
match object():
|
||||
case int():
|
||||
pass
|
||||
case _:
|
||||
pass
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:2:1
|
||||
|
|
||||
2 | match object():
|
||||
| ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
|
||||
3 | case int():
|
||||
4 | pass
|
||||
|
|
||||
info: Python 3.8 was assumed when parsing syntax
|
||||
--> pyproject.toml:3:19
|
||||
|
|
||||
2 | [project]
|
||||
3 | requires-python = ">=3.8"
|
||||
| ^^^^^^^ Python 3.8 assumed due to this configuration setting
|
||||
|
|
||||
|
||||
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[invalid-syntax]
|
||||
--> test.py:2:1
|
||||
|
|
||||
2 | match object():
|
||||
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
3 | case int():
|
||||
4 | pass
|
||||
|
|
||||
info: Python 3.9 was assumed when parsing syntax because it was specified 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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_cli_argument_virtual_environment() -> anyhow::Result<()> {
|
||||
let path_to_executable = if cfg!(windows) {
|
||||
"my-venv/Scripts/python.exe"
|
||||
} else {
|
||||
"my-venv/bin/python"
|
||||
};
|
||||
|
||||
let other_venv_path = "my-venv/foo/some_other_file.txt";
|
||||
|
||||
let case = CliTest::with_files([
|
||||
("test.py", ""),
|
||||
(
|
||||
if cfg!(windows) {
|
||||
"my-venv/Lib/site-packages/foo.py"
|
||||
} else {
|
||||
"my-venv/lib/python3.13/site-packages/foo.py"
|
||||
},
|
||||
"",
|
||||
),
|
||||
(path_to_executable, ""),
|
||||
(other_venv_path, ""),
|
||||
])?;
|
||||
|
||||
// Passing a path to the installation works
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg("my-venv"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// And so does passing a path to the executable inside the installation
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// But random other paths inside the installation are rejected
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg(other_venv_path), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `<temp_dir>/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk
|
||||
");
|
||||
|
||||
// And so are paths that do not exist on disk
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg("not-a-directory-or-executable"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `<temp_dir>/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_cli_argument_system_installation() -> anyhow::Result<()> {
|
||||
let path_to_executable = if cfg!(windows) {
|
||||
"Python3.11/python.exe"
|
||||
} else {
|
||||
"Python3.11/bin/python"
|
||||
};
|
||||
|
||||
let case = CliTest::with_files([
|
||||
("test.py", ""),
|
||||
(
|
||||
if cfg!(windows) {
|
||||
"Python3.11/Lib/site-packages/foo.py"
|
||||
} else {
|
||||
"Python3.11/lib/python3.11/site-packages/foo.py"
|
||||
},
|
||||
"",
|
||||
),
|
||||
(path_to_executable, ""),
|
||||
])?;
|
||||
|
||||
// Passing a path to the installation works
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg("Python3.11"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// And so does passing a path to the executable inside the installation
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_broken_python_setting() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
description = "Some description"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
|
||||
[tool.ty.environment]
|
||||
python = "not-a-directory-or-executable"
|
||||
"#,
|
||||
),
|
||||
("test.py", ""),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting
|
||||
|
||||
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||
|
|
||||
9 |
|
||||
10 | [tool.ty.environment]
|
||||
11 | python = "not-a-directory-or-executable"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk
|
||||
|
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python = "directory-but-no-site-packages"
|
||||
"#,
|
||||
),
|
||||
("directory-but-no-site-packages/lib/foo.py", ""),
|
||||
("test.py", ""),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting
|
||||
|
||||
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||
|
|
||||
1 |
|
||||
2 | [tool.ty.environment]
|
||||
3 | python = "directory-but-no-site-packages"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable
|
||||
|
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This error message is never emitted on Windows, because Windows installations have simpler layouts
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python = "directory-but-no-site-packages"
|
||||
"#,
|
||||
),
|
||||
("directory-but-no-site-packages/foo.py", ""),
|
||||
("test.py", ""),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Failed to iterate over the contents of the `lib` directory of the Python installation
|
||||
|
||||
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||
|
|
||||
1 |
|
||||
2 | [tool.ty.environment]
|
||||
3 | python = "directory-but-no-site-packages"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"ty.toml",
|
||||
&*format!(
|
||||
r#"
|
||||
[environment]
|
||||
python-version = "{}"
|
||||
python-platform = "linux"
|
||||
"#,
|
||||
PythonVersion::default()
|
||||
),
|
||||
),
|
||||
(
|
||||
"main.py",
|
||||
r#"
|
||||
import os
|
||||
|
||||
os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-attribute]: Type `<module 'os'>` has no attribute `grantpt`
|
||||
--> main.py:4:1
|
||||
|
|
||||
2 | import os
|
||||
3 |
|
||||
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
| ^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-attribute` 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.
|
||||
");
|
||||
|
||||
// Use default (which should be latest supported)
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[environment]
|
||||
python-platform = "linux"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"main.py",
|
||||
r#"
|
||||
import os
|
||||
|
||||
os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `site-packages` directory is used by ty for external import.
|
||||
/// Ty does the following checks to discover the `site-packages` directory in the order:
|
||||
/// 1) If `VIRTUAL_ENV` environment variable is set
|
||||
/// 2) If `CONDA_PREFIX` environment variable is set
|
||||
/// 3) If a `.venv` directory exists at the project root
|
||||
///
|
||||
/// This test is aiming at validating the logic around `CONDA_PREFIX`.
|
||||
///
|
||||
/// A conda-like environment file structure is used
|
||||
/// We test by first not setting the `CONDA_PREFIX` and expect a fail.
|
||||
/// Then we test by setting `CONDA_PREFIX` to `conda-env` and expect a pass.
|
||||
///
|
||||
/// ├── project
|
||||
/// │ └── test.py
|
||||
/// └── conda-env
|
||||
/// └── lib
|
||||
/// └── python3.13
|
||||
/// └── site-packages
|
||||
/// └── package1
|
||||
/// └── __init__.py
|
||||
///
|
||||
/// test.py imports package1
|
||||
/// And the command is run in the `child` directory.
|
||||
#[test]
|
||||
fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
|
||||
let conda_package1_path = if cfg!(windows) {
|
||||
"conda-env/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/test.py",
|
||||
r#"
|
||||
import package1
|
||||
"#,
|
||||
),
|
||||
(
|
||||
conda_package1_path,
|
||||
r#"
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `package1`
|
||||
--> test.py:2:8
|
||||
|
|
||||
2 | import package1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
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 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// do command : CONDA_PREFIX=<temp_dir>/conda_env
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")).env("CONDA_PREFIX", case.root().join("conda-env")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
292
crates/ty/tests/cli/rule_selection.rs
Normal file
292
crates/ty/tests/cli/rule_selection.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
/// The rule severity can be changed in the configuration file
|
||||
#[test]
|
||||
fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's an `unresolved-reference` diagnostic (error).
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `prin` used when not defined
|
||||
--> test.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
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.
|
||||
"###);
|
||||
|
||||
case.write_file(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.rules]
|
||||
division-by-zero = "warn" # promote to warn
|
||||
unresolved-reference = "ignore"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[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` was selected in the configuration file
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// The rule severity can be changed using `--ignore`, `--warn`, and `--error`
|
||||
#[test]
|
||||
fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
import does_not_exit
|
||||
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's an `unresolved-reference` diagnostic (error)
|
||||
// and an unresolved-import (error) diagnostic by default.
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `does_not_exit`
|
||||
--> test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
| ^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
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[unresolved-reference]: Name `prin` used when not defined
|
||||
--> test.py:9:1
|
||||
|
|
||||
7 | x = a
|
||||
8 |
|
||||
9 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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
|
||||
.command()
|
||||
.arg("--ignore")
|
||||
.arg("unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("division-by-zero")
|
||||
.arg("--warn")
|
||||
.arg("unresolved-import"),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[unresolved-import]: Cannot resolve imported module `does_not_exit`
|
||||
--> test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
| ^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
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` was selected on the command line
|
||||
|
||||
warning[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` was selected on the command line
|
||||
|
||||
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.
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` and
|
||||
/// values specified last override previous severities.
|
||||
#[test]
|
||||
fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's a `unresolved-reference` diagnostic (error) by default.
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `prin` used when not defined
|
||||
--> test.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
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("--warn")
|
||||
.arg("unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("division-by-zero")
|
||||
.arg("--ignore")
|
||||
.arg("unresolved-reference"),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[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` 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.
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ty warns about unknown rules specified in a configuration file
|
||||
#[test]
|
||||
fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.rules]
|
||||
division-by-zer = "warn" # incorrect rule name
|
||||
"#,
|
||||
),
|
||||
("test.py", "print(10)"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[unknown-rule]
|
||||
--> pyproject.toml:3:1
|
||||
|
|
||||
2 | [tool.ty.rules]
|
||||
3 | division-by-zer = "warn" # incorrect rule name
|
||||
| ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// ty warns about unknown rules specified in a CLI argument
|
||||
#[test]
|
||||
fn cli_unknown_rules() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", "print(10)")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[unknown-rule]: Unknown lint rule `division-by-zer`
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::{ParsedModule, parsed_module};
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_parser::{Token, TokenAt, TokenKind};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
@@ -15,9 +15,9 @@ pub struct Completion {
|
||||
}
|
||||
|
||||
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion> {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let parsed = parsed_module(db.upcast(), file).load(db.upcast());
|
||||
|
||||
let Some(target) = CompletionTargetTokens::find(parsed, offset).ast(parsed) else {
|
||||
let Some(target) = CompletionTargetTokens::find(&parsed, offset).ast(&parsed) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ enum CompletionTargetTokens<'t> {
|
||||
|
||||
impl<'t> CompletionTargetTokens<'t> {
|
||||
/// Look for the best matching token pattern at the given offset.
|
||||
fn find(parsed: &ParsedModule, offset: TextSize) -> CompletionTargetTokens<'_> {
|
||||
fn find(parsed: &ParsedModuleRef, offset: TextSize) -> CompletionTargetTokens<'_> {
|
||||
static OBJECT_DOT_EMPTY: [TokenKind; 2] = [TokenKind::Name, TokenKind::Dot];
|
||||
static OBJECT_DOT_NON_EMPTY: [TokenKind; 3] =
|
||||
[TokenKind::Name, TokenKind::Dot, TokenKind::Name];
|
||||
@@ -97,7 +97,7 @@ impl<'t> CompletionTargetTokens<'t> {
|
||||
/// Returns a corresponding AST node for these tokens.
|
||||
///
|
||||
/// If no plausible AST node could be found, then `None` is returned.
|
||||
fn ast(&self, parsed: &'t ParsedModule) -> Option<CompletionTargetAst<'t>> {
|
||||
fn ast(&self, parsed: &'t ParsedModuleRef) -> Option<CompletionTargetAst<'t>> {
|
||||
match *self {
|
||||
CompletionTargetTokens::ObjectDot { object, .. } => {
|
||||
let covering_node = covering_node(parsed.syntax().into(), object.range())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::find_node::covering_node;
|
||||
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::{ParsedModule, parsed_module};
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
@@ -13,8 +13,8 @@ pub fn goto_type_definition(
|
||||
file: File,
|
||||
offset: TextSize,
|
||||
) -> Option<RangedValue<NavigationTargets>> {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let goto_target = find_goto_target(parsed, offset)?;
|
||||
let module = parsed_module(db.upcast(), file).load(db.upcast());
|
||||
let goto_target = find_goto_target(&module, offset)?;
|
||||
|
||||
let model = SemanticModel::new(db.upcast(), file);
|
||||
let ty = goto_target.inferred_type(&model)?;
|
||||
@@ -128,8 +128,8 @@ pub(crate) enum GotoTarget<'a> {
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> GotoTarget<'db> {
|
||||
pub(crate) fn inferred_type(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
|
||||
impl GotoTarget<'_> {
|
||||
pub(crate) fn inferred_type<'db>(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
|
||||
let ty = match self {
|
||||
GotoTarget::Expression(expression) => expression.inferred_type(model),
|
||||
GotoTarget::FunctionDef(function) => function.inferred_type(model),
|
||||
@@ -183,7 +183,10 @@ impl Ranged for GotoTarget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
|
||||
pub(crate) fn find_goto_target(
|
||||
parsed: &ParsedModuleRef,
|
||||
offset: TextSize,
|
||||
) -> Option<GotoTarget<'_>> {
|
||||
let token = parsed
|
||||
.tokens()
|
||||
.at_offset(offset)
|
||||
|
||||
@@ -8,9 +8,9 @@ use std::fmt::Formatter;
|
||||
use ty_python_semantic::SemanticModel;
|
||||
use ty_python_semantic::types::Type;
|
||||
|
||||
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover>> {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let goto_target = find_goto_target(parsed, offset)?;
|
||||
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
|
||||
let parsed = parsed_module(db.upcast(), file).load(db.upcast());
|
||||
let goto_target = find_goto_target(&parsed, offset)?;
|
||||
|
||||
if let GotoTarget::Expression(expr) = goto_target {
|
||||
if expr.is_literal_expr() {
|
||||
|
||||
@@ -54,7 +54,7 @@ impl fmt::Display for DisplayInlayHint<'_, '_> {
|
||||
pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec<InlayHint<'_>> {
|
||||
let mut visitor = InlayHintVisitor::new(db, file, range);
|
||||
|
||||
let ast = parsed_module(db.upcast(), file);
|
||||
let ast = parsed_module(db.upcast(), file).load(db.upcast());
|
||||
|
||||
visitor.visit_body(ast.suite());
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user