1// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5#include "cbpparser.h"
6
7#include <QDir>
8#include <QTextStream>
9
10using FileNameList = QList<QString>;
11
12template<typename Container>
13inline void sort(Container &container)
14{
15 std::sort(std::begin(container), std::end(container));
16}
17
18template<typename R, typename S, typename T>
19decltype(auto) equal(R S::*member, T value)
20{
21 return std::bind<bool>(std::equal_to<T>(), value, std::bind(member, std::placeholders::_1));
22}
23
24template<typename C, typename F>
25int indexOf(const C &container, F function)
26{
27 typename C::const_iterator begin = std::begin(container);
28 typename C::const_iterator end = std::end(container);
29
30 typename C::const_iterator it = std::find_if(begin, end, function);
31 return it == end ? -1 : static_cast<int>(std::distance(begin, it));
32}
33
34int distance(const QString& srcPath, const QString& dstPath) {
35 // calculate distance based on edit distance metric
36 int n = srcPath.length();
37 int m = dstPath.length();
38 QVector<QVector<int>> dp(n + 1, QVector<int>(m + 1));
39 for (int i = 0; i <= n; ++i) {
40 dp[i][0] = i;
41 }
42 for (int j = 0; j <= m; ++j) {
43 dp[0][j] = j;
44 }
45 for (int i = 1; i <= n; ++i) {
46 for (int j = 1; j <= m; ++j) {
47 dp[i][j] = std::min({dp[i - 1][j] + 1, dp[i][j - 1] + 1,
48 dp[i - 1][j - 1] + (srcPath[i - 1] == dstPath[j - 1] ? 0 : 1)});
49 }
50 }
51 return dp[n][m];
52}
53
54void CMakeCbpParser::sortFiles()
55{
56 QList<QString> fileNames;
57 for (const auto& fileNode : srcFileList) {
58 fileNames.append(fileNode->getfilePath());
59 }
60 std::sort(fileNames.begin(), fileNames.end());
61
62 int fallbackIndex = 0;
63 int bestIncludeCount = -1;
64 for (int i = 0; i < buildTargets.size(); ++i) {
65 const auto &target = buildTargets.at(i);
66 if (target.includeFiles.isEmpty()) {
67 continue;
68 }
69 if (target.sourceDirectory == sourceDirectory && target.includeFiles.count() > bestIncludeCount) {
70 bestIncludeCount = target.includeFiles.count();
71 fallbackIndex = i;
72 }
73 }
74
75 QString parentDirectory;
76 CMakeBuildTarget *last = nullptr;
77 for (const auto &fileName : fileNames) {
78 const auto unitTargets = unitTargetMap[fileName];
79 if (!unitTargets.isEmpty()) {
80 for (const auto &unitTarget : unitTargets) {
81 const auto index = indexOf(buildTargets, equal(&CMakeBuildTarget::title, unitTarget));
82 if (index != -1) {
83 buildTargets[index].srcfiles.append(fileName);
84 continue;
85 }
86 }
87 continue;
88 }
89 if (QFileInfo(fileName).dir().path() == parentDirectory && last) {
90 last->srcfiles.append(fileName);
91 } else {
92 int bestDistance = std::numeric_limits<int>::max();
93 int bestIndex = -1;
94 int bestIncludeCount = -1;
95 for (int i = 0; i < buildTargets.size(); ++i) {
96 const auto &target = buildTargets.at(i);
97 if (target.includeFiles.isEmpty()) {
98 continue;
99 }
100 const auto dist = distance(target.sourceDirectory, fileName);
101
102 if (dist < bestDistance || (dist == bestDistance && target.includeFiles.count() > bestIncludeCount)) {
103 bestDistance = dist;
104 bestIncludeCount = target.includeFiles.count();
105 bestIndex = i;
106 }
107 }
108 if (bestIndex == -1 && !buildTargets.isEmpty()) {
109 bestIndex = fallbackIndex;
110 }
111 if (bestIndex != -1) {
112 buildTargets[bestIndex].srcfiles.append(fileName);
113 last = &buildTargets[bestIndex];
114 parentDirectory = QFileInfo(fileName).dir().path();
115 }
116 }
117 }
118}
119
120bool CMakeCbpParser::parseCbpFile(const QString &fileName,
121 const QString &_sourceDirectory)
122{
123 buildDirectory = fileName;
124 sourceDirectory = _sourceDirectory;
125
126 QFile fi(fileName);
127 if (fi.exists() && fi.open(QFile::ReadOnly)) {
128 setDevice(&fi);
129
130 while (!atEnd()) {
131 readNext();
132 if (name() == "CodeBlocks_project_file")
133 parseCodeBlocks_project_file();
134 else if (isStartElement())
135 parseUnknownElement();
136 }
137
138 sortFiles();
139
140 fi.close();
141
142 return true;
143 }
144 return false;
145}
146
147void CMakeCbpParser::parseCodeBlocks_project_file()
148{
149 while (!atEnd()) {
150 readNext();
151 if (isEndElement())
152 return;
153 else if (name() == "Project")
154 parseProject();
155 else if (isStartElement())
156 parseUnknownElement();
157 }
158}
159
160void CMakeCbpParser::parseProject()
161{
162 while (!atEnd()) {
163 readNext();
164 if (isEndElement())
165 return;
166 else if (name() == "Option")
167 parseOption();
168 else if (name() == "Unit")
169 parseUnit();
170 else if (name() == "Build")
171 parseBuild();
172 else if (isStartElement())
173 parseUnknownElement();
174 }
175}
176
177void CMakeCbpParser::parseBuild()
178{
179 while (!atEnd()) {
180 readNext();
181 if (isEndElement())
182 return;
183 else if (name() == "Target")
184 parseBuildTarget();
185 else if (isStartElement())
186 parseUnknownElement();
187 }
188}
189
190void CMakeCbpParser::parseBuildTarget()
191{
192 buildTarget.clear();
193
194 if (attributes().hasAttribute("title"))
195 buildTarget.title = attributes().value("title").toString();
196 while (!atEnd()) {
197 readNext();
198 if (isEndElement()) {
199 if (!buildTarget.title.endsWith("/fast")
200 && !buildTarget.title.endsWith("_automoc")) {
201 if (buildTarget.output.isEmpty() && buildTarget.type == kExecutable)
202 buildTarget.type = kUtility;
203 buildTargets.append(buildTarget);
204 }
205 return;
206 } else if (name() == "Compiler") {
207 parseCompiler();
208 } else if (name() == "Option") {
209 parseBuildTargetOption();
210 } else if (name() == "MakeCommands") {
211 parseMakeCommands();
212 } else if (isStartElement()) {
213 parseUnknownElement();
214 }
215 }
216}
217
218void CMakeCbpParser::parseBuildTargetOption()
219{
220 if (attributes().hasAttribute("output")) {
221 buildTarget.output = (attributes().value("output").toString());
222 } else if (attributes().hasAttribute("type")) {
223 const QStringRef value = attributes().value("type");
224 if (value == "0" || value == "1")
225 buildTarget.type = kExecutable;
226 else if (value == "2")
227 buildTarget.type = kStaticLibrary;
228 else if (value == "3")
229 buildTarget.type = kDynamicLibrary;
230 else
231 buildTarget.type = kUtility;
232 } else if (attributes().hasAttribute("working_dir")) {
233 buildTarget.workingDirectory = attributes().value("working_dir").toString();
234
235 QFile cmakeSourceInfoFile(buildTarget.workingDirectory
236 + QStringLiteral("/CMakeFiles/CMakeDirectoryInformation.cmake"));
237 if (cmakeSourceInfoFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
238 QTextStream stream(&cmakeSourceInfoFile);
239 const QLatin1String searchSource("SET(CMAKE_RELATIVE_PATH_TOP_SOURCE \"");
240 while (!stream.atEnd()) {
241 const QString lineTopSource = stream.readLine().trimmed();
242 if (lineTopSource.startsWith(searchSource, Qt::CaseInsensitive)) {
243 QString src = lineTopSource.mid(searchSource.size());
244 src.chop(2);
245 buildTarget.sourceDirectory = src;
246 break;
247 }
248 }
249 }
250
251 if (buildTarget.sourceDirectory.isEmpty()) {
252 buildTarget.sourceDirectory = sourceDirectory;
253 }
254 }
255 while (!atEnd()) {
256 readNext();
257 if (isEndElement())
258 return;
259 else if (isStartElement())
260 parseUnknownElement();
261 }
262}
263
264QString CMakeCbpParser::getProjectName() const
265{
266 return projectName;
267}
268
269void CMakeCbpParser::parseOption()
270{
271 if (attributes().hasAttribute("title"))
272 projectName = attributes().value("title").toString();
273
274 if (attributes().hasAttribute("compiler"))
275 compiler = attributes().value("compiler").toString();
276
277 while (!atEnd()) {
278 readNext();
279 if (isEndElement())
280 return;
281 else if (isStartElement())
282 parseUnknownElement();
283 }
284}
285
286void CMakeCbpParser::parseMakeCommands()
287{
288 while (!atEnd()) {
289 readNext();
290 if (isEndElement())
291 return;
292 else if (name() == "Build")
293 parseBuildTargetBuild();
294 else if (name() == "Clean")
295 parseBuildTargetClean();
296 else if (isStartElement())
297 parseUnknownElement();
298 }
299}
300
301void CMakeCbpParser::parseBuildTargetBuild()
302{
303 if (attributes().hasAttribute("command"))
304 buildTarget.makeCommand = (attributes().value("command").toString());
305 while (!atEnd()) {
306 readNext();
307 if (isEndElement())
308 return;
309 else if (isStartElement())
310 parseUnknownElement();
311 }
312}
313
314void CMakeCbpParser::parseBuildTargetClean()
315{
316 while (!atEnd()) {
317 readNext();
318 if (isEndElement())
319 return;
320 else if (isStartElement())
321 parseUnknownElement();
322 }
323}
324
325void CMakeCbpParser::parseCompiler()
326{
327 while (!atEnd()) {
328 readNext();
329 if (isEndElement())
330 return;
331 else if (name() == "Add")
332 parseAdd();
333 else if (isStartElement())
334 parseUnknownElement();
335 }
336}
337
338void CMakeCbpParser::parseAdd()
339{
340 // CMake only supports <Add option=\> and <Add directory=\>
341 const QXmlStreamAttributes addAttributes = attributes();
342
343 QString includeDirectory = (addAttributes.value("directory").toString());
344
345 // allow adding multiple times because order happens
346 if (!includeDirectory.isEmpty())
347 buildTarget.includeFiles.append(includeDirectory);
348
349 QString compilerOption = addAttributes.value("option").toString();
350 // defining multiple times a macro to the same value makes no sense
351 if (!compilerOption.isEmpty() && !buildTarget.compilerOptions.contains(compilerOption)) {
352 buildTarget.compilerOptions.append(compilerOption);
353 }
354
355 while (!atEnd()) {
356 readNext();
357 if (isEndElement())
358 return;
359 else if (isStartElement())
360 parseUnknownElement();
361 }
362}
363
364void CMakeCbpParser::parseUnit()
365{
366 QString fileName = attributes().value("filename").toString();
367
368 parsingCMakeUnit = false;
369 unitTargets.clear();
370 while (!atEnd()) {
371 readNext();
372 if (isEndElement()) {
373 if (!fileName.endsWith(".rule") && !processedUnits.contains(fileName)) {
374 // Now check whether we found a virtual element beneath
375 if (parsingCMakeUnit) {
376 cmakeFileList.emplace_back(
377 std::make_unique<ProjectFile>(fileName, FileType::Project, false));
378 } else {
379 bool generated = false;
380 QString onlyFileName = fileName;
381 if ((onlyFileName.startsWith("moc_") && onlyFileName.endsWith(".cxx"))
382 || (onlyFileName.startsWith("ui_") && onlyFileName.endsWith(".h"))
383 || (onlyFileName.startsWith("qrc_") && onlyFileName.endsWith(".cxx")))
384 generated = true;
385
386 if (fileName.endsWith(".qrc")) {
387 srcFileList.emplace_back(
388 std::make_unique<ProjectFile>(fileName, FileType::Resource,
389 generated));
390 } else {
391 srcFileList.emplace_back(
392 std::make_unique<ProjectFile>(fileName, FileType::Source,
393 generated));
394 }
395 }
396 unitTargetMap.insert(fileName, unitTargets);
397 processedUnits.insert(fileName);
398 }
399 return;
400 } else if (name() == "Option") {
401 parseUnitOption();
402 } else if (isStartElement()) {
403 parseUnknownElement();
404 }
405 }
406}
407
408void CMakeCbpParser::parseUnitOption()
409{
410 const QXmlStreamAttributes optionAttributes = attributes();
411 parsingCMakeUnit = optionAttributes.hasAttribute("virtualFolder");
412 const QString target = optionAttributes.value("target").toString();
413 if (!target.isEmpty())
414 unitTargets.append(target);
415
416 while (!atEnd()) {
417 readNext();
418
419 if (isEndElement())
420 break;
421
422 if (isStartElement())
423 parseUnknownElement();
424 }
425}
426
427void CMakeCbpParser::parseUnknownElement()
428{
429 Q_ASSERT(isStartElement());
430
431 while (!atEnd()) {
432 readNext();
433
434 if (isEndElement())
435 break;
436
437 if (isStartElement())
438 parseUnknownElement();
439 }
440}
441
442bool CMakeCbpParser::hasCMakeFiles()
443{
444 return cmakeFileList.size() > 0;
445}
446
447const QList<CMakeBuildTarget> &CMakeCbpParser::getBuildTargets() const
448{
449 return buildTargets;
450}
451
452QString CMakeCbpParser::getCompilerName() const
453{
454 return compiler;
455}
456