From 2a99c0be02ce6bb30247ca564833410eb08156d6 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 23 Dec 2024 11:15:28 +0100 Subject: [PATCH] Add `unused-ignore-comment` rule (#15084) --- .../mdtest/suppressions/knot-ignore.md | 52 ++++- .../mdtest/suppressions/type-ignore.md | 4 + crates/red_knot_python_semantic/src/lib.rs | 2 + .../src/suppression.rs | 199 +++++++++++++++++- crates/red_knot_python_semantic/src/types.rs | 3 + .../src/types/context.rs | 15 +- .../src/types/diagnostic.rs | 69 +++--- crates/ruff_benchmark/benches/red_knot.rs | 1 + 8 files changed, 284 insertions(+), 61 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot-ignore.md b/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot-ignore.md index 9fe1a952a9..b6b25a0700 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot-ignore.md +++ b/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot-ignore.md @@ -14,26 +14,55 @@ a = 4 + test # knot: ignore a = 4 + test # knot: ignore[unresolved-reference] ``` -## Useless suppression - -TODO: Red Knot should emit an `unused-suppression` diagnostic for the -`possibly-unresolved-reference` suppression. +## Unused suppression ```py test = 10 +# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'" a = test + 3 # knot: ignore[possibly-unresolved-reference] ``` -## Useless suppression if the error codes don't match - -TODO: Red Knot should emit a `unused-suppression` diagnostic for the `possibly-unresolved-reference` -suppression because it doesn't match the actual `unresolved-reference` diagnostic. +## Unused suppression if the error codes don't match ```py # error: [unresolved-reference] +# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'" a = test + 3 # knot: ignore[possibly-unresolved-reference] ``` +## Suppressed unused comment + +```py +# error: [unused-ignore-comment] +a = 10 / 2 # knot: ignore[division-by-zero] +a = 10 / 2 # knot: ignore[division-by-zero, unused-ignore-comment] +a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero] +a = 10 / 2 # knot: ignore[unused-ignore-comment] # type: ignore +a = 10 / 2 # type: ignore # knot: ignore[unused-ignore-comment] +``` + +## Unused ignore comment + +```py +# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unused-ignore-comment'" +a = 10 / 0 # knot: ignore[division-by-zero, unused-ignore-comment] +``` + +## Multiple unused comments + +Today, Red Knot emits a diagnostic for every unused code. We might want to group the codes by +comment at some point in the future. + +```py +# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'division-by-zero'" +# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'" +a = 10 / 2 # knot: ignore[division-by-zero, unresolved-reference] + +# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'invalid-assignment'" +# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'" +a = 10 / 0 # knot: ignore[invalid-assignment, division-by-zero, unresolved-reference] +``` + ## Multiple suppressions ```py @@ -47,6 +76,7 @@ def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... ```py # error: [invalid-syntax] +# error: [unused-ignore-comment] def test( # knot: ignore ``` @@ -54,10 +84,12 @@ def test( # knot: ignore ## Can't suppress `revealed-type` diagnostics +TODO: Emit an error that the rule code is unknown: `unknown-rule` + ```py a = 10 # revealed: Literal[10] -reveal_type(a) # knot: ignore +reveal_type(a) # knot: ignore[revealed-type] ``` ## Extra whitespace in type ignore comments is allowed @@ -116,6 +148,7 @@ An empty codes array suppresses no-diagnostics and is always useless ```py # error: [division-by-zero] +# error: [unused-ignore-comment] "Unused `knot: ignore` without a code" a = 4 / 0 # knot: ignore[] ``` @@ -126,6 +159,7 @@ if they should use a different syntax that also supports enabling rules or chang severity: `knot: possibly-undefined-reference=error` ```py +# error: [unused-ignore-comment] # knot: ignore[division-by-zero] a = 4 / 0 # error: [division-by-zero] diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/type-ignore.md b/crates/red_knot_python_semantic/resources/mdtest/suppressions/type-ignore.md index 355b0bbae3..380d5c34d5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/type-ignore.md +++ b/crates/red_knot_python_semantic/resources/mdtest/suppressions/type-ignore.md @@ -43,6 +43,8 @@ from typing import cast y = ( cast(int, "test" + + # TODO: Remove the expected error after implementing `invalid-operator` for binary expressions + # error: [unused-ignore-comment] 2 # type: ignore ) + "other" # TODO: expected-error[invalid-operator] @@ -118,6 +120,7 @@ in Pyright. Neither Ruff, nor mypy support this and neither does Red Knot. ```py # fmt: off +# error: [unused-ignore-comment] a = ( # type: ignore test + 4 # error: [unresolved-reference] ) @@ -150,6 +153,7 @@ File level suppressions must come before any non-trivia token, including module docstrings. """ +# error: [unused-ignore-comment] "Unused blanket `type: ignore` directive" # type: ignore a = 10 / 0 # error: [division-by-zero] diff --git a/crates/red_knot_python_semantic/src/lib.rs b/crates/red_knot_python_semantic/src/lib.rs index 01b399bd17..6de0190e3c 100644 --- a/crates/red_knot_python_semantic/src/lib.rs +++ b/crates/red_knot_python_semantic/src/lib.rs @@ -3,6 +3,7 @@ use std::hash::BuildHasherDefault; use rustc_hash::FxHasher; use crate::lint::{LintRegistry, LintRegistryBuilder}; +use crate::suppression::UNUSED_IGNORE_COMMENT; pub use db::Db; pub use module_name::ModuleName; pub use module_resolver::{resolve_module, system_module_search_paths, KnownModule, Module}; @@ -47,4 +48,5 @@ pub fn default_lint_registry() -> &'static LintRegistry { /// Register all known semantic lints. pub fn register_lints(registry: &mut LintRegistryBuilder) { types::register_lints(registry); + registry.register_lint(&UNUSED_IGNORE_COMMENT); } diff --git a/crates/red_knot_python_semantic/src/suppression.rs b/crates/red_knot_python_semantic/src/suppression.rs index a6998eb151..4c34bdf9f7 100644 --- a/crates/red_knot_python_semantic/src/suppression.rs +++ b/crates/red_knot_python_semantic/src/suppression.rs @@ -1,11 +1,39 @@ +use crate::lint::{Level, LintRegistry, LintStatus}; +use crate::types::{TypeCheckDiagnostic, TypeCheckDiagnostics}; +use crate::{declare_lint, lint::LintId, Db}; +use ruff_db::diagnostic::DiagnosticId; use ruff_db::{files::File, parsed::parsed_module, source::source_text}; use ruff_python_parser::TokenKind; use ruff_python_trivia::Cursor; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use smallvec::{smallvec, SmallVec}; +use std::fmt; +use std::fmt::Formatter; -use crate::lint::LintRegistry; -use crate::{lint::LintId, Db}; +declare_lint! { + /// ## What it does + /// Checks for `type: ignore` or `knot: ignore` directives that are no longer applicable. + /// + /// ## Why is this bad? + /// A `type: ignore` directive that no longer matches any diagnostic violations is likely + /// included by mistake, and should be removed to avoid confusion. + /// + /// ## Examples + /// ```py + /// a = 20 / 2 # knot: ignore[division-by-zero] + /// ``` + /// + /// Use instead: + /// + /// ```py + /// a = 20 / 2 + /// ``` + pub(crate) static UNUSED_IGNORE_COMMENT = { + summary: "detects unused `type: ignore` comments", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} #[salsa::tracked(return_ref)] pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { @@ -25,7 +53,7 @@ pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { let parser = SuppressionParser::new(&source, token.range()); for comment in parser { - builder.add_comment(comment, line_start); + builder.add_comment(&comment, TextRange::new(line_start, token.end())); } } TokenKind::Newline | TokenKind::NonLogicalNewline => { @@ -38,6 +66,89 @@ pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { builder.finish() } +/// Checks for unused suppression comments in `file` and +/// adds diagnostic for each of them to `diagnostics`. +/// +/// Does nothing if the [`UNUSED_IGNORE_COMMENT`] rule is disabled. +pub(crate) fn check_unused_suppressions( + db: &dyn Db, + file: File, + diagnostics: &mut TypeCheckDiagnostics, +) { + let Some(severity) = db + .rule_selection() + .severity(LintId::of(&UNUSED_IGNORE_COMMENT)) + else { + return; + }; + + let all = suppressions(db, file); + let mut unused = Vec::with_capacity( + all.file + .len() + .saturating_add(all.line.len()) + .saturating_sub(diagnostics.used_len()), + ); + + // Collect all suppressions that are unused after type-checking. + for suppression in all { + if diagnostics.is_used(suppression.id()) { + continue; + } + + // `unused-ignore-comment` diagnostics can only be suppressed by specifying a + // code. This is necessary because every `type: ignore` would implicitly also + // suppress its own unused-ignore-comment diagnostic. + if let Some(unused_suppression) = all + .lint_suppressions(suppression.range, LintId::of(&UNUSED_IGNORE_COMMENT)) + .find(|unused_ignore_suppression| unused_ignore_suppression.target.is_lint()) + { + // A `unused-ignore-comment` suppression can't ignore itself. + // It can only ignore other suppressions. + if unused_suppression.id() != suppression.id() { + diagnostics.mark_used(unused_suppression.id()); + continue; + } + } + + unused.push(suppression); + } + + for suppression in unused { + // This looks silly but it's necessary to check again if a `unused-ignore-comment` is indeed unused + // in case the "unused" directive comes after it: + // ```py + // a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero] + // ``` + if diagnostics.is_used(suppression.id()) { + continue; + } + + let message = match suppression.target { + SuppressionTarget::All => { + format!("Unused blanket `{}` directive", suppression.kind) + } + SuppressionTarget::Lint(lint) => { + format!( + "Unused `{kind}` directive: '{code}'", + kind = suppression.kind, + code = lint.name() + ) + } + SuppressionTarget::Empty => { + format!("Unused `{}` without a code", suppression.kind) + } + }; + diagnostics.push(TypeCheckDiagnostic { + id: DiagnosticId::Lint(UNUSED_IGNORE_COMMENT.name()), + message, + range: suppression.range, + severity, + file, + }); + } +} + /// The suppressions of a single file. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Suppressions { @@ -59,10 +170,19 @@ pub(crate) struct Suppressions { impl Suppressions { pub(crate) fn find_suppression(&self, range: TextRange, id: LintId) -> Option<&Suppression> { + self.lint_suppressions(range, id).next() + } + + /// Returns all suppressions for the given lint + fn lint_suppressions( + &self, + range: TextRange, + id: LintId, + ) -> impl Iterator + '_ { self.file .iter() .chain(self.line_suppressions(range)) - .find(|suppression| suppression.matches(id)) + .filter(move |suppression| suppression.matches(id)) } /// Returns the line-level suppressions that apply for `range`. @@ -94,6 +214,22 @@ impl Suppressions { || suppression.suppressed_range.contains(range.end()) }) } + + fn iter(&self) -> SuppressionsIter { + self.file.iter().chain(&self.line) + } +} + +pub(crate) type SuppressionsIter<'a> = + std::iter::Chain, std::slice::Iter<'a, Suppression>>; + +impl<'a> IntoIterator for &'a Suppressions { + type Item = &'a Suppression; + type IntoIter = SuppressionsIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } } /// A `type: ignore` or `knot: ignore` suppression. @@ -104,6 +240,7 @@ impl Suppressions { #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Suppression { target: SuppressionTarget, + kind: SuppressionKind, /// The range of this specific suppression. /// This is the same as `comment_range` except for suppression comments that suppress multiple @@ -127,10 +264,24 @@ impl Suppression { match self.target { SuppressionTarget::All => true, SuppressionTarget::Lint(suppressed_id) => tested_id == suppressed_id, + SuppressionTarget::Empty => false, } } + + pub(crate) fn id(&self) -> FileSuppressionId { + FileSuppressionId(self.range) + } } +/// Unique ID for a suppression in a file. +/// +/// ## Implementation +/// The wrapped `TextRange` is the suppression's range. +/// This is unique enough because it is its exact +/// location in the source. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub(crate) struct FileSuppressionId(TextRange); + #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum SuppressionTarget { /// Suppress all lints @@ -138,6 +289,15 @@ enum SuppressionTarget { /// Suppress the lint with the given id Lint(LintId), + + /// Suppresses no lint, e.g. `knot: ignore[]` + Empty, +} + +impl SuppressionTarget { + const fn is_lint(self) -> bool { + matches!(self, SuppressionTarget::Lint(_)) + } } struct SuppressionsBuilder<'a> { @@ -177,7 +337,7 @@ impl<'a> SuppressionsBuilder<'a> { } } - fn add_comment(&mut self, comment: SuppressionComment, line_start: TextSize) { + fn add_comment(&mut self, comment: &SuppressionComment, line_range: TextRange) { let (suppressions, suppressed_range) = // `type: ignore` comments at the start of the file apply to the entire range. // > A # type: ignore comment on a line by itself at the top of a file, before any docstrings, @@ -193,15 +353,16 @@ impl<'a> SuppressionsBuilder<'a> { } else { ( &mut self.line, - TextRange::new(line_start, comment.range.end()), + line_range, ) }; - match comment.codes { + match comment.codes.as_deref() { // `type: ignore` None => { suppressions.push(Suppression { target: SuppressionTarget::All, + kind: comment.kind, comment_range: comment.range, range: comment.range, suppressed_range, @@ -214,15 +375,27 @@ impl<'a> SuppressionsBuilder<'a> { Some(_) if comment.kind.is_type_ignore() => { suppressions.push(Suppression { target: SuppressionTarget::All, + kind: comment.kind, comment_range: comment.range, range: comment.range, suppressed_range, }); } + // `knot: ignore[]` + Some([]) => { + suppressions.push(Suppression { + target: SuppressionTarget::Empty, + kind: comment.kind, + range: comment.range, + comment_range: comment.range, + suppressed_range, + }); + } + // `knot: ignore[a, b]` Some(codes) => { - for code_range in &codes { + for code_range in codes { let code = &self.source[*code_range]; match self.lint_registry.get(code) { Ok(lint) => { @@ -234,6 +407,7 @@ impl<'a> SuppressionsBuilder<'a> { suppressions.push(Suppression { target: SuppressionTarget::Lint(lint), + kind: comment.kind, range, comment_range: comment.range, suppressed_range, @@ -448,6 +622,15 @@ impl SuppressionKind { } } +impl fmt::Display for SuppressionKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + SuppressionKind::TypeIgnore => f.write_str("type: ignore"), + SuppressionKind::Knot => f.write_str("knot: ignore"), + } + } +} + #[cfg(test)] mod tests { use crate::suppression::{SuppressionComment, SuppressionParser}; diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 59dcb09eba..6a888b9aec 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -27,6 +27,7 @@ use crate::semantic_index::{ DeclarationsIterator, }; use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol}; +use crate::suppression::check_unused_suppressions; use crate::symbol::{Boundness, Symbol}; use crate::types::call::{CallDunderResult, CallOutcome}; use crate::types::class_base::ClassBase; @@ -65,6 +66,8 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics { diagnostics.extend(result.diagnostics()); } + check_unused_suppressions(db, file, &mut diagnostics); + diagnostics } diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/red_knot_python_semantic/src/types/context.rs index 9c98fdd650..062a57c424 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/red_knot_python_semantic/src/types/context.rs @@ -58,9 +58,7 @@ impl<'db> InferContext<'db> { where T: WithDiagnostics, { - self.diagnostics - .get_mut() - .extend(other.diagnostics().iter().cloned()); + self.diagnostics.get_mut().extend(other.diagnostics()); } /// Reports a lint located at `node`. @@ -68,7 +66,7 @@ impl<'db> InferContext<'db> { &self, lint: &'static LintMetadata, node: AnyNodeRef, - message: std::fmt::Arguments, + message: fmt::Arguments, ) { // Skip over diagnostics if the rule is disabled. let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else { @@ -77,10 +75,8 @@ impl<'db> InferContext<'db> { let suppressions = suppressions(self.db, self.file); - if suppressions - .find_suppression(node.range(), LintId::of(lint)) - .is_some() - { + if let Some(suppression) = suppressions.find_suppression(node.range(), LintId::of(lint)) { + self.diagnostics.borrow_mut().mark_used(suppression.id()); return; } @@ -95,7 +91,7 @@ impl<'db> InferContext<'db> { node: AnyNodeRef, id: DiagnosticId, severity: Severity, - message: std::fmt::Arguments, + message: fmt::Arguments, ) { if !self.db.is_file_open(self.file) { return; @@ -106,7 +102,6 @@ impl<'db> InferContext<'db> { // * The rule is disabled for this file. We probably want to introduce a new query that // returns a rule selector for a given file that respects the package's settings, // any global pragma comments in the file, and any per-file-ignores. - // * Check for suppression comments, bump a counter if the diagnostic is suppressed. self.diagnostics.borrow_mut().push(TypeCheckDiagnostic { file: self.file, diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 5b0549cab2..4d4fb738e4 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -1,5 +1,7 @@ +use super::context::InferContext; use crate::declare_lint; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; +use crate::suppression::FileSuppressionId; use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION, IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, @@ -10,13 +12,12 @@ use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::files::File; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange}; +use rustc_hash::FxHashSet; use std::borrow::Cow; use std::fmt::Formatter; use std::ops::Deref; use std::sync::Arc; -use super::context::InferContext; - /// Registers all known type check lints. pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&CALL_NON_CALLABLE); @@ -512,11 +513,11 @@ declare_lint! { #[derive(Debug, Eq, PartialEq, Clone)] pub struct TypeCheckDiagnostic { - pub(super) id: DiagnosticId, - pub(super) message: String, - pub(super) range: TextRange, - pub(super) severity: Severity, - pub(super) file: File, + pub(crate) id: DiagnosticId, + pub(crate) message: String, + pub(crate) range: TextRange, + pub(crate) severity: Severity, + pub(crate) file: File, } impl TypeCheckDiagnostic { @@ -570,41 +571,41 @@ impl Ranged for TypeCheckDiagnostic { /// each Salsa-struct comes with an overhead. #[derive(Default, Eq, PartialEq)] pub struct TypeCheckDiagnostics { - inner: Vec>, + diagnostics: Vec>, + used_suppressions: FxHashSet, } impl TypeCheckDiagnostics { - pub(super) fn push(&mut self, diagnostic: TypeCheckDiagnostic) { - self.inner.push(Arc::new(diagnostic)); + pub(crate) fn push(&mut self, diagnostic: TypeCheckDiagnostic) { + self.diagnostics.push(Arc::new(diagnostic)); + } + + pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) { + self.diagnostics.extend_from_slice(&other.diagnostics); + self.used_suppressions.extend(&other.used_suppressions); + } + + pub(crate) fn mark_used(&mut self, suppression_id: FileSuppressionId) { + self.used_suppressions.insert(suppression_id); + } + + pub(crate) fn is_used(&self, suppression_id: FileSuppressionId) -> bool { + self.used_suppressions.contains(&suppression_id) + } + + pub(crate) fn used_len(&self) -> usize { + self.used_suppressions.len() } pub(crate) fn shrink_to_fit(&mut self) { - self.inner.shrink_to_fit(); - } -} - -impl Extend for TypeCheckDiagnostics { - fn extend>(&mut self, iter: T) { - self.inner.extend(iter.into_iter().map(std::sync::Arc::new)); - } -} - -impl Extend> for TypeCheckDiagnostics { - fn extend>>(&mut self, iter: T) { - self.inner.extend(iter); - } -} - -impl<'a> Extend<&'a std::sync::Arc> for TypeCheckDiagnostics { - fn extend>>(&mut self, iter: T) { - self.inner - .extend(iter.into_iter().map(std::sync::Arc::clone)); + self.used_suppressions.shrink_to_fit(); + self.diagnostics.shrink_to_fit(); } } impl std::fmt::Debug for TypeCheckDiagnostics { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.inner.fmt(f) + self.diagnostics.fmt(f) } } @@ -612,7 +613,7 @@ impl Deref for TypeCheckDiagnostics { type Target = [std::sync::Arc]; fn deref(&self) -> &Self::Target { - &self.inner + &self.diagnostics } } @@ -621,7 +622,7 @@ impl IntoIterator for TypeCheckDiagnostics { type IntoIter = std::vec::IntoIter>; fn into_iter(self) -> Self::IntoIter { - self.inner.into_iter() + self.diagnostics.into_iter() } } @@ -630,7 +631,7 @@ impl<'a> IntoIterator for &'a TypeCheckDiagnostics { type IntoIter = std::slice::Iter<'a, std::sync::Arc>; fn into_iter(self) -> Self::IntoIter { - self.inner.iter() + self.diagnostics.iter() } } diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index aebc2e55b3..fa6c966294 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -43,6 +43,7 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[ "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined", + "warning[lint:unused-ignore-comment] /src/tomllib/_parser.py:682:31 Unused blanket `type: ignore` directive" ]; fn get_test_file(name: &str) -> TestFile {