Flutter Engine
The Flutter Engine
cpp_indexer.py
Go to the documentation of this file.
1# Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2# for details. All rights reserved. Use of this source code is governed by a
3# BSD-style license that can be found in the LICENSE file.
4#
5"""Clang based C++ code indexer which produces xref.json."""
6from __future__ import annotations
7
8import glob
9import json
10import logging
11import os
12import platform
13import re
14import sys
15import subprocess
16
17from contextlib import contextmanager
18from dataclasses import dataclass, field
19from typing import Dict, Optional, Set
20
21from marshmallow_dataclass import class_schema
22from progress.bar import ShadyBar
23
24_libclang_path = None
25_clang_include_dir = None
26if platform.system() == 'Darwin':
27 _path_to_llvm = subprocess.check_output(['brew', '--prefix', 'llvm'],
28 encoding='utf-8').strip()
29 _clang_include_dir = glob.glob(f'{_path_to_llvm}/lib/clang/**/include')[0]
30 _libclang_path = f'{_path_to_llvm}/lib/libclang.dylib'
31 sys.path.append(
32 f'{_path_to_llvm}/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages'
33 )
34
35# pylint: disable=wrong-import-position,line-too-long
36from clang.cindex import Config, CompilationDatabase, Cursor, CursorKind, Index, SourceLocation, TranslationUnit
37
38if _libclang_path is not None:
39 Config.set_library_file(_libclang_path)
40
41_DART_CONFIGURATION = 'Release' + (
42 'ARM64' if platform.uname().machine == 'arm64' else 'X64')
43_DART_BUILD_DIR = ('xcodebuild' if platform.system() == 'Darwin' else
44 'out') + '/' + _DART_CONFIGURATION
45
46
47@contextmanager
48def _change_working_directory_to(path):
49 oldpwd = os.getcwd()
50 os.chdir(path)
51 try:
52 yield
53 finally:
54 os.chdir(oldpwd)
55
56
57def _create_compilation_database():
58 """Create compilation database for the default configuration.
59
60 Extacts compilation database (compile_commands.json) for the default
61 build configuration and filters it down to commands taht builds just
62 the core VM pieces in the host configuration.
63 """
64 logging.info('Extracting compilation commands from build files for %s',
65 _DART_CONFIGURATION)
66 with _change_working_directory_to(_DART_BUILD_DIR):
67 commands = json.loads(
68 subprocess.check_output(['ninja', '-C', '.', '-t', 'compdb',
69 'cxx']))
70 pattern = re.compile(
71 r'libdart(_vm|_compiler)?_precompiler_host_targeting_host\.')
72 with open('compile_commands.json', 'w', encoding='utf-8') as outfile:
73 json.dump([
74 cmd for cmd in commands
75 if pattern.search(cmd['command']) is not None
76 ], outfile)
77
78
79def _get_current_commit_hash():
80 return subprocess.check_output(['git', 'merge-base', 'main', 'HEAD'],
81 text=True).strip()
82
83
84@dataclass
86 location: str
87 members: Dict[str, str] = field(default_factory=dict)
88
89
90@dataclass
92 """Symbol location referring to a line in a specific file."""
93 filename: str
94 lineno: int
95
96
97@dataclass
99 """Index of C++ symbols extracted from source."""
100 commit: str
101 files: list[str] = field(default_factory=list)
102 classes: Dict[str, _ClassInfo] = field(default_factory=dict)
103 functions: Dict[str, str] = field(default_factory=dict)
104
105 def try_resolve(self, ref: str) -> Optional[Location]:
106 """Resolve the location of the given reference."""
107 loc = self._try_resolve_impl(ref)
108 if loc is not None:
109 return self._location_from_string(loc)
110 return None
111
112 def _try_resolve_impl(self, ref: str) -> Optional[str]:
113 if ref in self.functions:
114 return self.functions[ref]
115 if ref in self.classes:
116 return self.classes[ref].location
117 if '::' in ref:
118 (class_name, function_name) = ref.rsplit('::', 1)
119 if class_name in self.classes:
120 return self.classes[class_name].members.get(function_name)
121 return None
122
123 def _location_from_string(self, loc: str) -> Location:
124 (file_idx, line_idx) = loc.split(':', 1)
125 return Location(self.files[int(file_idx)], int(line_idx))
126
127
129 symbols_index: SymbolsIndex
130
131 processed_files: Set[str]
132 classes_by_usr: Dict[str, _ClassInfo]
133
134 name_stack: list[str]
135 info_stack: list[_ClassInfo]
136 files_seen_in_unit: Set[str]
137
138 def __init__(self):
139 self.symbols_index = SymbolsIndex(commit=_get_current_commit_hash())
140
143 self.files_index = {}
144
145 self.name_stack = []
146 self.info_stack = []
148
149 def index(self, unit: TranslationUnit):
150 """Index the given translation unit and append new symbols to index."""
151 self.name_stack.clear()
152 self.info_stack.clear()
153 self.files_seen_in_unit.clear()
154 self._recurse(unit.cursor)
156
157 def _recurse(self, cursor: Cursor):
158 name = ""
159 kind = cursor.kind
160
161 if cursor.location.file is not None:
162 name = cursor.location.file.name
163 if name in self.processed_files or not name.startswith('../..'):
164 return
165 self.files_seen_in_unit.add(name)
166
167 if kind == CursorKind.CLASS_DECL:
168 if not cursor.is_definition():
169 return
170 usr = cursor.get_usr()
171 if usr in self.classes_by_usr:
172 return
173 self.name_stack.append(cursor.spelling)
174 class_name = '::'.join(self.name_stack)
175 class_info = _ClassInfo(self._format_location(cursor.location))
176 self.info_stack.append(class_info)
177 self.symbols_index.classes[class_name] = class_info
178 self.classes_by_usr[usr] = class_info
179 elif kind == CursorKind.NAMESPACE:
180 self.name_stack.append(cursor.spelling)
181 elif kind == CursorKind.FUNCTION_DECL and cursor.is_definition():
182 namespace_prefix = ""
183 if cursor.semantic_parent.kind == CursorKind.NAMESPACE:
184 namespace_prefix = '::'.join(self.name_stack) + (
185 '::' if len(self.name_stack) > 0 else "")
186 function_name = namespace_prefix + cursor.spelling
187 self.symbols_index.functions[function_name] = self._format_location(
188 cursor.location)
189 return
190 elif kind == CursorKind.CXX_METHOD and cursor.is_definition():
191 parent = cursor.semantic_parent
192 if parent.kind == CursorKind.CLASS_DECL:
193 class_info_or_none = self.classes_by_usr.get(parent.get_usr())
194 if class_info_or_none is None:
195 return
196 class_info_or_none.members[
197 cursor.spelling] = self._format_location(cursor.location)
198 return
199 elif kind == CursorKind.VAR_DECL and cursor.is_definition():
200 parent = cursor.semantic_parent
201 if parent.kind == CursorKind.CLASS_DECL:
202 class_info_or_none = self.classes_by_usr.get(parent.get_usr())
203 if class_info_or_none is None:
204 return
205 class_info_or_none.members[
206 cursor.spelling] = self._format_location(cursor.location)
207
208 for child in cursor.get_children():
209 self._recurse(child)
210
211 if kind == CursorKind.NAMESPACE:
212 self.name_stack.pop()
213 elif kind == CursorKind.CLASS_DECL:
214 self.name_stack.pop()
215 self.info_stack.pop()
216
217 def _format_location(self, loc: SourceLocation):
218 file_name = loc.file.name
219 lineno = loc.line
220 return f'{self._get_file_index(file_name)}:{lineno}'
221
222 def _get_file_index(self, file_name: str):
223 index = self.files_index.get(file_name)
224 if index is None:
225 index = len(self.symbols_index.files)
226 self.files_index[file_name] = index
227 self.symbols_index.files.append(
228 os.path.relpath(os.path.abspath(file_name),
229 os.path.abspath('../..')))
230 return index
231
232
233def _index_source() -> SymbolsIndex:
234 indexer = _Indexer()
235
236 _create_compilation_database()
237 with _change_working_directory_to(_DART_BUILD_DIR):
238 index = Index.create()
239 compdb = CompilationDatabase.fromDirectory('.')
240
241 commands = list(compdb.getAllCompileCommands())
242 with ShadyBar('Indexing',
243 max=len(commands),
244 suffix='%(percent)d%% eta %(eta_td)s') as progress_bar:
245 for command in commands:
246 args = [
247 arg for arg in command.arguments
248 if arg.startswith('-I') or arg.startswith('-W') or
249 arg.startswith('-D') or arg.startswith('-i') or
250 arg.startswith('sdk/') or arg.startswith('-std')
251 ] + [
252 '-Wno-macro-redefined', '-Wno-unused-const-variable',
253 '-Wno-unused-function', '-Wno-unused-variable'
254 ]
255
256 if _clang_include_dir is not None:
257 args.append(f'-I{_clang_include_dir}')
258
259 unit = index.parse(command.filename, args=args)
260 for diag in unit.diagnostics:
261 print(diag.format())
262
263 indexer.index(unit)
264 progress_bar.next()
265
266 return indexer.symbols_index
267
268
269_SymbolsIndexSchema = class_schema(SymbolsIndex)()
270
271
272def load_index(filename: str) -> SymbolsIndex:
273 """Load symbols index from the given file.
274
275 If index is out of date or missing it will be generated.
276 """
277 index: SymbolsIndex
278
279 if os.path.exists(filename):
280 with open(filename, 'r', encoding='utf-8') as json_file:
281 index = _SymbolsIndexSchema.loads(json_file.read())
282 if _get_current_commit_hash() == index.commit:
283 logging.info('Loaded symbols index from %s', filename)
284 return index
285 logging.warning(
286 '%s is generated for commit %s while current commit is %s',
287 filename, index.commit, _get_current_commit_hash())
288
289 index = _index_source()
290 with open(filename, 'w', encoding='utf-8') as json_file:
291 json_file.write(_SymbolsIndexSchema.dumps(index))
292 logging.info(
293 'Successfully indexed C++ source and written symbols index into %s',
294 filename)
295 return index
static std::function< void(void)> pop(std::deque< std::function< void(void)> > *list)
Definition: SkExecutor.cpp:62
Optional[Location] try_resolve(self, str ref)
Definition: cpp_indexer.py:105
Location _location_from_string(self, str loc)
Definition: cpp_indexer.py:123
Optional[str] _try_resolve_impl(self, str ref)
Definition: cpp_indexer.py:112
def _format_location(self, SourceLocation loc)
Definition: cpp_indexer.py:217
def index(self, TranslationUnit unit)
Definition: cpp_indexer.py:149
def _recurse(self, Cursor cursor)
Definition: cpp_indexer.py:157
static void append(char **dst, size_t *count, const char *src, size_t n)
Definition: editor.cpp:211
SymbolsIndex load_index(str filename)
Definition: cpp_indexer.py:272
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not set
Definition: switches.h:76
const myers::Point & get(const myers::Segment &)
def print(*args, **kwargs)
Definition: run_tests.py:49
static SkString join(const CommandLineFlags::StringArray &)
Definition: skpbench.cpp:741