[include-what-you-use] Initial tooling (#36508)
This PR introduces the basic tooling to have `include-what-you-use` run against the brave codebase in maintainance mode. This manual regular runs that cover more and more of the codebase. Bug: https://github.com/brave/brave-browser/issues/42212
This commit is contained in:
@@ -473,6 +473,30 @@ def CheckPlasterFiles(input_api, output_api):
|
||||
])
|
||||
|
||||
|
||||
def CheckJson5ParseErrors(input_api, output_api):
|
||||
"""Check that .json5 files parse without syntax errors.
|
||||
|
||||
Upstream `CheckParseErrors` covers `.idl` and `.json` but not `.json5`.
|
||||
`json5.loads` natively accepts `//` and `/* */` comments, trailing
|
||||
commas, and unquoted keys, so no comment-stripping pre-pass is needed.
|
||||
"""
|
||||
import json5
|
||||
|
||||
def _is_json5(affected_file):
|
||||
return affected_file.LocalPath().endswith('.json5')
|
||||
|
||||
results = []
|
||||
for affected_file in input_api.AffectedFiles(file_filter=_is_json5,
|
||||
include_deletes=False):
|
||||
try:
|
||||
json5.loads(input_api.ReadFile(affected_file))
|
||||
except ValueError as e:
|
||||
results.append(
|
||||
output_api.PresubmitError(
|
||||
f'{affected_file.LocalPath()} could not be parsed: {e}'))
|
||||
return results
|
||||
|
||||
|
||||
# DON'T ADD NEW BRAVE CHECKS AFTER THIS LINE.
|
||||
#
|
||||
# This call inlines Chromium checks into current scope from src/PRESUBMIT.py. We
|
||||
|
||||
@@ -92,5 +92,65 @@ class CheckTypeScriptSuppressionsHaveReasonsTest(unittest.TestCase):
|
||||
self.assertEqual(0, len(errors))
|
||||
|
||||
|
||||
class CheckJson5ParseErrorsTest(unittest.TestCase):
|
||||
|
||||
def testAcceptsJson5WithCommentsAndTrailingCommas(self):
|
||||
input_api = MockInputApi()
|
||||
input_api.files = [
|
||||
MockAffectedFile('brave/build/mappings.json5', [
|
||||
'// libc++ private headers -> public facades',
|
||||
'{',
|
||||
' "include": [',
|
||||
' [ "<__algorithm/sort.h>", "private",',
|
||||
' "<algorithm>", "public" ], /* trailing comma OK */',
|
||||
' ],',
|
||||
'}',
|
||||
]),
|
||||
]
|
||||
|
||||
errors = PRESUBMIT.CheckJson5ParseErrors(input_api, MockOutputApi())
|
||||
|
||||
self.assertEqual([], errors)
|
||||
|
||||
def testReportsInvalidJson5(self):
|
||||
input_api = MockInputApi()
|
||||
input_api.files = [
|
||||
MockAffectedFile('brave/build/broken.json5', [
|
||||
'{',
|
||||
' "key": "value"',
|
||||
' "missing_comma": true',
|
||||
'}',
|
||||
]),
|
||||
]
|
||||
|
||||
errors = PRESUBMIT.CheckJson5ParseErrors(input_api, MockOutputApi())
|
||||
|
||||
self.assertEqual(1, len(errors))
|
||||
self.assertIn('brave/build/broken.json5 could not be parsed',
|
||||
errors[0].message)
|
||||
|
||||
def testIgnoresNonJson5Files(self):
|
||||
input_api = MockInputApi()
|
||||
input_api.files = [
|
||||
MockAffectedFile('brave/build/mappings.json',
|
||||
['{ "not-json5": true,, }']),
|
||||
MockAffectedFile('brave/foo.ts', ['const x = 1;']),
|
||||
]
|
||||
|
||||
errors = PRESUBMIT.CheckJson5ParseErrors(input_api, MockOutputApi())
|
||||
|
||||
self.assertEqual([], errors)
|
||||
|
||||
def testIgnoresDeletedFiles(self):
|
||||
input_api = MockInputApi()
|
||||
input_api.files = [
|
||||
MockAffectedFile('brave/build/deleted.json5', [], action='D'),
|
||||
]
|
||||
|
||||
errors = PRESUBMIT.CheckJson5ParseErrors(input_api, MockOutputApi())
|
||||
|
||||
self.assertEqual([], errors)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
// Copyright (c) 2026 The Brave Authors. All rights reserved.
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// IWYU mapping file consumed by brave/tools/cr/iwyu/run_iwyu.py.
|
||||
//
|
||||
// Chromium ships libc++, whose standard headers (`<algorithm>`, `<atomic>`,
|
||||
// ...) are split into many private detail headers under
|
||||
// `third_party/libc++/src/include/__<name>/`. Without these mappings, IWYU
|
||||
// would suggest:
|
||||
//
|
||||
// #include "__algorithm/ranges_sort.h"
|
||||
// #include "__atomic/atomic.h"
|
||||
// #include "__fwd/sstream.h"
|
||||
//
|
||||
// instead of the public-facing:
|
||||
//
|
||||
// #include <algorithm>
|
||||
// #include <atomic>
|
||||
// #include <sstream>
|
||||
//
|
||||
// Each rule uses the `@` wildcard prefix so a single regex pattern remaps
|
||||
// every private header under a `__<name>/` subtree to its public facade.
|
||||
// `__fwd/<x>.h` is the libc++ forward-decl shim for `<x>` and must be
|
||||
// enumerated per-header.
|
||||
//
|
||||
// IMPORTANT: the `@` regex matches the include string *including* its
|
||||
// delimiters. Brave's compile commands produce both angle-bracket
|
||||
// (`<__algorithm/...>`) and quoted (`"__algorithm/..."`) forms depending
|
||||
// on platform / build config, so every rule has both variants.
|
||||
//
|
||||
// See `tools/cr/iwyu/README.md` and IWYU's IWYUMappings.md for the file format.
|
||||
|
||||
[
|
||||
// === Private detail subdirectories -- angle-bracket form ===
|
||||
|
||||
{ 'include': ['@<__algorithm/.*>', 'private', '<algorithm>', 'public'] },
|
||||
{ 'include': ['@<__atomic/.*>', 'private', '<atomic>', 'public'] },
|
||||
{ 'include': ['@<__bit/.*>', 'private', '<bit>', 'public'] },
|
||||
{ 'include': ['@<__charconv/.*>', 'private', '<charconv>', 'public'] },
|
||||
{ 'include': ['@<__chrono/.*>', 'private', '<chrono>', 'public'] },
|
||||
{ 'include': ['@<__compare/.*>', 'private', '<compare>', 'public'] },
|
||||
{ 'include': ['@<__concepts/.*>', 'private', '<concepts>', 'public'] },
|
||||
{
|
||||
'include': [
|
||||
'@<__condition_variable/.*>',
|
||||
'private',
|
||||
'<condition_variable>',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{ 'include': ['@<__coroutine/.*>', 'private', '<coroutine>', 'public'] },
|
||||
{ 'include': ['@<__exception/.*>', 'private', '<exception>', 'public'] },
|
||||
{ 'include': ['@<__expected/.*>', 'private', '<expected>', 'public'] },
|
||||
{ 'include': ['@<__filesystem/.*>', 'private', '<filesystem>', 'public'] },
|
||||
{ 'include': ['@<__format/.*>', 'private', '<format>', 'public'] },
|
||||
{ 'include': ['@<__functional/.*>', 'private', '<functional>', 'public'] },
|
||||
{ 'include': ['@<__ios/.*>', 'private', '<ios>', 'public'] },
|
||||
{ 'include': ['@<__iterator/.*>', 'private', '<iterator>', 'public'] },
|
||||
{ 'include': ['@<__locale/.*>', 'private', '<locale>', 'public'] },
|
||||
{ 'include': ['@<__math/.*>', 'private', '<cmath>', 'public'] },
|
||||
{ 'include': ['@<__memory/.*>', 'private', '<memory>', 'public'] },
|
||||
{
|
||||
'include': [
|
||||
'@<__memory_resource/.*>',
|
||||
'private',
|
||||
'<memory_resource>',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{ 'include': ['@<__mutex/.*>', 'private', '<mutex>', 'public'] },
|
||||
{ 'include': ['@<__new/.*>', 'private', '<new>', 'public'] },
|
||||
{ 'include': ['@<__numeric/.*>', 'private', '<numeric>', 'public'] },
|
||||
{ 'include': ['@<__ostream/.*>', 'private', '<ostream>', 'public'] },
|
||||
{ 'include': ['@<__random/.*>', 'private', '<random>', 'public'] },
|
||||
{ 'include': ['@<__ranges/.*>', 'private', '<ranges>', 'public'] },
|
||||
{ 'include': ['@<__stop_token/.*>', 'private', '<stop_token>', 'public'] },
|
||||
{ 'include': ['@<__string/.*>', 'private', '<string>', 'public'] },
|
||||
{
|
||||
'include': ['@<__system_error/.*>', 'private', '<system_error>', 'public'],
|
||||
},
|
||||
{ 'include': ['@<__thread/.*>', 'private', '<thread>', 'public'] },
|
||||
{ 'include': ['@<__tuple/.*>', 'private', '<tuple>', 'public'] },
|
||||
{ 'include': ['@<__type_traits/.*>', 'private', '<type_traits>', 'public'] },
|
||||
{ 'include': ['@<__utility/.*>', 'private', '<utility>', 'public'] },
|
||||
{ 'include': ['@<__variant/.*>', 'private', '<variant>', 'public'] },
|
||||
{ 'include': ['@<__vector/.*>', 'private', '<vector>', 'public'] },
|
||||
|
||||
// === Private detail subdirectories -- quoted form ===
|
||||
|
||||
{ 'include': ['@"__algorithm/.*"', 'private', '<algorithm>', 'public'] },
|
||||
{ 'include': ['@"__atomic/.*"', 'private', '<atomic>', 'public'] },
|
||||
{ 'include': ['@"__bit/.*"', 'private', '<bit>', 'public'] },
|
||||
{ 'include': ['@"__charconv/.*"', 'private', '<charconv>', 'public'] },
|
||||
{ 'include': ['@"__chrono/.*"', 'private', '<chrono>', 'public'] },
|
||||
{ 'include': ['@"__compare/.*"', 'private', '<compare>', 'public'] },
|
||||
{ 'include': ['@"__concepts/.*"', 'private', '<concepts>', 'public'] },
|
||||
{
|
||||
'include': [
|
||||
'@"__condition_variable/.*"',
|
||||
'private',
|
||||
'<condition_variable>',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{ 'include': ['@"__coroutine/.*"', 'private', '<coroutine>', 'public'] },
|
||||
{ 'include': ['@"__exception/.*"', 'private', '<exception>', 'public'] },
|
||||
{ 'include': ['@"__expected/.*"', 'private', '<expected>', 'public'] },
|
||||
{ 'include': ['@"__filesystem/.*"', 'private', '<filesystem>', 'public'] },
|
||||
{ 'include': ['@"__format/.*"', 'private', '<format>', 'public'] },
|
||||
{ 'include': ['@"__functional/.*"', 'private', '<functional>', 'public'] },
|
||||
{ 'include': ['@"__ios/.*"', 'private', '<ios>', 'public'] },
|
||||
{ 'include': ['@"__iterator/.*"', 'private', '<iterator>', 'public'] },
|
||||
{ 'include': ['@"__locale/.*"', 'private', '<locale>', 'public'] },
|
||||
{ 'include': ['@"__math/.*"', 'private', '<cmath>', 'public'] },
|
||||
{ 'include': ['@"__memory/.*"', 'private', '<memory>', 'public'] },
|
||||
{
|
||||
'include': [
|
||||
'@"__memory_resource/.*"',
|
||||
'private',
|
||||
'<memory_resource>',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{ 'include': ['@"__mutex/.*"', 'private', '<mutex>', 'public'] },
|
||||
{ 'include': ['@"__new/.*"', 'private', '<new>', 'public'] },
|
||||
{ 'include': ['@"__numeric/.*"', 'private', '<numeric>', 'public'] },
|
||||
{ 'include': ['@"__ostream/.*"', 'private', '<ostream>', 'public'] },
|
||||
{ 'include': ['@"__random/.*"', 'private', '<random>', 'public'] },
|
||||
{ 'include': ['@"__ranges/.*"', 'private', '<ranges>', 'public'] },
|
||||
{ 'include': ['@"__stop_token/.*"', 'private', '<stop_token>', 'public'] },
|
||||
{ 'include': ['@"__string/.*"', 'private', '<string>', 'public'] },
|
||||
{
|
||||
'include': ['@"__system_error/.*"', 'private', '<system_error>', 'public'],
|
||||
},
|
||||
{ 'include': ['@"__thread/.*"', 'private', '<thread>', 'public'] },
|
||||
{ 'include': ['@"__tuple/.*"', 'private', '<tuple>', 'public'] },
|
||||
{
|
||||
'include': ['@"__type_traits/.*"', 'private', '<type_traits>', 'public'],
|
||||
},
|
||||
{ 'include': ['@"__utility/.*"', 'private', '<utility>', 'public'] },
|
||||
{ 'include': ['@"__variant/.*"', 'private', '<variant>', 'public'] },
|
||||
{ 'include': ['@"__vector/.*"', 'private', '<vector>', 'public'] },
|
||||
|
||||
// === Forward-decl shims -- angle-bracket form ===
|
||||
// `__fwd/<x>.h` is libc++'s forward-decl-only header for `<x>`.
|
||||
|
||||
{ 'include': ['<__fwd/array.h>', 'private', '<array>', 'public'] },
|
||||
{ 'include': ['<__fwd/byte.h>', 'private', '<cstddef>', 'public'] },
|
||||
{ 'include': ['<__fwd/complex.h>', 'private', '<complex>', 'public'] },
|
||||
{ 'include': ['<__fwd/deque.h>', 'private', '<deque>', 'public'] },
|
||||
{ 'include': ['<__fwd/format.h>', 'private', '<format>', 'public'] },
|
||||
{ 'include': ['<__fwd/fstream.h>', 'private', '<fstream>', 'public'] },
|
||||
{ 'include': ['<__fwd/functional.h>', 'private', '<functional>', 'public'] },
|
||||
{ 'include': ['<__fwd/ios.h>', 'private', '<ios>', 'public'] },
|
||||
{ 'include': ['<__fwd/istream.h>', 'private', '<istream>', 'public'] },
|
||||
{ 'include': ['<__fwd/mdspan.h>', 'private', '<mdspan>', 'public'] },
|
||||
{ 'include': ['<__fwd/memory.h>', 'private', '<memory>', 'public'] },
|
||||
{
|
||||
'include': [
|
||||
'<__fwd/memory_resource.h>',
|
||||
'private',
|
||||
'<memory_resource>',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{ 'include': ['<__fwd/ostream.h>', 'private', '<ostream>', 'public'] },
|
||||
{ 'include': ['<__fwd/pair.h>', 'private', '<utility>', 'public'] },
|
||||
{ 'include': ['<__fwd/queue.h>', 'private', '<queue>', 'public'] },
|
||||
{ 'include': ['<__fwd/span.h>', 'private', '<span>', 'public'] },
|
||||
{ 'include': ['<__fwd/sstream.h>', 'private', '<sstream>', 'public'] },
|
||||
{ 'include': ['<__fwd/stack.h>', 'private', '<stack>', 'public'] },
|
||||
{ 'include': ['<__fwd/streambuf.h>', 'private', '<streambuf>', 'public'] },
|
||||
{ 'include': ['<__fwd/string.h>', 'private', '<string>', 'public'] },
|
||||
{
|
||||
'include': ['<__fwd/string_view.h>', 'private', '<string_view>', 'public'],
|
||||
},
|
||||
{ 'include': ['<__fwd/subrange.h>', 'private', '<ranges>', 'public'] },
|
||||
{ 'include': ['<__fwd/tuple.h>', 'private', '<tuple>', 'public'] },
|
||||
{ 'include': ['<__fwd/variant.h>', 'private', '<variant>', 'public'] },
|
||||
{ 'include': ['<__fwd/vector.h>', 'private', '<vector>', 'public'] },
|
||||
|
||||
// === Forward-decl shims -- quoted form ===
|
||||
|
||||
{ 'include': ['"__fwd/array.h"', 'private', '<array>', 'public'] },
|
||||
{ 'include': ['"__fwd/byte.h"', 'private', '<cstddef>', 'public'] },
|
||||
{ 'include': ['"__fwd/complex.h"', 'private', '<complex>', 'public'] },
|
||||
{ 'include': ['"__fwd/deque.h"', 'private', '<deque>', 'public'] },
|
||||
{ 'include': ['"__fwd/format.h"', 'private', '<format>', 'public'] },
|
||||
{ 'include': ['"__fwd/fstream.h"', 'private', '<fstream>', 'public'] },
|
||||
{
|
||||
'include': ['"__fwd/functional.h"', 'private', '<functional>', 'public'],
|
||||
},
|
||||
{ 'include': ['"__fwd/ios.h"', 'private', '<ios>', 'public'] },
|
||||
{ 'include': ['"__fwd/istream.h"', 'private', '<istream>', 'public'] },
|
||||
{ 'include': ['"__fwd/mdspan.h"', 'private', '<mdspan>', 'public'] },
|
||||
{ 'include': ['"__fwd/memory.h"', 'private', '<memory>', 'public'] },
|
||||
{
|
||||
'include': [
|
||||
'"__fwd/memory_resource.h"',
|
||||
'private',
|
||||
'<memory_resource>',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{ 'include': ['"__fwd/ostream.h"', 'private', '<ostream>', 'public'] },
|
||||
{ 'include': ['"__fwd/pair.h"', 'private', '<utility>', 'public'] },
|
||||
{ 'include': ['"__fwd/queue.h"', 'private', '<queue>', 'public'] },
|
||||
{ 'include': ['"__fwd/span.h"', 'private', '<span>', 'public'] },
|
||||
{ 'include': ['"__fwd/sstream.h"', 'private', '<sstream>', 'public'] },
|
||||
{ 'include': ['"__fwd/stack.h"', 'private', '<stack>', 'public'] },
|
||||
{ 'include': ['"__fwd/streambuf.h"', 'private', '<streambuf>', 'public'] },
|
||||
{ 'include': ['"__fwd/string.h"', 'private', '<string>', 'public'] },
|
||||
{
|
||||
'include': ['"__fwd/string_view.h"', 'private', '<string_view>', 'public'],
|
||||
},
|
||||
{ 'include': ['"__fwd/subrange.h"', 'private', '<ranges>', 'public'] },
|
||||
{ 'include': ['"__fwd/tuple.h"', 'private', '<tuple>', 'public'] },
|
||||
{ 'include': ['"__fwd/variant.h"', 'private', '<variant>', 'public'] },
|
||||
{ 'include': ['"__fwd/vector.h"', 'private', '<vector>', 'public'] },
|
||||
|
||||
// `third_party/jni_zero/jni_zero.h` is Chromium's wrapper over `<jni.h>`
|
||||
// and pulls it in transitively. Anywhere IWYU would suggest the raw
|
||||
// `<jni.h>`, route through jni_zero so files that already include the
|
||||
// wrapper don't pick up a redundant `<jni.h>`.
|
||||
{
|
||||
'include': [
|
||||
'<jni.h>',
|
||||
'private',
|
||||
'"third_party/jni_zero/jni_zero.h"',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
|
||||
// We use a short version for rust's cxx headers
|
||||
{
|
||||
'include': [
|
||||
'"third_party/rust/chromium_crates_io/vendor/cxx-v1/include/cxx.h"',
|
||||
'private',
|
||||
'"third_party/rust/cxx/v1/cxx.h"',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
|
||||
// This one seems to be IWYU doing something weird, as I can get both the
|
||||
// forwarding header and span.h pulled in some cases.
|
||||
{
|
||||
'include': [
|
||||
'"base/containers/span_forward_internal.h"',
|
||||
'private',
|
||||
'"base/containers/span.h"',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
|
||||
// BoringSSL: the project convention is to spell these as
|
||||
// `"openssl/<name>.h"` since `third_party/boringssl:external_config` adds
|
||||
// `src/include` to every consumer's `-I` path (see boringssl/BUILD.gn).
|
||||
// Map the full physical path back to that short form.
|
||||
{
|
||||
'include': [
|
||||
'"third_party/boringssl/src/include/openssl/base.h"',
|
||||
'private',
|
||||
'"openssl/base.h"',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{
|
||||
'include': [
|
||||
'"third_party/boringssl/src/include/openssl/digest.h"',
|
||||
'private',
|
||||
'"openssl/digest.h"',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
|
||||
// Pin the basic integer and size typedefs to their canonical C headers.
|
||||
// `openssl/base.h` re-exports `<stddef.h>` and `<stdint.h>` via
|
||||
// `IWYU pragma: export`, which would otherwise make it a valid provider
|
||||
// for these types and cause IWYU to keep a vestigial `openssl/base.h`
|
||||
// include just to satisfy a `size_t` / `uint8_t` use.
|
||||
{ 'symbol': ['size_t', 'private', '<stddef.h>', 'public'] },
|
||||
{ 'symbol': ['ptrdiff_t', 'private', '<stddef.h>', 'public'] },
|
||||
{ 'symbol': ['int8_t', 'private', '<stdint.h>', 'public'] },
|
||||
{ 'symbol': ['int16_t', 'private', '<stdint.h>', 'public'] },
|
||||
{ 'symbol': ['int32_t', 'private', '<stdint.h>', 'public'] },
|
||||
{ 'symbol': ['int64_t', 'private', '<stdint.h>', 'public'] },
|
||||
{ 'symbol': ['uint8_t', 'private', '<stdint.h>', 'public'] },
|
||||
{ 'symbol': ['uint16_t', 'private', '<stdint.h>', 'public'] },
|
||||
{ 'symbol': ['uint32_t', 'private', '<stdint.h>', 'public'] },
|
||||
{ 'symbol': ['uint64_t', 'private', '<stdint.h>', 'public'] },
|
||||
{
|
||||
'include': [
|
||||
'"ethash/hash_types.h"',
|
||||
'private',
|
||||
'"brave/third_party/ethash/src/include/ethash/hash_types.h"',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
|
||||
// mappings that require further investigation.
|
||||
{
|
||||
'include': [
|
||||
'"gtest/gtest.h"',
|
||||
'private',
|
||||
'"testing/gtest/include/gtest/gtest.h"',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{
|
||||
'include': [
|
||||
'"gmock/gmock.h"',
|
||||
'private',
|
||||
'"testing/gmock/include/gmock/gmock.h"',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{
|
||||
'include': ['"span.h"', 'private', '"base/containers/span.h"', 'public'],
|
||||
},
|
||||
|
||||
// this seems to occur only on windows build
|
||||
{ 'include': ['"map"', 'private', '<map>', 'public'] },
|
||||
{ 'include': ['"tuple"', 'private', '<tuple>', 'public'] },
|
||||
{ 'include': ['"string_view"', 'private', '<string_view>', 'public'] },
|
||||
|
||||
// Using <new> as an include blackhole
|
||||
//
|
||||
// These headers are being blackholed because they are not desired to ever be
|
||||
// shown in our code. They are usually cases where we could add an export
|
||||
// pragma in upstream code, or where include-what-you-use is confused about
|
||||
// things.
|
||||
{ 'include': ['<vcruntime_new.h>', 'private', '<new>', 'public'] },
|
||||
{ 'include': ['<vcruntime_string.h>', 'private', '<new>', 'public'] },
|
||||
{
|
||||
'include': [
|
||||
'"mojo/public/cpp/bindings/struct_ptr.h"',
|
||||
'private',
|
||||
'<new>',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{
|
||||
'include': [
|
||||
'"base/containers/checked_iterators.h"',
|
||||
'private',
|
||||
'<new>',
|
||||
'public',
|
||||
],
|
||||
},
|
||||
{ 'include': ['"base/macros/if.h"', 'private', '<new>', 'public'] },
|
||||
{ 'include': ['"base/macros/is_empty.h"', 'private', '<new>', 'public'] },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) 2026 The Brave Authors. All rights reserved.
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
# All paths listes are relative to `src/`. Use + prefix for the paths you wnat
|
||||
# to include, and - prefix for paths you want to exclude in the inclusions.
|
||||
# For how this file is used, see `tools/cr/iwyu/README.md`.
|
||||
|
||||
+brave/components/brave_ads/browser
|
||||
+brave/components/brave_wallet/common
|
||||
@@ -0,0 +1,102 @@
|
||||
# Brave IWYU tooling
|
||||
|
||||
Two scripts that drive [include-what-you-use](https://include-what-you-use.org/)
|
||||
over Brave sources:
|
||||
|
||||
* `build_iwyu.py` — builds the IWYU binary against Chromium's pinned Clang.
|
||||
* `run_iwyu.py` — runs IWYU against an existing Brave build dir and applies
|
||||
the suggested include fixes in place.
|
||||
|
||||
IWYU is **not** part of the normal Brave build; run these scripts on demand.
|
||||
|
||||
## One-time setup: build IWYU
|
||||
|
||||
```sh
|
||||
vpython3 tools/cr/iwyu/build_iwyu.py
|
||||
```
|
||||
|
||||
Clones LLVM at Chromium's pinned `CLANG_REVISION` and IWYU at the pinned
|
||||
`IWYU_REVISION` into `out/iwyu/`, then builds:
|
||||
|
||||
```
|
||||
out/iwyu/tools/clang/third_party/llvm/build/bin/include-what-you-use
|
||||
```
|
||||
|
||||
Re-run after Chromium's Clang pin moves, or bump `IWYU_REVISION` in
|
||||
`build_iwyu.py` to pick up an upstream IWYU fix.
|
||||
|
||||
## Enable the paths you want IWYU to scan
|
||||
|
||||
Edit `brave/build/include_what_you_use_paths.cfg`. Each non-comment line is:
|
||||
|
||||
```
|
||||
+path/to/enable
|
||||
-path/to/disable
|
||||
```
|
||||
|
||||
Paths are relative to `src/`. Longest-matching prefix wins, so you can
|
||||
enable a directory and carve exceptions out of it:
|
||||
|
||||
```
|
||||
+brave/components/brave_ads/browser
|
||||
-brave/components/brave_ads/browser/third_party
|
||||
```
|
||||
|
||||
## Run IWYU
|
||||
|
||||
IWYU needs an existing Brave build in some `out/<config>` to pick up its
|
||||
compile flags and generated headers, **and** several GN args have to be
|
||||
flipped — without them IWYU crashes during analysis:
|
||||
|
||||
```sh
|
||||
npm run build -- Static \
|
||||
--target_os=android \
|
||||
--ignore_compile_failure \
|
||||
--target=brave:all \
|
||||
--gn=clang_use_chrome_plugins:false \
|
||||
--gn=force_enable_raw_ptr_exclusion:true \
|
||||
--gn=enable_precompiled_headers:false \
|
||||
--gn=treat_warnings_as_errors:false \
|
||||
--gn=clang_warning_suppression_file:"" \
|
||||
--gn=symbol_level:0
|
||||
```
|
||||
|
||||
Required (IWYU crashes without these):
|
||||
|
||||
* `clang_use_chrome_plugins:false`.
|
||||
* `force_enable_raw_ptr_exclusion:true`
|
||||
* `enable_precompiled_headers:false`
|
||||
* `treat_warnings_as_errors:false`
|
||||
|
||||
Then finally run `include-what-you-use` with `run_iwyu.py`, bearing in mind that `--out` is relative to `src/`, rather than the current directory.
|
||||
|
||||
```sh
|
||||
vpython3 tools/cr/iwyu/run_iwyu.py --out out/Static --verbose
|
||||
```
|
||||
|
||||
This:
|
||||
|
||||
1. Generates a compile database from `out/<config>` and filters it to
|
||||
sources enabled by `include_what_you_use_paths.cfg`.
|
||||
2. Runs `iwyu_tool.py` with our libc++ mapping file
|
||||
(`brave/build/include_what_you_use_mappings.json5`).
|
||||
3. Applies suggestions in place via `fix_includes.py`.
|
||||
4. Strips blackholed includes (e.g. `<new>`) from modified files.
|
||||
5. Runs `npm run format` so the resulting diff matches Brave's style.
|
||||
|
||||
Artifacts written next to the build:
|
||||
|
||||
* `out/<config>/iwyu_compile_commands.json` — filtered compile DB.
|
||||
* `out/<config>/iwyu_suggestions.txt` — raw IWYU output, handy when a
|
||||
rewrite looks wrong.
|
||||
|
||||
## Defending an include from IWYU
|
||||
|
||||
If IWYU keeps removing an include you need, mark it:
|
||||
|
||||
```c++
|
||||
#include "foo/bar.h" // IWYU pragma: keep
|
||||
```
|
||||
|
||||
`run_iwyu.py` leaves any line containing `IWYU pragma:` untouched, including
|
||||
during the blackhole-includes pass.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) 2026 The Brave Authors. All rights reserved.
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
"""Import this to add the parent tools/cr to sys.path."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_cr = str(Path(__file__).parent.parent)
|
||||
if _cr not in sys.path:
|
||||
sys.path.insert(0, _cr)
|
||||
Executable
+216
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env vpython3
|
||||
# Copyright (c) 2026 The Brave Authors. All rights reserved.
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
"""Build `include-what-you-use` against Chromium's pinned LLVM revision.
|
||||
|
||||
Keep this script standalone, with no dependencies on other local python sources
|
||||
in tools/cr, as we may want to run the build in CI, in the future, using only
|
||||
a vanilla Chromium checkout.
|
||||
|
||||
What this script does:
|
||||
|
||||
1. Clones the LLVM monorepo into `out/iwyu/tools/clang/third_party/llvm`,
|
||||
pinned to `CLANG_REVISION` from `tools/clang/scripts/update.py`. This
|
||||
matches the revision Chromium's bundled Clang is built from, so the
|
||||
resulting IWYU binary speaks the same Clang AST as the rest of the
|
||||
toolchain.
|
||||
2. Clones include-what-you-use into
|
||||
`out/iwyu/tools/clang/third_party/iwyu`, alongside the LLVM checkout
|
||||
(pinned to the `IWYU_REVISION` constant below).
|
||||
3. Configures a CMake build that pulls IWYU in as an LLVM external project
|
||||
(`LLVM_EXTERNAL_PROJECTS=iwyu` +
|
||||
`LLVM_EXTERNAL_IWYU_SOURCE_DIR=…/iwyu`). Building IWYU as part of the
|
||||
LLVM build avoids the standalone-build resource-dir gymnastics
|
||||
(see IWYU README's "How to install" section).
|
||||
4. Invokes `ninja include-what-you-use` inside the LLVM build tree.
|
||||
|
||||
The resulting binary lands at:
|
||||
|
||||
out/iwyu/tools/clang/third_party/llvm/build/bin/include-what-you-use
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path, PurePath
|
||||
|
||||
CHROMIUM_SRC: Path = Path(__file__).resolve().parents[4]
|
||||
|
||||
# Importing clang toolchain helper from Chromium.
|
||||
sys.path.append(str(CHROMIUM_SRC / 'tools' / 'clang' / 'scripts'))
|
||||
|
||||
# Static analysers can't see through the sys.path mutation above.
|
||||
# pylint: disable=wrong-import-position
|
||||
from build import AddCMakeToPath, CheckoutGitRepo, LLVM_GIT_URL # type: ignore # noqa: E402
|
||||
from update import CLANG_REVISION # type: ignore # noqa: E402
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
# The repo to clone from for the build.
|
||||
IWYU_GIT_URL = ('https://github.com/include-what-you-use/'
|
||||
'include-what-you-use.git')
|
||||
|
||||
# Pinned IWYU revision. IWYU master tracks LLVM main; we pin to a specific
|
||||
# commit that compiles against the Clang revision Chromium has currently
|
||||
# pinned (`update.CLANG_REVISION`). Bump when bumping Chromium's Clang.
|
||||
#
|
||||
# `ece9edb` is the commit immediately before
|
||||
# c36eacb "[clang compat] Handle new HLSL builtin trait", which references
|
||||
# `clang::TypeTrait::UTT_IsConstantBufferElementCompatible` -- a symbol that
|
||||
# doesn't exist in `llvmorg-23-init-5669-g8a0be0bc` (our current pin).
|
||||
IWYU_REVISION = 'ece9edb'
|
||||
|
||||
# Constants for the the directory layout of where these tools live under.
|
||||
THIRD_PARTY_REL = PurePath('tools') / 'clang' / 'third_party'
|
||||
LLVM_REL = THIRD_PARTY_REL / 'llvm'
|
||||
IWYU_REL = THIRD_PARTY_REL / 'iwyu'
|
||||
BUILD_REL = LLVM_REL / 'build'
|
||||
|
||||
# We always build it at `out/iwyu` in Chromium.
|
||||
OUT_DIR: Path = CHROMIUM_SRC / 'out' / 'iwyu'
|
||||
|
||||
|
||||
def _check_call(*command, cwd=None):
|
||||
"""Run *command* as a subprocess, logging the invocation.
|
||||
|
||||
Stdout and stderr are inherited from this process so the user sees ninja
|
||||
progress and compiler diagnostics live -- important for long-running
|
||||
cmake/ninja invocations where errors otherwise scroll past or get lost.
|
||||
"""
|
||||
logging.info(' >>>> %s', ' '.join(str(a) for a in command))
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
# Resolve to an absolute path so .bat shims (e.g. `cmake.bat`) are
|
||||
# found without `shell=True`.
|
||||
resolved = shutil.which(command[0])
|
||||
if resolved is None:
|
||||
raise RuntimeError(f'Command not found: {command[0]}')
|
||||
if resolved != command[0]:
|
||||
command = [resolved] + list(command[1:])
|
||||
|
||||
subprocess.run(command, cwd=cwd, check=True)
|
||||
|
||||
|
||||
class IwyuBuilder:
|
||||
"""Clone LLVM + IWYU and build the IWYU binary against pinned Clang.
|
||||
|
||||
Three phases:
|
||||
|
||||
1. **Checkout** (`_checkout_llvm`, `_checkout_iwyu`): clones (or updates)
|
||||
LLVM and IWYU at the requested revisions into the layout described
|
||||
in the module docstring. Uses `build.CheckoutGitRepo` from the
|
||||
Chromium clang scripts, which idempotently fetches/updates an
|
||||
existing clone or re-clones from scratch if the working tree is
|
||||
broken.
|
||||
|
||||
2. **Configure** (`_configure`): runs `cmake -GNinja` against
|
||||
`<llvm-clone>/llvm/CMakeLists.txt` with IWYU wired in as an LLVM
|
||||
external project. Build dir lives at `<llvm-clone>/build/`, matching
|
||||
`build_clang_tools_extra.py`.
|
||||
|
||||
3. **Build** (`_build`): `ninja <targets>` (default:
|
||||
`include-what-you-use`).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.llvm_dir: Path = OUT_DIR / LLVM_REL
|
||||
self.iwyu_dir: Path = OUT_DIR / IWYU_REL
|
||||
self.build_dir: Path = OUT_DIR / BUILD_REL
|
||||
|
||||
def _checkout_llvm(self):
|
||||
"""Clone or update LLVM at the pinned Chromium Clang revision."""
|
||||
self.llvm_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
CheckoutGitRepo('LLVM monorepo', LLVM_GIT_URL, CLANG_REVISION,
|
||||
str(self.llvm_dir))
|
||||
|
||||
def _checkout_iwyu(self):
|
||||
"""Clone or update IWYU at the pinned revision."""
|
||||
self.iwyu_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
CheckoutGitRepo('include-what-you-use', IWYU_GIT_URL, IWYU_REVISION,
|
||||
str(self.iwyu_dir))
|
||||
|
||||
def _configure(self):
|
||||
"""Configure the LLVM build with IWYU added as an external project.
|
||||
|
||||
The LLVM monorepo's CMake root is `<llvm-clone>/llvm`, not the
|
||||
repo root. IWYU is pulled in via the standard LLVM external-project
|
||||
knobs, so the build picks up the in-tree Clang's resource directory
|
||||
without any of the `IWYU_RESOURCE_RELATIVE_TO` gymnastics needed for
|
||||
a standalone IWYU build.
|
||||
"""
|
||||
# Download Chromium's pinned cmake into third_party/llvm-build-tools/
|
||||
# and prepend it to PATH. Sidesteps "wrong cmake on PATH" failures --
|
||||
# most often on macOS where Homebrew cmake versions skew vs. the
|
||||
# llvm-project requirements.
|
||||
AddCMakeToPath()
|
||||
|
||||
self.build_dir.mkdir(parents=True, exist_ok=True)
|
||||
llvm_cmake_root = self.llvm_dir / 'llvm'
|
||||
cmake_args = [
|
||||
'cmake',
|
||||
'-GNinja',
|
||||
'-DLLVM_ENABLE_PROJECTS=clang',
|
||||
'-DLLVM_EXTERNAL_PROJECTS=iwyu',
|
||||
f'-DLLVM_EXTERNAL_IWYU_SOURCE_DIR={self.iwyu_dir}',
|
||||
'-DCMAKE_BUILD_TYPE=Release',
|
||||
'-DLLVM_ENABLE_ASSERTIONS=On',
|
||||
# IWYU is a Clang frontend tool and needs no codegen backends.
|
||||
# Restricting to the host backend skips AMDGPU/MIPS/PowerPC/etc.
|
||||
# which would otherwise take the bulk of the build time (and have
|
||||
# caused AMDGPU compile failures on some hosts).
|
||||
'-DLLVM_TARGETS_TO_BUILD=host',
|
||||
# Trim other optional bits that are irrelevant for a tool build.
|
||||
'-DLLVM_INCLUDE_TESTS=OFF',
|
||||
'-DLLVM_INCLUDE_EXAMPLES=OFF',
|
||||
'-DLLVM_INCLUDE_BENCHMARKS=OFF',
|
||||
'-DLLVM_INCLUDE_DOCS=OFF',
|
||||
str(llvm_cmake_root),
|
||||
]
|
||||
_check_call(*cmake_args, cwd=self.build_dir)
|
||||
|
||||
def _build(self):
|
||||
"""Build the IWYU binary."""
|
||||
_check_call('ninja', 'include-what-you-use', cwd=self.build_dir)
|
||||
|
||||
def run(self):
|
||||
"""Execute the full checkout / configure / build pipeline."""
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logging.info('LLVM revision: %s', CLANG_REVISION)
|
||||
logging.info('IWYU revision: %s', IWYU_REVISION)
|
||||
logging.info('LLVM checkout: %s', self.llvm_dir)
|
||||
logging.info('IWYU checkout: %s', self.iwyu_dir)
|
||||
logging.info('Build dir: %s', self.build_dir)
|
||||
|
||||
self._checkout_llvm()
|
||||
self._checkout_iwyu()
|
||||
self._configure()
|
||||
self._build()
|
||||
|
||||
produced = self.build_dir / 'bin' / 'include-what-you-use'
|
||||
logging.info('Build complete. Expected binary at: %s', produced)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Build include-what-you-use against Chromium-pinned LLVM.')
|
||||
parser.add_argument('--verbose',
|
||||
action='store_true',
|
||||
help='Enable verbose (debug) logging.')
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
||||
|
||||
IwyuBuilder().run()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Executable
+304
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env vpython3
|
||||
# Copyright (c) 2026 The Brave Authors. All rights reserved.
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
"""Run `include-what-you-use` across the Brave paths enabled in
|
||||
`brave/build/include_what_you_use_paths.cfg`.
|
||||
|
||||
What this script does:
|
||||
|
||||
1. Generates a compile database for the build dir given via `--out`, using
|
||||
the helpers behind Chromium's `tools/clang/scripts/generate_compdb.py`
|
||||
(`tools/clang/pylib/clang/compile_db.py`). Building must already be
|
||||
done in that directory.
|
||||
2. Reads `brave/build/include_what_you_use_paths.cfg` and filters the
|
||||
compile DB down to entries whose source files fall under enabled paths.
|
||||
3. Writes the filtered DB to `<out>/iwyu_compile_commands.json` and invokes
|
||||
`iwyu_tool.py` against it with `IWYU_BINARY` pointed at the IWYU binary
|
||||
produced by `build_iwyu.py`.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
* `brave/tools/cr/iwyu/build_iwyu.py` has been run, producing the IWYU
|
||||
binary at `<src>/out/iwyu/tools/clang/third_party/llvm/build/bin/`.
|
||||
* The Brave build directory passed via `--out` has been built (so that
|
||||
`build.ninja` and any generated headers exist).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import _boot # noqa: F401 -- adds parent tools/cr to sys.path
|
||||
import repository # noqa: E402 -- after _boot
|
||||
|
||||
# Make Chromium helpers importable. Same trick
|
||||
# `tools/clang/scripts/generate_compdb.py` itself uses. Derived from
|
||||
# __file__ rather than `repository.chromium.root` so the sys.path entries are
|
||||
# stable regardless of the cwd `repository` was initialised against.
|
||||
_SRC_ROOT: Path = Path(__file__).resolve().parents[4]
|
||||
sys.path.append(str(_SRC_ROOT / 'tools' / 'clang' / 'pylib'))
|
||||
sys.path.append(str(_SRC_ROOT / 'tools' / 'json_comment_eater'))
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
import json_comment_eater # type: ignore # noqa: E402
|
||||
from clang import compile_db # type: ignore # noqa: E402
|
||||
from terminal import terminal # type: ignore # noqa: E402
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
# IWYU artefacts produced by `build_iwyu.py`. Keeping the paths in sync with
|
||||
# that script is intentional -- run_iwyu.py is a no-op without it.
|
||||
_IWYU_OUT_DIR: Path = (repository.chromium.root / 'out' / 'iwyu' / 'tools' /
|
||||
'clang' / 'third_party')
|
||||
IWYU_BINARY: Path = (_IWYU_OUT_DIR / 'llvm' / 'build' / 'bin' /
|
||||
'include-what-you-use')
|
||||
IWYU_TOOL: Path = _IWYU_OUT_DIR / 'iwyu' / 'iwyu_tool.py'
|
||||
# Applies the textual suggestions produced by iwyu_tool to source files in
|
||||
# place. Lives alongside iwyu_tool.py in the same IWYU clone.
|
||||
FIX_INCLUDES: Path = _IWYU_OUT_DIR / 'iwyu' / 'fix_includes.py'
|
||||
|
||||
# Brave-managed path filter file. See module docstring.
|
||||
PATHS_FILE: Path = (repository.brave.root / 'build' /
|
||||
'include_what_you_use_paths.cfg')
|
||||
|
||||
# IWYU mapping file: remaps libc++ private detail headers (e.g.
|
||||
# `__algorithm/ranges_sort.h`) to their public facades (`<algorithm>`).
|
||||
# Passed to IWYU via `-Xiwyu --mapping_file=<absolute path>` so the lookup
|
||||
# is independent of each compile DB entry's cwd. A comment-stripped copy
|
||||
# is written under `--out` at run time and fed to iwyu_tool (see main).
|
||||
MAPPINGS_FILE: Path = (repository.brave.root / 'build' /
|
||||
'include_what_you_use_mappings.json5')
|
||||
|
||||
# Headers we never want to see in Brave's source after IWYU.
|
||||
BLACKHOLE_INCLUDES: frozenset[str] = frozenset([
|
||||
'<new>',
|
||||
])
|
||||
|
||||
# Matches `#include <hdr>` or `#include "hdr"`, capturing the delimited
|
||||
# token (with its brackets/quotes intact) for comparison against
|
||||
# BLACKHOLE_INCLUDES.
|
||||
_INCLUDE_RE = re.compile(r'^\s*#\s*include\s+([<"][^<>"]+[>"])')
|
||||
|
||||
|
||||
def parse_paths_file(path: Path) -> list[tuple[str, str]]:
|
||||
"""Parse the path-rule file into an ordered list of (sign, path) rules.
|
||||
|
||||
Each non-blank, non-comment line is either `+<path>/` (enable) or
|
||||
`-<path>/` (disable). Trailing `#` comments are stripped. Paths are
|
||||
normalised to end with `/` so prefix matching is unambiguous (e.g.
|
||||
`brave/browser/` does not match `brave/browser_other/foo.cc`).
|
||||
"""
|
||||
rules: list[tuple[str, str]] = []
|
||||
text = path.read_bytes().decode('utf-8')
|
||||
for line_no, raw_line in enumerate(text.splitlines(), 1):
|
||||
# Strip trailing comments.
|
||||
comment_idx = raw_line.find('#')
|
||||
if comment_idx != -1:
|
||||
raw_line = raw_line[:comment_idx]
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line[0] not in ('+', '-'):
|
||||
raise ValueError(
|
||||
f'{path}:{line_no}: rule lines must start with `+` or `-`, '
|
||||
f'got: {line!r}')
|
||||
sign = line[0]
|
||||
rule_path = line[1:].strip()
|
||||
if not rule_path:
|
||||
raise ValueError(f'{path}:{line_no}: empty path after `{sign}`')
|
||||
if not rule_path.endswith('/'):
|
||||
rule_path += '/'
|
||||
rules.append((sign, rule_path))
|
||||
return rules
|
||||
|
||||
|
||||
def is_path_enabled(source_rel: str, rules: list[tuple[str, str]]) -> bool:
|
||||
"""Return whether `source_rel` (relative to src/) is enabled by `rules`.
|
||||
|
||||
Longest-matching prefix wins. If no rule matches, the path is disabled.
|
||||
"""
|
||||
best_len = -1
|
||||
best_sign = '-'
|
||||
for sign, rule_path in rules:
|
||||
if source_rel.startswith(rule_path) and len(rule_path) > best_len:
|
||||
best_len = len(rule_path)
|
||||
best_sign = sign
|
||||
return best_sign == '+'
|
||||
|
||||
|
||||
def blackhole_unwanted_includes() -> None:
|
||||
"""Remove BLACKHOLE_INCLUDES from any file modified in the working tree.
|
||||
|
||||
Runs after `npm run format` to clean up includes IWYU/fix_includes
|
||||
re-added that we never want. Files to scan are taken from
|
||||
`git diff --name-only` against brave-core's HEAD, so only the files
|
||||
the pipeline actually touched get rewritten.
|
||||
|
||||
Lines containing `IWYU pragma:` are preserved verbatim so callers can
|
||||
defend a specific include with `// IWYU pragma: keep`.
|
||||
"""
|
||||
diff = terminal.run_git('-C', str(repository.brave.root), 'diff',
|
||||
'--name-only')
|
||||
files = diff.splitlines() if diff else []
|
||||
for rel in files:
|
||||
target = repository.brave.root / rel
|
||||
if not target.is_file():
|
||||
continue
|
||||
text = target.read_bytes().decode('utf-8')
|
||||
kept: list[str] = []
|
||||
removed_any = False
|
||||
for line in text.splitlines(keepends=True):
|
||||
if 'IWYU pragma:' in line:
|
||||
kept.append(line)
|
||||
continue
|
||||
match = _INCLUDE_RE.match(line)
|
||||
if match and match.group(1) in BLACKHOLE_INCLUDES:
|
||||
removed_any = True
|
||||
continue
|
||||
kept.append(line)
|
||||
if removed_any:
|
||||
target.write_text(''.join(kept), encoding='utf-8', newline='')
|
||||
logging.info('Blackholed unwanted includes from %s', rel)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Run IWYU on enabled Brave paths against an existing '
|
||||
'Brave build directory.')
|
||||
parser.add_argument(
|
||||
'--out',
|
||||
required=True,
|
||||
help='Brave build directory, relative to Chromium\'s src/ '
|
||||
'(e.g. `out/Component`).')
|
||||
parser.add_argument('--verbose',
|
||||
action='store_true',
|
||||
help='Enable verbose (debug) logging.')
|
||||
args = parser.parse_args()
|
||||
|
||||
out_dir = repository.chromium.root / args.out
|
||||
if not (out_dir / 'build.ninja').exists():
|
||||
raise RuntimeError(
|
||||
f'--out does not look like a build dir (no build.ninja): '
|
||||
f'{out_dir}')
|
||||
|
||||
if not IWYU_BINARY.exists():
|
||||
raise RuntimeError(f'IWYU binary not found at {IWYU_BINARY}. '
|
||||
f'Run brave/tools/cr/iwyu/build_iwyu.py first.')
|
||||
if not IWYU_TOOL.exists():
|
||||
raise RuntimeError(f'iwyu_tool.py not found at {IWYU_TOOL}. '
|
||||
f'Run brave/tools/cr/iwyu/build_iwyu.py first.')
|
||||
if not FIX_INCLUDES.exists():
|
||||
raise RuntimeError(f'fix_includes.py not found at {FIX_INCLUDES}. '
|
||||
f'Run brave/tools/cr/iwyu/build_iwyu.py first.')
|
||||
if not MAPPINGS_FILE.exists():
|
||||
raise RuntimeError(f'IWYU mapping file not found at {MAPPINGS_FILE}.')
|
||||
|
||||
rules = parse_paths_file(PATHS_FILE)
|
||||
logging.info('Loaded %d path rule(s) from %s', len(rules), PATHS_FILE)
|
||||
|
||||
logging.info('Generating compile database for %s', out_dir)
|
||||
raw_db = compile_db.GenerateWithNinja(str(out_dir))
|
||||
full_db = compile_db.ProcessCompileDatabase(raw_db, filtered_args=None)
|
||||
logging.info('Compile DB has %d entries', len(full_db))
|
||||
|
||||
filtered_db = []
|
||||
for entry in full_db:
|
||||
# `entry['file']` may be relative to `entry['directory']` (the build
|
||||
# dir) or already absolute; Path / handles both.
|
||||
source_path = Path(entry['directory']) / entry['file']
|
||||
try:
|
||||
source_rel = repository.chromium.to_repo_relative(source_path)
|
||||
except ValueError:
|
||||
# Source outside the Chromium tree -- not something we own.
|
||||
continue
|
||||
if is_path_enabled(source_rel.as_posix(), rules):
|
||||
filtered_db.append(entry)
|
||||
|
||||
logging.info('Running IWYU on %d source file(s)', len(filtered_db))
|
||||
if not filtered_db:
|
||||
logging.warning('No source files enabled. Edit %s to enable paths.',
|
||||
PATHS_FILE)
|
||||
return 0
|
||||
|
||||
# Write the filtered DB next to the build outputs, distinct from the
|
||||
# build's own compile_commands.json (if any).
|
||||
filtered_db_path = out_dir / 'iwyu_compile_commands.json'
|
||||
filtered_db_path.write_text(json.dumps(filtered_db, indent=2),
|
||||
encoding='utf-8',
|
||||
newline='')
|
||||
logging.info('Wrote filtered compile DB to %s', filtered_db_path)
|
||||
|
||||
env = os.environ.copy()
|
||||
env['IWYU_BINARY'] = str(IWYU_BINARY)
|
||||
|
||||
# Strip `//` and `/* */` comments from the mapping file into a copy
|
||||
# under `--out` so strict JSON parsers (presubmit, json.load) can
|
||||
# consume it; IWYU itself is permissive but downstream tooling isn't.
|
||||
# Living under `--out` keeps this artefact out of the source tree
|
||||
# without needing a .gitignore entry.
|
||||
normalised_mappings_path = out_dir / 'iwyu_normalised_mappings.json5'
|
||||
normalised_mappings_path.write_text(json_comment_eater.Nom(
|
||||
MAPPINGS_FILE.read_bytes().decode('utf-8')),
|
||||
encoding='utf-8',
|
||||
newline='')
|
||||
logging.info('Wrote normalised mapping file to %s',
|
||||
normalised_mappings_path)
|
||||
|
||||
cpu_count = os.cpu_count() or 1
|
||||
# Step 1: run iwyu_tool to produce textual fix suggestions on stdout.
|
||||
# We capture stdout (rather than `interactive=True`) so we can pipe it
|
||||
# into fix_includes.py in step 2.
|
||||
#
|
||||
# Args after `--` are forwarded to each IWYU subprocess by iwyu_tool.
|
||||
# `-Xiwyu --mapping_file=...` tells IWYU to use our libc++ mapping
|
||||
# file; the path must be absolute since IWYU runs each unit cd'd to
|
||||
# the compile DB entry's `directory`.
|
||||
logging.info('Running iwyu_tool.py')
|
||||
iwyu_result = terminal.run([
|
||||
sys.executable, IWYU_TOOL, '-p', filtered_db_path, '-j', cpu_count,
|
||||
'--', '-Xiwyu', f'--mapping_file={normalised_mappings_path.resolve()}'
|
||||
],
|
||||
env=env)
|
||||
|
||||
# Persist the raw suggestions next to the filtered DB for inspection /
|
||||
# post-mortem diffing if a fix goes wrong.
|
||||
suggestions_path = out_dir / 'iwyu_suggestions.txt'
|
||||
suggestions_path.write_text(iwyu_result.stdout,
|
||||
encoding='utf-8',
|
||||
newline='')
|
||||
logging.info('Wrote IWYU suggestions to %s', suggestions_path)
|
||||
|
||||
# Step 2: apply the suggestions in place. fix_includes.py reads the
|
||||
# iwyu_tool report on stdin and rewrites the affected source files.
|
||||
#
|
||||
# The filenames in the report are relative to each compile DB entry's
|
||||
# `directory` field, which for a Chromium-style build is the build dir
|
||||
# (e.g. `../../brave/common/importer/foo.cc` rooted at `out/<config>`).
|
||||
# Run fix_includes.py with cwd=<build dir> so those paths resolve.
|
||||
logging.info('Applying suggestions via fix_includes.py')
|
||||
# FIX_INCLUDES is cwd-relative (via repository.chromium.root); absolutise
|
||||
# it before changing cwd or the lookup will fail.
|
||||
terminal.run([sys.executable, FIX_INCLUDES.resolve()],
|
||||
stdin=iwyu_result.stdout,
|
||||
cwd=out_dir)
|
||||
|
||||
# Step 3: strip BLACKHOLE_INCLUDES from any file the pipeline modified.
|
||||
logging.info('Stripping blackholed includes')
|
||||
blackhole_unwanted_includes()
|
||||
|
||||
# Step 4: re-format the rewritten files so the resulting diff matches
|
||||
logging.info('Running npm run format')
|
||||
terminal.run_npm_command('format')
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env vpython3
|
||||
# Copyright (c) 2026 The Brave Authors. All rights reserved.
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from run_iwyu import is_path_enabled, parse_paths_file
|
||||
|
||||
|
||||
class ParsePathsFileTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._tmp_dir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self._tmp_dir.cleanup)
|
||||
self.cfg = Path(self._tmp_dir.name) / 'paths.cfg'
|
||||
|
||||
def _write(self, content: str) -> Path:
|
||||
self.cfg.write_text(content, encoding='utf-8', newline='')
|
||||
return self.cfg
|
||||
|
||||
def test_empty_file(self):
|
||||
self.assertEqual(parse_paths_file(self._write('')), [])
|
||||
|
||||
def test_blank_and_comment_lines_only(self):
|
||||
self._write('\n \n# a comment\n\t# indented comment\n')
|
||||
self.assertEqual(parse_paths_file(self.cfg), [])
|
||||
|
||||
def test_enable_and_disable_rules(self):
|
||||
self._write('+brave/browser/\n-brave/browser/internal/\n')
|
||||
self.assertEqual(parse_paths_file(self.cfg), [
|
||||
('+', 'brave/browser/'),
|
||||
('-', 'brave/browser/internal/'),
|
||||
])
|
||||
|
||||
def test_trailing_slash_is_added_when_missing(self):
|
||||
self._write('+brave/browser\n-brave/browser/internal\n')
|
||||
self.assertEqual(parse_paths_file(self.cfg), [
|
||||
('+', 'brave/browser/'),
|
||||
('-', 'brave/browser/internal/'),
|
||||
])
|
||||
|
||||
def test_trailing_comment_is_stripped(self):
|
||||
self._write('+brave/browser/ # enable browser\n'
|
||||
'-brave/browser/internal/# nested disable\n')
|
||||
self.assertEqual(parse_paths_file(self.cfg), [
|
||||
('+', 'brave/browser/'),
|
||||
('-', 'brave/browser/internal/'),
|
||||
])
|
||||
|
||||
def test_line_that_is_only_a_comment_is_skipped(self):
|
||||
self._write('+brave/browser/\n'
|
||||
'# -brave/browser/internal/\n'
|
||||
'-brave/components/\n')
|
||||
self.assertEqual(parse_paths_file(self.cfg), [
|
||||
('+', 'brave/browser/'),
|
||||
('-', 'brave/components/'),
|
||||
])
|
||||
|
||||
def test_surrounding_whitespace_is_tolerated(self):
|
||||
self._write(' +brave/browser/ \n\t-brave/components/\t\n')
|
||||
self.assertEqual(parse_paths_file(self.cfg), [
|
||||
('+', 'brave/browser/'),
|
||||
('-', 'brave/components/'),
|
||||
])
|
||||
|
||||
def test_rules_preserve_file_order(self):
|
||||
self._write('+a/\n-b/\n+c/\n-d/\n')
|
||||
self.assertEqual(parse_paths_file(self.cfg), [
|
||||
('+', 'a/'),
|
||||
('-', 'b/'),
|
||||
('+', 'c/'),
|
||||
('-', 'd/'),
|
||||
])
|
||||
|
||||
def test_invalid_prefix_raises(self):
|
||||
self._write('brave/browser/\n')
|
||||
with self.assertRaisesRegex(ValueError,
|
||||
'must start with `\\+` or `-`'):
|
||||
parse_paths_file(self.cfg)
|
||||
|
||||
def test_empty_path_after_sign_raises(self):
|
||||
self._write('+\n')
|
||||
with self.assertRaisesRegex(ValueError, 'empty path after `\\+`'):
|
||||
parse_paths_file(self.cfg)
|
||||
|
||||
def test_error_line_number_matches_offending_line(self):
|
||||
self._write('+brave/browser/\n'
|
||||
'\n'
|
||||
'# a comment\n'
|
||||
'oops\n')
|
||||
with self.assertRaisesRegex(ValueError, ':4:'):
|
||||
parse_paths_file(self.cfg)
|
||||
|
||||
|
||||
class IsPathEnabledTest(unittest.TestCase):
|
||||
|
||||
def test_no_rules_means_disabled(self):
|
||||
self.assertFalse(is_path_enabled('brave/browser/foo.cc', []))
|
||||
|
||||
def test_unmatched_path_is_disabled(self):
|
||||
rules = [('+', 'brave/browser/')]
|
||||
self.assertFalse(is_path_enabled('brave/components/foo.cc', rules))
|
||||
|
||||
def test_single_enable_rule(self):
|
||||
rules = [('+', 'brave/browser/')]
|
||||
self.assertTrue(is_path_enabled('brave/browser/foo.cc', rules))
|
||||
|
||||
def test_single_disable_rule(self):
|
||||
rules = [('-', 'brave/browser/')]
|
||||
self.assertFalse(is_path_enabled('brave/browser/foo.cc', rules))
|
||||
|
||||
def test_longest_prefix_wins_disable_overrides_enable(self):
|
||||
rules = [
|
||||
('+', 'brave/browser/'),
|
||||
('-', 'brave/browser/internal/'),
|
||||
]
|
||||
self.assertTrue(is_path_enabled('brave/browser/foo.cc', rules))
|
||||
self.assertFalse(
|
||||
is_path_enabled('brave/browser/internal/foo.cc', rules))
|
||||
|
||||
def test_longest_prefix_wins_enable_overrides_disable(self):
|
||||
rules = [
|
||||
('-', 'brave/browser/'),
|
||||
('+', 'brave/browser/public/'),
|
||||
]
|
||||
self.assertFalse(is_path_enabled('brave/browser/foo.cc', rules))
|
||||
self.assertTrue(is_path_enabled('brave/browser/public/foo.cc', rules))
|
||||
|
||||
def test_rule_order_does_not_matter(self):
|
||||
forward = [
|
||||
('+', 'brave/browser/'),
|
||||
('-', 'brave/browser/internal/'),
|
||||
]
|
||||
reverse = list(reversed(forward))
|
||||
path = 'brave/browser/internal/foo.cc'
|
||||
self.assertEqual(is_path_enabled(path, forward),
|
||||
is_path_enabled(path, reverse))
|
||||
|
||||
def test_prefix_must_align_to_directory_boundary(self):
|
||||
# The trailing `/` is what prevents `brave/browser/` matching
|
||||
# `brave/browser_other/...`.
|
||||
rules = [('+', 'brave/browser/')]
|
||||
self.assertFalse(is_path_enabled('brave/browser_other/foo.cc', rules))
|
||||
|
||||
def test_exact_directory_match_is_enabled(self):
|
||||
# Source exactly under the enabled prefix.
|
||||
rules = [('+', 'brave/browser/')]
|
||||
self.assertTrue(is_path_enabled('brave/browser/a/b/c.cc', rules))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+15
-1
@@ -3,6 +3,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
import platform
|
||||
@@ -188,7 +190,8 @@ class Terminal:
|
||||
*,
|
||||
env: dict[str, str] | None = None,
|
||||
cwd=None,
|
||||
interactive: bool = False):
|
||||
interactive: bool = False,
|
||||
stdin: str | None = None):
|
||||
"""Runs a command on the terminal.
|
||||
|
||||
When `interactive=True`, the subprocess inherits the parent's
|
||||
@@ -201,6 +204,11 @@ class Terminal:
|
||||
the parent's environment, a dict fully replaces it. If you want to
|
||||
add a few keys on top of `os.environ`, copy it first
|
||||
(`env={**os.environ, ...}`).
|
||||
|
||||
Pass `stdin=` to feed data into the subprocess's stdin. Captured
|
||||
(non-interactive) mode encodes it with utf-8 to match the
|
||||
captured streams; interactive mode rejects `stdin=` because the
|
||||
subprocess owns the tty.
|
||||
"""
|
||||
# Convert all arguments to strings, to avoid issues with `PurePath`
|
||||
# being passed arguments
|
||||
@@ -245,11 +253,17 @@ class Terminal:
|
||||
text=True,
|
||||
encoding='utf-8')
|
||||
|
||||
if interactive and stdin is not None:
|
||||
raise ValueError(
|
||||
'terminal.run(): `stdin=` is not supported with '
|
||||
'`interactive=True` (the subprocess owns the tty).')
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd,
|
||||
check=True,
|
||||
env=env,
|
||||
cwd=cwd,
|
||||
input=stdin,
|
||||
**capture_kwargs)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.stderr:
|
||||
|
||||
Reference in New Issue
Block a user