1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3# You can obtain one at http://mozilla.org/MPL/2.0/.
4
5# This modules provides functionality for dealing with code completion.
6
7from __future__ import absolute_import, print_function
8
9import os
10
11from mozbuild.backend.common import CommonBackend
12from mozbuild.frontend.data import (
13    ComputedFlags,
14    Sources,
15    GeneratedSources,
16    DirectoryTraversal,
17    PerSourceFlag,
18    VariablePassthru,
19)
20from mozbuild.shellutil import quote as shell_quote
21from mozbuild.util import expand_variables
22import mozpack.path as mozpath
23from collections import (
24    defaultdict,
25    OrderedDict,
26)
27
28
29class CompileDBBackend(CommonBackend):
30    def _init(self):
31        CommonBackend._init(self)
32
33        # The database we're going to dump out to.
34        self._db = OrderedDict()
35
36        # The cache for per-directory flags
37        self._flags = {}
38
39        self._envs = {}
40        self._local_flags = defaultdict(dict)
41        self._per_source_flags = defaultdict(list)
42
43    def _build_cmd(self, cmd, filename, unified):
44        cmd = list(cmd)
45        if unified is None:
46            cmd.append(filename)
47        else:
48            cmd.append(unified)
49
50        return cmd
51
52    def consume_object(self, obj):
53        # Those are difficult directories, that will be handled later.
54        if obj.relsrcdir in (
55            "build/unix/elfhack",
56            "build/unix/elfhack/inject",
57            "build/clang-plugin",
58            "build/clang-plugin/tests",
59        ):
60            return True
61
62        consumed = CommonBackend.consume_object(self, obj)
63
64        if consumed:
65            return True
66
67        if isinstance(obj, DirectoryTraversal):
68            self._envs[obj.objdir] = obj.config
69
70        elif isinstance(obj, (Sources, GeneratedSources)):
71            # For other sources, include each source file.
72            for f in obj.files:
73                self._build_db_line(
74                    obj.objdir, obj.relsrcdir, obj.config, f, obj.canonical_suffix
75                )
76
77        elif isinstance(obj, VariablePassthru):
78            for var in ("MOZBUILD_CMFLAGS", "MOZBUILD_CMMFLAGS"):
79                if var in obj.variables:
80                    self._local_flags[obj.objdir][var] = obj.variables[var]
81
82        elif isinstance(obj, PerSourceFlag):
83            self._per_source_flags[obj.file_name].extend(obj.flags)
84
85        elif isinstance(obj, ComputedFlags):
86            for var, flags in obj.get_flags():
87                self._local_flags[obj.objdir]["COMPUTED_%s" % var] = flags
88
89        return True
90
91    def consume_finished(self):
92        CommonBackend.consume_finished(self)
93
94        db = []
95
96        for (directory, filename, unified), cmd in self._db.items():
97            env = self._envs[directory]
98            cmd = self._build_cmd(cmd, filename, unified)
99            variables = {
100                "DIST": mozpath.join(env.topobjdir, "dist"),
101                "DEPTH": env.topobjdir,
102                "MOZILLA_DIR": env.topsrcdir,
103                "topsrcdir": env.topsrcdir,
104                "topobjdir": env.topobjdir,
105            }
106            variables.update(self._local_flags[directory])
107            c = []
108            for a in cmd:
109                accum = ""
110                for word in expand_variables(a, variables).split():
111                    # We can't just split() the output of expand_variables since
112                    # there can be spaces enclosed by quotes, e.g. '"foo bar"'.
113                    # Handle that case by checking whether there are an even
114                    # number of double-quotes in the word and appending it to
115                    # the accumulator if not. Meanwhile, shlex.split() and
116                    # mozbuild.shellutil.split() aren't able to properly handle
117                    # this and break in various ways, so we can't use something
118                    # off-the-shelf.
119                    has_quote = bool(word.count('"') % 2)
120                    if accum and has_quote:
121                        c.append(accum + " " + word)
122                        accum = ""
123                    elif accum and not has_quote:
124                        accum += " " + word
125                    elif not accum and has_quote:
126                        accum = word
127                    else:
128                        c.append(word)
129            # Tell clangd to keep parsing to the end of a file, regardless of
130            # how many errors are encountered. (Unified builds mean that we
131            # encounter a lot of errors parsing some files.)
132            c.insert(-1, "-ferror-limit=0")
133
134            per_source_flags = self._per_source_flags.get(filename)
135            if per_source_flags is not None:
136                c.extend(per_source_flags)
137            db.append(
138                {
139                    "directory": directory,
140                    "command": " ".join(shell_quote(a) for a in c),
141                    "file": mozpath.join(directory, filename),
142                }
143            )
144
145        import json
146
147        outputfile = self._outputfile_path()
148        with self._write_file(outputfile) as jsonout:
149            json.dump(db, jsonout, indent=0)
150
151    def _outputfile_path(self):
152        # Output the database (a JSON file) to objdir/compile_commands.json
153        return os.path.join(self.environment.topobjdir, "compile_commands.json")
154
155    def _process_unified_sources(self, obj):
156        if not obj.have_unified_mapping:
157            for f in list(sorted(obj.files)):
158                self._build_db_line(
159                    obj.objdir, obj.relsrcdir, obj.config, f, obj.canonical_suffix
160                )
161            return
162
163        # For unified sources, only include the unified source file.
164        # Note that unified sources are never used for host sources.
165        for f in obj.unified_source_mapping:
166            self._build_db_line(
167                obj.objdir, obj.relsrcdir, obj.config, f[0], obj.canonical_suffix
168            )
169            for entry in f[1]:
170                self._build_db_line(
171                    obj.objdir,
172                    obj.relsrcdir,
173                    obj.config,
174                    entry,
175                    obj.canonical_suffix,
176                    unified=f[0],
177                )
178
179    def _handle_idl_manager(self, idl_manager):
180        pass
181
182    def _handle_ipdl_sources(
183        self,
184        ipdl_dir,
185        sorted_ipdl_sources,
186        sorted_nonstatic_ipdl_sources,
187        sorted_static_ipdl_sources,
188        unified_ipdl_cppsrcs_mapping,
189    ):
190        for f in unified_ipdl_cppsrcs_mapping:
191            self._build_db_line(ipdl_dir, None, self.environment, f[0], ".cpp")
192
193    def _handle_webidl_build(
194        self,
195        bindings_dir,
196        unified_source_mapping,
197        webidls,
198        expected_build_output_files,
199        global_define_files,
200    ):
201        for f in unified_source_mapping:
202            self._build_db_line(bindings_dir, None, self.environment, f[0], ".cpp")
203
204    COMPILERS = {
205        ".c": "CC",
206        ".cpp": "CXX",
207        ".m": "CC",
208        ".mm": "CXX",
209    }
210
211    CFLAGS = {
212        ".c": "CFLAGS",
213        ".cpp": "CXXFLAGS",
214        ".m": "CFLAGS",
215        ".mm": "CXXFLAGS",
216    }
217
218    def _get_compiler_args(self, cenv, canonical_suffix):
219        if canonical_suffix not in self.COMPILERS:
220            return None
221        return cenv.substs[self.COMPILERS[canonical_suffix]].split()
222
223    def _build_db_line(
224        self, objdir, reldir, cenv, filename, canonical_suffix, unified=None
225    ):
226        compiler_args = self._get_compiler_args(cenv, canonical_suffix)
227        if compiler_args is None:
228            return
229        db = self._db.setdefault(
230            (objdir, filename, unified),
231            compiler_args + ["-o", "/dev/null", "-c"],
232        )
233        reldir = reldir or mozpath.relpath(objdir, cenv.topobjdir)
234
235        def append_var(name):
236            value = cenv.substs.get(name)
237            if not value:
238                return
239            if isinstance(value, str):
240                value = value.split()
241            db.extend(value)
242
243        db.append("$(COMPUTED_%s)" % self.CFLAGS[canonical_suffix])
244        if canonical_suffix == ".m":
245            append_var("OS_COMPILE_CMFLAGS")
246            db.append("$(MOZBUILD_CMFLAGS)")
247        elif canonical_suffix == ".mm":
248            append_var("OS_COMPILE_CMMFLAGS")
249            db.append("$(MOZBUILD_CMMFLAGS)")
250