Skip to content

Commit

Permalink
Experimental support for setting the '__cplusplus' macro
Browse files Browse the repository at this point in the history
`__cplusplus` is a macro defined by the compiler specifying if C++ is used to compile the file and which C++ standard is used.
`__cplusplus` typically is not passed on the compiler command line as defined value, but set internally by the preprocessor while processing a file.
Thus, DWYU does not know about it when performing the processing to resolve preprocessor statements.

To work around this, DWYU we can set `__cplusplus` based on a heuristic.
This heuristic is for now a best guess and thus the feature is experimental without any guarantee for stability.
  • Loading branch information
martis42 committed Sep 1, 2024
1 parent 33b6e5d commit 7184949
Show file tree
Hide file tree
Showing 19 changed files with 254 additions and 19 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Use DWYU](#use-dwyu)
- [Applying automatic fixes](#applying-automatic-fixes)
- [Assumptions of use](#assumptions-of-use)
- [Known problems](#known-problems)
- [Supported Platforms](#supported-platforms)
- [Alternatives to DWYU](#alternatives-to-dwyu)
- [Versioning](#versioning)
Expand Down Expand Up @@ -169,6 +170,10 @@ For example, including header files which do not exist at the expected path.
There shall not be multiple header files in the dependency tree of a target matching an include statement.
Even if analysing the code works initially, it might break at any time if the ordering of paths in the analysis changes.

# Known problems

TODO add info about compiler internal defines and especially `__cplusplus`

# Supported Platforms

### Aspect
Expand Down
7 changes: 4 additions & 3 deletions docs/dwyu_aspect.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<pre>
load("@depend_on_what_you_use//src/aspect:factory.bzl", "dwyu_aspect_factory")

dwyu_aspect_factory(<a href="#dwyu_aspect_factory-ignored_includes">ignored_includes</a>, <a href="#dwyu_aspect_factory-recursive">recursive</a>, <a href="#dwyu_aspect_factory-skip_external_targets">skip_external_targets</a>, <a href="#dwyu_aspect_factory-skipped_tags">skipped_tags</a>,
<a href="#dwyu_aspect_factory-target_mapping">target_mapping</a>, <a href="#dwyu_aspect_factory-use_implementation_deps">use_implementation_deps</a>, <a href="#dwyu_aspect_factory-verbose">verbose</a>)
dwyu_aspect_factory(<a href="#dwyu_aspect_factory-experimental_set_cplusplus">experimental_set_cplusplus</a>, <a href="#dwyu_aspect_factory-ignored_includes">ignored_includes</a>, <a href="#dwyu_aspect_factory-recursive">recursive</a>, <a href="#dwyu_aspect_factory-skip_external_targets">skip_external_targets</a>,
<a href="#dwyu_aspect_factory-skipped_tags">skipped_tags</a>, <a href="#dwyu_aspect_factory-target_mapping">target_mapping</a>, <a href="#dwyu_aspect_factory-use_implementation_deps">use_implementation_deps</a>, <a href="#dwyu_aspect_factory-verbose">verbose</a>)
</pre>

Create a "Depend on What You Use" (DWYU) aspect.
Expand All @@ -28,7 +28,8 @@ your_dwyu_aspect = dwyu_aspect_factory(<aspect_options>)

| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="dwyu_aspect_factory-ignored_includes"></a>ignored_includes | By default, DWYU ignores all headers from the standard library when comparing include statements to the dependencies. This list of headers can be seen in [std_header.py](/src/analyze_includes/std_header.py).<br> You can extend this list of ignored headers or replace it with a custom one by providing a json file with the information to this attribute.<br> Specification of possible files in the json file: <ul><li> `ignore_include_paths` : List of include paths which are ignored by the analysis. Setting this **disables ignoring the standard library include paths**. </li><li> `extra_ignore_include_paths` : List of concrete include paths which are ignored by the analysis. Those are always ignored, no matter what other fields you provide. </li><li> `ignore_include_patterns` : List of patterns for include paths which are ignored by the analysis. Patterns have to be compatible to Python [regex syntax](https://docs.python.org/3/library/re.html#regular-expression-syntax). The [match](https://docs.python.org/3/library/re.html#re.match) function is used to process the patterns. </li></ul> This feature is demonstrated in the [ignoring_includes example](/examples/ignoring_includes). | `None` |
| <a id="dwyu_aspect_factory-experimental_set_cplusplus"></a>experimental_set_cplusplus | **Experimental** feature whose behavior is not yet stable and an change at any time.<br> `__cplusplus` is a macro defined by the compiler specifying if C++ is used to compile the file and which C++ standard is used. It is comon to use preprocessor conditionals to change what code is used based on this.<br> DWYU cannot treat this like other preprocessor defines, as this is often not coming from the user or the Bazel C++ toolchain. The compiler itself defines the value for and sets it internally during preprocessing `__cplusplus`.<br> This option enables a heuristic to set `__cplusplus` for the preprocessor used internally by DWYU: <ul><li> If at least one source file is not using file extension [`.c`, `.h`], set `__cplusplus` to 1. </li><li> If a common compiler option is used to set the C++ standard with an unknown value, set `__cplusplus` to 1. </li><li> If a common compiler option is used to set the C++ standard with an known value, set `__cplusplus` according to [this map](https://en.cppreference.com/w/cpp/preprocessor/replace#Predefined_macros). </li></ul> This feature is demonstrated in the [set_cpp_standard example](/examples/set_cpp_standard). | `False` |
| <a id="dwyu_aspect_factory-ignored_includes"></a>ignored_includes | By default, DWYU ignores all headers from the standard library when comparing include statements to the dependencies. This list of headers can be seen in [std_header.py](/src/analyze_includes/std_header.py).<br> You can extend this list of ignored headers or replace it with a custom one by providing a json file with the information to this attribute.<br> Specification of possible files in the json file: <ul><li> `ignore_include_paths` : List of include paths which are ignored by the analysis. Setting this **disables ignoring the standard library include paths**. </li><li> `extra_ignore_include_paths` : List of concrete include paths which are ignored by the analysis. Those are always ignored, no matter what other fields you provide. </li><li> `ignore_include_patterns` : List of patterns for include paths which are ignored by the analysis. Patterns have to be compatible to Python [regex syntax](https://docs.python.org/3/library/re.html#regular-expression-syntax). The [match](https://docs.python.org/3/library/re.html#re.match) function is used to process the patterns. </li></ul> This feature is demonstrated in the [ignoring_includes example](/examples/ignoring_includes). | `None` |
| <a id="dwyu_aspect_factory-recursive"></a>recursive | By default, the DWYU aspect analyzes only the target it is being applied to. You can change this to recursively analyzing dependencies following the `deps` and `implementation_deps` attributes by setting this to True.<br> This feature is demonstrated in the [recursion example](/examples/recursion). | `False` |
| <a id="dwyu_aspect_factory-skip_external_targets"></a>skip_external_targets | Sometimes external dependencies are not our under control and thus analyzing them is of little value. If this flag is True, DWYU will automatically skip all targets from external workspaces. This can be useful in combination with the recursive analysis mode.<br> This feature is demonstrated in the [skipping_targets example](/examples/skipping_targets). | `False` |
| <a id="dwyu_aspect_factory-skipped_tags"></a>skipped_tags | Do not execute the DWYU analysis on targets with at least one of those tags. By default skips the analysis for targets tagged with 'no-dwyu'.<br> This feature is demonstrated in the [skipping_targets example](/examples/skipping_targets). | `["no-dwyu"]` |
Expand Down
1 change: 1 addition & 0 deletions examples/aspect.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dwyu = dwyu_aspect_factory(use_implementation_deps = True)
dwyu_recursive = dwyu_aspect_factory(recursive = True)
dwyu_recursive_skip_external = dwyu_aspect_factory(recursive = True, skip_external_targets = True)
dwyu_custom_skipping = dwyu_aspect_factory(skipped_tags = ["my_tag"])
dwyu_set_cplusplus = dwyu_aspect_factory(experimental_set_cplusplus = True)

# We need to explicitly pass labels as passing strings does not work with a bzlmod setup.
dwyu_ignoring_includes = dwyu_aspect_factory(ignored_includes = Label("@//ignoring_includes:ignore_includes.json"))
Expand Down
11 changes: 11 additions & 0 deletions examples/set_cpp_standard/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cc_library(
name = "cpp_lib",
srcs = ["cpp_lib.cpp"],
hdrs = ["cpp_lib.h"],
)

cc_library(
name = "use_specific_cpp_standard",
hdrs = ["use_specific_cpp_standard.h"],
copts = ["-std=c++17"],
)
14 changes: 14 additions & 0 deletions examples/set_cpp_standard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
**Beware, this is an experimental feature which has not yet a stable behavior!**

`__cplusplus` is a macro defined by the compiler specifying if C++ is used to compile the file and which C++ standard is used.
`__cplusplus` typically is not passed on the compiler command line as defined value, but set internally by the preprocessor while processing a file.
Thus, DWYU does not know about it when performing the processing to resolve preprocessor statements.
To work around this, DWYU we can set `__cplusplus` based on a heuristic.

Executing <br>
`bazel build --aspects=//:aspect.bzl%dwyu_set_cplusplus --output_groups=dwyu //set_cpp_standard:cpp_lib` <br>
showcases that DWYU can process `__cplusplus` based preprocessor statements with a heuristic.

Executing <br>
`bazel build --aspects=//:aspect.bzl%dwyu_set_cplusplus --output_groups=dwyu //set_cpp_standard:use_specific_cpp_standard` <br>
showcases that checking for a specific C++ standard works as long as the compiler command specifies the desired C++ standard.
5 changes: 5 additions & 0 deletions examples/set_cpp_standard/cpp_lib.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "set_cpp_standard/cpp_lib.h"

void someFunction() {
// Do something
}
7 changes: 7 additions & 0 deletions examples/set_cpp_standard/cpp_lib.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#ifndef __cplusplus

#include "not/existing/dep.h"

#endif

void someFunction();
5 changes: 5 additions & 0 deletions examples/set_cpp_standard/use_specific_cpp_standard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#if __cplusplus != 201703

#include "not/existing/dep.h"

#endif
8 changes: 8 additions & 0 deletions examples/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ class Example:
build_cmd="//rule_using_dwyu:dwyu",
expected_success=False,
),
Example(
build_cmd="--aspects=//:aspect.bzl%dwyu_set_cplusplus --output_groups=dwyu //set_cpp_standard:cpp_lib",
expected_success=True,
),
Example(
build_cmd="--aspects=//:aspect.bzl%dwyu_set_cplusplus --output_groups=dwyu //set_cpp_standard:use_specific_cpp_standard",
expected_success=True,
),
Example(
build_cmd="--aspects=//:aspect.bzl%dwyu --output_groups=dwyu //skipping_targets:bad_target",
expected_success=False,
Expand Down
109 changes: 95 additions & 14 deletions src/aspect/dwyu.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@ load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
load("@depend_on_what_you_use//src/cc_info_mapping:providers.bzl", "DwyuCcInfoRemappingsInfo")
load("@rules_cc//cc:defs.bzl", "CcInfo", "cc_common")

# Based on those references:
# https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html
# https://clang.llvm.org/cxx_status.html
# https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html
# https://learn.microsoft.com/en-us/cpp/build/reference/std-specify-language-standard-version?view=msvc-170
#
# We ignore the fuzzy variants (e.g. 'c++1X' or 'c++2X') as their values seem to be not constant but depend on the
# compiler version.
_STD_FLAG_TO_STANDARD = {
"c++03": 199711,
"c++11": 201103,
"c++14": 201402,
"c++17": 201703,
"c++20": 202002,
"c++23": 202302,
"c++98": 199711,
"gnu++03": 199711,
"gnu++11": 201103,
"gnu++14": 201402,
"gnu++17": 201703,
"gnu++20": 202002,
"gnu++23": 202302,
"gnu++98": 199711,
}

def _is_external(ctx):
return ctx.label.workspace_root.startswith("external")

Expand Down Expand Up @@ -66,17 +91,6 @@ def _process_dependencies(ctx, target, deps, verbose):
) for dep in deps]

def extract_defines_from_compiler_flags(compiler_flags):
"""
We extract the relevant defines from the compiler command line flags. We utilize the compiler flags since the
toolchain can set defines which are not available through CcInfo or the cpp fragments. Furthermore, defines
potentially overwrite or deactivate each other depending on the order in which they appear in the compiler
command. Thus, this is the only way to make sure DWYU analyzes what would actually happen during compilation.
Args:
compiler_flags: List of flags making up the compilation command
Returns:
List of defines
"""
defines = {}

for cflag in compiler_flags:
Expand All @@ -95,7 +109,45 @@ def extract_defines_from_compiler_flags(compiler_flags):

return defines.values()

def _gather_defines(ctx, target_compilation_context):
def extract_cpp_standard_from_compiler_flags(compiler_flags):
cpp_standard = None

for cflag in compiler_flags:
standard_value = None

# gcc/clang
if cflag.startswith("-std="):
standard_value = cflag.split("=", 1)[1]

# MSVC
if cflag.startswith("/std:"):
standard_value = cflag.split(":", 1)[1]

if standard_value:
standard = _STD_FLAG_TO_STANDARD.get(standard_value, None)
if standard:
cpp_standard = standard
elif not cpp_standard:
# We see a standard flag, but don't understands its value. We ensure the C++ standard macro is defined
# Even if we can't define its exact value.
cpp_standard = 1

return cpp_standard

def _is_c_file(file):
"""
Heuristic for finding C files by looking for the established file extensions
"""
return file.extension in ["h", "c"]

def _gather_defines(ctx, target_compilation_context, target_files):
"""
We extract the relevant defines from the compiler command line flags. We utilize the compiler flags since the
toolchain can set defines which are not available through CcInfo or the cpp fragments. Furthermore, defines
potentially overwrite or deactivate each other depending on the order in which they appear in the compiler
command. Thus, this is the only way to make sure DWYU analyzes what would actually happen during compilation.
"""

cc_toolchain = find_cpp_toolchain(ctx)

feature_configuration = cc_common.configure_features(
Expand All @@ -104,6 +156,7 @@ def _gather_defines(ctx, target_compilation_context):
requested_features = ctx.features,
unsupported_features = ctx.disabled_features,
)

compile_variables = cc_common.create_compile_variables(
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
Expand All @@ -121,7 +174,30 @@ def _gather_defines(ctx, target_compilation_context):
variables = compile_variables,
)

return extract_defines_from_compiler_flags(compiler_command_line_flags)
defines = extract_defines_from_compiler_flags(compiler_command_line_flags)

if ctx.attr._set_cplusplus:
cpp_standard = None
compiler_cpp_standard = extract_cpp_standard_from_compiler_flags(compiler_command_line_flags)

# If we can extract a c++ standard from the compiler invocation, we use it as we consider this our most reliable
# source of information
if compiler_cpp_standard:
cpp_standard = compiler_cpp_standard

# If we could not determine the C++ standard based on the compiler arguments, we use a heuristic based on the
# file types. If any file extension other than ['.h', '.c'] is used, we assume C++.
if not cpp_standard:
# We don't know the exact C++ standard, but at least we can enable preprocessor control statements caring
# only about '__cplusplus' being defined at all or not.
if not all([_is_c_file(file) for file in target_files]):
cpp_standard = 1

# If we assume this is a C++ compilation, add the corresponding constant to the defines list
if cpp_standard:
defines.append("__cplusplus={}".format(cpp_standard))

return defines

def _exchange_cc_info(deps, mapping):
transformed = []
Expand Down Expand Up @@ -214,10 +290,15 @@ def dwyu_aspect_impl(target, ctx):
if not public_files and not private_files:
return []

defines = _gather_defines(
ctx,
target_compilation_context = target[CcInfo].compilation_context,
target_files = public_files + private_files,
)
processed_target = _process_target(
ctx,
target = struct(label = target.label, cc_info = target[CcInfo]),
defines = _gather_defines(ctx, target_compilation_context = target[CcInfo].compilation_context),
defines = defines,
output_path = "{}_processed_target_under_inspection.json".format(target.label.name),
is_target_under_inspection = True,
verbose = ctx.attr._verbose,
Expand Down
Loading

0 comments on commit 7184949

Please sign in to comment.