Hi all.

The other day I noticed this in the Code Style Guidelines
(https://webkit.org/code-style-guidelines/#include-statements):

> Header files should never include config.h.

Is there a rationale for it?

config.h is a header file that pulls the compilation flags set in CMake,
some macro functions to query them plus some defaults for specific
platforms (e.g. if platform is Cocoa and WebRTC is enabled,
USE_LIBWEBRTC is defined automatically in WTF/wtf/Platform.h).

As of now, there are lots of instances of header files where macros such
as USE() or ENABLED() are used, yet config.h is not included (even
transitively) by that header file, thus breaking the "include what you
use" rule.

This is not a big deal for the compiler, as every .cpp file in the
project includes config.h before any other header, so by the time those
usages are really compiled those macros will be already defined.

On the other hand, static analysis tools, such as those in IDEs may not
be able to notice this. Unlike compilers, editors and human programmers
often treat files as independent units. For instance, in an editor you
open "LibWebRTCAudioCaptureSource.h", not "LibWebRTCAudioCaptureSource.h
as included by LibWebRTCAudioCaptureSource.cpp in line 28".

Ultimately this can be blamed to C++ fault because includes are a really
sloppy way to require dependencies, but I think we can agree that we
want headers to work as if they were independent units most of the time.

I've seen this issue manifest in QtCreator, as the IDE grays out all the
code inside #if USE() blocks as these macros are not defined or the
expressions inside evaluate to false in the context of that header file.

As an experiment to improve this situation, I first added #pragma once
in config.h files so that they can be included by any header file. Then,
I wrote a Python script that finds all usages of USE() and ENABLED()
macros in header files and adds a #include "config.h" where required.
Objective C headers are ignored by the script using some heuristics.

I built webkitgtk successfully both with and without the patch in gcc
and did not found any noticeable increase in compilation time: the
#pragma once guard is very fast, as expected, so that cannot be ruled as
a reason against. The result is the same but with better static analysis.

As the guidelines are right now I can't get this patch upstream because
they forbid from including config.h... but at least I would like to know
if there a compelling reason for them doing so.

(I attached the patch for the script and a few small edits needed so
that the includes work in all compilation contexts. More edits may be
needed in order for it to build in other platforms, but I could not test
that.)

Regards,
Alicia.
diff --git a/Source/JavaScriptCore/config.h b/Source/JavaScriptCore/config.h
index 1bd9d09741b..2203f266085 100644
--- a/Source/JavaScriptCore/config.h
+++ b/Source/JavaScriptCore/config.h
@@ -19,11 +19,13 @@
  *
  */
 
+#pragma once
+
 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE)
 #include "cmakeconfig.h"
 #endif
 
-#include "JSExportMacros.h"
+#include "runtime/JSExportMacros.h"
 
 #ifdef __cplusplus
 #undef new
diff --git a/Source/WTF/config.h b/Source/WTF/config.h
index ddc4f102190..58eeb534544 100644
--- a/Source/WTF/config.h
+++ b/Source/WTF/config.h
@@ -19,6 +19,8 @@
  *
  */
 
+#pragma once
+
 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE)
 #include "cmakeconfig.h"
 #endif
diff --git a/Source/WTF/wtf/glib/GTypedefs.h b/Source/WTF/wtf/glib/GTypedefs.h
index 51053c03d86..3f0610056b0 100644
--- a/Source/WTF/wtf/glib/GTypedefs.h
+++ b/Source/WTF/wtf/glib/GTypedefs.h
@@ -20,6 +20,11 @@
 #ifndef WTF_GTypedefs_h
 #define WTF_GTypedefs_h
 
+/* config.h cannot be included here from all contexts.
+ * For instance, this file is included transitively from ThirdParty/gtest/src/gtest.cc,
+ * which is compiled without any "config.h" in path. */
+#include <wtf/Platform.h>
+
 /* Vanilla C code does not seem to be able to handle forward-declaration typedefs. */
 #ifdef  __cplusplus
 
