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