Coverage for src/griffe/_internal/agents/visitor.py: 98.36%
282 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-11 13:44 +0200
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-11 13:44 +0200
1# This module contains our static analysis agent,
2# capable of parsing and visiting sources, statically.
4from __future__ import annotations
6import ast
7import sys
8from contextlib import suppress
9from typing import TYPE_CHECKING, Any, Final
11from griffe._internal.agents.nodes.assignments import get_instance_names, get_names
12from griffe._internal.agents.nodes.ast import (
13 ast_children,
14 ast_kind,
15 ast_next,
16)
17from griffe._internal.agents.nodes.docstrings import get_docstring
18from griffe._internal.agents.nodes.exports import safe_get__all__
19from griffe._internal.agents.nodes.imports import relative_to_absolute
20from griffe._internal.agents.nodes.parameters import get_parameters
21from griffe._internal.collections import LinesCollection, ModulesCollection
22from griffe._internal.enumerations import Kind, TypeParameterKind
23from griffe._internal.exceptions import AliasResolutionError, CyclicAliasError, LastNodeError
24from griffe._internal.expressions import (
25 Expr,
26 ExprName,
27 safe_get_annotation,
28 safe_get_base_class,
29 safe_get_condition,
30 safe_get_expression,
31)
32from griffe._internal.extensions.base import Extensions, load_extensions
33from griffe._internal.models import (
34 Alias,
35 Attribute,
36 Class,
37 Decorator,
38 Docstring,
39 Function,
40 Module,
41 Parameter,
42 Parameters,
43 TypeAlias,
44 TypeParameter,
45 TypeParameters,
46)
48if TYPE_CHECKING:
49 from pathlib import Path
51 from griffe._internal.docstrings.parsers import DocstringStyle
52 from griffe._internal.enumerations import Parser
55builtin_decorators = {
56 "property": "property",
57 "staticmethod": "staticmethod",
58 "classmethod": "classmethod",
59}
60"""Mapping of builtin decorators to labels."""
62stdlib_decorators = {
63 "abc.abstractmethod": {"abstractmethod"},
64 "functools.cache": {"cached"},
65 "functools.cached_property": {"cached", "property"},
66 "cached_property.cached_property": {"cached", "property"},
67 "functools.lru_cache": {"cached"},
68 "dataclasses.dataclass": {"dataclass"},
69}
70"""Mapping of standard library decorators to labels."""
72typing_overload = {"typing.overload", "typing_extensions.overload"}
73"""Set of recognized typing overload decorators.
75When such a decorator is found, the decorated function becomes an overload.
76"""
79def visit(
80 module_name: str,
81 filepath: Path,
82 code: str,
83 *,
84 extensions: Extensions | None = None,
85 parent: Module | None = None,
86 docstring_parser: DocstringStyle | Parser | None = None,
87 docstring_options: dict[str, Any] | None = None,
88 lines_collection: LinesCollection | None = None,
89 modules_collection: ModulesCollection | None = None,
90) -> Module:
91 """Parse and visit a module file.
93 We provide this function for static analysis. It uses a [`NodeVisitor`][ast.NodeVisitor]-like class,
94 the [`Visitor`][griffe.Visitor], to compile and parse code (using [`compile`][])
95 then visit the resulting AST (Abstract Syntax Tree).
97 Important:
98 This function is generally not used directly.
99 In most cases, users can rely on the [`GriffeLoader`][griffe.GriffeLoader]
100 and its accompanying [`load`][griffe.load] shortcut and their respective options
101 to load modules using static analysis.
103 Parameters:
104 module_name: The module name (as when importing [from] it).
105 filepath: The module file path.
106 code: The module contents.
107 extensions: The extensions to use when visiting the AST.
108 parent: The optional parent of this module.
109 docstring_parser: The docstring parser to use. By default, no parsing is done.
110 docstring_options: Additional docstring parsing options.
111 lines_collection: A collection of source code lines.
112 modules_collection: A collection of modules.
114 Returns:
115 The module, with its members populated.
116 """
117 return Visitor(
118 module_name,
119 filepath,
120 code,
121 extensions or load_extensions(),
122 parent,
123 docstring_parser=docstring_parser,
124 docstring_options=docstring_options,
125 lines_collection=lines_collection,
126 modules_collection=modules_collection,
127 ).get_module()
130class Visitor:
131 """This class is used to instantiate a visitor.
133 Visitors iterate on AST nodes to extract data from them.
134 """
136 def __init__(
137 self,
138 module_name: str,
139 filepath: Path,
140 code: str,
141 extensions: Extensions,
142 parent: Module | None = None,
143 docstring_parser: DocstringStyle | Parser | None = None,
144 docstring_options: dict[str, Any] | None = None,
145 lines_collection: LinesCollection | None = None,
146 modules_collection: ModulesCollection | None = None,
147 ) -> None:
148 """Initialize the visitor.
150 Parameters:
151 module_name: The module name.
152 filepath: The module filepath.
153 code: The module source code.
154 extensions: The extensions to use when visiting.
155 parent: An optional parent for the final module object.
156 docstring_parser: The docstring parser to use.
157 docstring_options: The docstring parsing options.
158 lines_collection: A collection of source code lines.
159 modules_collection: A collection of modules.
160 """
161 super().__init__()
163 self.module_name: str = module_name
164 """The module name."""
166 self.filepath: Path = filepath
167 """The module filepath."""
169 self.code: str = code
170 """The module source code."""
172 self.extensions: Extensions = extensions
173 """The extensions to use when visiting the AST."""
175 self.parent: Module | None = parent
176 """An optional parent for the final module object."""
178 self.current: Module | Class = None # type: ignore[assignment]
179 """The current object being visited."""
181 self.docstring_parser: DocstringStyle | Parser | None = docstring_parser
182 """The docstring parser to use."""
184 self.docstring_options: dict[str, Any] = docstring_options or {}
185 """The docstring parsing options."""
187 self.lines_collection: LinesCollection = lines_collection or LinesCollection()
188 """A collection of source code lines."""
190 self.modules_collection: ModulesCollection = modules_collection or ModulesCollection()
191 """A collection of modules."""
193 self.type_guarded: bool = False
194 """Whether the current code branch is type-guarded."""
196 def _get_docstring(self, node: ast.AST, *, strict: bool = False) -> Docstring | None:
197 value, lineno, endlineno = get_docstring(node, strict=strict)
198 if value is None:
199 return None
200 return Docstring(
201 value,
202 lineno=lineno,
203 endlineno=endlineno,
204 parser=self.docstring_parser,
205 parser_options=self.docstring_options,
206 )
208 # YORE: EOL 3.11: Replace block with lines 2-36.
209 if sys.version_info >= (3, 12):
210 _type_parameter_kind_map: Final[dict[type[ast.type_param], TypeParameterKind]] = {
211 ast.TypeVar: TypeParameterKind.type_var,
212 ast.TypeVarTuple: TypeParameterKind.type_var_tuple,
213 ast.ParamSpec: TypeParameterKind.param_spec,
214 }
216 def _get_type_parameters(
217 self,
218 node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.TypeAlias,
219 *,
220 scope: str | None = None,
221 ) -> list[TypeParameter]:
222 return [
223 TypeParameter(
224 type_param.name, # type: ignore[attr-defined]
225 kind=self._type_parameter_kind_map[type(type_param)],
226 bound=safe_get_annotation(getattr(type_param, "bound", None), parent=self.current, member=scope),
227 default=safe_get_annotation(
228 getattr(type_param, "default_value", None),
229 parent=self.current,
230 member=scope,
231 ),
232 )
233 for type_param in node.type_params
234 ]
235 else:
237 def _get_type_parameters(
238 self,
239 node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef, # noqa: ARG002,
240 *,
241 scope: str | None = None, # noqa: ARG002,
242 ) -> list[TypeParameter]:
243 return []
245 def get_module(self) -> Module:
246 """Build and return the object representing the module attached to this visitor.
248 This method triggers a complete visit of the module nodes.
250 Returns:
251 A module instance.
252 """
253 # Optimization: equivalent to `ast.parse`, but with `optimize=1` to remove assert statements.
254 # TODO: With options, could use `optimize=2` to remove docstrings.
255 top_node = compile(self.code, mode="exec", filename=str(self.filepath), flags=ast.PyCF_ONLY_AST, optimize=1)
256 self.visit(top_node)
257 return self.current.module
259 def visit(self, node: ast.AST) -> None:
260 """Extend the base visit with extensions.
262 Parameters:
263 node: The node to visit.
264 """
265 getattr(self, f"visit_{ast_kind(node)}", self.generic_visit)(node)
267 def generic_visit(self, node: ast.AST) -> None:
268 """Extend the base generic visit with extensions.
270 Parameters:
271 node: The node to visit.
272 """
273 for child in ast_children(node):
274 self.visit(child)
276 def visit_module(self, node: ast.Module) -> None:
277 """Visit a module node.
279 Parameters:
280 node: The node to visit.
281 """
282 self.extensions.call("on_node", node=node, agent=self)
283 self.extensions.call("on_module_node", node=node, agent=self)
284 self.current = module = Module(
285 name=self.module_name,
286 filepath=self.filepath,
287 parent=self.parent,
288 docstring=self._get_docstring(node),
289 lines_collection=self.lines_collection,
290 modules_collection=self.modules_collection,
291 )
292 self.extensions.call("on_instance", node=node, obj=module, agent=self)
293 self.extensions.call("on_module_instance", node=node, mod=module, agent=self)
294 self.generic_visit(node)
295 self.extensions.call("on_members", node=node, obj=module, agent=self)
296 self.extensions.call("on_module_members", node=node, mod=module, agent=self)
298 def visit_classdef(self, node: ast.ClassDef) -> None:
299 """Visit a class definition node.
301 Parameters:
302 node: The node to visit.
303 """
304 self.extensions.call("on_node", node=node, agent=self)
305 self.extensions.call("on_class_node", node=node, agent=self)
307 # Handle decorators.
308 decorators: list[Decorator] = []
309 if node.decorator_list:
310 lineno = node.decorator_list[0].lineno
311 decorators.extend(
312 Decorator(
313 safe_get_expression(decorator_node, parent=self.current, parse_strings=False), # type: ignore[arg-type]
314 lineno=decorator_node.lineno,
315 endlineno=decorator_node.end_lineno,
316 )
317 for decorator_node in node.decorator_list
318 )
319 else:
320 lineno = node.lineno
322 # Handle base classes.
323 bases = [safe_get_base_class(base, parent=self.current, member=node.name) for base in node.bases]
325 class_ = Class(
326 name=node.name,
327 lineno=lineno,
328 endlineno=node.end_lineno,
329 docstring=self._get_docstring(node),
330 decorators=decorators,
331 type_parameters=TypeParameters(*self._get_type_parameters(node, scope=node.name)),
332 bases=bases, # type: ignore[arg-type]
333 runtime=not self.type_guarded,
334 )
335 class_.labels |= self.decorators_to_labels(decorators)
337 self.current.set_member(node.name, class_)
338 self.current = class_
339 self.extensions.call("on_instance", node=node, obj=class_, agent=self)
340 self.extensions.call("on_class_instance", node=node, cls=class_, agent=self)
341 self.generic_visit(node)
342 self.extensions.call("on_members", node=node, obj=class_, agent=self)
343 self.extensions.call("on_class_members", node=node, cls=class_, agent=self)
344 self.current = self.current.parent # type: ignore[assignment]
346 def decorators_to_labels(self, decorators: list[Decorator]) -> set[str]:
347 """Build and return a set of labels based on decorators.
349 Parameters:
350 decorators: The decorators to check.
352 Returns:
353 A set of labels.
354 """
355 labels = set()
356 for decorator in decorators:
357 callable_path = decorator.callable_path
358 if callable_path in builtin_decorators:
359 labels.add(builtin_decorators[callable_path])
360 elif callable_path in stdlib_decorators:
361 labels |= stdlib_decorators[callable_path]
362 return labels
364 def get_base_property(self, decorators: list[Decorator], function: Function) -> str | None:
365 """Check decorators to return the base property in case of setters and deleters.
367 Parameters:
368 decorators: The decorators to check.
370 Returns:
371 base_property: The property for which the setter/deleted is set.
372 property_function: Either `"setter"` or `"deleter"`.
373 """
374 for decorator in decorators:
375 try:
376 path, prop_function = decorator.callable_path.rsplit(".", 1)
377 except ValueError:
378 continue
379 property_setter_or_deleter = (
380 prop_function in {"setter", "deleter"}
381 and path == function.path
382 and self.current.get_member(function.name).has_labels("property")
383 )
384 if property_setter_or_deleter:
385 return prop_function
386 return None
388 def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels: set | None = None) -> None:
389 """Handle a function definition node.
391 Parameters:
392 node: The node to visit.
393 labels: Labels to add to the data object.
394 """
395 self.extensions.call("on_node", node=node, agent=self)
396 self.extensions.call("on_function_node", node=node, agent=self)
398 labels = labels or set()
400 # Handle decorators.
401 decorators = []
402 overload = False
403 if node.decorator_list:
404 lineno = node.decorator_list[0].lineno
405 for decorator_node in node.decorator_list:
406 decorator_value = safe_get_expression(decorator_node, parent=self.current, parse_strings=False)
407 if decorator_value is None: 407 ↛ 408line 407 didn't jump to line 408 because the condition on line 407 was never true
408 continue
409 decorator = Decorator(
410 decorator_value,
411 lineno=decorator_node.lineno,
412 endlineno=decorator_node.end_lineno,
413 )
414 decorators.append(decorator)
415 overload |= decorator.callable_path in typing_overload
416 else:
417 lineno = node.lineno
419 labels |= self.decorators_to_labels(decorators)
421 if "property" in labels:
422 attribute = Attribute(
423 name=node.name,
424 value=None,
425 annotation=safe_get_annotation(node.returns, parent=self.current, member=node.name),
426 lineno=node.lineno,
427 endlineno=node.end_lineno,
428 docstring=self._get_docstring(node),
429 runtime=not self.type_guarded,
430 )
431 attribute.labels |= labels
432 self.current.set_member(node.name, attribute)
433 self.extensions.call("on_instance", node=node, obj=attribute, agent=self)
434 self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self)
435 return
437 # Handle parameters.
438 parameters = Parameters(
439 *[
440 Parameter(
441 name,
442 kind=kind,
443 annotation=safe_get_annotation(annotation, parent=self.current, member=node.name),
444 default=default
445 if isinstance(default, str)
446 else safe_get_expression(default, parent=self.current, parse_strings=False),
447 )
448 for name, annotation, kind, default in get_parameters(node.args)
449 ],
450 )
452 function = Function(
453 name=node.name,
454 lineno=lineno,
455 endlineno=node.end_lineno,
456 parameters=parameters,
457 returns=safe_get_annotation(node.returns, parent=self.current, member=node.name),
458 decorators=decorators,
459 type_parameters=TypeParameters(*self._get_type_parameters(node, scope=node.name)),
460 docstring=self._get_docstring(node),
461 runtime=not self.type_guarded,
462 parent=self.current,
463 )
465 property_function = self.get_base_property(decorators, function)
467 if overload:
468 self.current.overloads[function.name].append(function)
469 elif property_function:
470 base_property: Attribute = self.current.members[node.name] # type: ignore[assignment]
471 if property_function == "setter":
472 base_property.setter = function
473 base_property.labels.add("writable")
474 elif property_function == "deleter": 474 ↛ 483line 474 didn't jump to line 483 because the condition on line 474 was always true
475 base_property.deleter = function
476 base_property.labels.add("deletable")
477 else:
478 self.current.set_member(node.name, function)
479 if self.current.kind in {Kind.MODULE, Kind.CLASS} and self.current.overloads[function.name]:
480 function.overloads = self.current.overloads[function.name]
481 del self.current.overloads[function.name]
483 function.labels |= labels
485 self.extensions.call("on_instance", node=node, obj=function, agent=self)
486 self.extensions.call("on_function_instance", node=node, func=function, agent=self)
487 if self.current.kind is Kind.CLASS and function.name == "__init__":
488 self.current = function # type: ignore[assignment]
489 self.generic_visit(node)
490 self.current = self.current.parent # type: ignore[assignment]
492 def visit_functiondef(self, node: ast.FunctionDef) -> None:
493 """Visit a function definition node.
495 Parameters:
496 node: The node to visit.
497 """
498 self.handle_function(node)
500 def visit_asyncfunctiondef(self, node: ast.AsyncFunctionDef) -> None:
501 """Visit an async function definition node.
503 Parameters:
504 node: The node to visit.
505 """
506 self.handle_function(node, labels={"async"})
508 # YORE: EOL 3.11: Replace block with lines 2-36.
509 if sys.version_info >= (3, 12):
511 def visit_typealias(self, node: ast.TypeAlias) -> None:
512 """Visit a type alias node.
514 Parameters:
515 node: The node to visit.
516 """
517 self.extensions.call("on_node", node=node, agent=self)
518 self.extensions.call("on_type_alias_node", node=node, agent=self)
520 # A type alias's name attribute is syntactically a single NAME,
521 # but represented as an expression in the AST.
522 # https://jellezijlstra.github.io/pep695#ast
524 name = node.name.id
526 value = safe_get_expression(node.value, parent=self.current, member=name)
528 try:
529 docstring = self._get_docstring(ast_next(node), strict=True)
530 except (LastNodeError, AttributeError):
531 docstring = None
533 type_alias = TypeAlias(
534 name=name,
535 value=value,
536 lineno=node.lineno,
537 endlineno=node.end_lineno,
538 type_parameters=TypeParameters(*self._get_type_parameters(node, scope=name)),
539 docstring=docstring,
540 parent=self.current,
541 )
542 self.current.set_member(name, type_alias)
543 self.extensions.call("on_instance", node=node, obj=type_alias, agent=self)
544 self.extensions.call("on_type_alias_instance", node=node, type_alias=type_alias, agent=self)
546 def visit_import(self, node: ast.Import) -> None:
547 """Visit an import node.
549 Parameters:
550 node: The node to visit.
551 """
552 for name in node.names:
553 alias_path = name.name if name.asname else name.name.split(".", 1)[0]
554 alias_name = name.asname or alias_path.split(".", 1)[0]
555 self.current.imports[alias_name] = alias_path
556 alias = Alias(
557 alias_name,
558 alias_path,
559 lineno=node.lineno,
560 endlineno=node.end_lineno,
561 runtime=not self.type_guarded,
562 )
563 self.current.set_member(alias_name, alias)
564 self.extensions.call("on_alias", alias=alias, node=node, agent=self)
566 def visit_importfrom(self, node: ast.ImportFrom) -> None:
567 """Visit an "import from" node.
569 Parameters:
570 node: The node to visit.
571 """
572 for name in node.names:
573 if not node.module and node.level == 1 and not name.asname and self.current.module.is_init_module:
574 # Special case: when being in `a/__init__.py` and doing `from . import b`,
575 # we are effectively creating a member `b` in `a` that is pointing to `a.b`
576 # -> cyclic alias! In that case, we just skip it, as both the member and module
577 # have the same name and can be accessed the same way.
578 continue
580 alias_path = relative_to_absolute(node, name, self.current.module)
581 if name.name == "*":
582 alias_name = alias_path.replace(".", "/")
583 alias_path = alias_path.replace(".*", "")
584 else:
585 alias_name = name.asname or name.name
586 self.current.imports[alias_name] = alias_path
587 # Do not create aliases pointing to themselves (it happens with
588 # `from package.current_module import Thing as Thing` or
589 # `from . import thing as thing`).
590 if alias_path != f"{self.current.path}.{alias_name}":
591 alias = Alias(
592 alias_name,
593 alias_path,
594 lineno=node.lineno,
595 endlineno=node.end_lineno,
596 runtime=not self.type_guarded,
597 )
598 self.current.set_member(alias_name, alias)
599 self.extensions.call("on_alias", alias=alias, node=node, agent=self)
601 def handle_attribute(
602 self,
603 node: ast.Assign | ast.AnnAssign,
604 annotation: str | Expr | None = None,
605 ) -> None:
606 """Handle an attribute (assignment) node.
608 Parameters:
609 node: The node to visit.
610 annotation: A potential annotation.
611 """
612 self.extensions.call("on_node", node=node, agent=self)
613 self.extensions.call("on_attribute_node", node=node, agent=self)
614 parent = self.current
615 labels = set()
617 if parent.kind is Kind.MODULE:
618 try:
619 names = get_names(node)
620 except KeyError: # Unsupported nodes, like subscript.
621 return
622 labels.add("module-attribute")
623 elif parent.kind is Kind.CLASS:
624 try:
625 names = get_names(node)
626 except KeyError: # Unsupported nodes, like subscript.
627 return
629 if isinstance(annotation, Expr) and annotation.is_classvar:
630 # Explicit `ClassVar`: class attribute only.
631 annotation = annotation.slice # type: ignore[attr-defined]
632 labels.add("class-attribute")
633 elif node.value:
634 # Attribute assigned at class-level: available in instances as well.
635 labels.add("class-attribute")
636 labels.add("instance-attribute")
637 else:
638 # Annotated attribute only: not available at class-level.
639 labels.add("instance-attribute")
641 elif parent.kind is Kind.FUNCTION: 641 ↛ 651line 641 didn't jump to line 651 because the condition on line 641 was always true
642 if parent.name != "__init__": 642 ↛ 643line 642 didn't jump to line 643 because the condition on line 642 was never true
643 return
644 try:
645 names = get_instance_names(node)
646 except KeyError: # Unsupported nodes, like subscript.
647 return
648 parent = parent.parent # type: ignore[assignment]
649 labels.add("instance-attribute")
651 if not names:
652 return
654 value = safe_get_expression(node.value, parent=self.current, parse_strings=False)
656 try:
657 docstring = self._get_docstring(ast_next(node), strict=True)
658 except (LastNodeError, AttributeError):
659 docstring = None
661 for name in names:
662 # TODO: Handle assigns like `x.y = z`.
663 # We need to resolve `x.y` and add `z` in its members.
664 if "." in name:
665 continue
667 if name in parent.members:
668 # Assigning multiple times.
669 # TODO: Might be better to inspect.
670 if isinstance(node.parent, (ast.If, ast.ExceptHandler)): # type: ignore[union-attr]
671 continue # Prefer "no-exception" case.
673 existing_member = parent.members[name]
674 with suppress(AliasResolutionError, CyclicAliasError):
675 labels |= existing_member.labels
676 # Forward previous docstring and annotation instead of erasing them.
677 if existing_member.docstring and not docstring:
678 docstring = existing_member.docstring
679 with suppress(AttributeError):
680 if existing_member.annotation and not annotation: # type: ignore[union-attr]
681 annotation = existing_member.annotation # type: ignore[union-attr]
683 attribute = Attribute(
684 name=name,
685 value=value,
686 annotation=annotation,
687 lineno=node.lineno,
688 endlineno=node.end_lineno,
689 docstring=docstring,
690 runtime=not self.type_guarded,
691 )
692 attribute.labels |= labels
693 parent.set_member(name, attribute)
695 if name == "__all__":
696 with suppress(AttributeError):
697 parent.exports = [
698 name if isinstance(name, str) else ExprName(name.name, parent=name.parent)
699 for name in safe_get__all__(node, self.current) # type: ignore[arg-type]
700 ]
701 self.extensions.call("on_instance", node=node, obj=attribute, agent=self)
702 self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self)
704 def visit_assign(self, node: ast.Assign) -> None:
705 """Visit an assignment node.
707 Parameters:
708 node: The node to visit.
709 """
710 self.handle_attribute(node)
712 def visit_annassign(self, node: ast.AnnAssign) -> None:
713 """Visit an annotated assignment node.
715 Parameters:
716 node: The node to visit.
717 """
718 self.handle_attribute(node, safe_get_annotation(node.annotation, parent=self.current))
720 def visit_augassign(self, node: ast.AugAssign) -> None:
721 """Visit an augmented assignment node.
723 Parameters:
724 node: The node to visit.
725 """
726 with suppress(AttributeError):
727 all_augment = (
728 node.target.id == "__all__" # type: ignore[union-attr]
729 and self.current.is_module
730 and isinstance(node.op, ast.Add)
731 )
732 if all_augment:
733 # We assume `exports` is not `None` at this point.
734 self.current.exports.extend( # type: ignore[union-attr]
735 [
736 name if isinstance(name, str) else ExprName(name.name, parent=name.parent)
737 for name in safe_get__all__(node, self.current) # type: ignore[arg-type]
738 ],
739 )
741 def visit_if(self, node: ast.If) -> None:
742 """Visit an "if" node.
744 Parameters:
745 node: The node to visit.
746 """
747 if isinstance(node.parent, (ast.Module, ast.ClassDef)): # type: ignore[attr-defined]
748 condition = safe_get_condition(node.test, parent=self.current, log_level=None)
749 if str(condition) in {"typing.TYPE_CHECKING", "TYPE_CHECKING"}:
750 self.type_guarded = True
751 self.generic_visit(node)
752 self.type_guarded = False