Просмотр исходного кода

Support CSA

# О чем этот PR?
Добавляем возможность автоматического запуска Clang Static Analyzer (CSA) при сборке бинарников
# Уже же есть clang tidy зачем нам еще и clang static analyzer?
Да, clang tidy включает в себя возможности static analyzer, но данный PR расширает возможности CSA, а конкретно:
1. Фильтрация анализируемых файлов -- CSA по дефолту такое делать не умеет, а стандартные чекеры находят всякий мусор в либах контриба
2. Возможность подгружать свои собственные плагины -- они же чекеры
# А нам точно нужен CSA?
Да, нужен, так как он находит провисшие ссылки https://a.yandex-team.ru/review/4679116/details -- проезды по памяти ловить ооочень сложно

## Чтобы следить за процессом
В CI падает большое количество тестов, поэтому параллельно идет CI в котором изменений никаких нет, для отслеживания динамики падающих тестов
https://a.yandex-team.ru/review/4868604/details
nechda 1 год назад
Родитель
Сommit
e1cb6168fb

+ 1 - 0
build/conf/compilers/gnu_compiler.conf

@@ -184,6 +184,7 @@ when (($TIME_TRACE == "yes" || $COMPILER_TIME_TRACE == "yes") && $_HAS_TIME_TRAC
 
 _C_CPP_KV_STYLE=${hide;kv:"p CC"} ${hide;kv:"pc green"}
 _CPP_ARGS=\
+    $CLANG_STATIC_ANALYZER_OPTIONS && \
     $CLANG_TIDY_ARGS \
     $YNDEXER_ARGS \
     $RETRY_ARGS \

+ 98 - 0
build/scripts/clang_static_analyzer.py

@@ -0,0 +1,98 @@
+import subprocess
+import sys
+import os
+import re
+import argparse
+import yaml
+
+CLANG_SA_CONFIG='static_analyzer.yaml'
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--testing-src", required=True)
+    parser.add_argument("--clang-bin", required=True)
+    parser.add_argument("--source-root", required=True)
+    parser.add_argument("--config-file", required=True)
+    parser.add_argument("--plugins-begin", dest='plugins', action='append', nargs='+', required=True)
+    parser.add_argument("--plugins-end", action='store_true', required=True)
+    return parser.parse_known_args()
+
+def find_config(config_path):
+    # For unifying config files names
+    basename = os.path.basename(config_path)
+    if basename != CLANG_SA_CONFIG:
+        msg = "The config file should be called {}, but {} passed".format(CLANG_SA_CONFIG, basename)
+        raise ValueError(msg)
+    if not os.path.isfile(config_path):
+        raise ValueError("Cant find config file {}".format(config_path))
+    return config_path
+
+def parse_config(config_file):
+    conf = None
+    try:
+        with open(config_file, 'r') as afile:
+            conf = yaml.safe_load(afile)
+    except:
+        conf = None
+    return conf
+
+def should_analyze(filename, conf):
+    include_files = conf.get('include_files')
+    exclude_files = conf.get('exclude_files')
+
+    if not include_files:
+        return False
+
+    include = re.match(include_files, filename)
+    exclude = re.match(exclude_files, filename) if exclude_files else False
+
+    return include and not exclude
+
+def load_plugins(conf, plugins):
+    load_cmds = []
+    for plugin in filter(lambda path: os.path.isfile(path), plugins):
+        load_cmds.extend(["-Xclang", "-load", "-Xclang", plugin])
+    return load_cmds
+
+def main():
+    args, clang_cmd = parse_args()
+
+    # Try to find config file and parse them
+    config_file = find_config(args.config_file)
+    conf = parse_config(config_file)
+
+    # Ensure we have at least one check
+    if ('checks' not in conf) or (not conf['checks']):
+        raise ValueError("There are no checks in the config file")
+
+    # Ensure that file match regex
+    if not should_analyze(args.testing_src, conf):
+        return 0
+
+    # Prepare args
+    analyzer_opts = [
+        '-Wno-unused-command-line-argument',
+        '--analyze',
+        '--analyzer-outputtext',
+        '--analyzer-no-default-checks'
+    ]
+    analyzer_opts.extend(['-Xanalyzer', '-analyzer-werror'])
+    analyzer_opts.extend(['-Xanalyzer', '-analyzer-checker=' + ','.join(conf['checks'])])
+
+    # Load additional plugins
+    analyzer_opts.extend(load_plugins(conf, args.plugins[0]))
+
+    run_cmd = [args.clang_bin, args.testing_src] + clang_cmd + analyzer_opts
+    p = subprocess.run(run_cmd)
+
+    return p.returncode
+
+if __name__ == '__main__':
+    ret_code = 0
+    try:
+        ret_code = main()
+    except Exception as e:
+        print >> sys.stderr, "\n[Error]: " + str(e)
+        ret_code = 1
+    exit(ret_code)
+

+ 21 - 0
build/ymake.core.conf

@@ -103,6 +103,27 @@ when ($USE_PREBUILT_TOOLS == "yes") {
 
 }
 
+### @usage: SELECT_CLANG_SA_CONFIG(static_analyzer.yaml)
+###
+### Select config file for clang static analyzer.
+### The file should be called static_analyzer.yaml.
+macro SELECT_CLANG_SA_CONFIG(config) {
+    SET(_CLANG_SA_CONFIG ${input:config})
+}
+
+# Helper macro for unwrapping sequence of files
+macro _CLANG_SA_UNWRAP_PLUGINS(Plugins{input}[]) {
+     .CMD=${input:Plugins}
+}
+
+CLANG_SA_PLUGINS=
+when ($CLANG_SA_ENABLE == "yes" && $_CLANG_SA_CONFIG) {
+    CLANG_STATIC_ANALYZER_OPTIONS=$YMAKE_PYTHON ${input:"build/scripts/clang_static_analyzer.py"} "--testing-src" ${input:SRC} "--clang-bin" $CXX_COMPILER "--source-root" $(SOURCE_ROOT) "--config-file" ${input:_CLANG_SA_CONFIG} "--plugins-begin" "dummy_param" $_CLANG_SA_UNWRAP_PLUGINS($CLANG_SA_PLUGINS) "--plugins-end" $C_FLAGS_PLATFORM $GCC_COMPILE_FLAGS $CXXFLAGS $SRCFLAGS
+}
+otherwise {
+    CLANG_STATIC_ANALYZER_OPTIONS=
+}
+
 FAIL_MODULE_CMD=$YMAKE_PYTHON3 ${input:"build/scripts/fail_module_cmd.py"} $TARGET ${kv;hide:"p ER"} ${kv;hide:"pc red"}
 DEFAULT_TIDY_CONFIG=build/config/tests/clang_tidy/config.yaml
 PROJECT_TIDY_CONFIG=build/config/tests/clang_tidy/config.yaml