Skip to content

Feature: libClang PETSc Macro Static Analyzer

Jacob Faibussowitsch requested to merge jacobf/2021-03-26/clang-static-linter into main

Requires

  • libclang python bindings
pip install clang
  • Working clang installation, specifically location of libclang.dylib.

Note for macOS v11.3 the following is no longer relevant, you may just use either of the two standard locations, or let the linter find libclang automatically.

Getting this to work on macOS v11.2.3 and lower was an absolute nightmare, you must do the following:

// install CLT if not already installed, should already be installed
xcode-select --install
// Switch from XCode app to CLT, as the math.h header files from XCode are broken as of latest macOS 11.2.3 [1]
sudo xcode-select -s /Library/Developer/CommandLineTools

[1]: see here.

Then when running petscClangParser.py you must set option --clang_[dir|lib] to use the XCode libclang. No that is not a typo, using either XCode headers+CLT libclang or CLT headers+CLT libclang or any other combination does NOT work, believe me I tried them all:

$ [python3] petscClangParser.py --clang_dir = '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/'

You may have to uninstall and reinstall XCode the above to work...

Alternatively you can do brew install llvm and use the headers and libclang all from the LLVM install (but only from LLVM! mixing these with either xcode or CLT won't work either).

Functionality

Allows linting of all of the bug-prone macros in petsc source with the introspective powers of clangs AST. Currently catches errors in PetscValid[XXX]Pointer(), PetscValidLogicalCollective[XXX](), PetscValid[Type|HeaderSpecific[Type]](), PetscCheckSame[Type|Comm](), MatCheckXXX(), VecNestCheckCompatible[2|3]().

It is capable of:

  • Catching type mismatches between the object and the provided PetscClassId (although requires manual tabulation of this mapping).
PetscErrorCode somePetscVecFunction(Vec v)
{
  ...
  PetscValidHeaderSpecific(v,MAT_CLASSID,1); // Error! Expected VEC_CLASSID
  • Catching pointer type mismatch of object.
PetscErrorCode somePetscVecFunction(Vec *v)
{
  ...
  PetscValidHeaderSpecific(v,VEC_CLASSID,1); // Error! Expected _p_Vec * not _p_Vec **
  • Catching argument number mismatch. (An example from current main in HDF5 PetscViewer impls)
PetscErrorCode PetscViewerHDF5ReadObjectAttribute(PetscViewer viewer, PetscObject obj, const char name[], PetscDataType datatype, void *defaultValue, void *value)
{
  ...
  PetscValidPointer(value, 5);    

In this particular example the output is:

ERROR 0: /Users/jacobfaibussowitsch/NoSync/petsc/src/sys/classes/viewer/impls/hdf5/hdf5v.c:28:1070
'5' of derived type 'int', canonical type 'int'
> 1070: PetscValidPointer(value, 5);
                                 ^

Argument number doesn't match for 'value'. Found '5' expected '6' from

> 1062: PetscErrorCode PetscViewerHDF5ReadObjectAttribute(PetscViewer viewer, PetscObject obj, const char name[], PetscDataType datatype, void *defaultValue, void *value)
                                                                                                                                                              ^^^^^^^^^^^

Extending it

It is built to be trivially extensible to more macros as each "test" is built from fundamental test building blocks. To add an additional macro (say PetscCheckMyMacro(object,argidx,object2,classid) to the analyzer one simply must:

  1. Add a template definition of the macro to petscastfix.hpp.
template <typename Ta, typename Tb>
void PetscCheckMyMacro(T,int,Tb,int);
  1. Gate the original macro definition around #if !defined(PETSC_AST_FIX) #endif.
#if !defined(PETSC_AST_FIX)
#define PetscCheckMyMacro(obj,argidx,object2,classid) do {\
...
} while (0)
#endif
  1. Create a test function and register it in checkFunctionMap dictionary in petscClangLinter.py
def checkPetscCheckMyMacro(linter,func,parent):
  # (Almost) universal header for all tests, formats the arguments and extracts any necessary informationn
  try:
    funcArgs   = tuple(PetscCursor(a,i+1) for i,a in enumerate(func.get_arguments()))
    parentArgs = tuple(PetscCursor(a,i+1) for i,a in enumerate(parent.get_arguments()))
  except ParsingError as pe:
    linter.addWarning(pe)
    return
  # extract each argument
  objA,idxA,objB,classid = funcArgs
  # perform tests!
  checkMatchingArgNum(badSource,objA,idxA,parentArgs) # check the argument index is correct
  checkMatchingClassid(badSource,objB,classid)        # check the classid is sane
  fltr = FilterFunctor(scalarTypes,func,successHook=checkIsPetscScalarAndNotPetscReal)
  checkMatchingSpecificType(badSource,obj,fltr)       # can even perform checks for type-correctness between fuzzy petsc-types
  return

checkFunctionMap = {
 ...
 "PetscCheckMyMacro" : checkPetscCheckMyMacro,
}

How it works

Normally macros cannot carry any type information, and are handled/expanded before the AST is built, however I get around this by:

  1. #if def'ing out the original macros:
#if !defined(PETSC_CLANG_STATIC_ANALYZER)
#define PetscValidHeaderSpecific(...) 
...
  1. Redefining them as a templated function below
#else
template <typename T>
void PetscValidHeaderSpecific(T,PetscClassId,int);
#endif
  1. Defining PETSC_CLANG_STATIC_ANALYZER during compilation and forcing everything to be compiled as cpp
flags = ['-x','c++','-D','PETSC_CLANG_STATIC_ANALYZER',...]
index = clang.cindex.Index.create()
tu = index.parse(src,args=flags) 
Edited by Jacob Faibussowitsch

Merge request reports