[pyflakes] Improve error message for UndefinedName when a builtin was added in a newer version than specified in Ruff config (F821) (#13293)

This commit is contained in:
Alex Waygood 2024-09-10 14:03:52 -04:00 committed by GitHub
parent b7cef6c999
commit 1d5bd89987
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 223 additions and 165 deletions

View file

@ -1,5 +1,6 @@
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_python_semantic::Exceptions; use ruff_python_semantic::Exceptions;
use ruff_python_stdlib::builtins::version_builtin_was_added;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::codes::Rule; use crate::codes::Rule;
@ -35,9 +36,12 @@ pub(crate) fn unresolved_references(checker: &mut Checker) {
} }
} }
let symbol_name = reference.name(checker.locator);
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName { pyflakes::rules::UndefinedName {
name: reference.name(checker.locator).to_string(), name: symbol_name.to_string(),
minor_version_builtin_added: version_builtin_was_added(symbol_name),
}, },
reference.range(), reference.range(),
)); ));

View file

@ -208,6 +208,18 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn f821_with_builtin_added_on_new_py_version_but_old_target_version_specified() {
let diagnostics = test_snippet(
"PythonFinalizationError",
&LinterSettings {
target_version: crate::settings::types::PythonVersion::Py312,
..LinterSettings::for_rule(Rule::UndefinedName)
},
);
assert_messages!(diagnostics);
}
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))] #[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
#[test_case(Rule::UnusedImport, Path::new("__init__.py"))] #[test_case(Rule::UnusedImport, Path::new("__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))]

View file

@ -19,17 +19,35 @@ use ruff_macros::{derive_message_formats, violation};
/// return n * 2 /// return n * 2
/// ``` /// ```
/// ///
/// ## Options
/// - [`target-version`]: Can be used to configure which symbols Ruff will understand
/// as being available in the `builtins` namespace.
///
/// ## References /// ## References
/// - [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding) /// - [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)
#[violation] #[violation]
pub struct UndefinedName { pub struct UndefinedName {
pub(crate) name: String, pub(crate) name: String,
pub(crate) minor_version_builtin_added: Option<u8>,
} }
impl Violation for UndefinedName { impl Violation for UndefinedName {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let UndefinedName { name } = self; let UndefinedName {
format!("Undefined name `{name}`") name,
minor_version_builtin_added,
} = self;
let tip = minor_version_builtin_added.map(|version_added| {
format!(
r#"Consider specifying `requires-python = ">= 3.{version_added}"` or `tool.ruff.target-version = "py3{version_added}"` in your `pyproject.toml` file."#
)
});
if let Some(tip) = tip {
format!("Undefined name `{name}`. {tip}")
} else {
format!("Undefined name `{name}`")
}
} }
} }

View file

@ -0,0 +1,8 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
<filename>:1:1: F821 Undefined name `PythonFinalizationError`. Consider specifying `requires-python = ">= 3.13"` or `tool.ruff.target-version = "py313"` in your `pyproject.toml` file.
|
1 | PythonFinalizationError
| ^^^^^^^^^^^^^^^^^^^^^^^ F821
|

View file

