mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-15 08:00:46 +00:00
Add a method to Checker
for cached parsing of stringified type annotations (#13158)
This commit is contained in:
parent
ea0246c51a
commit
b7c7b4b387
5 changed files with 186 additions and 114 deletions
|
@ -26,10 +26,12 @@
|
||||||
//! represents the lint-rule analysis phase. In the future, these steps may be separated into
|
//! represents the lint-rule analysis phase. In the future, these steps may be separated into
|
||||||
//! distinct passes over the AST.
|
//! distinct passes over the AST.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, IsolationLevel};
|
use ruff_diagnostics::{Diagnostic, IsolationLevel};
|
||||||
use ruff_notebook::{CellOffsets, NotebookIndex};
|
use ruff_notebook::{CellOffsets, NotebookIndex};
|
||||||
|
@ -40,13 +42,13 @@ use ruff_python_ast::str::Quote;
|
||||||
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
|
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
self as ast, AnyParameterRef, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext,
|
self as ast, AnyParameterRef, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext,
|
||||||
FStringElement, Keyword, MatchCase, ModExpression, ModModule, Parameter, Parameters, Pattern,
|
FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern, Stmt, Suite,
|
||||||
Stmt, Suite, UnaryOp,
|
UnaryOp,
|
||||||
};
|
};
|
||||||
use ruff_python_ast::{helpers, str, visitor, PySourceType};
|
use ruff_python_ast::{helpers, str, visitor, PySourceType};
|
||||||
use ruff_python_codegen::{Generator, Stylist};
|
use ruff_python_codegen::{Generator, Stylist};
|
||||||
use ruff_python_index::Indexer;
|
use ruff_python_index::Indexer;
|
||||||
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
|
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind, ParsedAnnotation};
|
||||||
use ruff_python_parser::{Parsed, Tokens};
|
use ruff_python_parser::{Parsed, Tokens};
|
||||||
use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags};
|
use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags};
|
||||||
use ruff_python_semantic::analyze::{imports, typing};
|
use ruff_python_semantic::analyze::{imports, typing};
|
||||||
|
@ -177,8 +179,10 @@ impl ExpectedDocstringKind {
|
||||||
pub(crate) struct Checker<'a> {
|
pub(crate) struct Checker<'a> {
|
||||||
/// The [`Parsed`] output for the source code.
|
/// The [`Parsed`] output for the source code.
|
||||||
parsed: &'a Parsed<ModModule>,
|
parsed: &'a Parsed<ModModule>,
|
||||||
|
/// An internal cache for parsed string annotations
|
||||||
|
parsed_annotations_cache: ParsedAnnotationsCache<'a>,
|
||||||
/// The [`Parsed`] output for the type annotation the checker is currently in.
|
/// The [`Parsed`] output for the type annotation the checker is currently in.
|
||||||
parsed_type_annotation: Option<&'a Parsed<ModExpression>>,
|
parsed_type_annotation: Option<&'a ParsedAnnotation>,
|
||||||
/// The [`Path`] to the file under analysis.
|
/// The [`Path`] to the file under analysis.
|
||||||
path: &'a Path,
|
path: &'a Path,
|
||||||
/// The [`Path`] to the package containing the current file.
|
/// The [`Path`] to the package containing the current file.
|
||||||
|
@ -229,6 +233,7 @@ impl<'a> Checker<'a> {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
parsed: &'a Parsed<ModModule>,
|
parsed: &'a Parsed<ModModule>,
|
||||||
|
parsed_annotations_arena: &'a typed_arena::Arena<ParsedAnnotation>,
|
||||||
settings: &'a LinterSettings,
|
settings: &'a LinterSettings,
|
||||||
noqa_line_for: &'a NoqaMapping,
|
noqa_line_for: &'a NoqaMapping,
|
||||||
noqa: flags::Noqa,
|
noqa: flags::Noqa,
|
||||||
|
@ -245,6 +250,7 @@ impl<'a> Checker<'a> {
|
||||||
Checker {
|
Checker {
|
||||||
parsed,
|
parsed,
|
||||||
parsed_type_annotation: None,
|
parsed_type_annotation: None,
|
||||||
|
parsed_annotations_cache: ParsedAnnotationsCache::new(parsed_annotations_arena),
|
||||||
settings,
|
settings,
|
||||||
noqa_line_for,
|
noqa_line_for,
|
||||||
noqa,
|
noqa,
|
||||||
|
@ -333,8 +339,8 @@ impl<'a> Checker<'a> {
|
||||||
/// Returns the [`Tokens`] for the parsed type annotation if the checker is in a typing context
|
/// Returns the [`Tokens`] for the parsed type annotation if the checker is in a typing context
|
||||||
/// or the parsed source code.
|
/// or the parsed source code.
|
||||||
pub(crate) fn tokens(&self) -> &'a Tokens {
|
pub(crate) fn tokens(&self) -> &'a Tokens {
|
||||||
if let Some(parsed_type_annotation) = self.parsed_type_annotation {
|
if let Some(type_annotation) = self.parsed_type_annotation {
|
||||||
parsed_type_annotation.tokens()
|
type_annotation.parsed().tokens()
|
||||||
} else {
|
} else {
|
||||||
self.parsed.tokens()
|
self.parsed.tokens()
|
||||||
}
|
}
|
||||||
|
@ -405,6 +411,19 @@ impl<'a> Checker<'a> {
|
||||||
.map(|node_id| IsolationLevel::Group(node_id.into()))
|
.map(|node_id| IsolationLevel::Group(node_id.into()))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a stringified type annotation as an AST expression,
|
||||||
|
/// e.g. `"List[str]"` in `x: "List[str]"`
|
||||||
|
///
|
||||||
|
/// This method is a wrapper around [`ruff_python_parser::typing::parse_type_annotation`]
|
||||||
|
/// that adds caching.
|
||||||
|
pub(crate) fn parse_type_annotation(
|
||||||
|
&self,
|
||||||
|
annotation: &ast::ExprStringLiteral,
|
||||||
|
) -> Option<&'a ParsedAnnotation> {
|
||||||
|
self.parsed_annotations_cache
|
||||||
|
.lookup_or_parse(annotation, self.locator.contents())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Visitor<'a> for Checker<'a> {
|
impl<'a> Visitor<'a> for Checker<'a> {
|
||||||
|
@ -2168,18 +2187,13 @@ impl<'a> Checker<'a> {
|
||||||
///
|
///
|
||||||
/// class Bar: pass
|
/// class Bar: pass
|
||||||
/// ```
|
/// ```
|
||||||
fn visit_deferred_string_type_definitions(
|
fn visit_deferred_string_type_definitions(&mut self) {
|
||||||
&mut self,
|
|
||||||
allocator: &'a typed_arena::Arena<Parsed<ModExpression>>,
|
|
||||||
) {
|
|
||||||
let snapshot = self.semantic.snapshot();
|
let snapshot = self.semantic.snapshot();
|
||||||
while !self.visit.string_type_definitions.is_empty() {
|
while !self.visit.string_type_definitions.is_empty() {
|
||||||
let type_definitions = std::mem::take(&mut self.visit.string_type_definitions);
|
let type_definitions = std::mem::take(&mut self.visit.string_type_definitions);
|
||||||
for (string_expr, snapshot) in type_definitions {
|
for (string_expr, snapshot) in type_definitions {
|
||||||
if let Ok((parsed_annotation, kind)) =
|
let annotation_parse_result = self.parse_type_annotation(string_expr);
|
||||||
parse_type_annotation(string_expr, self.locator.contents())
|
if let Some(parsed_annotation) = annotation_parse_result {
|
||||||
{
|
|
||||||
let parsed_annotation = allocator.alloc(parsed_annotation);
|
|
||||||
self.parsed_type_annotation = Some(parsed_annotation);
|
self.parsed_type_annotation = Some(parsed_annotation);
|
||||||
|
|
||||||
let annotation = string_expr.value.to_str();
|
let annotation = string_expr.value.to_str();
|
||||||
|
@ -2198,7 +2212,7 @@ impl<'a> Checker<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let type_definition_flag = match kind {
|
let type_definition_flag = match parsed_annotation.kind() {
|
||||||
AnnotationKind::Simple => SemanticModelFlags::SIMPLE_STRING_TYPE_DEFINITION,
|
AnnotationKind::Simple => SemanticModelFlags::SIMPLE_STRING_TYPE_DEFINITION,
|
||||||
AnnotationKind::Complex => {
|
AnnotationKind::Complex => {
|
||||||
SemanticModelFlags::COMPLEX_STRING_TYPE_DEFINITION
|
SemanticModelFlags::COMPLEX_STRING_TYPE_DEFINITION
|
||||||
|
@ -2207,7 +2221,7 @@ impl<'a> Checker<'a> {
|
||||||
|
|
||||||
self.semantic.flags |=
|
self.semantic.flags |=
|
||||||
SemanticModelFlags::TYPE_DEFINITION | type_definition_flag;
|
SemanticModelFlags::TYPE_DEFINITION | type_definition_flag;
|
||||||
self.visit_expr(parsed_annotation.expr());
|
self.visit_expr(parsed_annotation.expression());
|
||||||
self.parsed_type_annotation = None;
|
self.parsed_type_annotation = None;
|
||||||
} else {
|
} else {
|
||||||
if self.enabled(Rule::ForwardAnnotationSyntaxError) {
|
if self.enabled(Rule::ForwardAnnotationSyntaxError) {
|
||||||
|
@ -2220,6 +2234,35 @@ impl<'a> Checker<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're parsing string annotations inside string annotations
|
||||||
|
// (which is the only reason we might enter a second iteration of this loop),
|
||||||
|
// the cache is no longer valid. We must invalidate it to avoid an infinite loop.
|
||||||
|
//
|
||||||
|
// For example, consider the following annotation:
|
||||||
|
// ```python
|
||||||
|
// x: "list['str']"
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The first time we visit the AST, we see `"list['str']"`
|
||||||
|
// and identify it as a stringified annotation.
|
||||||
|
// We store it in `self.visit.string_type_definitions` to be analyzed later.
|
||||||
|
//
|
||||||
|
// After the entire tree has been visited, we look through
|
||||||
|
// `self.visit.string_type_definitions` and find `"list['str']"`.
|
||||||
|
// We parse it, and it becomes `list['str']`.
|
||||||
|
// After parsing it, we call `self.visit_expr()` on the `list['str']` node,
|
||||||
|
// and that `visit_expr` call is going to find `'str'` inside that node and
|
||||||
|
// identify it as a string type definition, appending it to
|
||||||
|
// `self.visit.string_type_definitions`, ensuring that there will be one
|
||||||
|
// more iteration of this loop.
|
||||||
|
//
|
||||||
|
// Unfortunately, the `TextRange` of `'str'`
|
||||||
|
// here will be *relative to the parsed `list['str']` node* rather than
|
||||||
|
// *relative to the original module*, meaning the cache
|
||||||
|
// (which uses `TextSize` as the key) becomes invalid on the second
|
||||||
|
// iteration of this loop.
|
||||||
|
self.parsed_annotations_cache.clear();
|
||||||
}
|
}
|
||||||
self.semantic.restore(snapshot);
|
self.semantic.restore(snapshot);
|
||||||
}
|
}
|
||||||
|
@ -2283,14 +2326,14 @@ impl<'a> Checker<'a> {
|
||||||
/// After initial traversal of the source tree has been completed,
|
/// After initial traversal of the source tree has been completed,
|
||||||
/// recursively visit all AST nodes that were deferred on the first pass.
|
/// recursively visit all AST nodes that were deferred on the first pass.
|
||||||
/// This includes lambdas, functions, type parameters, and type annotations.
|
/// This includes lambdas, functions, type parameters, and type annotations.
|
||||||
fn visit_deferred(&mut self, allocator: &'a typed_arena::Arena<Parsed<ModExpression>>) {
|
fn visit_deferred(&mut self) {
|
||||||
while !self.visit.is_empty() {
|
while !self.visit.is_empty() {
|
||||||
self.visit_deferred_class_bases();
|
self.visit_deferred_class_bases();
|
||||||
self.visit_deferred_functions();
|
self.visit_deferred_functions();
|
||||||
self.visit_deferred_type_param_definitions();
|
self.visit_deferred_type_param_definitions();
|
||||||
self.visit_deferred_lambdas();
|
self.visit_deferred_lambdas();
|
||||||
self.visit_deferred_future_type_definitions();
|
self.visit_deferred_future_type_definitions();
|
||||||
self.visit_deferred_string_type_definitions(allocator);
|
self.visit_deferred_string_type_definitions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2358,6 +2401,42 @@ impl<'a> Checker<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ParsedAnnotationsCache<'a> {
|
||||||
|
arena: &'a typed_arena::Arena<ParsedAnnotation>,
|
||||||
|
by_offset: RefCell<FxHashMap<TextSize, Option<&'a ParsedAnnotation>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ParsedAnnotationsCache<'a> {
|
||||||
|
fn new(arena: &'a typed_arena::Arena<ParsedAnnotation>) -> Self {
|
||||||
|
Self {
|
||||||
|
arena,
|
||||||
|
by_offset: RefCell::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_or_parse(
|
||||||
|
&self,
|
||||||
|
annotation: &ast::ExprStringLiteral,
|
||||||
|
source: &str,
|
||||||
|
) -> Option<&'a ParsedAnnotation> {
|
||||||
|
*self
|
||||||
|
.by_offset
|
||||||
|
.borrow_mut()
|
||||||
|
.entry(annotation.start())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
if let Ok(annotation) = parse_type_annotation(annotation, source) {
|
||||||
|
Some(self.arena.alloc(annotation))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&self) {
|
||||||
|
self.by_offset.borrow_mut().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn check_ast(
|
pub(crate) fn check_ast(
|
||||||
parsed: &Parsed<ModModule>,
|
parsed: &Parsed<ModModule>,
|
||||||
|
@ -2393,8 +2472,10 @@ pub(crate) fn check_ast(
|
||||||
python_ast: parsed.suite(),
|
python_ast: parsed.suite(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let allocator = typed_arena::Arena::new();
|
||||||
let mut checker = Checker::new(
|
let mut checker = Checker::new(
|
||||||
parsed,
|
parsed,
|
||||||
|
&allocator,
|
||||||
settings,
|
settings,
|
||||||
noqa_line_for,
|
noqa_line_for,
|
||||||
noqa,
|
noqa,
|
||||||
|
@ -2417,8 +2498,7 @@ pub(crate) fn check_ast(
|
||||||
// Visit any deferred syntax nodes. Take care to visit in order, such that we avoid adding
|
// Visit any deferred syntax nodes. Take care to visit in order, such that we avoid adding
|
||||||
// new deferred nodes after visiting nodes of that kind. For example, visiting a deferred
|
// new deferred nodes after visiting nodes of that kind. For example, visiting a deferred
|
||||||
// function can add a deferred lambda, but the opposite is not true.
|
// function can add a deferred lambda, but the opposite is not true.
|
||||||
let allocator = typed_arena::Arena::new();
|
checker.visit_deferred();
|
||||||
checker.visit_deferred(&allocator);
|
|
||||||
checker.visit_exports();
|
checker.visit_exports();
|
||||||
|
|
||||||
// Check docstrings, bindings, and unresolved references.
|
// Check docstrings, bindings, and unresolved references.
|
||||||
|
|
|
@ -4,7 +4,6 @@ use ruff_python_ast::helpers::ReturnStatementVisitor;
|
||||||
use ruff_python_ast::identifier::Identifier;
|
use ruff_python_ast::identifier::Identifier;
|
||||||
use ruff_python_ast::visitor::Visitor;
|
use ruff_python_ast::visitor::Visitor;
|
||||||
use ruff_python_ast::{self as ast, Expr, ParameterWithDefault, Stmt};
|
use ruff_python_ast::{self as ast, Expr, ParameterWithDefault, Stmt};
|
||||||
use ruff_python_parser::typing::parse_type_annotation;
|
|
||||||
use ruff_python_semantic::analyze::visibility;
|
use ruff_python_semantic::analyze::visibility;
|
||||||
use ruff_python_semantic::Definition;
|
use ruff_python_semantic::Definition;
|
||||||
use ruff_python_stdlib::typing::simple_magic_return_type;
|
use ruff_python_stdlib::typing::simple_magic_return_type;
|
||||||
|
@ -514,13 +513,10 @@ fn check_dynamically_typed<F>(
|
||||||
{
|
{
|
||||||
if let Expr::StringLiteral(string_expr) = annotation {
|
if let Expr::StringLiteral(string_expr) = annotation {
|
||||||
// Quoted annotations
|
// Quoted annotations
|
||||||
if let Ok((parsed_annotation, _)) =
|
if let Some(parsed_annotation) = checker.parse_type_annotation(string_expr) {
|
||||||
parse_type_annotation(string_expr, checker.locator().contents())
|
|
||||||
{
|
|
||||||
if type_hint_resolves_to_any(
|
if type_hint_resolves_to_any(
|
||||||
parsed_annotation.expr(),
|
parsed_annotation.expression(),
|
||||||
checker.semantic(),
|
checker,
|
||||||
checker.locator(),
|
|
||||||
checker.settings.target_version.minor(),
|
checker.settings.target_version.minor(),
|
||||||
) {
|
) {
|
||||||
diagnostics.push(Diagnostic::new(
|
diagnostics.push(Diagnostic::new(
|
||||||
|
@ -530,12 +526,7 @@ fn check_dynamically_typed<F>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if type_hint_resolves_to_any(
|
if type_hint_resolves_to_any(annotation, checker, checker.settings.target_version.minor()) {
|
||||||
annotation,
|
|
||||||
checker.semantic(),
|
|
||||||
checker.locator(),
|
|
||||||
checker.settings.target_version.minor(),
|
|
||||||
) {
|
|
||||||
diagnostics.push(Diagnostic::new(
|
diagnostics.push(Diagnostic::new(
|
||||||
AnyType { name: func() },
|
AnyType { name: func() },
|
||||||
annotation.range(),
|
annotation.range(),
|
||||||
|
|
|
@ -7,7 +7,6 @@ use ruff_macros::{derive_message_formats, violation};
|
||||||
|
|
||||||
use ruff_python_ast::name::Name;
|
use ruff_python_ast::name::Name;
|
||||||
use ruff_python_ast::{self as ast, Expr, Operator, ParameterWithDefault, Parameters};
|
use ruff_python_ast::{self as ast, Expr, Operator, ParameterWithDefault, Parameters};
|
||||||
use ruff_python_parser::typing::parse_type_annotation;
|
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
@ -180,13 +179,10 @@ pub(crate) fn implicit_optional(checker: &mut Checker, parameters: &Parameters)
|
||||||
|
|
||||||
if let Expr::StringLiteral(string_expr) = annotation.as_ref() {
|
if let Expr::StringLiteral(string_expr) = annotation.as_ref() {
|
||||||
// Quoted annotation.
|
// Quoted annotation.
|
||||||
if let Ok((parsed_annotation, kind)) =
|
if let Some(parsed_annotation) = checker.parse_type_annotation(string_expr) {
|
||||||
parse_type_annotation(string_expr, checker.locator().contents())
|
|
||||||
{
|
|
||||||
let Some(expr) = type_hint_explicitly_allows_none(
|
let Some(expr) = type_hint_explicitly_allows_none(
|
||||||
parsed_annotation.expr(),
|
parsed_annotation.expression(),
|
||||||
checker.semantic(),
|
checker,
|
||||||
checker.locator(),
|
|
||||||
checker.settings.target_version.minor(),
|
checker.settings.target_version.minor(),
|
||||||
) else {
|
) else {
|
||||||
continue;
|
continue;
|
||||||
|
@ -195,7 +191,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, parameters: &Parameters)
|
||||||
|
|
||||||
let mut diagnostic =
|
let mut diagnostic =
|
||||||
Diagnostic::new(ImplicitOptional { conversion_type }, expr.range());
|
Diagnostic::new(ImplicitOptional { conversion_type }, expr.range());
|
||||||
if kind.is_simple() {
|
if parsed_annotation.kind().is_simple() {
|
||||||
diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr));
|
diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr));
|
||||||
}
|
}
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
|
@ -204,8 +200,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, parameters: &Parameters)
|
||||||
// Unquoted annotation.
|
// Unquoted annotation.
|
||||||
let Some(expr) = type_hint_explicitly_allows_none(
|
let Some(expr) = type_hint_explicitly_allows_none(
|
||||||
annotation,
|
annotation,
|
||||||
checker.semantic(),
|
checker,
|
||||||
checker.locator(),
|
|
||||||
checker.settings.target_version.minor(),
|
checker.settings.target_version.minor(),
|
||||||
) else {
|
) else {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -2,10 +2,9 @@ use itertools::Either::{Left, Right};
|
||||||
use ruff_python_ast::name::QualifiedName;
|
use ruff_python_ast::name::QualifiedName;
|
||||||
use ruff_python_ast::{self as ast, Expr, Operator};
|
use ruff_python_ast::{self as ast, Expr, Operator};
|
||||||
|
|
||||||
use ruff_python_parser::typing::parse_type_annotation;
|
|
||||||
use ruff_python_semantic::SemanticModel;
|
|
||||||
use ruff_python_stdlib::sys::is_known_standard_library;
|
use ruff_python_stdlib::sys::is_known_standard_library;
|
||||||
use ruff_source_file::Locator;
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
/// Returns `true` if the given qualified name is a known type.
|
/// Returns `true` if the given qualified name is a known type.
|
||||||
///
|
///
|
||||||
|
@ -40,7 +39,7 @@ enum TypingTarget<'a> {
|
||||||
Object,
|
Object,
|
||||||
|
|
||||||
/// Forward reference to a type e.g., `"List[str]"`.
|
/// Forward reference to a type e.g., `"List[str]"`.
|
||||||
ForwardReference(Expr),
|
ForwardReference(&'a Expr),
|
||||||
|
|
||||||
/// A `typing.Union` type e.g., `Union[int, str]`.
|
/// A `typing.Union` type e.g., `Union[int, str]`.
|
||||||
Union(&'a Expr),
|
Union(&'a Expr),
|
||||||
|
@ -71,12 +70,8 @@ enum TypingTarget<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TypingTarget<'a> {
|
impl<'a> TypingTarget<'a> {
|
||||||
fn try_from_expr(
|
fn try_from_expr(expr: &'a Expr, checker: &'a Checker, minor_version: u8) -> Option<Self> {
|
||||||
expr: &'a Expr,
|
let semantic = checker.semantic();
|
||||||
semantic: &SemanticModel,
|
|
||||||
locator: &Locator,
|
|
||||||
minor_version: u8,
|
|
||||||
) -> Option<Self> {
|
|
||||||
match expr {
|
match expr {
|
||||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||||
semantic.resolve_qualified_name(value).map_or(
|
semantic.resolve_qualified_name(value).map_or(
|
||||||
|
@ -112,15 +107,12 @@ impl<'a> TypingTarget<'a> {
|
||||||
..
|
..
|
||||||
}) => Some(TypingTarget::PEP604Union(left, right)),
|
}) => Some(TypingTarget::PEP604Union(left, right)),
|
||||||
Expr::NoneLiteral(_) => Some(TypingTarget::None),
|
Expr::NoneLiteral(_) => Some(TypingTarget::None),
|
||||||
Expr::StringLiteral(string_expr) => parse_type_annotation(
|
Expr::StringLiteral(string_expr) => checker
|
||||||
string_expr,
|
.parse_type_annotation(string_expr)
|
||||||
locator.contents(),
|
.as_ref()
|
||||||
)
|
.map(|parsed_annotation| {
|
||||||
.map_or(None, |(parsed_annotation, _)| {
|
TypingTarget::ForwardReference(parsed_annotation.expression())
|
||||||
Some(TypingTarget::ForwardReference(
|
}),
|
||||||
parsed_annotation.into_expr(),
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
_ => semantic.resolve_qualified_name(expr).map_or(
|
_ => semantic.resolve_qualified_name(expr).map_or(
|
||||||
// If we can't resolve the call path, it must be defined in the
|
// If we can't resolve the call path, it must be defined in the
|
||||||
// same file, so we assume it's `Any` as it could be a type alias.
|
// same file, so we assume it's `Any` as it could be a type alias.
|
||||||
|
@ -149,12 +141,7 @@ impl<'a> TypingTarget<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the [`TypingTarget`] explicitly allows `None`.
|
/// Check if the [`TypingTarget`] explicitly allows `None`.
|
||||||
fn contains_none(
|
fn contains_none(&self, checker: &Checker, minor_version: u8) -> bool {
|
||||||
&self,
|
|
||||||
semantic: &SemanticModel,
|
|
||||||
locator: &Locator,
|
|
||||||
minor_version: u8,
|
|
||||||
) -> bool {
|
|
||||||
match self {
|
match self {
|
||||||
TypingTarget::None
|
TypingTarget::None
|
||||||
| TypingTarget::Optional(_)
|
| TypingTarget::Optional(_)
|
||||||
|
@ -166,43 +153,43 @@ impl<'a> TypingTarget<'a> {
|
||||||
TypingTarget::Literal(slice) => resolve_slice_value(slice).any(|element| {
|
TypingTarget::Literal(slice) => resolve_slice_value(slice).any(|element| {
|
||||||
// Literal can only contain `None`, a literal value, other `Literal`
|
// Literal can only contain `None`, a literal value, other `Literal`
|
||||||
// or an enum value.
|
// or an enum value.
|
||||||
match TypingTarget::try_from_expr(element, semantic, locator, minor_version) {
|
match TypingTarget::try_from_expr(element, checker, minor_version) {
|
||||||
None | Some(TypingTarget::None) => true,
|
None | Some(TypingTarget::None) => true,
|
||||||
Some(new_target @ TypingTarget::Literal(_)) => {
|
Some(new_target @ TypingTarget::Literal(_)) => {
|
||||||
new_target.contains_none(semantic, locator, minor_version)
|
new_target.contains_none(checker, minor_version)
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
TypingTarget::Union(slice) => resolve_slice_value(slice).any(|element| {
|
TypingTarget::Union(slice) => resolve_slice_value(slice).any(|element| {
|
||||||
TypingTarget::try_from_expr(element, semantic, locator, minor_version)
|
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||||
.map_or(true, |new_target| {
|
.map_or(true, |new_target| {
|
||||||
new_target.contains_none(semantic, locator, minor_version)
|
new_target.contains_none(checker, minor_version)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
TypingTarget::PEP604Union(left, right) => [left, right].iter().any(|element| {
|
TypingTarget::PEP604Union(left, right) => [left, right].iter().any(|element| {
|
||||||
TypingTarget::try_from_expr(element, semantic, locator, minor_version)
|
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||||
.map_or(true, |new_target| {
|
.map_or(true, |new_target| {
|
||||||
new_target.contains_none(semantic, locator, minor_version)
|
new_target.contains_none(checker, minor_version)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
TypingTarget::Annotated(expr) => {
|
TypingTarget::Annotated(expr) => {
|
||||||
TypingTarget::try_from_expr(expr, semantic, locator, minor_version)
|
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||||
.map_or(true, |new_target| {
|
.map_or(true, |new_target| {
|
||||||
new_target.contains_none(semantic, locator, minor_version)
|
new_target.contains_none(checker, minor_version)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
TypingTarget::ForwardReference(expr) => {
|
TypingTarget::ForwardReference(expr) => {
|
||||||
TypingTarget::try_from_expr(expr, semantic, locator, minor_version)
|
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||||
.map_or(true, |new_target| {
|
.map_or(true, |new_target| {
|
||||||
new_target.contains_none(semantic, locator, minor_version)
|
new_target.contains_none(checker, minor_version)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the [`TypingTarget`] explicitly allows `Any`.
|
/// Check if the [`TypingTarget`] explicitly allows `Any`.
|
||||||
fn contains_any(&self, semantic: &SemanticModel, locator: &Locator, minor_version: u8) -> bool {
|
fn contains_any(&self, checker: &Checker, minor_version: u8) -> bool {
|
||||||
match self {
|
match self {
|
||||||
TypingTarget::Any => true,
|
TypingTarget::Any => true,
|
||||||
// `Literal` cannot contain `Any` as it's a dynamic value.
|
// `Literal` cannot contain `Any` as it's a dynamic value.
|
||||||
|
@ -213,27 +200,27 @@ impl<'a> TypingTarget<'a> {
|
||||||
| TypingTarget::Known
|
| TypingTarget::Known
|
||||||
| TypingTarget::Unknown => false,
|
| TypingTarget::Unknown => false,
|
||||||
TypingTarget::Union(slice) => resolve_slice_value(slice).any(|element| {
|
TypingTarget::Union(slice) => resolve_slice_value(slice).any(|element| {
|
||||||
TypingTarget::try_from_expr(element, semantic, locator, minor_version)
|
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||||
.map_or(true, |new_target| {
|
.map_or(true, |new_target| {
|
||||||
new_target.contains_any(semantic, locator, minor_version)
|
new_target.contains_any(checker, minor_version)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
TypingTarget::PEP604Union(left, right) => [left, right].iter().any(|element| {
|
TypingTarget::PEP604Union(left, right) => [left, right].iter().any(|element| {
|
||||||
TypingTarget::try_from_expr(element, semantic, locator, minor_version)
|
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||||
.map_or(true, |new_target| {
|
.map_or(true, |new_target| {
|
||||||
new_target.contains_any(semantic, locator, minor_version)
|
new_target.contains_any(checker, minor_version)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
TypingTarget::Annotated(expr) | TypingTarget::Optional(expr) => {
|
TypingTarget::Annotated(expr) | TypingTarget::Optional(expr) => {
|
||||||
TypingTarget::try_from_expr(expr, semantic, locator, minor_version)
|
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||||
.map_or(true, |new_target| {
|
.map_or(true, |new_target| {
|
||||||
new_target.contains_any(semantic, locator, minor_version)
|
new_target.contains_any(checker, minor_version)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
TypingTarget::ForwardReference(expr) => {
|
TypingTarget::ForwardReference(expr) => {
|
||||||
TypingTarget::try_from_expr(expr, semantic, locator, minor_version)
|
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||||
.map_or(true, |new_target| {
|
.map_or(true, |new_target| {
|
||||||
new_target.contains_any(semantic, locator, minor_version)
|
new_target.contains_any(checker, minor_version)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,11 +236,10 @@ impl<'a> TypingTarget<'a> {
|
||||||
/// This function assumes that the annotation is a valid typing annotation expression.
|
/// This function assumes that the annotation is a valid typing annotation expression.
|
||||||
pub(crate) fn type_hint_explicitly_allows_none<'a>(
|
pub(crate) fn type_hint_explicitly_allows_none<'a>(
|
||||||
annotation: &'a Expr,
|
annotation: &'a Expr,
|
||||||
semantic: &SemanticModel,
|
checker: &'a Checker,
|
||||||
locator: &Locator,
|
|
||||||
minor_version: u8,
|
minor_version: u8,
|
||||||
) -> Option<&'a Expr> {
|
) -> Option<&'a Expr> {
|
||||||
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
|
match TypingTarget::try_from_expr(annotation, checker, minor_version) {
|
||||||
None |
|
None |
|
||||||
// Short circuit on top level `None`, `Any` or `Optional`
|
// Short circuit on top level `None`, `Any` or `Optional`
|
||||||
Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None,
|
Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None,
|
||||||
|
@ -262,10 +248,10 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>(
|
||||||
// is found nested inside another type, then the outer type should
|
// is found nested inside another type, then the outer type should
|
||||||
// be returned.
|
// be returned.
|
||||||
Some(TypingTarget::Annotated(expr)) => {
|
Some(TypingTarget::Annotated(expr)) => {
|
||||||
type_hint_explicitly_allows_none(expr, semantic, locator, minor_version)
|
type_hint_explicitly_allows_none(expr, checker, minor_version)
|
||||||
}
|
}
|
||||||
Some(target) => {
|
Some(target) => {
|
||||||
if target.contains_none(semantic, locator, minor_version) {
|
if target.contains_none(checker, minor_version) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(annotation)
|
Some(annotation)
|
||||||
|
@ -279,20 +265,19 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>(
|
||||||
/// This function assumes that the annotation is a valid typing annotation expression.
|
/// This function assumes that the annotation is a valid typing annotation expression.
|
||||||
pub(crate) fn type_hint_resolves_to_any(
|
pub(crate) fn type_hint_resolves_to_any(
|
||||||
annotation: &Expr,
|
annotation: &Expr,
|
||||||
semantic: &SemanticModel,
|
checker: &Checker,
|
||||||
locator: &Locator,
|
|
||||||
minor_version: u8,
|
minor_version: u8,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
|
match TypingTarget::try_from_expr(annotation, checker, minor_version) {
|
||||||
None |
|
None |
|
||||||
// Short circuit on top level `Any`
|
// Short circuit on top level `Any`
|
||||||
Some(TypingTarget::Any) => true,
|
Some(TypingTarget::Any) => true,
|
||||||
// Top-level `Annotated` node should check if the inner type resolves
|
// Top-level `Annotated` node should check if the inner type resolves
|
||||||
// to `Any`.
|
// to `Any`.
|
||||||
Some(TypingTarget::Annotated(expr)) => {
|
Some(TypingTarget::Annotated(expr)) => {
|
||||||
type_hint_resolves_to_any(expr, semantic, locator, minor_version)
|
type_hint_resolves_to_any(expr, checker, minor_version)
|
||||||
}
|
}
|
||||||
Some(target) => target.contains_any(semantic, locator, minor_version),
|
Some(target) => target.contains_any(checker, minor_version),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,33 @@
|
||||||
|
|
||||||
use ruff_python_ast::relocate::relocate_expr;
|
use ruff_python_ast::relocate::relocate_expr;
|
||||||
use ruff_python_ast::str::raw_contents;
|
use ruff_python_ast::str::raw_contents;
|
||||||
use ruff_python_ast::{ExprStringLiteral, ModExpression, StringFlags, StringLiteral};
|
use ruff_python_ast::{Expr, ExprStringLiteral, ModExpression, StringFlags, StringLiteral};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::{parse_expression, parse_expression_range, ParseError, Parsed};
|
use crate::{parse_expression, parse_expression_range, ParseError, Parsed};
|
||||||
|
|
||||||
|
type AnnotationParseResult = Result<ParsedAnnotation, ParseError>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ParsedAnnotation {
|
||||||
|
parsed: Parsed<ModExpression>,
|
||||||
|
kind: AnnotationKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParsedAnnotation {
|
||||||
|
pub fn parsed(&self) -> &Parsed<ModExpression> {
|
||||||
|
&self.parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expression(&self) -> &Expr {
|
||||||
|
self.parsed.expr()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> AnnotationKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum AnnotationKind {
|
pub enum AnnotationKind {
|
||||||
/// The annotation is defined as part a simple string literal,
|
/// The annotation is defined as part a simple string literal,
|
||||||
|
@ -34,7 +56,7 @@ impl AnnotationKind {
|
||||||
pub fn parse_type_annotation(
|
pub fn parse_type_annotation(
|
||||||
string_expr: &ExprStringLiteral,
|
string_expr: &ExprStringLiteral,
|
||||||
source: &str,
|
source: &str,
|
||||||
) -> Result<(Parsed<ModExpression>, AnnotationKind), ParseError> {
|
) -> AnnotationParseResult {
|
||||||
let expr_text = &source[string_expr.range()];
|
let expr_text = &source[string_expr.range()];
|
||||||
|
|
||||||
if let [string_literal] = string_expr.value.as_slice() {
|
if let [string_literal] = string_expr.value.as_slice() {
|
||||||
|
@ -58,23 +80,22 @@ pub fn parse_type_annotation(
|
||||||
fn parse_simple_type_annotation(
|
fn parse_simple_type_annotation(
|
||||||
string_literal: &StringLiteral,
|
string_literal: &StringLiteral,
|
||||||
source: &str,
|
source: &str,
|
||||||
) -> Result<(Parsed<ModExpression>, AnnotationKind), ParseError> {
|
) -> AnnotationParseResult {
|
||||||
Ok((
|
let range_excluding_quotes = string_literal
|
||||||
parse_expression_range(
|
.range()
|
||||||
source,
|
.add_start(string_literal.flags.opener_len())
|
||||||
string_literal
|
.sub_end(string_literal.flags.closer_len());
|
||||||
.range()
|
Ok(ParsedAnnotation {
|
||||||
.add_start(string_literal.flags.opener_len())
|
parsed: parse_expression_range(source, range_excluding_quotes)?,
|
||||||
.sub_end(string_literal.flags.closer_len()),
|
kind: AnnotationKind::Simple,
|
||||||
)?,
|
})
|
||||||
AnnotationKind::Simple,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_complex_type_annotation(
|
fn parse_complex_type_annotation(string_expr: &ExprStringLiteral) -> AnnotationParseResult {
|
||||||
string_expr: &ExprStringLiteral,
|
|
||||||
) -> Result<(Parsed<ModExpression>, AnnotationKind), ParseError> {
|
|
||||||
let mut parsed = parse_expression(string_expr.value.to_str())?;
|
let mut parsed = parse_expression(string_expr.value.to_str())?;
|
||||||
relocate_expr(parsed.expr_mut(), string_expr.range());
|
relocate_expr(parsed.expr_mut(), string_expr.range());
|
||||||
Ok((parsed, AnnotationKind::Complex))
|
Ok(ParsedAnnotation {
|
||||||
|
parsed,
|
||||||
|
kind: AnnotationKind::Complex,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue