1 #include "compiledbreader.h"
2 
3 #include <QDir>
4 #include <QFile>
5 #include <QFileInfo>
6 #include <QJsonArray>
7 #include <QJsonDocument>
8 #include <QJsonObject>
9 #include <QJsonValue>
10 #include <QProcess>
11 #include <QVariant>
12 
13 #include <KTextEditor/MainWindow>
14 
15 #include "gitprocess.h"
16 
17 #include <optional>
18 
19 // TODO: copied from kate project plugin, move to shared/
getDotGitPath(const QString & repo)20 std::optional<QString> getDotGitPath(const QString &repo)
21 {
22     /* This call is intentionally blocking because we need git path for everything else */
23     QProcess git;
24     if (!setupGitProcess(git, repo, {QStringLiteral("rev-parse"), QStringLiteral("--absolute-git-dir")})) {
25         return std::nullopt;
26     }
27     git.start(QProcess::ReadOnly);
28     if (git.waitForStarted() && git.waitForFinished(-1)) {
29         if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
30             return std::nullopt;
31         }
32         QString dotGitPath = QString::fromUtf8(git.readAllStandardOutput());
33         if (dotGitPath.endsWith(QLatin1String("\n"))) {
34             dotGitPath.remove(QLatin1String(".git\n"));
35         } else {
36             dotGitPath.remove(QLatin1String(".git"));
37         }
38         return dotGitPath;
39     }
40     return std::nullopt;
41 }
42 
locateCompileCommands(KTextEditor::MainWindow * mw,const QString & openedFile)43 QString CompileDBReader::locateCompileCommands(KTextEditor::MainWindow *mw, const QString &openedFile)
44 {
45     Q_ASSERT(mw);
46 
47     // Check using project
48     QObject *project = mw->pluginView(QStringLiteral("kateprojectplugin"));
49     if (project) {
50         QString baseDir = project->property("projectBaseDir").toString();
51         if (baseDir.endsWith(QLatin1Char('/'))) {
52             baseDir.chop(1);
53         }
54 
55         if (QFile::exists(baseDir + QStringLiteral("/compile_commands.json"))) {
56             return baseDir + QStringLiteral("/compile_commands.json");
57         }
58     }
59 
60     // Check using git
61     // For now it only checks for compile_commands in the .git/../ directory
62     QFileInfo fi(openedFile);
63     if (fi.exists()) {
64         QString base = fi.absolutePath();
65         auto basePathOptional = getDotGitPath(base);
66         if (basePathOptional.has_value()) {
67             auto basePath = basePathOptional.value();
68             if (basePath.endsWith(QLatin1Char('/'))) {
69                 basePath.chop(1);
70             }
71             if (QFile::exists(basePath + QStringLiteral("/compile_commands.json"))) {
72                 return basePath + QStringLiteral("/compile_commands.json");
73             }
74         }
75     }
76 
77     qWarning() << "Compile DB not found for file: " << openedFile;
78 
79     return QString();
80 }
81 
argsForFile(const QString & compile_commandsPath,const QString & file)82 QString CompileDBReader::argsForFile(const QString &compile_commandsPath, const QString &file)
83 {
84     QFile f(compile_commandsPath);
85     if (!f.open(QFile::ReadOnly)) {
86         // TODO: Use Output view to report error
87         qWarning() << "Failed to load compile_commands: " << f.errorString();
88         return {};
89     }
90 
91     QJsonParseError error;
92     QJsonDocument cmdCmds = QJsonDocument::fromJson(f.readAll(), &error);
93     if (error.error != QJsonParseError::NoError) {
94         qWarning() << "Failed to read compile_commands: " << error.errorString();
95         return {};
96     }
97 
98     if (!cmdCmds.isArray()) {
99         qWarning() << "Invalid compile_commands, root element is not an array";
100         return {};
101     }
102 
103     QJsonArray commandsArray = cmdCmds.array();
104 
105     for (const auto &cmdJV : commandsArray) {
106         auto compileCommand = cmdJV.toObject();
107         auto cmpCmdFile = compileCommand.value(QStringLiteral("file")).toString();
108 
109         QFileInfo fi(cmpCmdFile);
110         if (fi.isRelative()) {
111             QString dir = QDir::cleanPath(compileCommand.value(QStringLiteral("directory")).toString());
112             //             QString file = QDir::cleanPath(dir + QStringLiteral("/") + cmpCmdFile);
113         } else {
114             if (fi.canonicalFilePath() == file) {
115                 return compileCommand.value(QStringLiteral("command")).toString();
116             }
117         }
118     }
119 
120     qWarning() << "compile_command for " << file << " not found";
121     return {};
122 }
123 
addCurrentFilePathToCompileCommands(const QString & currentCompiler,QStringList & commands,const QString & fileBasePath)124 static void addCurrentFilePathToCompileCommands(const QString &currentCompiler, QStringList &commands, const QString &fileBasePath)
125 {
126     // For these compilers we include the current file path to
127     // the compiler commands
128     QStringList compilers = {
129         QStringLiteral("c++"),
130         QStringLiteral("g++"),
131         QStringLiteral("gcc"),
132         QStringLiteral("clang"),
133         QStringLiteral("clang++"),
134     };
135 
136     for (const auto &c : compilers) {
137         if (currentCompiler.contains(c)) {
138             commands << QStringLiteral("-I") + fileBasePath;
139         }
140     }
141 }
142 
143 /*
144  * Remove args like "-include xyz.h"
145  * CompilerExplorer doesn't like them
146  */
removeIncludeArgument(QStringList & commands)147 static void removeIncludeArgument(QStringList &commands)
148 {
149     QStringList toRemove;
150     for (int i = 0; i < commands.size(); ++i) {
151         if (commands.at(i) == QStringLiteral("-include")) {
152             if (i + 1 < commands.size()) {
153                 toRemove << commands.at(i);
154                 toRemove << commands.at(i + 1);
155                 ++i;
156             }
157         }
158     }
159 
160     for (const auto &rem : qAsConst(toRemove)) {
161         commands.removeAll(rem);
162     }
163 }
164 
filteredArgsForFile(const QString & compile_commandsPath,const QString & file)165 QString CompileDBReader::filteredArgsForFile(const QString &compile_commandsPath, const QString &file)
166 {
167     QString args = argsForFile(compile_commandsPath, file);
168 
169     QFileInfo fi(file);
170     QString fileBasePath = fi.canonicalPath();
171 
172     QStringList argsList = args.split(QLatin1Char(' '));
173     QString currentCompiler = argsList.takeFirst(); // First is the compiler, drop it
174     QStringList finalArgs;
175     finalArgs.reserve(argsList.size() - 2);
176 
177     for (auto &&arg : argsList) {
178         if (arg == QStringLiteral("-o"))
179             continue;
180         if (arg.endsWith(QStringLiteral(".o")))
181             continue;
182         if (arg == QStringLiteral("-c"))
183             continue;
184         if (file == arg || file.contains(arg))
185             continue;
186 
187         finalArgs << arg;
188     }
189 
190     removeIncludeArgument(finalArgs);
191 
192     addCurrentFilePathToCompileCommands(currentCompiler, finalArgs, fileBasePath);
193 
194     return finalArgs.join(QLatin1Char(' '));
195 }
196