@ -23,181 +23,181 @@ pub const MAGIC_GLOBALS: &[&str] = &[
"__file__", "__file__",
]; ];
static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[
"ArithmeticError",
"AssertionError",
"AttributeError",
"BaseException",
"BlockingIOError",
"BrokenPipeError",
"BufferError",
"BytesWarning",
"ChildProcessError",
"ConnectionAbortedError",
"ConnectionError",
"ConnectionRefusedError",
"ConnectionResetError",
"DeprecationWarning",
"EOFError",
"Ellipsis",
"EnvironmentError",
"Exception",
"False",
"FileExistsError",
"FileNotFoundError",
"FloatingPointError",
"FutureWarning",
"GeneratorExit",
"IOError",
"ImportError",
"ImportWarning",
"IndentationError",
"IndexError",
"InterruptedError",
"IsADirectoryError",
"KeyError",
"KeyboardInterrupt",
"LookupError",
"MemoryError",
"ModuleNotFoundError",
"NameError",
"None",
"NotADirectoryError",
"NotImplemented",
"NotImplementedError",
"OSError",
"OverflowError",
"PendingDeprecationWarning",
"PermissionError",
"ProcessLookupError",
"RecursionError",
"ReferenceError",
"ResourceWarning",
"RuntimeError",
"RuntimeWarning",
"StopAsyncIteration",
"StopIteration",
"SyntaxError",
"SyntaxWarning",
"SystemError",
"SystemExit",
"TabError",
"TimeoutError",
"True",
"TypeError",
"UnboundLocalError",
"UnicodeDecodeError",
"UnicodeEncodeError",
"UnicodeError",
"UnicodeTranslateError",
"UnicodeWarning",
"UserWarning",
"ValueError",
"Warning",
"ZeroDivisionError",
"__build_class__",
"__debug__",
"__doc__",
"__import__",
"__loader__",
"__name__",
"__package__",
"__spec__",
"abs",
"all",
"any",
"ascii",
"bin",
"bool",
"breakpoint",
"bytearray",
"bytes",
"callable",
"chr",
"classmethod",
"compile",
"complex",
"copyright",
"credits",
"delattr",
"dict",
"dir",
"divmod",
"enumerate",
"eval",
"exec",
"exit",
"filter",
"float",
"format",
"frozenset",
"getattr",
"globals",
"hasattr",
"hash",
"help",
"hex",
"id",
"input",
"int",
"isinstance",
"issubclass",
"iter",
"len",
"license",
"list",
"locals",
"map",
"max",
"memoryview",
"min",
"next",
"object",
"oct",
"open",
"ord",
"pow",
"print",
"property",
"quit",
"range",
"repr",
"reversed",
"round",
"set",
"setattr",
"slice",
"sorted",
"staticmethod",
"str",
"sum",
"super",
"tuple",
"type",
"vars",
"zip",
];
static PY310_PLUS_BUILTINS: &[&str] = &["EncodingWarning", "aiter", "anext"];
static PY311_PLUS_BUILTINS: &[&str] = &["BaseExceptionGroup", "ExceptionGroup"];
static PY313_PLUS_BUILTINS: &[&str] = &["PythonFinalizationError"];
/// Return the list of builtins for the given Python minor version. /// Return the list of builtins for the given Python minor version.
/// ///
/// Intended to be kept in sync with [`is_python_builtin`]. /// Intended to be kept in sync with [`is_python_builtin`].
pub fn python_builtins(minor_version: u8, is_notebook: bool) -> Vec<&'static str> { pub fn python_builtins(minor_version: u8, is_notebook: bool) -> Vec<&'static str> {
let mut builtins = vec![ let mut builtins = ALWAYS_AVAILABLE_BUILTINS.to_vec();
"ArithmeticError",
"AssertionError",
"AttributeError",
"BaseException",
"BlockingIOError",
"BrokenPipeError",
"BufferError",
"BytesWarning",
"ChildProcessError",
"ConnectionAbortedError",
"ConnectionError",
"ConnectionRefusedError",
"ConnectionResetError",
"DeprecationWarning",
"EOFError",
"Ellipsis",
"EnvironmentError",
"Exception",
"False",
"FileExistsError",
"FileNotFoundError",
"FloatingPointError",
"FutureWarning",
"GeneratorExit",
"IOError",
"ImportError",
"ImportWarning",
"IndentationError",
"IndexError",
"InterruptedError",
"IsADirectoryError",
"KeyError",
"KeyboardInterrupt",
"LookupError",
"MemoryError",
"ModuleNotFoundError",
"NameError",
"None",
"NotADirectoryError",
"NotImplemented",
"NotImplementedError",
"OSError",
"OverflowError",
"PendingDeprecationWarning",
"PermissionError",
"ProcessLookupError",
"RecursionError",
"ReferenceError",
"ResourceWarning",
"RuntimeError",
"RuntimeWarning",
"StopAsyncIteration",
"StopIteration",
"SyntaxError",
"SyntaxWarning",
"SystemError",
"SystemExit",
"TabError",
"TimeoutError",
"True",
"TypeError",
"UnboundLocalError",
"UnicodeDecodeError",
"UnicodeEncodeError",
"UnicodeError",
"UnicodeTranslateError",
"UnicodeWarning",
"UserWarning",
"ValueError",
"Warning",
"ZeroDivisionError",
"__build_class__",
"__debug__",
"__doc__",
"__import__",
"__loader__",
"__name__",
"__package__",
"__spec__",
"abs",
"all",
"any",
"ascii",
"bin",
"bool",
"breakpoint",
"bytearray",
"bytes",
"callable",
"chr",
"classmethod",
"compile",
"complex",
"copyright",
"credits",
"delattr",
"dict",
"dir",
"divmod",
"enumerate",
"eval",
"exec",
"exit",
"filter",
"float",
"format",
"frozenset",
"getattr",
"globals",
"hasattr",
"hash",
"help",
"hex",
"id",
"input",
"int",
"isinstance",
"issubclass",
"iter",
"len",
"license",
"list",
"locals",
"map",
"max",
"memoryview",
"min",
"next",
"object",
"oct",
"open",
"ord",
"pow",
"print",
"property",
"quit",
"range",
"repr",
"reversed",
"round",
"set",
"setattr",
"slice",
"sorted",
"staticmethod",
"str",
"sum",
"super",
"tuple",
"type",
"vars",
"zip",
];
if minor_version >= 10 { if minor_version >= 10 {
builtins.extend(&["EncodingWarning", "aiter", "anext"]); builtins.extend(PY310_PLUS_BUILTINS);
} }
if minor_version >= 11 { if minor_version >= 11 {
builtins.extend(&["BaseExceptionGroup", "ExceptionGroup"]); builtins.extend(PY311_PLUS_BUILTINS);
} }
if minor_version >= 13 { if minor_version >= 13 {
builtins.push("PythonFinalizationError"); builtins.extend(PY313_PLUS_BUILTINS);
} }
if is_notebook { if is_notebook {
builtins.extend(IPYTHON_BUILTINS); builtins.extend(IPYTHON_BUILTINS);
} }
builtins builtins
} }
@ -370,6 +370,22 @@ pub fn is_python_builtin(name: &str, minor_version: u8, is_notebook: bool) -> bo
) )
} }
/// Return `Some(version)`, where `version` corresponds to the Python minor version
/// in which the builtin was added
pub fn version_builtin_was_added(name: &str) -> Option<u8> {
if PY310_PLUS_BUILTINS.contains(&name) {
Some(10)
} else if PY311_PLUS_BUILTINS.contains(&name) {
Some(11)
} else if PY313_PLUS_BUILTINS.contains(&name) {
Some(13)
} else if ALWAYS_AVAILABLE_BUILTINS.contains(&name) {
Some(0)
} else {
None
}
}
/// Returns `true` if the given name is that of a Python builtin iterator. /// Returns `true` if the given name is that of a Python builtin iterator.
pub fn is_iterator(name: &str) -> bool { pub fn is_iterator(name: &str) -> bool {
matches!( matches!(