summaryrefslogtreecommitdiff
path: root/lint/run_iwyu.py
blob: ef8413b55dc865a07af8f292d53876e4f8d9f07f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#!/usr/bin/env python3
# Copyright 2025 Huawei Cloud Computing Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import os
import shutil
import subprocess
import sys

from typing import List

CXX_LIB_VERSION = "13.3.0"


def dump_meta(src: str, cmd: List[str]) -> None:
    """Dump linter action metadata for further analysis."""
    OUT = os.environ.get("OUT")
    if OUT:
        with open(os.path.join(OUT, "config.json"), "w") as f:
            json.dump({"src": src, "cmd": cmd}, f)


def run_lint(src: str, cmd: List[str]) -> int:
    """Run the lint command for the specified source file."""
    dump_meta(src, cmd)

    CONFIG = os.environ.get("CONFIG")
    if CONFIG is None:
        print("Failed to get CONFIG", file=sys.stderr)
        return 1

    shutil.copyfile(os.path.join(CONFIG, "iwyu-mapping"), "iwyu-mapping.imp")

    # Currently, .tpp files cannot be processed to produce meaningful suggestions
    # Their corresponding .hpp files are analyzed separately, so just skip
    if src.endswith(".tpp"):
        return 0

    # IWYU doesn't process headers by default
    if src.endswith(".hpp"):
        # To analyze .hpp files, we need to "pack" them into a source file:
        # _fake.cpp includes the header and marks it as associated.
        #   We could simply change the command line, but this may cause
        #   IWYU to search for associated files by default (using stem).
        #   Example:
        #   // src/utils/cpp/gsl.hpp considered a source file:
        #   #include "gsl/gsl" // analysis happens because of names
        #   ...
        TMPDIR = os.environ.get("TMPDIR")
        if TMPDIR:
            fake_stem = os.path.join(TMPDIR, "_fake")
            with open(fake_stem + ".cpp", "w+") as fake:
                content = "#include \"%s\" // IWYU pragma: associated" % (
                    src.split(os.path.sep, 1)[1])
                fake.write(content)
                src = os.path.realpath(fake.name)
        else:
            print("Failed to get TMPDIR", file=sys.stderr)
            return 1

        # The command line must be adjusted as well
        e_index = cmd.index("-E")
        cmd = cmd[:e_index] + [
            "-c", src, "-o",
            os.path.realpath(fake_stem) + ".o"
        ] + cmd[e_index + 2:]

    iwyu_flags: List[str] = [
        "--cxx17ns",  # suggest the more concise syntax introduced in C++17
        "--no_fwd_decls",  # do not use forward declarations
        "--no_default_mappings",  # do not add iwyu's default mappings
        "--mapping_file=iwyu-mapping.imp",  # specifies a mapping file
        "--error",  # return 1 if there are any suggestions
        "--verbose=2",  # provide explanations
        "--max_line_length=1000"  # don't limit explanation messages
    ]

    iwyu_options: List[str] = []
    for option in iwyu_flags:
        iwyu_options.append("-Xiwyu")
        iwyu_options.append(option)

    # add include paths from the bundled toolchain
    baseincludepath = os.path.join(CONFIG, "toolchain", "include", "c++",
                                   CXX_LIB_VERSION)
    # We're using the native toolchain, so arch-specific headers are
    # only available for one arch. Hence we can try all supported candidates
    # and add the ones found
    for arch in ["x86_64", "arm"]:
        idir = os.path.join(baseincludepath, "%s-pc-linux-gnu" % (arch, ))
        if os.path.exists(idir):
            iwyu_options += ["-isystem", idir]
    iwyu_options += [
        "-isystem",
        baseincludepath,
    ]

    iwyu_options.extend(cmd[1:])

    # IWYU uses <> for headers from -isystem and quoted for -I
    # https://github.com/include-what-you-use/include-what-you-use/issues/1070#issuecomment-1163177107
    # So we "ask" IWYU here to include all non-system dependencies with quotes
    iwyu_options = ["-I" if x == "-isystem" else x for x in iwyu_options]

    new_cmd = [os.path.join(CONFIG, "toolchain", "bin", "include-what-you-use")]
    new_cmd.extend(iwyu_options)
    new_cmd.append(src)

    print("Running cmd %r" % (new_cmd), file=sys.stderr)
    return subprocess.run(new_cmd, stdout=subprocess.DEVNULL).returncode


if __name__ == "__main__":
    sys.exit(run_lint(sys.argv[1], sys.argv[2:]))