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