Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 38 additions & 188 deletions src/per_file.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ Rulesets for running codechecker in a different Bazel action
for each translation unit.
"""

load(
"compile_commands.bzl",
"compile_commands_aspect",
"compile_commands_impl",
"platforms_transition",
"SourceFilesInfo",
)
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
load("codechecker_config.bzl", "get_config_file")
Expand All @@ -26,6 +33,7 @@ def _run_code_checker(
ctx,
src,
arguments,
target,
label,
options,
config_file,
Expand All @@ -49,14 +57,13 @@ def _run_code_checker(
inputs = [compile_commands_json, config_file] + sources_and_headers
else:
# NOTE: we collect only headers, so CTU may not work!
headers = depset([src], transitive = [compilation_context.headers])
headers = depset(transitive = target[SourceFilesInfo].headers.to_list())
inputs = depset([compile_commands_json, config_file, src], transitive = [headers])

outputs = [clang_tidy_plist, clangsa_plist, codechecker_log]

analyzer_output_paths = "clangsa," + clangsa_plist.path + \
";clang-tidy," + clang_tidy_plist.path

# Action to run CodeChecker for a file
ctx.actions.run(
inputs = inputs,
Expand Down Expand Up @@ -92,190 +99,22 @@ def check_valid_file_type(src):
return True
return False

def _rule_sources(ctx):
srcs = []
if hasattr(ctx.rule.attr, "srcs"):
for src in ctx.rule.attr.srcs:
srcs += [src for src in src.files.to_list() if check_valid_file_type(src)]
return srcs

def _toolchain_flags(ctx, action_name = ACTION_NAMES.cpp_compile):
cc_toolchain = find_cpp_toolchain(ctx)
feature_configuration = cc_common.configure_features(
ctx = ctx,
cc_toolchain = cc_toolchain,
)
user_comp_flag_builder = ctx.fragments.cpp.copts
if action_name == ACTION_NAMES.cpp_compile:
user_comp_flag_builder += ctx.fragments.cpp.cxxopts
elif action_name == ACTION_NAMES.c_compile:
user_comp_flag_builder += ctx.fragments.cpp.conlyopts
else:
fail("Unhandled action name!")
compile_variables = cc_common.create_compile_variables(
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
user_compile_flags = user_comp_flag_builder,
)
flags = cc_common.get_memory_inefficient_command_line(
feature_configuration = feature_configuration,
action_name = action_name,
variables = compile_variables,
)
compiler = cc_common.get_tool_for_action(
feature_configuration = feature_configuration,
action_name = action_name,
)
return [compiler] + flags

def _compile_args(compilation_context):
compile_args = []
for define in compilation_context.defines.to_list():
compile_args.append("-D" + define)
for define in compilation_context.local_defines.to_list():
compile_args.append("-D" + define)
for include in compilation_context.framework_includes.to_list():
compile_args.append("-F" + include)
for include in compilation_context.includes.to_list():
compile_args.append("-I" + include)
for include in compilation_context.quote_includes.to_list():
compile_args.append("-iquote " + include)
for include in compilation_context.system_includes.to_list():
compile_args.append("-isystem " + include)
return compile_args

def _safe_flags(flags):
# Some flags might be used by GCC, but not understood by Clang.
# Remove them here, to allow users to run clang-tidy, without having
# a clang toolchain configured (that would produce a good command line with --compiler clang)
unsupported_flags = [
"-fno-canonical-system-headers",
"-fstack-usage",
]

return [flag for flag in flags if flag not in unsupported_flags]

CompileInfo = provider(
doc = "Source files and corresponding compilation arguments",
fields = {
"arguments": "dict: file -> list of arguments",
},
)

def _compile_info_sources(deps):
sources = []
if type(deps) == "list":
for dep in deps:
if CompileInfo in dep:
if hasattr(dep[CompileInfo], "arguments"):
srcs = dep[CompileInfo].arguments.keys()
sources += srcs
return sources

def _collect_all_sources(ctx):
sources = _rule_sources(ctx)
for attr in SOURCE_ATTR:
if hasattr(ctx.rule.attr, attr):
deps = getattr(ctx.rule.attr, attr)
sources += _compile_info_sources(deps)

# Remove duplicates
sources = depset(sources).to_list()
return sources

def _compile_info_aspect_impl(target, ctx):
if not CcInfo in target:
return []
compilation_context = target[CcInfo].compilation_context

rule_flags = ctx.rule.attr.copts if hasattr(ctx.rule.attr, "copts") else []
c_flags = _safe_flags(_toolchain_flags(ctx, ACTION_NAMES.c_compile) + rule_flags) # + ["-xc"]
cxx_flags = _safe_flags(_toolchain_flags(ctx, ACTION_NAMES.cpp_compile) + rule_flags) # + ["-xc++"]

srcs = _collect_all_sources(ctx)

compile_args = _compile_args(compilation_context)
arguments = {}
for src in srcs:
if src.extension.lower() in ["c"]:
flags = c_flags
elif src.extension.lower() in ["cc", "cpp", "cxx", "c++"]:
flags = cxx_flags
else:
print("Unknown file extension for", src.short_path, "defaulting to C++ compile flags")
flags = cxx_flags
arguments[src] = flags + compile_args + [src.path]
return [
CompileInfo(
arguments = arguments,
),
]

compile_info_aspect = aspect(
implementation = _compile_info_aspect_impl,
fragments = ["cpp"],
attrs = {
"_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
},
attr_aspects = SOURCE_ATTR,
toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
)

def _compile_commands_json(compile_commands):
json_file = "[\n"
entries = [json.encode(entry) for entry in compile_commands]
json_file += ",\n".join(entries)
json_file += "]\n"
return json_file

def _compile_commands_data(ctx):
compile_commands = []
for target in ctx.attr.targets:
if not CcInfo in target:
continue
if CompileInfo in target:
if hasattr(target[CompileInfo], "arguments"):
srcs = target[CompileInfo].arguments.keys()
for src in srcs:
args = target[CompileInfo].arguments[src]

# print("args =", str(args))
record = struct(
file = src.path,
command = " ".join(args),
directory = ".",
)
compile_commands.append(record)
return compile_commands

def _compile_commands_impl(ctx):
compile_commands = _compile_commands_data(ctx)
content = _compile_commands_json(compile_commands)
file_name = ctx.attr.name + "/data/compile_commands.json"
compile_commands_json = ctx.actions.declare_file(file_name)
ctx.actions.write(
output = compile_commands_json,
content = content,
)
return compile_commands_json

def _collect_all_sources_and_headers(ctx):
# NOTE: we are only using this function for CTU
all_files = []
headers = depset()
for target in ctx.attr.targets:
if not CcInfo in target:
continue
if CompileInfo in target:
if hasattr(target[CompileInfo], "arguments"):
srcs = target[CompileInfo].arguments.keys()
all_files += srcs
compilation_context = target[CcInfo].compilation_context
if SourceFilesInfo in target:
if (hasattr(target[SourceFilesInfo], "transitive_source_files")
and hasattr(target[SourceFilesInfo], "headers")):
srcs = target[SourceFilesInfo].transitive_source_files.to_list()
headers = depset(
transitive = [headers, compilation_context.headers],
)
sources_and_headers = all_files + headers.to_list()
return sources_and_headers
transitive = target[SourceFilesInfo].headers.to_list()
).to_list()
all_files += srcs
all_files += headers
return all_files

def _create_wrapper_script(ctx, options, compile_commands_json, config_file):
options_str = ""
Expand All @@ -294,31 +133,41 @@ def _create_wrapper_script(ctx, options, compile_commands_json, config_file):
)

def _per_file_impl(ctx):
compile_commands_json = _compile_commands_impl(ctx)
compile_commands = None
for output in compile_commands_impl(ctx):
if type(output) == "DefaultInfo":
compile_commands = output.files.to_list()[0]
if not compile_commands:
fail("Failed to generate compile_commands.json file!")
if compile_commands != ctx.outputs.compile_commands:
fail("Seems compile_commands.json file is incorrect!")
sources_and_headers = _collect_all_sources_and_headers(ctx)
options = ctx.attr.default_options + ctx.attr.options
all_files = [compile_commands_json]
all_files = [compile_commands]
config_file, env_vars = get_config_file(ctx)
_create_wrapper_script(ctx, options, compile_commands_json, config_file)
_create_wrapper_script(ctx, options, compile_commands, config_file)
for target in ctx.attr.targets:
if not CcInfo in target:
continue
if CompileInfo in target:
if hasattr(target[CompileInfo], "arguments"):
srcs = target[CompileInfo].arguments.keys()
if SourceFilesInfo in target:
if hasattr(target[SourceFilesInfo], "transitive_source_files"):
srcs = target[SourceFilesInfo].transitive_source_files.to_list()
all_files += srcs
compilation_context = target[CcInfo].compilation_context
for src in srcs:
args = target[CompileInfo].arguments[src]
if not check_valid_file_type(src):
continue
args = target[SourceFilesInfo].compilation_db.to_list()
outputs = _run_code_checker(
ctx,
src,
args,
target,
ctx.attr.name,
options,
config_file,
env_vars,
compile_commands_json,
compile_commands,
compilation_context,
sources_and_headers,
)
Expand Down Expand Up @@ -363,7 +212,7 @@ per_file_test = rule(
),
"targets": attr.label_list(
aspects = [
compile_info_aspect,
compile_commands_aspect,
],
doc = "List of compilable targets which should be checked.",
),
Expand All @@ -381,6 +230,7 @@ per_file_test = rule(

},
outputs = {
"compile_commands": "%{name}/compile_commands.json",
"test_script": "%{name}/test_script.sh",
"per_file_script": "%{name}/per_file_script.py",
},
Expand Down
1 change: 0 additions & 1 deletion test/unit/compile_flags/test_compile_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ def test_bazel_test_per_file_filter(self):
compile_commands = os.path.join(
self.BAZEL_BIN_DIR,
"per_file_filter",
"data",
"compile_commands.json",
)
self.assertTrue(os.path.exists(compile_commands))
Expand Down
3 changes: 1 addition & 2 deletions test/unit/external_repository/test_external_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ def test_per_file_external_lib(self):
"""Test: bazel build :per_file_external_deps"""
ret, _, _ = self.run_command(
"bazel build :per_file_external_deps")
# TODO: set to 1, the nothing header should be found
self.assertEqual(ret, 1)
self.assertEqual(ret, 0)


if __name__ == "__main__":
Expand Down
3 changes: 1 addition & 2 deletions test/unit/virtual_include/test_virtual_include.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ def test_bazel_per_file_implementation_deps_virtual_include(self):
ret, _, _ = self.run_command(
"bazel build --experimental_cc_implementation_deps //test/unit/virtual_include:per_file_impl_deps_include"
)
# TODO: change to 0, CodeChecker should finish analysis successfully
self.assertEqual(ret, 1)
self.assertEqual(ret, 0)


if __name__ == "__main__":
Expand Down