diff --git a/Tools/Scripts/include-config-where-needed.py b/Tools/Scripts/include-config-where-needed.py
new file mode 100755
index 00000000000..fa7ed651df0
--- /dev/null
+++ b/Tools/Scripts/include-config-where-needed.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+import os
+import re
+import sys
+
+re_is_guard = re.compile(r'#if.*[^\w](ENABLE|USE)\s*\(')
+re_is_include = re.compile(r'^\s*#\s*include\s*[<"]config.h[>"]\s*')
+re_is_pragma_once = re.compile(r'^\s*#\s*pragma\s+once\s*')
+re_is_ifndef = re.compile(r'^s*#\s*ifndef\s.*')
+re_is_define = re.compile(r'^s*#\s*define\s.*')
+re_is_objc_import = re.compile('^s*#\s*import.*')
+re_is_objc_keyword = re.compile('^s*@\s*(class|interface|property|end)\s.*')
+
+
+def find_pragma_once_line(lines):
+    return next((
+        n_line
+        for n_line, line in enumerate(lines)
+        if re_is_pragma_once.match(line)
+    ), None)
+
+
+def find_traditional_header_guard(lines):
+    ifndef_line = next((
+        n_line
+        for n_line, line in enumerate(lines)
+        if re_is_ifndef.match(line)
+    ), None)
+
+    if ifndef_line is None:
+        return None
+
+    define_line = next((
+        n_line
+        for n_line, line in enumerate(lines)
+        if n_line > ifndef_line and re_is_define.match(line)
+    ), None)
+    return define_line
+
+
+def find_line_to_add_include_config(lines):
+    pragma_once_line = find_pragma_once_line(lines)
+    if pragma_once_line is not None:
+        return pragma_once_line
+    else:
+        return find_traditional_header_guard(lines)
+
+
+def is_objc_header(lines):
+    return any(
+        re_is_objc_import.match(line) or re_is_objc_keyword.match(line)
+        for line in lines
+    )
+
+
+class CouldNotFindWhereToInsertInclude(Exception):
+    def __init__(self, file_path):
+        super(CouldNotFindWhereToInsertInclude, self).__init__(
+            'Could not find #pragma once or include guard in "%s"' % file_path)
+
+
+def process_file(file_path):
+    with open(file_path, "r") as f:
+        lines = f.read().split("\n")
+
+    if is_objc_header(lines):
+        return
+
+    has_guards = any(
+        re_is_guard.match(line)
+        for line in lines
+    )
+
+    has_include = any(
+        re_is_include.match(line)
+        for line in lines
+    )
+
+    if has_guards and not has_include:
+        # Add the include the line after #pragma once
+        include_after_line = find_line_to_add_include_config(lines)
+        if include_after_line is None:
+            raise CouldNotFindWhereToInsertInclude(file_path)
+
+        lines[include_after_line + 1:include_after_line + 1] = ['', '#include "config.h"']
+
+        # Ensure there is an empty line after the new include in order to
+        # avoid merge conflicts
+        if lines[include_after_line + 3].strip() != "":
+            lines.insert(include_after_line + 3, "")
+
+        # Write the changes
+        with open(file_path, "w") as f:
+            f.write("\n".join(lines))
+
+        return True
+
+def main():
+    base_path = os.path.dirname(__file__) + "/../../Source"
+    modified_files = 0
+    total_files = 0
+    failed_files = []
+
+    ignored_dirs = [
+        "ThirdParty",
+    ]
+
+    ignored_files = [
+        "Platform.h",  # USE() and ENABLED() macros are defined here
+        "FeatureDefines.h",
+        "ExportMacros.h",
+        "JSExportMacros.h",
+        "Assertions.h",
+        "GTypedefs.h",
+    ]
+
+    for dir_path, dir_names, file_names in os.walk(base_path):
+        for ignored_dir_name in ignored_dirs:
+            if ignored_dir_name in dir_names:
+                dir_names.remove(ignored_dir_name)
+
+        for file_name in file_names:
+            if file_name.endswith(".h") and file_name not in ignored_files:
+                try:
+                    did_modify_file = process_file(os.path.join(dir_path, file_name))
+                    if did_modify_file:
+                        modified_files += 1
+                except CouldNotFindWhereToInsertInclude:
+                    failed_files.append(os.path.join(dir_path, file_name))
+                total_files += 1
+
+    print("Modified %d/%d header files." % (modified_files, total_files))
+
+    if len(failed_files) > 0:
+        print("Failed at %d files because they don't use #pragma once or an include guard:" % len(failed_files))
+        print("\n".join(failed_files))
+
+
+if __name__ == '__main__':
+    main()
_______________________________________________
webkit-dev mailing list
webkit-dev@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-dev

Reply via email to