Skip to content

Commit 84cf8bd

Browse files
ntBreGlyphack
authored andcommitted
Default to latest supported Python version for version-related syntax errors (astral-sh#17529)
## Summary This PR partially addresses astral-sh#16418 via the following: - `LinterSettings::unresolved_python_version` is now a `TargetVersion`, which is a thin wrapper around an `Option<PythonVersion>` - `Checker::target_version` now calls `TargetVersion::linter_version` internally, which in turn uses `unwrap_or_default` to preserve the current default behavior - Calls to the parser now call `TargetVersion::parser_version`, which calls `unwrap_or_else(PythonVersion::latest)` - The `Checker`'s implementation of `SemanticSyntaxContext::python_version` also uses `TargetVersion::parser_version` to use `PythonVersion::latest` for semantic errors In short, all lint rule behavior should be unchanged, but we default to the latest Python version for the new syntax errors, which should minimize confusing version-related syntax errors for users without a version configured. ## Test Plan Existing tests, which showed no changes (except for printing default settings).
1 parent b88e5ac commit 84cf8bd

22 files changed

Lines changed: 139 additions & 71 deletions

File tree

crates/ruff/src/cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ mod tests {
616616
let settings = Settings {
617617
cache_dir,
618618
linter: LinterSettings {
619-
unresolved_target_version: PythonVersion::latest(),
619+
unresolved_target_version: PythonVersion::latest().into(),
620620
..Default::default()
621621
},
622622
..Settings::default()

crates/ruff/tests/lint.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3930,7 +3930,7 @@ from typing import Union;foo: Union[int, str] = 1
39303930
linter.per_file_ignores = {}
39313931
linter.safety_table.forced_safe = []
39323932
linter.safety_table.forced_unsafe = []
3933-
linter.unresolved_target_version = 3.9
3933+
linter.unresolved_target_version = none
39343934
linter.per_file_target_version = {}
39353935
linter.preview = disabled
39363936
linter.explicit_preview_rules = false
@@ -4215,7 +4215,7 @@ from typing import Union;foo: Union[int, str] = 1
42154215
linter.per_file_ignores = {}
42164216
linter.safety_table.forced_safe = []
42174217
linter.safety_table.forced_unsafe = []
4218-
linter.unresolved_target_version = 3.9
4218+
linter.unresolved_target_version = none
42194219
linter.per_file_target_version = {}
42204220
linter.preview = disabled
42214221
linter.explicit_preview_rules = false

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ use crate::rules::pyflakes::rules::{
7272
};
7373
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
7474
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
75-
use crate::settings::{flags, LinterSettings};
75+
use crate::settings::{flags, LinterSettings, TargetVersion};
7676
use crate::{docstrings, noqa, Locator};
7777

7878
mod analyze;
@@ -232,7 +232,7 @@ pub(crate) struct Checker<'a> {
232232
/// A state describing if a docstring is expected or not.
233233
docstring_state: DocstringState,
234234
/// The target [`PythonVersion`] for version-dependent checks.
235-
target_version: PythonVersion,
235+
target_version: TargetVersion,
236236
/// Helper visitor for detecting semantic syntax errors.
237237
#[expect(clippy::struct_field_names)]
238238
semantic_checker: SemanticSyntaxChecker,
@@ -257,7 +257,7 @@ impl<'a> Checker<'a> {
257257
source_type: PySourceType,
258258
cell_offsets: Option<&'a CellOffsets>,
259259
notebook_index: Option<&'a NotebookIndex>,
260-
target_version: PythonVersion,
260+
target_version: TargetVersion,
261261
) -> Checker<'a> {
262262
let semantic = SemanticModel::new(&settings.typing_modules, path, module);
263263
Self {
@@ -523,9 +523,16 @@ impl<'a> Checker<'a> {
523523
}
524524
}
525525

526-
/// Return the [`PythonVersion`] to use for version-related checks.
527-
pub(crate) const fn target_version(&self) -> PythonVersion {
528-
self.target_version
526+
/// Return the [`PythonVersion`] to use for version-related lint rules.
527+
///
528+
/// If the user did not provide a target version, this defaults to the lowest supported Python
529+
/// version ([`PythonVersion::default`]).
530+
///
531+
/// Note that this method should not be used for version-related syntax errors emitted by the
532+
/// parser or the [`SemanticSyntaxChecker`], which should instead default to the _latest_
533+
/// supported Python version.
534+
pub(crate) fn target_version(&self) -> PythonVersion {
535+
self.target_version.linter_version()
529536
}
530537

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

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

589599
fn global(&self, name: &str) -> Option<TextRange> {
@@ -1366,7 +1376,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
13661376
// we can't defer again, or we'll infinitely recurse!
13671377
&& !self.semantic.in_deferred_type_definition()
13681378
&& self.semantic.in_type_definition()
1369-
&& (self.semantic.future_annotations_or_stub()||self.target_version.defers_annotations())
1379+
&& (self.semantic.future_annotations_or_stub()||self.target_version().defers_annotations())
13701380
&& (self.semantic.in_annotation() || self.source_type.is_stub())
13711381
{
13721382
if let Expr::StringLiteral(string_literal) = expr {
@@ -2594,7 +2604,7 @@ impl<'a> Checker<'a> {
25942604
// annotations` is active, or they are type definitions in a stub file.
25952605
debug_assert!(
25962606
(self.semantic.future_annotations_or_stub()
2597-
|| self.target_version.defers_annotations())
2607+
|| self.target_version().defers_annotations())
25982608
&& (self.source_type.is_stub() || self.semantic.in_annotation())
25992609
);
26002610

@@ -2932,7 +2942,7 @@ pub(crate) fn check_ast(
29322942
source_type: PySourceType,
29332943
cell_offsets: Option<&CellOffsets>,
29342944
notebook_index: Option<&NotebookIndex>,
2935-
target_version: PythonVersion,
2945+
target_version: TargetVersion,
29362946
) -> (Vec<Diagnostic>, Vec<SemanticSyntaxError>) {
29372947
let module_path = package
29382948
.map(PackageRoot::path)

crates/ruff_linter/src/linter.rs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use crate::registry::{AsRule, Rule, RuleSet};
3535
#[cfg(any(feature = "test-rules", test))]
3636
use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES};
3737
use crate::settings::types::UnsafeFixes;
38-
use crate::settings::{flags, LinterSettings};
38+
use crate::settings::{flags, LinterSettings, TargetVersion};
3939
use crate::source_kind::SourceKind;
4040
use crate::{directives, fs, warn_user_once, Locator};
4141

@@ -111,7 +111,7 @@ pub fn check_path(
111111
source_kind: &SourceKind,
112112
source_type: PySourceType,
113113
parsed: &Parsed<ModModule>,
114-
target_version: PythonVersion,
114+
target_version: TargetVersion,
115115
) -> Vec<Message> {
116116
// Aggregate all diagnostics.
117117
let mut diagnostics = vec![];
@@ -160,7 +160,7 @@ pub fn check_path(
160160
locator,
161161
comment_ranges,
162162
settings,
163-
target_version,
163+
target_version.linter_version(),
164164
));
165165
}
166166

@@ -215,7 +215,7 @@ pub fn check_path(
215215
package,
216216
source_type,
217217
cell_offsets,
218-
target_version,
218+
target_version.linter_version(),
219219
);
220220

221221
diagnostics.extend(import_diagnostics);
@@ -390,7 +390,7 @@ pub fn add_noqa_to_path(
390390
) -> Result<usize> {
391391
// Parse once.
392392
let target_version = settings.resolve_target_version(path);
393-
let parsed = parse_unchecked_source(source_kind, source_type, target_version);
393+
let parsed = parse_unchecked_source(source_kind, source_type, target_version.parser_version());
394394

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

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

458-
let parsed = source.into_parsed(source_kind, source_type, target_version);
460+
let parsed = source.into_parsed(source_kind, source_type, target_version.parser_version());
459461

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

564566
let target_version = settings.resolve_target_version(path);
565567

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

570574
// Continuously fix until the source code stabilizes.
571575
loop {
572576
// Parse once.
573-
let parsed = parse_unchecked_source(&transformed, source_type, target_version);
577+
let parsed =
578+
parse_unchecked_source(&transformed, source_type, target_version.parser_version());
574579

575580
// Map row and column locations to byte slices (lazily).
576581
let locator = Locator::new(transformed.source_code());
@@ -972,8 +977,9 @@ mod tests {
972977
settings: &LinterSettings,
973978
) -> Vec<Message> {
974979
let source_type = PySourceType::from(path);
980+
let target_version = settings.resolve_target_version(path);
975981
let options =
976-
ParseOptions::from(source_type).with_target_version(settings.unresolved_target_version);
982+
ParseOptions::from(source_type).with_target_version(target_version.parser_version());
977983
let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options)
978984
.try_into_module()
979985
.expect("PySourceType always parses into a module");
@@ -998,7 +1004,7 @@ mod tests {
9981004
source_kind,
9991005
source_type,
10001006
&parsed,
1001-
settings.unresolved_target_version,
1007+
target_version,
10021008
);
10031009
messages.sort_by_key(Ranged::start);
10041010
messages
@@ -1121,7 +1127,7 @@ mod tests {
11211127
contents,
11221128
&LinterSettings {
11231129
rules: settings::rule_table::RuleTable::empty(),
1124-
unresolved_target_version: python_version,
1130+
unresolved_target_version: python_version.into(),
11251131
preview: settings::types::PreviewMode::Enabled,
11261132
..Default::default()
11271133
},
@@ -1139,7 +1145,7 @@ mod tests {
11391145
&SourceKind::IpyNotebook(Notebook::from_path(path)?),
11401146
path,
11411147
&LinterSettings {
1142-
unresolved_target_version: python_version,
1148+
unresolved_target_version: python_version.into(),
11431149
rules: settings::rule_table::RuleTable::empty(),
11441150
preview: settings::types::PreviewMode::Enabled,
11451151
..Default::default()
@@ -1205,7 +1211,7 @@ mod tests {
12051211
"pyi019_adds_typing_extensions",
12061212
PYI019_EXAMPLE,
12071213
&LinterSettings {
1208-
unresolved_target_version: PythonVersion::PY310,
1214+
unresolved_target_version: PythonVersion::PY310.into(),
12091215
typing_extensions: true,
12101216
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
12111217
}
@@ -1214,7 +1220,7 @@ mod tests {
12141220
"pyi019_does_not_add_typing_extensions",
12151221
PYI019_EXAMPLE,
12161222
&LinterSettings {
1217-
unresolved_target_version: PythonVersion::PY310,
1223+
unresolved_target_version: PythonVersion::PY310.into(),
12181224
typing_extensions: false,
12191225
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
12201226
}
@@ -1223,7 +1229,7 @@ mod tests {
12231229
"pyi019_adds_typing_without_extensions_disabled",
12241230
PYI019_EXAMPLE,
12251231
&LinterSettings {
1226-
unresolved_target_version: PythonVersion::PY311,
1232+
unresolved_target_version: PythonVersion::PY311.into(),
12271233
typing_extensions: true,
12281234
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
12291235
}
@@ -1232,7 +1238,7 @@ mod tests {
12321238
"pyi019_adds_typing_with_extensions_disabled",
12331239
PYI019_EXAMPLE,
12341240
&LinterSettings {
1235-
unresolved_target_version: PythonVersion::PY311,
1241+
unresolved_target_version: PythonVersion::PY311.into(),
12361242
typing_extensions: false,
12371243
..LinterSettings::for_rule(Rule::CustomTypeVarForSelf)
12381244
}
@@ -1244,7 +1250,7 @@ mod tests {
12441250
def __new__(cls) -> C: ...
12451251
",
12461252
&LinterSettings {
1247-
unresolved_target_version: PythonVersion { major: 3, minor: 10 },
1253+
unresolved_target_version: PythonVersion { major: 3, minor: 10 }.into(),
12481254
typing_extensions: false,
12491255
..LinterSettings::for_rule(Rule::NonSelfReturnType)
12501256
}
@@ -1261,7 +1267,7 @@ mod tests {
12611267
return commons
12621268
"#,
12631269
&LinterSettings {
1264-
unresolved_target_version: PythonVersion { major: 3, minor: 8 },
1270+
unresolved_target_version: PythonVersion { major: 3, minor: 8 }.into(),
12651271
typing_extensions: false,
12661272
..LinterSettings::for_rule(Rule::FastApiNonAnnotatedDependency)
12671273
}
@@ -1276,7 +1282,7 @@ mod tests {
12761282
"pyi026_disabled",
12771283
"Vector = list[float]",
12781284
&LinterSettings {
1279-
unresolved_target_version: PythonVersion { major: 3, minor: 9 },
1285+
unresolved_target_version: PythonVersion { major: 3, minor: 9 }.into(),
12801286
typing_extensions: false,
12811287
..LinterSettings::for_rule(Rule::TypeAliasWithoutAnnotation)
12821288
}

crates/ruff_linter/src/rules/fastapi/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ mod tests {
3636
let diagnostics = test_path(
3737
Path::new("fastapi").join(path).as_path(),
3838
&settings::LinterSettings {
39-
unresolved_target_version: ruff_python_ast::PythonVersion::PY38,
39+
unresolved_target_version: ruff_python_ast::PythonVersion::PY38.into(),
4040
..settings::LinterSettings::for_rule(rule_code)
4141
},
4242
)?;

crates/ruff_linter/src/rules/flake8_annotations/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ mod tests {
128128
let diagnostics = test_path(
129129
Path::new("flake8_annotations/auto_return_type.py"),
130130
&LinterSettings {
131-
unresolved_target_version: PythonVersion::PY38,
131+
unresolved_target_version: PythonVersion::PY38.into(),
132132
..LinterSettings::for_rules(vec![
133133
Rule::MissingReturnTypeUndocumentedPublicFunction,
134134
Rule::MissingReturnTypePrivateFunction,

crates/ruff_linter/src/rules/flake8_async/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ mod tests {
4444
let diagnostics = test_path(
4545
Path::new("flake8_async").join(path),
4646
&LinterSettings {
47-
unresolved_target_version: PythonVersion::PY310,
47+
unresolved_target_version: PythonVersion::PY310.into(),
4848
..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout)
4949
},
5050
)?;

crates/ruff_linter/src/rules/flake8_bugbear/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ mod tests {
100100
let diagnostics = test_path(
101101
Path::new("flake8_bugbear").join(path).as_path(),
102102
&LinterSettings {
103-
unresolved_target_version: target_version,
103+
unresolved_target_version: target_version.into(),
104104
..LinterSettings::for_rule(rule_code)
105105
},
106106
)?;

crates/ruff_linter/src/rules/flake8_builtins/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ mod tests {
217217
let diagnostics = test_path(
218218
Path::new("flake8_builtins").join(path).as_path(),
219219
&LinterSettings {
220-
unresolved_target_version: PythonVersion::PY38,
220+
unresolved_target_version: PythonVersion::PY38.into(),
221221
..LinterSettings::for_rule(rule_code)
222222
},
223223
)?;

crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ mod tests {
3030
let diagnostics = test_path(
3131
Path::new("flake8_future_annotations").join(path).as_path(),
3232
&settings::LinterSettings {
33-
unresolved_target_version: PythonVersion::PY37,
33+
unresolved_target_version: PythonVersion::PY37.into(),
3434
..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation)
3535
},
3636
)?;
@@ -49,7 +49,7 @@ mod tests {
4949
let diagnostics = test_path(
5050
Path::new("flake8_future_annotations").join(path).as_path(),
5151
&settings::LinterSettings {
52-
unresolved_target_version: PythonVersion::PY37,
52+
unresolved_target_version: PythonVersion::PY37.into(),
5353
..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation)
5454
},
5555
)?;

0 commit comments

Comments
 (0)