xref: /petsc/lib/petsc/bin/maint/petsclinter/petsclinter/checks/_util.py (revision 51b144c619aff302b570817d6f78637b8418d403) !
1#!/usr/bin/env python3
2"""
3# Created: Mon Jun 20 17:06:22 2022 (-0400)
4# @author: Jacob Faibussowitsch
5"""
6from __future__ import annotations
7
8import itertools
9import clang.cindex as clx # type: ignore[import]
10
11from .._typing import *
12from .._error  import ParsingError, ClassidNotRegisteredError
13
14from ..classes._diag    import DiagnosticManager, Diagnostic
15from ..classes._cursor  import Cursor
16from ..classes._patch   import Patch
17from ..classes._src_pos import SourceRange, SourceLocation
18
19from ..util._clang import (
20  clx_scalar_type_kinds, clx_enum_type_kinds, clx_int_type_kinds, clx_conversion_cursor_kinds,
21  clx_function_type_kinds, clx_var_token_kinds, clx_math_cursor_kinds, clx_pointer_type_kinds,
22  clx_bool_type_kinds
23)
24
25# utilities for checking functions
26
27# a dummy class so that type checkers don't complain about functions not having diags attribute
28@DiagnosticManager.register(
29  ('incompatible-function', 'Verify that the correct function was used for a type'),
30  ('incompatible-type', 'Verify that a particular type matches the expected type'),
31  ('incompatible-type-petscobject', 'Verify that a symbol is a PetscObject'),
32  ('incompatible-classid', 'Verify that the given classid matches the PetscObject type'),
33  ('matching-arg-num', 'Verify that the given argument number matches')
34)
35class _CodeDiags:
36  diags: DiagnosticMap
37
38def add_function_fix_to_bad_source(linter: Linter, obj: Cursor, func_cursor: Cursor, valid_func_name: str) -> None:
39  r"""Shorthand for extracting a fix from a function cursor
40
41  Parameters
42  ----------
43  linter :
44    the linter instance
45  obj :
46    the cursor representing the object
47  func_cursor :
48    the cursor of the parent function
49  valid_func_name :
50    the name that should be used instead
51  """
52  call = [
53    c for c in func_cursor.get_children() if c.type.get_pointee().kind == clx.TypeKind.FUNCTIONPROTO
54  ]
55  assert len(call) == 1
56  mess = f'Incorrect use of {func_cursor.displayname}(), use {valid_func_name}() instead:\n{Cursor.get_formatted_source_from_cursor(func_cursor, num_context=2)}'
57  linter.add_diagnostic_from_cursor(
58    obj,
59    Diagnostic(
60      Diagnostic.Kind.ERROR, _CodeDiags.diags.incompatible_function, mess,
61      func_cursor.extent.start, patch=Patch.from_cursor(call[0], valid_func_name)
62    ).add_note(
63      f'Due to {obj.get_formatted_blurb()}', location=obj.extent.start
64    )
65  )
66  return
67
68def convert_to_correct_PetscValidLogicalCollectiveXXX(linter: Linter, obj: Cursor, obj_type: clx.Type, func_cursor: Cursor, **kwargs) -> bool:
69  r"""Try to glean the correct PetscValidLogicalCollectiveXXX from the type.
70
71  Parameters
72  ----------
73  linter :
74    the linter instance
75  obj :
76    the cursor for the object to check
77  obj_type :
78    the type of obj
79  func_cursor :
80    the cursor representing the parent function
81
82  Notes
83  -----
84  Used as a failure hook in the validlogicalcollective checks.
85  """
86  valid_func_name = ''
87  obj_type_kind   = obj_type.kind
88  if obj_type_kind in clx_scalar_type_kinds:
89    if 'PetscReal' in obj.derivedtypename:
90      valid_func_name = 'PetscValidLogicalCollectiveReal'
91    elif 'PetscScalar' in obj.derivedtypename:
92      valid_func_name = 'PetscValidLogicalCollectiveScalar'
93  elif obj_type_kind in clx_enum_type_kinds:
94    valid_func_name = 'PetscValidLogicalCollectiveEnum'
95  elif obj_type_kind in clx_bool_type_kinds:
96    valid_func_name = 'PetscValidLogicalCollectiveBool'
97  elif obj_type_kind in clx_int_type_kinds:
98    if 'PetscInt' in obj.derivedtypename:
99      valid_func_name = 'PetscValidLogicalCollectiveInt'
100    elif 'PetscMPIInt' in obj.derivedtypename:
101      valid_func_name = 'PetscValidLogicalCollectiveMPIInt'
102  if valid_func_name:
103    add_function_fix_to_bad_source(linter, obj, func_cursor, valid_func_name)
104  return bool(valid_func_name)
105
106def check_is_type_x_and_not_type_y(type_x: str, type_y: str, linter: Linter, obj: Cursor, obj_type: clx.Type, func_cursor: Optional[Cursor] = None, valid_func: str = '') -> bool:
107  r"""Check that a cursor is at least some form of derived type X and not some form of type Y
108
109  Parameters
110  ----------
111  type_x :
112    the name of type "x"
113  type_y :
114    the name that `type_x` should not be
115  linter :
116    the linter instance
117  obj :
118    the object which is of `type_x`
119  obj_type :
120    the type of `obj`
121  func_cursor : optional
122    the cursor representing the parent function
123  valid_func : optional
124    the name of the valid function name
125
126  Returns
127  -------
128  ret :
129    True, as this routine always fixes the problem
130
131  Notes
132  -----
133  i.e. for
134
135  myInt **********x;
136
137  you may check that 'x' is some form of 'myInt' instead of say 'PetscBool'
138  """
139  derived_name = obj.derivedtypename
140  if type_x not in derived_name:
141    if type_y in derived_name:
142      assert func_cursor is not None
143      add_function_fix_to_bad_source(linter, obj, func_cursor, valid_func)
144    else:
145      mess = f'Incorrect use of {valid_func}(), {valid_func}() should only be used for {type_x}'
146      linter.add_diagnostic_from_cursor(
147        obj,
148        Diagnostic(
149          Diagnostic.Kind.ERROR, _CodeDiags.diags.incompatible_type, mess,
150          obj.extent.start
151        )
152      )
153  return True
154
155def check_is_PetscScalar_and_not_PetscReal(*args, **kwargs) -> bool:
156  r"""Check that a cursor is a PetscScalar and not a PetscReal
157
158  Parameters
159  ----------
160  *args :
161    positional arguments to `check_is_type_x_and_not_type_y()`
162  **kwargs :
163    keyword arguments to `check_is_type_x_and_not_type_y()`
164
165  Returns
166  -------
167  ret :
168    the return value of `check_is_type_x_and_not_type_y()`
169  """
170  return check_is_type_x_and_not_type_y('PetscScalar', 'PetscReal', *args, **kwargs)
171
172def check_is_PetscReal_and_not_PetscScalar(*args, **kwargs) -> bool:
173  r"""Check that a cursor is a PetscReal and not a PetscScalar
174
175  Parameters
176  ----------
177  *args :
178    positional arguments to `check_is_type_x_and_not_type_y()`
179  **kwargs :
180    keyword arguments to `check_is_type_x_and_not_type_y()`
181
182  Returns
183  -------
184  ret :
185    the return value of `check_is_type_x_and_not_type_y()`
186  """
187  return check_is_type_x_and_not_type_y('PetscReal', 'PetscScalar', *args, **kwargs)
188
189def check_is_not_type(typename: str, linter: Linter, obj: Cursor, func_cursor: Cursor, valid_func: str = '') -> bool:
190  r"""Check a cursor is not of type `typename`
191
192  Parameters
193  ----------
194  typename :
195    the type that the cursor should not be
196  linter :
197    the linter instance
198  obj :
199    the cursor representing the object
200  func_cursor : optional
201    the cursor representing the parent function
202  valid_func : optional
203    the name of the valid function name
204
205  Returns
206  -------
207  ret :
208    True, since this routine always fixes the problem
209  """
210  if typename in obj.derivedtypename:
211    add_function_fix_to_bad_source(linter, obj, func_cursor, valid_func)
212  return True
213
214def check_int_is_not_PetscBool(linter: Linter, obj: Cursor, *args, **kwargs) -> bool:
215  r"""Check an int-like object is not a PetscBool
216
217  Parameters
218  ----------
219  linter :
220    the linter instance
221  obj :
222    the cursor representing the object
223  *args :
224    additional positional arguments to `check_is_not_type()`
225  **kwargs :
226    additional keyword arguments to `check_is_not_type()`
227
228  Returns
229  -------
230  ret :
231    the return value of `check_is_not_type()`
232  """
233  return check_is_not_type('PetscBool', linter, obj, **kwargs)
234
235def check_MPIInt_is_not_PetscInt(linter: Linter, obj: Cursor, *args, **kwargs) -> bool:
236  r"""Check a PetscMPIInt object is not a PetscBool
237
238  Parameters
239  ----------
240  linter :
241    the linter instance
242  obj :
243    the cursor representing the object
244  *args :
245    additional positional arguments to `check_is_not_type()`
246  **kwargs :
247    additional keyword arguments to `check_is_not_type()`
248
249  Returns
250  -------
251  ret :
252    the return value of `check_is_not_type()`
253  """
254  return check_is_not_type('PetscInt', linter, obj, **kwargs)
255
256def check_is_PetscBool(linter: Linter, obj: Cursor, obj_type: clx.Type, func_cursor: Optional[Cursor] = None, valid_func: str = '') -> bool:
257  r"""Check that a cursor is exactly a PetscBool
258
259  Parameters
260  ----------
261  linter :
262    the linter instance
263  obj :
264    the cursor representing the object
265  obj_type :
266    the type of obj
267  func_cursor : optional
268    the cursor representing the parent function
269  valid_func_name : optional
270    the name that should be used instead, unused
271  """
272  if ('PetscBool' not in obj.derivedtypename) and ('bool' not in obj.typename):
273    assert func_cursor is not None
274    func_name = func_cursor.displayname
275    mess      = f'Incorrect use of {func_name}(), {func_name}() should only be used for PetscBool or bool:{Cursor.get_formatted_source_from_cursor(func_cursor, num_context=2)}'
276    linter.add_diagnostic_from_cursor(
277      obj,
278      Diagnostic(
279        Diagnostic.Kind.ERROR, _CodeDiags.diags.incompatible_function, mess, obj.extent.start
280      )
281    )
282  return True
283
284def check_is_petsc_object(linter: Linter, obj: Cursor) -> bool:
285  r"""Check if `obj` is a PetscObject
286
287  Parameters
288  ----------
289  linter :
290    the linter instance
291  obj :
292    the cursor to check
293
294  Returns
295  -------
296  is_valid :
297    True if `obj` is a valid PetscObject, False otherwise.
298
299  Raises
300  ------
301  ClassidNotRegisteredError
302    if `obj` is a PetscObject that isn't registered in the classid_map.
303  """
304  from ._register import classid_map
305
306  if obj.typename not in classid_map:
307    # Raise exception here since this isn't a bad source, moreso a failure of this script
308    # since it should know about all PETSc classes
309    err_message = f"{obj}\nUnknown or invalid PETSc class '{obj.derivedtypename}'. If you are introducing a new class, you must register it with this linter! See lib/petsc/bin/maint/petsclinter/petsclinter/checks/_register.py and search for 'Adding new classes' for more information\n"
310    raise ClassidNotRegisteredError(err_message)
311  petsc_object_type = obj.type.get_canonical().get_pointee()
312  # Must have a struct here, e.g. _p_Vec
313  assert petsc_object_type.kind == clx.TypeKind.RECORD, 'Symbol does not appear to be a struct!'
314  is_valid = True
315  # PetscObject is of course a valid PetscObject, no need to check its members
316  if petsc_object_type.spelling != '_p_PetscObject':
317    struct_fields = [f for f in petsc_object_type.get_fields()]
318    if len(struct_fields) >= 2:
319      if Cursor.get_typename_from_cursor(struct_fields[0]) != '_p_PetscObject':
320        is_valid = False
321    else:
322      is_valid = False
323
324  if not is_valid:
325    obj_decl = Cursor.cast(petsc_object_type.get_declaration())
326    typename = obj_decl.typename
327    diag     = Diagnostic(
328      Diagnostic.Kind.ERROR, _CodeDiags.diags.incompatible_type_petscobject,
329      f'Invalid type \'{typename}\', expected a PetscObject (or derived class):\n{obj_decl.formatted(num_before_context=1, num_after_context=2)}',
330      obj_decl.extent.start
331    )
332    if typename.startswith('_p_'):
333      if len(struct_fields) == 0:
334        reason = 'cannot determine fields. Likely the header containing definition of the object is in a nonstandard place'
335        # raise a warning instead of an error since this is a failure of the linter
336        diag.kind = Diagnostic.Kind.WARNING
337      else:
338        reason = 'its definition is missing a PETSCHEADER as the first struct member'
339      mess = f'Type \'{typename}\' is prefixed with \'_p_\' to indicate it is a PetscObject but {reason}. Either replace \'_p_\' with \'_n_\' to indicate it is not a PetscObject or add a PETSCHEADER declaration as the first member.'
340      diag.add_note(mess).add_note(
341        f'It is ambiguous whether \'{obj.name}\' is intended to be a PetscObject.'
342      )
343    linter.add_diagnostic_from_cursor(obj, diag)
344  return is_valid
345
346def check_matching_classid(linter: Linter, obj: Cursor, obj_classid: Cursor) -> None:
347  r"""Does the classid match the particular PETSc type
348
349  Parameters
350  ----------
351  linter :
352    the linter instance
353  obj :
354    the cursor to check
355  obj_classid :
356    the cursor for the classid
357  """
358  from ._register import classid_map
359
360  check_is_petsc_object(linter, obj)
361  expected = classid_map[obj.typename]
362  name     = obj_classid.name
363  if name != expected:
364    mess     = f"Classid doesn't match. Expected '{expected}' found '{name}':\n{obj_classid.formatted(num_context=2)}"
365    diag = Diagnostic(
366      Diagnostic.Kind.ERROR, _CodeDiags.diags.incompatible_classid, mess, obj_classid.extent.start,
367      patch=Patch.from_cursor(obj_classid, expected)
368    ).add_note(
369      f'For {obj.get_formatted_blurb()}', location=obj.extent.start
370    )
371    linter.add_diagnostic_from_cursor(obj, diag)
372  return
373
374def _do_check_traceable_to_parent_args(obj: Cursor, parent_arg_names: tuple[str, ...], trace: list[CursorLike]) -> tuple[int, list[CursorLike]]:
375  """
376  The actual workhorse of `check_traceable_to_parent_args()`
377  """
378
379  potential_parents: list[CursorLike] = []
380  if def_cursor := obj.get_definition():
381    assert def_cursor.location != obj.location, 'Object has definition cursor, yet the cursor did not move. This should be handled!'
382    if def_cursor.kind == clx.CursorKind.VAR_DECL:
383      # found definition, so were in business
384      # Parents here is an odd choice of words since on the very same line I loop
385      # over children, but then again clangs AST has an odd semantic for parents/children
386      convert_or_dereference_cursors = clx_conversion_cursor_kinds | {clx.CursorKind.UNARY_OPERATOR}
387      for def_child in def_cursor.get_children():
388        if def_child.kind in convert_or_dereference_cursors:
389          decl_ref_gen = (
390            child for child in def_child.walk_preorder() if child.kind == clx.CursorKind.DECL_REF_EXPR
391          )
392          # Weed out any self-references
393          potential_parents_gen = (
394            parent for parent in decl_ref_gen if parent.spelling != def_cursor.spelling
395          )
396          potential_parents.extend(potential_parents_gen)
397    elif def_cursor.kind == clx.CursorKind.FIELD_DECL:
398      # we have deduced that the original cursor may refer to a struct member
399      # reference, so we go back and see if indeed this is the case
400      for member_child in obj.get_children():
401        if member_child.kind == clx.CursorKind.MEMBER_REF_EXPR:
402          decl_ref_gen = (
403            c for c in member_child.walk_preorder() if c.kind == clx.CursorKind.DECL_REF_EXPR
404          )
405          assert member_child.spelling == def_cursor.spelling, f'{member_child.spelling=}, {def_cursor.spelling=}'
406          potential_parents_gen = (
407            parent for parent in decl_ref_gen if parent.spelling != member_child.spelling
408          )
409          potential_parents.extend(potential_parents_gen)
410  elif obj.kind in clx_conversion_cursor_kinds:
411    curs = [
412      Cursor(c, obj.argidx) for c in obj.walk_preorder() if c.kind == clx.CursorKind.DECL_REF_EXPR
413    ]
414    if len(curs) > 1:
415      curs = [c for c in curs if c.displayname == obj.name]
416    assert len(curs) == 1, f'Could not uniquely determine base cursor from conversion cursor {obj}'
417    obj = curs[0]
418    # for cases with casting + struct member reference:
419    #
420    # macro((type *)bar->baz, barIdx);
421    #
422    # the object "name" will (rightly) refer to 'baz', but since this is an inline
423    # "definition" it doesn't show up in get_definition(), thus we check here
424    potential_parents.append(obj)
425
426  if not potential_parents:
427    # this is the if-all-else-fails approach, first we search the __entire__ file for
428    # references to the cursor. Once we have some matches we take the earliest one
429    # as this one is in theory where the current cursor is instantiated. Then we select
430    # the best match for the possible instantiating cursor and recursively call this
431    # function. This section stops when the cursor definition is of type PARM_DECL (i.e.
432    # defined as the function parameter to the parent function).
433    all_possible_references = obj.find_cursor_references()
434    # don't care about uses of object __after__ the macro, and don't want to pick up
435    # the actual macro location either
436    all_possible_references = [
437      r for r in all_possible_references if r.location.line < obj.location.line
438    ]
439    # we just tried those and they didn't work, also more importantly weeds out the
440    # instantiation line if this is an intermediate cursor in a recursive call to this
441    # function
442    decl_cursor_kinds = {clx.CursorKind.VAR_DECL, clx.CursorKind.FIELD_DECL}
443    arg_refs          = [r for r in all_possible_references if r.kind not in decl_cursor_kinds]
444    if not len(arg_refs):
445      # it's not traceable to a function argument, so maybe its a global static variable
446      if len([r for r in all_possible_references if r.storage_class == clx.StorageClass.STATIC]):
447        # a global variable is not a function argumment, so this is unhandleable
448        raise ParsingError('PETSC_CLANG_STATIC_ANALYZER_IGNORE')
449
450    assert len(arg_refs), f'Could not determine the origin of cursor {obj}'
451    # take the first, as this is the earliest
452    first_ref = arg_refs[0]
453    tu, line  = first_ref.translation_unit, first_ref.location.line
454    src_len   = len(first_ref.raw())
455    # Why the following song and dance? Because you cannot walk the AST backwards, and in
456    # the case that the current cursor is in a function call we need to access our
457    # co-arguments to the function, i.e. "adjacent" branches since they should link to (or
458    # be) in the parent functions argument list. So we have to essentially reparse this
459    # line to be able to start from the top.
460    line_start         = SourceLocation.from_position(tu, line, 1)
461    line_end           = SourceLocation.from_position(tu, line, src_len + 1)
462    line_range         = SourceRange.from_locations(line_start, line_end).source_range
463    token_group        = list(clx.TokenGroup.get_tokens(tu, line_range))
464    function_prototype = [
465      i for i, t in enumerate(token_group) if t.cursor.type.get_canonical().kind in clx_function_type_kinds
466    ]
467    if function_prototype:
468      if len(function_prototype) > 1:
469        # nested function calls likely from PetscCall(), so discard that
470        function_prototype = [
471          i for i in function_prototype if not token_group[i].spelling.startswith('PetscCall')
472        ]
473      assert len(function_prototype) == 1, 'Could not determine unique function prototype from {} for provenance of {}'.format(''.join([t.spelling for t in token_group]), obj)
474
475      idx         = function_prototype[0]
476      lambda_expr = lambda t: (t.spelling not in  {'(', ')'}) and t.kind in clx_var_token_kinds
477      iterator    = (x.cursor for x in itertools.takewhile(lambda_expr, token_group[idx + 2:]))
478    # we now have completely different cursor selected, so we recursively call this
479    # function
480    else:
481      # not a function call, must be an assignment statement, meaning we should now
482      # assert that the current obj is being assigned to
483      assert Cursor.get_name_from_cursor(token_group[0].cursor) == obj.name
484      # find the binary operator, it will contain the most comprehensive AST
485      cursor_gen = token_group[[x.spelling for x in token_group].index('=')].cursor.walk_preorder()
486      iterator   = (c for c in cursor_gen if c.kind == clx.CursorKind.DECL_REF_EXPR)
487
488    alternate_cursor = (c for c in iterator if Cursor.get_name_from_cursor(c) != obj.name)
489    potential_parents.extend(alternate_cursor)
490
491  if not potential_parents:
492    raise ParsingError
493  # arguably at this point anything other than len(potential_parents) should be 1,
494  # and anything else can be considered a failure of this routine (therefore a RTE)
495  # as it should be able to detect the definition.
496  assert len(potential_parents) == 1, 'Cannot determine a unique definition cursor for object'
497  # If >1 cursor, probably a bug since we should have weeded something out
498  parent = potential_parents[0]
499  trace.append(parent)
500  par_def = parent.get_definition()
501  if par_def and par_def.kind == clx.CursorKind.PARM_DECL:
502    name = Cursor.get_name_from_cursor(parent)
503    try:
504      loc = parent_arg_names.index(name)
505    except ValueError as ve:
506      # name isn't in the parent arguments, so we raise parsing error from it
507      raise ParsingError from ve
508  else:
509    parent = Cursor(parent, obj.argidx)
510    # deeper into the rabbit hole
511    loc, trace = _do_check_traceable_to_parent_args(parent, parent_arg_names, trace)
512  return loc, trace
513
514def check_traceable_to_parent_args(obj: Cursor, parent_arg_names: tuple[str, ...]) -> tuple[int, list[CursorLike]]:
515  r"""Try and see if the cursor can be linked to parent function arguments.
516
517  Parameters
518  ----------
519  obj :
520    the cursor to check
521  parent_arg_names :
522    the set of argument names in the parent function
523
524  Returns
525  -------
526  idx :
527    the index into the `parent_arg_names` to which `obj` has been traced
528  trace :
529    if `obj` is not directly in `parent_arg_names`, then the breadcrumb of cursors that lead from `obj`
530    to `parent_arg_names[idx]`
531
532  Raises
533  ------
534  ParsingError
535    if `obj` cannot be traced back to `parent_arg_names`
536
537  Notes
538  -----
539  Traces `foo` back to `bar` for example for these
540  ```
541  myFunction(barType bar)
542  ...
543  fooType foo = bar->baz;
544  macro(foo, barIdx);
545  /* or */
546  macro(bar->baz, barIdx);
547  /* or */
548  initFooFromBar(bar,&foo);
549  macro(foo, barIdx);
550  ```
551  """
552  return _do_check_traceable_to_parent_args(obj, parent_arg_names, [])
553
554def check_matching_arg_num(linter: Linter, obj: Cursor, idx_cursor: Cursor, parent_args: tuple[Cursor, ...]) -> None:
555  r"""Is the Arg # correct w.r.t. the function arguments?
556
557  Parameters
558  ----------
559  linter :
560    the linter instance
561  obj :
562    the cursor to check
563  idx :
564    the cursor of the arg idx
565  parent_args :
566    the set of parent function argument cursors
567  """
568  diag_name = _CodeDiags.diags.matching_arg_num
569  if idx_cursor.canonical.kind not in clx_math_cursor_kinds:
570    # sometimes it is impossible to tell if the index is correct so this is a warning not
571    # an error. For example in the case of a loop:
572    # for (i = 0; i < n; ++i) PetscAssertPointer(arr+i, i);
573    linter.add_diagnostic_from_cursor(
574      idx_cursor, Diagnostic(
575        Diagnostic.Kind.WARNING, diag_name,
576        f"Index value is of unexpected type '{idx_cursor.canonical.kind}'", obj.extent.start
577      )
578    )
579    return
580  try:
581    idx_num = int(idx_cursor.name)
582  except ValueError:
583    linter.add_diagnostic_from_cursor(
584      idx_cursor, Diagnostic(
585        Diagnostic.Kind.WARNING, diag_name,
586        'Potential argument mismatch, could not determine integer value', obj.extent.start
587      )
588    )
589    return
590  trace: list[CursorLike] = []
591  parent_arg_names        = tuple(s.name for s in parent_args)
592  try:
593    expected = parent_args[parent_arg_names.index(obj.name)]
594  except ValueError:
595    try:
596      parent_idx, trace = check_traceable_to_parent_args(obj, parent_arg_names)
597    except ParsingError as pe:
598      # If the parent arguments don't contain the symbol and we couldn't determine a
599      # definition then we cannot check for correct numbering, so we cannot do
600      # anything here but emit a warning
601      if 'PETSC_CLANG_STATIC_ANALYZER_IGNORE' in pe.args:
602        return
603      if len(parent_args):
604        parent_func      = Cursor(parent_args[0].semantic_parent)
605        parent_func_name = f'{parent_func.name}()'
606        parent_func_src  = parent_func.formatted()
607      else:
608        # parent function has no arguments (very likely that "obj" is a global variable)
609        parent_func_name = 'UNKNOWN FUNCTION'
610        parent_func_src  = '  <could not determine parent function signature from arguments>'
611      mess = f"Cannot determine index correctness, parent function '{parent_func_name}' seemingly does not contain the object:\n{parent_func_src}"
612      linter.add_diagnostic_from_cursor(
613        obj, Diagnostic(Diagnostic.Kind.WARNING, diag_name, mess, obj.extent.start)
614      )
615      return
616    else:
617      expected = parent_args[parent_idx]
618  exp_idx = expected.argidx
619  if idx_num != exp_idx:
620    diag = Diagnostic(
621      Diagnostic.Kind.ERROR, diag_name,
622      f"Argument number doesn't match for '{obj.name}'. Expected '{exp_idx}', found '{idx_num}':\n{idx_cursor.formatted(num_context=2)}",
623      idx_cursor.extent.start, patch=Patch.from_cursor(idx_cursor, str(exp_idx))
624    ).add_note(
625      f'\'{obj.name}\' is traceable to argument #{exp_idx} \'{expected.name}\' in enclosing function here:\n{expected.formatted(num_context=2)}',
626      location=expected.extent.start
627    )
628    if trace:
629      diag.add_note(f'starting with {obj.get_formatted_blurb().rstrip()}', location=obj.extent.start)
630    for cursor in trace:
631      diag.add_note(
632        f'via {Cursor.get_formatted_blurb_from_cursor(cursor).rstrip()}', location=cursor.extent.start
633      )
634    linter.add_diagnostic_from_cursor(idx_cursor, diag)
635  return
636
637if TYPE_CHECKING:
638  MatchingTypeCallback = Callable[[Linter, Cursor, Optional[Cursor], str], bool]
639
640def check_matching_specific_type(
641    linter: Linter, obj: Cursor, expected_type_kinds: Collection[clx.TypeKind], pointer: bool,
642    unexpected_not_pointer_function: Optional[MatchingTypeCallback] = None,
643    unexpected_pointer_function: Optional[MatchingTypeCallback] = None,
644    success_function: Optional[MatchingTypeCallback] = None,
645    failure_function: Optional[MatchingTypeCallback] = None,
646    permissive: bool = False, pointer_depth: int = 1, **kwargs
647) -> None:
648  r"""Checks that obj is of a particular kind, for example char. Can optionally handle pointers too.
649
650  Parameters
651  ----------
652  linter :
653    the linter instance
654  obj :
655    the cursor to check
656  expected_type_kinds :
657    the base `clang.cindex.TypeKind` that you want `obj` to be, e.g. clx.TypeKind.ENUM for PetscBool
658  pointer : optional
659    should `obj` be a pointer to your type?
660  unexpected_not_pointer_function : optional
661    callback for when `pointer` is True, and `obj` matches the base type but IS NOT a pointer
662  unexpected_pointer_function : optional
663    callback for when `pointer` is False`, the object matches the base type but IS a pointer
664  success_function : optional
665    callback for when `obj` matches the type and pointer specification
666  failure_function : optional
667    callback for when `obj` does NOT match the base type
668  permissive : optional
669    allow type mismatch (e.g. when checking generic functions like PetscAssertPointer() which can
670    accept anytype)
671  pointer_depth : optional
672    how many levels of pointer to remove (-1 for no limit)
673
674  Raises
675  ------
676  RuntimeError
677    if `success_function` returns False, indicating an unhandled error
678
679  Notes
680  -----
681  The hooks must return True or False to indicate whether they handled the particular situation.
682  This can mean either determining that the object was correct all along (and return True), or that a
683  more helpful error message was logged and/or that a fix was created.
684
685  Returning False indicates that the hook was not successful and that additional error messages should
686  be logged
687  """
688  def always_false(*args: Any, **kwargs: Any) -> bool:
689    return False
690
691  def always_true(*args: Any, **kwargs: Any) -> bool:
692    return True
693
694  if unexpected_not_pointer_function is None:
695    unexpected_not_pointer_function = always_false
696
697  if unexpected_pointer_function is None:
698    unexpected_pointer_function = always_false
699
700  if success_function is None:
701    success_function = always_true
702
703  if failure_function is None:
704    failure_function = always_false
705
706  if pointer_depth < 0:
707    pointer_depth = 100
708
709  diag_name = _CodeDiags.diags.incompatible_type
710  obj_type  = obj.canonical.type.get_canonical()
711  if pointer:
712    if obj_type.kind in expected_type_kinds and not permissive:
713      # expecting a pointer to type, but obj is already that type
714      handled = unexpected_not_pointer_function(linter, obj, obj_type, **kwargs)
715      if not handled:
716        mess = f'Object of clang type {obj_type.kind} is not a pointer. Expected pointer of one of the following types: {expected_type_kinds}'
717        linter.add_diagnostic_from_cursor(
718          obj, Diagnostic(Diagnostic.Kind.ERROR, diag_name, mess, obj.extent.start)
719        )
720      return
721
722    # get rid of any nested pointer types
723    def cycle_type(obj_type: clx.Type, get_obj_type: Callable[[clx.Type], clx.Type]) -> clx.Type:
724      for _ in range(pointer_depth):
725        tmp_type = get_obj_type(obj_type)
726        if tmp_type == obj_type:
727          break
728        obj_type = tmp_type
729      return obj_type
730
731    if obj_type.kind == clx.TypeKind.INCOMPLETEARRAY:
732      obj_type = cycle_type(obj_type, lambda otype: otype.element_type)
733    elif obj_type.kind == clx.TypeKind.POINTER:
734      obj_type = cycle_type(obj_type, lambda otype: otype.get_pointee())
735  else:
736    if obj_type.kind in clx_pointer_type_kinds:
737      handled = unexpected_pointer_function(linter, obj, obj_type, **kwargs)
738      if not handled:
739        mess = f'Object of clang type {obj_type.kind} is a pointer when it should not be'
740        linter.add_diagnostic_from_cursor(
741          obj, Diagnostic(Diagnostic.Kind.ERROR, diag_name, mess, obj.extent.start)
742        )
743      return
744
745  if permissive or obj_type.kind in expected_type_kinds:
746    handled = success_function(linter, obj, obj_type, **kwargs)
747    if not handled:
748      error_message = "{}\nType checker successfully matched object of type {} to (one of) expected types:\n- {}\n\nBut user supplied on-successful-match hook '{}' returned non-truthy value '{}' indicating unhandled error!".format(obj, obj_type.kind, '\n- '.join(map(str, expected_type_kinds)), success_function, handled)
749      raise RuntimeError(error_message)
750  else:
751    handled = failure_function(linter, obj, obj_type, **kwargs)
752    if not handled:
753      mess = f'Object of clang type {obj_type.kind} is not in expected types: {expected_type_kinds}'
754      linter.add_diagnostic_from_cursor(
755        obj, Diagnostic(Diagnostic.Kind.ERROR, diag_name, mess, obj.extent.start)
756      )
757  return
758