diff --git a/.clang-tidy b/.clang-tidy index 6861882e3..59ad5a5e7 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,13 +1,12 @@ Checks: " modernize-use-*, -modernize-use-trailing-return-type*, performance-*, mpi-*, openmp-*, bugprone-*, readability-*, -readability-magic-numbers, -readability-redundant-access-specifiers, -clang-analyzer-* " AnalyzeTemporaryDtors: false HeaderFilterRegex: 'src/.*' FormatStyle: file -UseColor: true diff --git a/.codeclimate.yml b/.codeclimate.yml index 7878866d0..1247c1b95 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,38 +1,42 @@ checks: duplicate: enabled: true exclude_patterns: - "test/" - "examples/" structure: enabled: true exclude_patterns: - "test/" plugins: editorconfig: enabled: false config: editorconfig: .editorconfig exclude_patterns: - ".clangd/" - ".cache/" pep8: enabled: true exclude_patterns: - "test/test_fe_engine/py_engine/py_engine.py" cppcheck: enabled: false project: compile_commands.json language: c++ check: warning, style, performance stds: [c++14] - fixme: enabled: true exclude_patterns: - "doc/" + clang-tidy: + enabled: true + exclude_patterns: + - "test/" + - "examples/" exclude_patterns: - "third-party/" - "build*/" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 92c14fc08..780deecb5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,326 +1,347 @@ stages: - configure - build - check-warnings - test - deploy .docker_build: image: 'docker:19.03.11' stage: .pre services: - docker:19.03.11-dind variables: # Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled DOCKER_HOST: tcp://docker:2376 DOCKER_TLS_CERTDIR: "/certs" before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - cd test/ci/${IMAGE_NAME}/ - docker build -t registry.gitlab.com/akantu/akantu/${IMAGE_NAME} . - docker push registry.gitlab.com/akantu/akantu/${IMAGE_NAME} docker build:debian-testing: variables: IMAGE_NAME: debian:testing extends: .docker_build rules: - changes: - test/ci/debian:testing/Dockerfile docker build:ubuntu-lts: variables: IMAGE_NAME: ubuntu:lts extends: .docker_build rules: - changes: - test/ci/ubuntu:lts/Dockerfile .configure: stage: configure except: - tags variables: BLA_VENDOR: 'Generic' script: - cmake -E make_directory build - cd build - cmake -DAKANTU_COHESIVE_ELEMENT:BOOL=TRUE -DAKANTU_IMPLICIT:BOOL=TRUE -DAKANTU_PARALLEL:BOOL=TRUE -DAKANTU_STRUCTURAL_MECHANICS:BOOL=TRUE -DAKANTU_HEAT_TRANSFER:BOOL=TRUE -DAKANTU_DAMAGE_NON_LOCAL:BOOL=TRUE -DAKANTU_PYTHON_INTERFACE:BOOL=TRUE -DAKANTU_EXAMPLES:BOOL=TRUE -DAKANTU_BUILD_ALL_EXAMPLES:BOOL=TRUE -DAKANTU_TEST_EXAMPLES:BOOL=FALSE -DAKANTU_TESTS:BOOL=TRUE -DAKANTU_RUN_IN_DOCKER:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Coverage .. - cp compile_commmands.json .. artifacts: when: on_success paths: - build - compile_commands.json expire_in: 10h .build: stage: build script: - cmake --build build/src > >(tee -a ${output}-out.log) 2> >(tee -a ${output}-err.log >&2) - cmake --build build/python > >(tee -a ${output}-out.log) 2> >(tee -a ${output}-err.log >&2) - cmake --build build/test/ > >(tee -a ${output}-out.log) 2> >(tee -a ${output}-err.log >&2) - cmake --build build/examples > >(tee -a ${output}-out.log) 2> >(tee -a ${output}-err.log >&2) artifacts: when: on_success paths: - build/ #- ${output}-out.log - ${output}-err.log - compile_commands.json expire_in: 10h .tests: stage: test script: - cd build - ctest -T test --no-compress-output --timeout 1800 after_script: - cd build - tag=$(head -n 1 < Testing/TAG) - if [ -e Testing/${tag}/Test.xml ]; then - xsltproc -o ./juint.xml ${CI_PROJECT_DIR}/test/ci/ctest2junit.xsl Testing/${tag}/Test.xml; - fi - gcovr --xml --gcov-executable "${GCOV_EXECUTABLE}" --output coverage.xml --object-directory ${CI_PROJECT_DIR}/build --root ${CI_PROJECT_DIR} -s || true artifacts: when: always paths: - build/juint.xml - build/coverage.xml reports: junit: - build/juint.xml cobertura: - build/coverage.xml .analyse_build: stage: check-warnings script: - if [[ $(cat ${output}-err.log | grep warning -i) ]]; then - cat ${output}-err.log; - exit 1; - fi allow_failure: true artifacts: when: on_failure paths: - "$output-err.log" # ------------------------------------------------------------------------------ .cache_build: variables: CCACHE_BASEDIRE: ${CI_PROJECT_DIR}/build CCACHE_DIR: ${CI_PROJECT_DIR}/.ccache CCACHE_NOHASHDIR: 1 CCACHE_COMPILERCHECK: content cache: key: ${output} policy: pull-push paths: - .ccache/ - third-party/google-test - third-party/pybind11 before_script: - ccache --zero-stats || true after_script: - ccache --show-stats || true # ------------------------------------------------------------------------------ .image_debian_testing: image: registry.gitlab.com/akantu/akantu/debian:testing .image_ubuntu_lts: image: registry.gitlab.com/akantu/akantu/ubuntu:lts # ------------------------------------------------------------------------------ .compiler_gcc: variables: CC: /usr/lib/ccache/gcc CXX: /usr/lib/ccache/g++ FC: gfortran GCOV_EXECUTABLE: gcov .compiler_clang: variables: CC: /usr/lib/ccache/clang CXX: /usr/lib/ccache/clang++ FC: gfortran GCOV_EXECUTABLE: llvm-cov gcov # ------------------------------------------------------------------------------ .debian_testing_gcc: variables: output: debian_testing_gcc extends: - .compiler_gcc - .image_debian_testing - .cache_build .debian_testing_clang: variables: output: debian_testing_clang extends: - .compiler_clang - .image_debian_testing - .cache_build .ubuntu_lts_gcc: variables: output: ubuntu_lts_gcc extends: - .compiler_gcc - .image_ubuntu_lts - .cache_build # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ configure:debian_testing_gcc: extends: - .debian_testing_gcc - .configure cache: policy: pull-push build:debian_testing_gcc: extends: - .debian_testing_gcc - .build dependencies: - configure:debian_testing_gcc test:debian_testing_gcc: extends: - .debian_testing_gcc - .tests dependencies: - build:debian_testing_gcc analyse_build:debian_testing_gcc: extends: - .debian_testing_gcc - .analyse_build dependencies: - build:debian_testing_gcc # ------------------------------------------------------------------------------ configure:debian_testing_clang: extends: - .debian_testing_clang - .configure cache: policy: pull-push build:debian_testing_clang: extends: - .debian_testing_clang - .build dependencies: - configure:debian_testing_clang test:debian_testing_clang: extends: - .debian_testing_clang - .tests dependencies: - build:debian_testing_clang analyse_build:debian_testing_clang: extends: - .debian_testing_clang - .analyse_build dependencies: - build:debian_testing_clang # ------------------------------------------------------------------------------ configure:ubuntu_lts_gcc: extends: - .ubuntu_lts_gcc - .configure cache: policy: pull-push build:ubuntu_lts_gcc: extends: - .ubuntu_lts_gcc - .build dependencies: - configure:ubuntu_lts_gcc analyse_build:ubuntu_lts_gcc: extends: - .ubuntu_lts_gcc - .analyse_build dependencies: - build:ubuntu_lts_gcc test:ubuntu_lts_gcc: extends: - .ubuntu_lts_gcc - .tests dependencies: - build:ubuntu_lts_gcc # ------------------------------------------------------------------------------ -# include: -# - template: Code-Quality.gitlab-ci.yml - -# code_quality: -# dependencies: -# - build:debian_testing_clang -# artifacts: -# paths: [gl-code-quality-report.json] - -code_quality_clang_tidy: - extends: - - .debian_testing_clang - dependencies: - - build:debian_testing_clang +code_quality: stage: test + image: docker:19.03.12 + allow_failure: true + services: + - docker:19.03.12-dind + variables: + DOCKER_DRIVER: overlay2 + DOCKER_HOST: tcp://docker:2376 + DOCKER_TLS_CERTDIR: "/certs" + CODECLIMATE_DEV: "" + CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.22" + needs: [] script: - - cmake --build build --target clang-tidy-all > >(tee -a clang-tidy-all-out.log) 2> >(tee -a clang-tidy-all-err.log >&2) - - python3 ./test/ci/scripts/clang-tidy2code-quality.py | tee code-quality-clang.json | jq - artifacts: - paths: - - clang-tidy-all-err.log - - clang-tidy-all-out.log - - code-quality-clang.json - expire_in: 10h + - export SOURCE_CODE=$PWD + - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage + function propagate_env_vars() { + CURRENT_ENV=$(printenv) + + for VAR_NAME; do + echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME " + done + } + - docker pull --quiet "$CODE_QUALITY_IMAGE" + - | + - docker build -t codeclimate/codeclimate-clang-tidy test/ci/codeclimate/codeclimate-clang-tidy + - | + docker run \ + $(propagate_env_vars \ + SOURCE_CODE \ + TIMEOUT_SECONDS \ + CODECLIMATE_DEBUG \ + CODECLIMATE_DEV \ + REPORT_STDOUT \ + REPORT_FORMAT \ + ENGINE_MEMORY_LIMIT_BYTES \ + ) \ + --volume "$PWD":/code \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + "$CODE_QUALITY_IMAGE" /code artifacts: reports: - codequality: - - code-quality-clang.json + codequality: gl-code-quality-report.json + expire_in: 1 week + dependencies: [] + rules: + - if: '$CODE_QUALITY_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' # ------------------------------------------------------------------------------ pages: stage: deploy extends: - .debian_testing_gcc script: - cd build - cmake -DAKANTU_DOCUMENTATION_DEVELOPER_MANUAL=ON .. - cmake --build . -t sphinx-doc - mv doc/dev-doc/html ../public dependencies: - build:debian_testing_gcc artifacts: paths: - public only: - features/doc diff --git a/test/ci/codeclimate/codeclimate-clang-tidy/Dockerfile b/test/ci/codeclimate/codeclimate-clang-tidy/Dockerfile new file mode 100644 index 000000000..7d291f8b3 --- /dev/null +++ b/test/ci/codeclimate/codeclimate-clang-tidy/Dockerfile @@ -0,0 +1,23 @@ +FROM alpine:edge +LABEL maintainer "Nicolas Richart " + +WORKDIR /usr/src/app + +RUN apk --update add --no-cache --upgrade \ + clang\ + clang-extra-tools \ + musl-dev \ + python3 \ + py3-lxml && \ + rm -rf /usr/share/ri && \ + adduser -u 9000 -D -s /bin/false app + +#COPY engine.json / +COPY . ./ +RUN chown -R app:app ./ +USER app + +VOLUME /code +WORKDIR /code + +CMD ["/usr/src/app/bin/codeclimate-clang-tidy"] diff --git a/test/ci/codeclimate/codeclimate-clang-tidy/bin/codeclimate-clang-tidy b/test/ci/codeclimate/codeclimate-clang-tidy/bin/codeclimate-clang-tidy new file mode 100755 index 000000000..598027114 --- /dev/null +++ b/test/ci/codeclimate/codeclimate-clang-tidy/bin/codeclimate-clang-tidy @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import os +import sys + +lib_path = os.path.abspath(os.path.join(__file__, '..', '..', 'lib')) +sys.path.append(lib_path) + +from runner import Runner # noqa + +if __name__ == '__main__': + print(f"Arguments: {sys.argv}", file=sys.stderr) + Runner().run() diff --git a/test/ci/codeclimate/codeclimate-clang-tidy/bin/run-clang-tidy b/test/ci/codeclimate/codeclimate-clang-tidy/bin/run-clang-tidy new file mode 100755 index 000000000..0dbac0b25 --- /dev/null +++ b/test/ci/codeclimate/codeclimate-clang-tidy/bin/run-clang-tidy @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +# +#===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +#===-----------------------------------------------------------------------===# +# FIXME: Integrate with clang-tidy-diff.py + + +""" +Parallel clang-tidy runner +========================== + +Runs clang-tidy over all files in a compilation database. Requires clang-tidy +and clang-apply-replacements in $PATH. + +Example invocations. +- Run clang-tidy on all files in the current working directory with a default + set of checks and show warnings in the cpp files and all project headers. + run-clang-tidy.py $PWD + +- Fix all header guards. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard + +- Fix all header guards included from clang-tidy and header guards + for clang-tidy headers. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \ + -header-filter=extra/clang-tidy + +Compilation database setup: +http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html +""" + +from __future__ import print_function + +import argparse +import glob +import json +import multiprocessing +import os +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import traceback + +try: + import yaml +except ImportError: + yaml = None + +is_py2 = sys.version[0] == '2' + +if is_py2: + import Queue as queue +else: + import queue as queue + + +def find_compilation_database(path): + """Adjusts the directory until a compilation database is found.""" + result = './' + while not os.path.isfile(os.path.join(result, path)): + if os.path.realpath(result) == '/': + print('Error: could not find compilation database.') + sys.exit(1) + result += '../' + return os.path.realpath(result) + + +def make_absolute(f, directory): + if os.path.isabs(f): + return f + return os.path.normpath(os.path.join(directory, f)) + + +def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, + header_filter, allow_enabling_alpha_checkers, + extra_arg, extra_arg_before, quiet, config): + """Gets a command line for clang-tidy.""" + start = [clang_tidy_binary] + if allow_enabling_alpha_checkers: + start.append('-allow-enabling-analyzer-alpha-checkers') + if header_filter is not None: + start.append('-header-filter=' + header_filter) + if checks: + start.append('-checks=' + checks) + if tmpdir is not None: + start.append('-export-fixes') + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) + os.close(handle) + start.append(name) + for arg in extra_arg: + start.append('-extra-arg=%s' % arg) + for arg in extra_arg_before: + start.append('-extra-arg-before=%s' % arg) + start.append('-p=' + build_path) + if quiet: + start.append('-quiet') + if config: + start.append('-config=' + config) + start.append(f) + return start + + +def merge_replacement_files(tmpdir, mergefile): + """Merge all replacement files in a directory into a single file""" + # The fixes suggested by clang-tidy >= 4.0.0 are given under + # the top level key 'Diagnostics' in the output yaml files + mergekey = "Diagnostics" + merged=[] + for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): + content = yaml.safe_load(open(replacefile, 'r')) + if not content: + continue # Skip empty files. + merged.extend(content.get(mergekey, [])) + + if merged: + # MainSourceFile: The key is required by the definition inside + # include/clang/Tooling/ReplacementsYaml.h, but the value + # is actually never used inside clang-apply-replacements, + # so we set it to '' here. + output = {'MainSourceFile': '', mergekey: merged} + with open(mergefile, 'w') as out: + yaml.safe_dump(output, out) + else: + # Empty the file: + open(mergefile, 'w').close() + + +def check_clang_apply_replacements_binary(args): + """Checks if invoking supplied clang-apply-replacements binary works.""" + try: + subprocess.check_call([args.clang_apply_replacements_binary, '--version']) + except: + print('Unable to run clang-apply-replacements. Is clang-apply-replacements ' + 'binary correctly specified?', file=sys.stderr) + traceback.print_exc() + sys.exit(1) + + +def apply_fixes(args, tmpdir): + """Calls clang-apply-fixes on a given directory.""" + invocation = [args.clang_apply_replacements_binary] + if args.format: + invocation.append('-format') + if args.style: + invocation.append('-style=' + args.style) + invocation.append(tmpdir) + subprocess.call(invocation) + + +def run_tidy(args, tmpdir, build_path, queue, lock, failed_files): + """Takes filenames out of queue and runs clang-tidy on them.""" + while True: + name = queue.get() + invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks, + tmpdir, build_path, args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, args.extra_arg_before, + args.quiet, args.config) + + proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, err = proc.communicate() + if proc.returncode != 0: + failed_files.append(name) + with lock: + sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8')) + if len(err) > 0: + sys.stdout.flush() + sys.stderr.write(err.decode('utf-8')) + queue.task_done() + + +def main(): + parser = argparse.ArgumentParser(description='Runs clang-tidy over all files ' + 'in a compilation database. Requires ' + 'clang-tidy and clang-apply-replacements in ' + '$PATH.') + parser.add_argument('-allow-enabling-alpha-checkers', + action='store_true', help='allow alpha checkers from ' + 'clang-analyzer.') + parser.add_argument('-clang-tidy-binary', metavar='PATH', + default='clang-tidy-11', + help='path to clang-tidy binary') + parser.add_argument('-clang-apply-replacements-binary', metavar='PATH', + default='clang-apply-replacements-11', + help='path to clang-apply-replacements binary') + parser.add_argument('-checks', default=None, + help='checks filter, when not specified, use clang-tidy ' + 'default') + parser.add_argument('-config', default=None, + help='Specifies a configuration in YAML/JSON format: ' + ' -config="{Checks: \'*\', ' + ' CheckOptions: [{key: x, ' + ' value: y}]}" ' + 'When the value is empty, clang-tidy will ' + 'attempt to find a file named .clang-tidy for ' + 'each source file in its parent directories.') + parser.add_argument('-header-filter', default=None, + help='regular expression matching the names of the ' + 'headers to output diagnostics from. Diagnostics from ' + 'the main file of each translation unit are always ' + 'displayed.') + if yaml: + parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes', + help='Create a yaml file to store suggested fixes in, ' + 'which can be applied with clang-apply-replacements.') + parser.add_argument('-j', type=int, default=0, + help='number of tidy instances to be run in parallel.') + parser.add_argument('files', nargs='*', default=['.*'], + help='files to be processed (regex on path)') + parser.add_argument('-fix', action='store_true', help='apply fix-its') + parser.add_argument('-format', action='store_true', help='Reformat code ' + 'after applying fixes') + parser.add_argument('-style', default='file', help='The style of reformat ' + 'code after applying fixes') + parser.add_argument('-p', dest='build_path', + help='Path used to read a compile command database.') + parser.add_argument('-extra-arg', dest='extra_arg', + action='append', default=[], + help='Additional argument to append to the compiler ' + 'command line.') + parser.add_argument('-extra-arg-before', dest='extra_arg_before', + action='append', default=[], + help='Additional argument to prepend to the compiler ' + 'command line.') + parser.add_argument('-quiet', action='store_true', + help='Run clang-tidy in quiet mode') + args = parser.parse_args() + + db_path = 'compile_commands.json' + + if args.build_path is not None: + build_path = args.build_path + else: + # Find our database + build_path = find_compilation_database(db_path) + + try: + invocation = [args.clang_tidy_binary, '-list-checks'] + if args.allow_enabling_alpha_checkers: + invocation.append('-allow-enabling-analyzer-alpha-checkers') + invocation.append('-p=' + build_path) + if args.checks: + invocation.append('-checks=' + args.checks) + invocation.append('-') + if args.quiet: + # Even with -quiet we still want to check if we can call clang-tidy. + with open(os.devnull, 'w') as dev_null: + subprocess.check_call(invocation, stdout=dev_null) + else: + subprocess.check_call(invocation) + except: + print("Unable to run clang-tidy.", file=sys.stderr) + sys.exit(1) + + # Load the database and extract all files. + database = json.load(open(os.path.join(build_path, db_path))) + files = [make_absolute(entry['file'], entry['directory']) + for entry in database] + + max_task = args.j + if max_task == 0: + max_task = multiprocessing.cpu_count() + + tmpdir = None + if args.fix or (yaml and args.export_fixes): + check_clang_apply_replacements_binary(args) + tmpdir = tempfile.mkdtemp() + + # Build up a big regexy filter from all command line arguments. + file_name_re = re.compile('|'.join(args.files)) + + return_code = 0 + try: + # Spin up a bunch of tidy-launching threads. + task_queue = queue.Queue(max_task) + # List of files with a non-zero return code. + failed_files = [] + lock = threading.Lock() + for _ in range(max_task): + t = threading.Thread(target=run_tidy, + args=(args, tmpdir, build_path, task_queue, lock, failed_files)) + t.daemon = True + t.start() + + # Fill the queue with files. + for name in files: + if file_name_re.search(name): + task_queue.put(name) + + # Wait for all threads to be done. + task_queue.join() + if len(failed_files): + return_code = 1 + + except KeyboardInterrupt: + # This is a sad hack. Unfortunately subprocess goes + # bonkers with ctrl-c and we start forking merrily. + print('\nCtrl-C detected, goodbye.') + if tmpdir: + shutil.rmtree(tmpdir) + os.kill(0, 9) + + if yaml and args.export_fixes: + print('Writing fixes to ' + args.export_fixes + ' ...') + try: + merge_replacement_files(tmpdir, args.export_fixes) + except: + print('Error exporting fixes.\n', file=sys.stderr) + traceback.print_exc() + return_code=1 + + if args.fix: + print('Applying fixes ...') + try: + apply_fixes(args, tmpdir) + except: + print('Error applying fixes.\n', file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if tmpdir: + shutil.rmtree(tmpdir) + sys.exit(return_code) + + +if __name__ == '__main__': + main() diff --git a/test/ci/codeclimate/codeclimate-clang-tidy/engine.json b/test/ci/codeclimate/codeclimate-clang-tidy/engine.json new file mode 100644 index 000000000..0e520cc6f --- /dev/null +++ b/test/ci/codeclimate/codeclimate-clang-tidy/engine.json @@ -0,0 +1,11 @@ +{ + "name": "codeclimate-clang-tidy", + "description": "clang-tidy is a static analysis tool for C/C++ code.", + "maintainer": { + "name": "Nicolas Richart", + "email": "nicolas.richart@epfl.ch" + }, + "languages" : ["C", "C++"], + "version": "0.0.1", + "spec_version": "0.2.0" +} diff --git a/test/ci/codeclimate/codeclimate-clang-tidy/lib/command.py b/test/ci/codeclimate/codeclimate-clang-tidy/lib/command.py new file mode 100644 index 000000000..584f86e63 --- /dev/null +++ b/test/ci/codeclimate/codeclimate-clang-tidy/lib/command.py @@ -0,0 +1,39 @@ +import os + + +class Command: + """Returns command line arguments by parsing codeclimate config file.""" + def __init__(self, config, file_list): + self.config = config + self.file_list = file_list + + def build(self): + command = ['/usr/src/app/bin/run-clang-tidy', + '-clang-tidy-binary', + '/usr/bin/clang-tidy'] + + if self.config.get('check'): + command.append( + '-checks {}'.format(self.config.get('check'))) + + if self.config.get('config'): + command.append( + '-config {}'.format(self.config.get('project'))) + + if self.config.get('header-filter'): + command.append( + '-header-filter {}'.format(self.config.get('language'))) + + if self.config.get('compilation_database_path'): + command.append( + '-p {}'.format(self.config.get('compilation_database_path'))) + + include_paths = [] + for file_ in self.file_list: + include_paths.append(os.path.dirname(file_)) + include_paths = [f'--extra-arg -I{path}' for path in set(include_paths)] + + command.extend(include_paths) + # command.extend(self.file_list) + + return command diff --git a/test/ci/codeclimate/codeclimate-clang-tidy/lib/issue_formatter.py b/test/ci/codeclimate/codeclimate-clang-tidy/lib/issue_formatter.py new file mode 100644 index 000000000..3e728fd3a --- /dev/null +++ b/test/ci/codeclimate/codeclimate-clang-tidy/lib/issue_formatter.py @@ -0,0 +1,73 @@ +import hashlib +import os + + +class IssueFormatter: + CLASSIFICATIONS = { + 'bugprone': { + 'categories': ['Bug Risk'], + 'severity': 'major', + }, + 'modernize': { + 'categories': ['Clarity', 'Compatibility', 'Style'], + 'severity': 'info' + }, + 'mpi': { + 'categories': ['Bug Risk', 'Performance'], + 'severity': 'critical', + }, + 'openmp': { + 'categories': ['Bug Risk', 'Performance'], + 'severity': 'critical', + }, + 'performance': { + 'categories': ['Performance'], + 'severity': 'minor', + }, + 'readability': { + 'categories': ['Clarity', 'Style'], + 'severity': 'info' + }, + } + + def __init__(self, issue): + self.issue_dict = issue + + def format(self): + self.issue_dict['file'] = os.path.relpath(self.issue_dict['file']) + issue = { + 'type': 'issue', + 'check_name': self.issue_dict['type'], + 'description': self.issue_dict['detail'], + 'location': { + "path": self.issue_dict['file'], + "positions": { + "begin": { + "line": int(self.issue_dict['line']), + "column": int(self.issue_dict['column']), + }, + "end": { + "line": int(self.issue_dict['line']), + "column": int(self.issue_dict['column']), + }, + }, + }, + } + + if 'content' in self.issue_dict: + issue['content'] = { + 'body': '```\n' + + '\n'.join(self.issue_dict['content']) + + '\n```' + } + + issue['fingerprint'] = hashlib.md5( + '{file}:{line}:{column}:{type}'.format(**self.issue_dict).encode() + ).hexdigest() + + type_ = self.issue_dict['type'].split('-')[0] + if type_ in self.CLASSIFICATIONS: + issue['categories'] = self.CLASSIFICATIONS[type_]['categories'] + issue['severity'] = self.CLASSIFICATIONS[type_]['severity'] + + return issue diff --git a/test/ci/codeclimate/codeclimate-clang-tidy/lib/runner.py b/test/ci/codeclimate/codeclimate-clang-tidy/lib/runner.py new file mode 100644 index 000000000..aa0b7ca47 --- /dev/null +++ b/test/ci/codeclimate/codeclimate-clang-tidy/lib/runner.py @@ -0,0 +1,106 @@ +import json +import subprocess +import sys +import re +import tempfile + +from command import Command +from issue_formatter import IssueFormatter +from workspace import Workspace + + +class Runner: + CONFIG_FILE_PATH = '/config.json' + + """Runs clang-tidy, collects and reports results.""" + def __init__(self): + self._config_file_path = self.CONFIG_FILE_PATH + #self._workspace_path = workspace_path + pass + + def run(self): + config = self._decode_config() + self._print_debug(f'[clang-tidy] config: {config}') + + workspace = Workspace(config.get('include_paths')) + workspace_files = workspace.calculate() + + if not len(workspace_files) > 0: + return + + self._print_debug(f'[clang-tidy] analyzing {len(workspace_files)} files') + + plugin_config = config.get('config', {}) + command = Command(plugin_config, workspace_files).build() + + command.append('src/') + self._print_debug(f'[clang-tidy] command: {command}') + + results = self._run_command(command) + issues = self._parse_results(results) + + for issue in issues: + print(f'{json.dumps(issue)}\0') + + def _decode_config(self): + self._print_debug(f"Decoding config file {self._config_file_path}") + + contents = "" + with open(self._config_file_path, "r") as config: + contents = config.read() + + return json.loads(contents) + + def _run_command(self, command): + process = subprocess.Popen(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + stdout, stderr = process.communicate() + + return_vals = stdout.decode('utf-8').split('\n') + return return_vals + + def _parse_results(self, results): + ansi_escape = re.compile(r''' + \x1B # ESC + (?: # 7-bit C1 Fe (except CSI) + [@-Z\\-_] + | # or [ for CSI, followed by a control sequence + \[ + [0-?]* # Parameter bytes + [ -/]* # Intermediate bytes + [@-~] # Final byte + )''', re.VERBOSE) + + re_log_parse = re.compile( + r'(?P.*\.(cc|hh)):(?P[0-9]+):(?P[0-9]+): warning: (?P.*) \[(?P.*)\]' # noqa + ) + issues = {} + issue = None + + for line in results: + clean_line = ansi_escape.sub('', line) + match = re_log_parse.match(clean_line) + if match: + if issue: + issue_ = IssueFormatter(issue).format() + issues[issue_['fingerprint']] = issue_ + issue = match.groupdict() + elif issue: + if 'content' in issue: + issue['content'].append(line) + else: + issue['content'] = [line] + else: + issue = None + + if issue: + issue_ = IssueFormatter(issue).format() + issues[issue_['fingerprint']] = issue_ + + issues = list(issues.values()) + return issues + + def _print_debug(self, message): + print(message, file=sys.stderr) diff --git a/test/ci/codeclimate/codeclimate-clang-tidy/lib/workspace.py b/test/ci/codeclimate/codeclimate-clang-tidy/lib/workspace.py new file mode 100644 index 000000000..2d3f0bc8d --- /dev/null +++ b/test/ci/codeclimate/codeclimate-clang-tidy/lib/workspace.py @@ -0,0 +1,33 @@ +import os + +SRC_SUFFIX = ['.c', '.cpp', '.cc', '.cxx'] + + +class Workspace: + def __init__(self, include_paths): + self.include_paths = include_paths + + def calculate(self): + paths = [] + + for path in self.include_paths: + if os.path.isdir(path): + paths.extend(self._walk(path)) + elif self.should_include(path): + paths.append(path) + + return paths + + def should_include(self, name): + return name.lower().endswith(tuple(SRC_SUFFIX)) + + def _walk(self, path): + paths = [] + + for root, _directories, files in os.walk(path): + for name in files: + if self.should_include(name): + path = os.path.join(root, name) + paths.append(path) + + return paths