#
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: Copyright TF-RMM Contributors.
#

if(RMM_STATIC_ANALYSIS)
    find_program(RMM_STATIC_ANALYSIS_CPPCHECK_PATH "cppcheck"
        DOC "Path to Cppcheck.")
endif()

if(EXISTS "${RMM_STATIC_ANALYSIS}")
    mark_as_advanced(FORCE RMM_STATIC_ANALYSIS_CPPCHECK_PATH)
else()
    mark_as_advanced(CLEAR RMM_STATIC_ANALYSIS_CPPCHECK_PATH)
endif()

arm_config_option(
    NAME RMM_STATIC_ANALYSIS_CPPCHECK
    HELP "Enable Cppcheck static analysis."
    DEFAULT TRUE
    DEPENDS (RMM_STATIC_ANALYSIS) AND
            (EXISTS "${RMM_STATIC_ANALYSIS_CPPCHECK_PATH}")
    ELSE FALSE)

arm_config_option(
    NAME RMM_STATIC_ANALYSIS_CPPCHECK_FLAGS
    HELP "Cppcheck command line options."
    TYPE STRING
    DEFAULT ""
    DEPENDS RMM_STATIC_ANALYSIS_CPPCHECK
    ADVANCED)

arm_config_option(
    NAME RMM_STATIC_ANALYSIS_CPPCHECK_CHECKER_CERT_C
    HELP "Enable Cppcheck's SEI CERT C checker."
    DEFAULT TRUE
    DEPENDS RMM_STATIC_ANALYSIS_CPPCHECK
    ELSE FALSE)

arm_config_option(
    NAME RMM_STATIC_ANALYSIS_CPPCHECK_CHECKER_MISRA
    HELP "Enable Cppcheck's MISRA C:2012 checker."
    DEFAULT TRUE
    DEPENDS RMM_STATIC_ANALYSIS_CPPCHECK
    ELSE FALSE)

arm_config_option(
    NAME RMM_STATIC_ANALYSIS_CPPCHECK_CHECKER_THREAD_SAFETY
    HELP "Enable Cppcheck's thread safety checker."
    DEFAULT TRUE
    DEPENDS RMM_STATIC_ANALYSIS_CPPCHECK
    ELSE FALSE)

if(RMM_STATIC_ANALYSIS_CPPCHECK)
    #
    # Set up checkers.
    #

    set(cppcheck-flags)

    list(APPEND cppcheck-flags "--enable=all")
    list(APPEND cppcheck-flags "--xml")
    list(APPEND cppcheck-flags "--xml-version=2")
    list(APPEND cppcheck-flags "--output-file=${CMAKE_CURRENT_BINARY_DIR}/cppcheck.xml")

    if(RMM_STATIC_ANALYSIS_CPPCHECK_CHECKER_CERT_C)
        list(APPEND cppcheck-flags "--addon=cert")
    endif()

    if(RMM_STATIC_ANALYSIS_CPPCHECK_CHECKER_MISRA)
	list(APPEND cppcheck-flags "--addon=${CMAKE_CURRENT_SOURCE_DIR}/misra.json")
    endif()

    if(RMM_STATIC_ANALYSIS_CPPCHECK_CHECKER_THREAD_SAFETY)
        list(APPEND cppcheck-flags "--addon=threadsafety")
    endif()

    #
    # Pass CHAR_BIT to Mbed TLS to supress error:
    # "mbed TLS requires a platform with 8-bit chars"
    #

    list(APPEND cppcheck-flags "-DCHAR_BIT=8")

    #
    # Suppress files or directories we don't want to receive warnings about. If
    # you want to suppress specific files without using an inline suppression,
    # do it in `suppressions.txt`.
    #

    list(APPEND cppcheck-flags
        "--inline-suppr" # Allow inline suppressions
        "--suppressions-list=${CMAKE_CURRENT_SOURCE_DIR}/suppressions.txt")

    #
    # Determine implicit C compiler definitions by pulling them from the
    # compiler and dumping them into a header file. This is for those situations
    # where we're relying on compiler implementation details, but Cppcheck
    # doesn't expose them.
    #

    file(TOUCH "${CMAKE_CURRENT_BINARY_DIR}/null.txt")

    separate_arguments(cflags NATIVE_COMMAND "${CMAKE_C_FLAGS}")

    execute_process(
        COMMAND ${CMAKE_C_COMPILER} ${cflags} -dM -E -
        INPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/null.txt"
        OUTPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/implicit-defines.h")

    list(APPEND cppcheck-flags
        "--include=${CMAKE_CURRENT_BINARY_DIR}/implicit-defines.h"
        "--suppress=*:${CMAKE_CURRENT_BINARY_DIR}/implicit-defines.h")

    #
    # Traditionally we would let Cppcheck use its own standard library headers,
    # but it appears to be lacking some critical symbols like `CHAR_BIT` from
    # `<limits.h>`. Luckily, CMake makes this relatively easy for us to do. We
    # don't analyze these headers, we just make them available for inclusion.
    #

    foreach(include IN LISTS CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES)
        list(APPEND cppcheck-flags "-I${include}")
        list(APPEND cppcheck-flags "--suppress=*:${include}/*")
    endforeach()

    #
    # Configure the platform file. This is done based on the current compiler,
    # if possible, so that we can communicate certain implementation details to
    # Cppcheck and avoid false positives.
    #

    set(platform-xml
        "${CMAKE_CURRENT_SOURCE_DIR}/compilers/${CMAKE_C_COMPILER_ID}.xml")

    if(EXISTS "${platform-xml}")
        list(APPEND cppcheck-flags "--platform=${platform-xml}")
    else()
        message(WARNING
            "No Cppcheck platform file is available for this compiler. Static "
            "analysis results may be inaccurate.")
    endif()

    separate_arguments(cppcheck-flags-user
        NATIVE_COMMAND "${RMM_STATIC_ANALYSIS_CPPCHECK_FLAGS}")


    set(COMPILE_COMMANDS_FILE "${CMAKE_BINARY_DIR}/compile_commands.json")

    add_custom_target(cppcheck
      COMMAND ${RMM_STATIC_ANALYSIS_CPPCHECK_PATH}
              --project=${COMPILE_COMMANDS_FILE} ${cppcheck-flags} ||
              (test ! -e ${COMPILE_COMMANDS_FILE} &&
              echo 'please generate with -DRMM_STATIC_ANALYSIS_CPPCHECK=ON')
    )
else()
    unset(CMAKE_C_CPPCHECK CACHE)
endif()
