Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4df7cbb
make LinterSettings::unresolved_target_version optional
ntBre Apr 21, 2025
fc4d465
add Checker::target_version_or_latest
ntBre Apr 21, 2025
70853eb
default to PythonVersion::latest for parsing
ntBre Apr 21, 2025
7d82da6
default to latest for import checking
ntBre Apr 21, 2025
040b5ec
default to latest for stdlib imports
ntBre Apr 21, 2025
82d0da1
add some TODOs for later
ntBre Apr 21, 2025
af1c355
return Option<PythonVersion> from Checker::target_version
ntBre Apr 21, 2025
cead271
use target_version.is_some_and for enabling rules and fix safety
ntBre Apr 21, 2025
48d550b
default to latest for typing imports
ntBre Apr 21, 2025
526dc4e
use default for semantic errors
ntBre Apr 21, 2025
56aa04c
use target_version.is_none_or for disabling rules
ntBre Apr 21, 2025
7f39aa8
use let-else in less clear cases
ntBre Apr 21, 2025
5349480
specify target-version for wasm test
ntBre Apr 21, 2025
f45a58e
bail out less aggressively for PLC2801
ntBre Apr 24, 2025
ada8154
bail out less aggressively for UP035
ntBre Apr 24, 2025
7a02d4f
add Checker::target_version_or_default
ntBre May 1, 2025
4bae9f0
move all lint rules back to `or_default` and remove `target_version`
ntBre May 1, 2025
ea188dd
revert import default to `default`
ntBre May 1, 2025
bdd5bd5
default to `latest` for semantic errors, inline helper
ntBre May 1, 2025
25f851e
revert to target_version from target_version_or_default
ntBre May 1, 2025
1fabd4b
delete TODOs, leave formatter and analyze settings alone for now
ntBre May 1, 2025
d67e285
unwrap version before check_path
ntBre May 1, 2025
e7a535d
expand target_version docs
ntBre May 1, 2025
4563cd0
save linter_target_version before unwrapping for formatter/analyze
ntBre May 5, 2025
2912092
document choice of PythonVersion::latest for semantic errors
ntBre May 5, 2025
cf284a2
use TargetVersion wrapper
ntBre May 5, 2025
5a1b5ce
make LinterSettings::unresolved_target_version a TargetVersion
ntBre May 5, 2025
99e50e3
Merge branch 'main' into brent/default-python-version
ntBre May 5, 2025
3ef26d0
call target_version instead of accessing the field
ntBre May 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/ruff/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ mod tests {
let settings = Settings {
cache_dir,
linter: LinterSettings {
unresolved_target_version: PythonVersion::latest(),
unresolved_target_version: PythonVersion::latest().into(),
..Default::default()
},
..Settings::default()
Expand Down
4 changes: 2 additions & 2 deletions crates/ruff/tests/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3930,7 +3930,7 @@ from typing import Union;foo: Union[int, str] = 1
linter.per_file_ignores = {}
linter.safety_table.forced_safe = []
linter.safety_table.forced_unsafe = []
linter.unresolved_target_version = 3.9
linter.unresolved_target_version = none
linter.per_file_target_version = {}
linter.preview = disabled
linter.explicit_preview_rules = false
Expand Down Expand Up @@ -4215,7 +4215,7 @@ from typing import Union;foo: Union[int, str] = 1
linter.per_file_ignores = {}
linter.safety_table.forced_safe = []
linter.safety_table.forced_unsafe = []
linter.unresolved_target_version = 3.9
linter.unresolved_target_version = none
linter.per_file_target_version = {}
linter.preview = disabled
linter.explicit_preview_rules = false
Expand Down
30 changes: 20 additions & 10 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ use crate::rules::pyflakes::rules::{
};
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
use crate::settings::{flags, LinterSettings};
use crate::settings::{flags, LinterSettings, TargetVersion};
use crate::{docstrings, noqa, Locator};

mod analyze;
Expand Down Expand Up @@ -232,7 +232,7 @@ pub(crate) struct Checker<'a> {
/// A state describing if a docstring is expected or not.
docstring_state: DocstringState,
/// The target [`PythonVersion`] for version-dependent checks.
target_version: PythonVersion,
target_version: TargetVersion,
/// Helper visitor for detecting semantic syntax errors.
#[expect(clippy::struct_field_names)]
semantic_checker: SemanticSyntaxChecker,
Expand All @@ -257,7 +257,7 @@ impl<'a> Checker<'a> {
source_type: PySourceType,
cell_offsets: Option<&'a CellOffsets>,
notebook_index: Option<&'a NotebookIndex>,
target_version: PythonVersion,
target_version: TargetVersion,
) -> Checker<'a> {
let semantic = SemanticModel::new(&settings.typing_modules, path, module);
Self {
Expand Down Expand Up @@ -523,9 +523,16 @@ impl<'a> Checker<'a> {
}
}

/// Return the [`PythonVersion`] to use for version-related checks.
pub(crate) const fn target_version(&self) -> PythonVersion {
self.target_version
/// Return the [`PythonVersion`] to use for version-related lint rules.
///
/// If the user did not provide a target version, this defaults to the lowest supported Python
/// version ([`PythonVersion::default`]).
///
/// Note that this method should not be used for version-related syntax errors emitted by the
/// parser or the [`SemanticSyntaxChecker`], which should instead default to the _latest_
/// supported Python version.
pub(crate) fn target_version(&self) -> PythonVersion {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this method is still useful. It's somewhat confusing that it's called target-version and returns a PythonVersion but renaming it to python_version might be equally confusing because of the method on SemanticSyntaxContext.

self.target_version.linter_version()
}

fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Checker)) {
Expand Down Expand Up @@ -583,7 +590,10 @@ impl TypingImporter<'_, '_> {

impl SemanticSyntaxContext for Checker<'_> {
fn python_version(&self) -> PythonVersion {
self.target_version
// Reuse `parser_version` here, which should default to `PythonVersion::latest` instead of
// `PythonVersion::default` to minimize version-related semantic syntax errors when
// `target_version` is unset.
self.target_version.parser_version()
}

fn global(&self, name: &str) -> Option<TextRange> {
Expand Down Expand Up @@ -1366,7 +1376,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
// we can't defer again, or we'll infinitely recurse!
&& !self.semantic.in_deferred_type_definition()
&& self.semantic.in_type_definition()
&& (self.semantic.future_annotations_or_stub()||self.target_version.defers_annotations())
&& (self.semantic.future_annotations_or_stub()||self.target_version().defers_annotations())
&& (self.semantic.in_annotation() || self.source_type.is_stub())
{
if let Expr::StringLiteral(string_literal) = expr {
Expand Down Expand Up @@ -2594,7 +2604,7 @@ impl<'a> Checker<'a> {
// annotations` is active, or they are type definitions in a stub file.
debug_assert!(
(self.semantic.future_annotations_or_stub()
|| self.target_version.defers_annotations())
|| self.target_version().defers_annotations())
&& (self.source_type.is_stub() || self.semantic.in_annotation())
);

Expand Down Expand Up @@ -2932,7 +2942,7 @@ pub(crate) fn check_ast(
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
notebook_index: Option<&NotebookIndex>,
target_version: PythonVersion,
target_version: TargetVersion,
) -> (Vec<Diagnostic>, Vec<SemanticSyntaxError>) {
let module_path = package
.map(PackageRoot::path)
Expand Down
46 changes: 26 additions & 20 deletions crates/ruff_linter/src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use crate::registry::{AsRule, Rule, RuleSet};
#[cfg(any(feature = "test-rules", test))]
use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES};
use crate::settings::types::UnsafeFixes;
use crate::settings::{flags, LinterSettings};
use crate::settings::{flags, LinterSettings, TargetVersion};
use crate::source_kind::SourceKind;
use crate::{directives, fs, warn_user_once, Locator};

Expand Down Expand Up @@ -111,7 +111,7 @@ pub fn check_path(
source_kind: &SourceKind,
source_type: PySourceType,
parsed: &Parsed<ModModule>,
target_version: PythonVersion,
target_version: TargetVersion,
) -> Vec<Message> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
Expand Down Expand Up @@ -160,7 +160,7 @@ pub fn check_path(
locator,
comment_ranges,
settings,
target_version,
target_version.linter_version(),
));
}

Expand Down Expand Up @@ -215,7 +215,7 @@ pub fn check_path(
package,
source_type,
cell_offsets,
target_version,
target_version.linter_version(),
);

diagnostics.extend(import_diagnostics);
Expand Down Expand Up @@ -390,7 +390,7 @@ pub fn add_noqa_to_path(
) -> Result<usize> {
// Parse once.
let target_version = settings.resolve_target_version(path);
let parsed = parse_unchecked_source(source_kind, source_type, target_version);
let parsed = parse_unchecked_source(source_kind, source_type, target_version.parser_version());

// Map row and column locations to byte slices (lazily).
let locator = Locator::new(source_kind.source_code());
Expand Down Expand Up @@ -451,11 +451,13 @@ pub fn lint_only(
) -> LinterResult {
let target_version = settings.resolve_target_version(path);

if matches!(target_version, PythonVersion::PY314) && !is_py314_support_enabled(settings) {
if matches!(target_version, TargetVersion(Some(PythonVersion::PY314)))
&& !is_py314_support_enabled(settings)
{
warn_user_once!("Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning.");
}

let parsed = source.into_parsed(source_kind, source_type, target_version);
let parsed = source.into_parsed(source_kind, source_type, target_version.parser_version());

// Map row and column locations to byte slices (lazily).
let locator = Locator::new(source_kind.source_code());
Expand Down Expand Up @@ -563,14 +565,17 @@ pub fn lint_fix<'a>(

let target_version = settings.resolve_target_version(path);

if matches!(target_version, PythonVersion::PY314) && !is_py314_support_enabled(settings) {
if matches!(target_version, TargetVersion(Some(PythonVersion::PY314)))
&& !is_py314_support_enabled(settings)
{
warn_user_once!("Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning.");
}

// Continuously fix until the source code stabilizes.
loop {
// Parse once.
let parsed = parse_unchecked_source(&transformed, source_type, target_version);
let parsed =
parse_unchecked_source(&transformed, source_type, target_version.parser_version());

// Map row and column locations to byte slices (lazily).
let locator = Locator::new(transformed.source_code());
Expand Down Expand Up @@ -972,8 +977,9 @@ mod tests {
settings: &LinterSettings,
) -> Vec<Message> {
let source_type = PySourceType::from(path);
let target_version = settings.resolve_target_version(path);
let options =
ParseOptions::from(source_type).with_target_version(settings.unresolved_target_version);
ParseOptions::from(source_type).with_target_version(target_version.parser_version());
let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options)
.try_into_module()
.expect("PySourceType always parses into a module");
Expand All @@ -998,7 +1004,7 @@ mod tests {
source_kind,
source_type,
&parsed,
settings.unresolved_target_version,
target_version,
);
messages.sort_by_key(Ranged::start);
messages
Expand Down Expand Up @@ -1121,7 +1127,7 @@ mod tests {
contents,
&LinterSettings {
rules: settings::rule_table::RuleTable::empty(),
unresolved_target_version: python_version,
unresolved_target_version: python_version.into(),
preview: settings::types::PreviewMode::Enabled,
..Default::default()
},
Expand All @@ -1139,7 +1145,7 @@ mod tests {
&SourceKind::IpyNotebook(Notebook::from_path(path)?),
path,
&LinterSettings {
unresolved_target_version: python_version,
unresolved_target_version: python_version.into(),
rules: settings::rule_table::RuleTable::empty(),
preview: settings::types::PreviewMode::Enabled,
..Default::default()
Expand Down Expand Up @@ -1205,7 +1211,7 @@ mod tests {
"pyi019_adds_typing_extensions",
PYI019_EXAMPLE,
&LinterSettings {
unresolved_target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310.into(),
typing_extensions: true,
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
}
Expand All @@ -1214,7 +1220,7 @@ mod tests {
"pyi019_does_not_add_typing_extensions",
PYI019_EXAMPLE,
&LinterSettings {
unresolved_target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310.into(),
typing_extensions: false,
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
}
Expand All @@ -1223,7 +1229,7 @@ mod tests {
"pyi019_adds_typing_without_extensions_disabled",
PYI019_EXAMPLE,
&LinterSettings {
unresolved_target_version: PythonVersion::PY311,
unresolved_target_version: PythonVersion::PY311.into(),
typing_extensions: true,
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
}
Expand All @@ -1232,7 +1238,7 @@ mod tests {
"pyi019_adds_typing_with_extensions_disabled",
PYI019_EXAMPLE,
&LinterSettings {
unresolved_target_version: PythonVersion::PY311,
unresolved_target_version: PythonVersion::PY311.into(),
typing_extensions: false,
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
}
Expand All @@ -1244,7 +1250,7 @@ mod tests {
def __new__(cls) -> C: ...
",
&LinterSettings {
unresolved_target_version: PythonVersion { major: 3, minor: 10 },
unresolved_target_version: PythonVersion { major: 3, minor: 10 }.into(),
typing_extensions: false,
..LinterSettings::for_rule(Rule::NonSelfReturnType)
}
Expand All @@ -1261,7 +1267,7 @@ mod tests {
return commons
"#,
&LinterSettings {
unresolved_target_version: PythonVersion { major: 3, minor: 8 },
unresolved_target_version: PythonVersion { major: 3, minor: 8 }.into(),
typing_extensions: false,
..LinterSettings::for_rule(Rule::FastApiNonAnnotatedDependency)
}
Expand All @@ -1276,7 +1282,7 @@ mod tests {
"pyi026_disabled",
"Vector = list[float]",
&LinterSettings {
unresolved_target_version: PythonVersion { major: 3, minor: 9 },
unresolved_target_version: PythonVersion { major: 3, minor: 9 }.into(),
typing_extensions: false,
..LinterSettings::for_rule(Rule::TypeAliasWithoutAnnotation)
}
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/fastapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ mod tests {
let diagnostics = test_path(
Path::new("fastapi").join(path).as_path(),
&settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY38,
unresolved_target_version: ruff_python_ast::PythonVersion::PY38.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/flake8_annotations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_annotations/auto_return_type.py"),
&LinterSettings {
unresolved_target_version: PythonVersion::PY38,
unresolved_target_version: PythonVersion::PY38.into(),
..LinterSettings::for_rules(vec![
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingReturnTypePrivateFunction,
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/flake8_async/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_async").join(path),
&LinterSettings {
unresolved_target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310.into(),
..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout)
},
)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
unresolved_target_version: target_version,
unresolved_target_version: target_version.into(),
..LinterSettings::for_rule(rule_code)
},
)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/flake8_builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_builtins").join(path).as_path(),
&LinterSettings {
unresolved_target_version: PythonVersion::PY38,
unresolved_target_version: PythonVersion::PY38.into(),
..LinterSettings::for_rule(rule_code)
},
)?;
Expand Down
4 changes: 2 additions & 2 deletions crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_future_annotations").join(path).as_path(),
&settings::LinterSettings {
unresolved_target_version: PythonVersion::PY37,
unresolved_target_version: PythonVersion::PY37.into(),
..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation)
},
)?;
Expand All @@ -49,7 +49,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_future_annotations").join(path).as_path(),
&settings::LinterSettings {
unresolved_target_version: PythonVersion::PY37,
unresolved_target_version: PythonVersion::PY37.into(),
..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation)
},
)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/flake8_pyi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_pyi").join(path).as_path(),
&settings::LinterSettings {
unresolved_target_version: PythonVersion::PY38,
unresolved_target_version: PythonVersion::PY38.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
unresolved_target_version: PythonVersion::PY39,
unresolved_target_version: PythonVersion::PY39.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/perflint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ mod tests {
Path::new("perflint").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
unresolved_target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310.into(),
..LinterSettings::for_rule(rule_code)
},
)?;
Expand Down
Loading
Loading