1 /*
2     SPDX-FileCopyrightText: 2018 Anton Anikin <anton@anikin.xyz>
3     SPDX-FileCopyrightText: 2020 Friedrich W. H. Kossebau <kossebau@kde.org>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "compileanalyzer.h"
9 
10 // lib
11 #include "compileanalyzeutils.h"
12 #include "compileanalyzejob.h"
13 #include "compileanalyzeproblemmodel.h"
14 // KDevPlatform
15 #include <interfaces/iplugin.h>
16 #include <interfaces/icore.h>
17 #include <interfaces/context.h>
18 #include <interfaces/contextmenuextension.h>
19 #include <interfaces/idocument.h>
20 #include <interfaces/idocumentcontroller.h>
21 #include <interfaces/ilanguagecontroller.h>
22 #include <interfaces/iplugincontroller.h>
23 #include <interfaces/iproject.h>
24 #include <interfaces/iprojectcontroller.h>
25 #include <interfaces/iruncontroller.h>
26 #include <interfaces/iuicontroller.h>
27 #include <project/interfaces/ibuildsystemmanager.h>
28 #include <project/projectconfigpage.h>
29 #include <project/projectmodel.h>
30 #include <shell/problemmodelset.h>
31 #include <util/jobstatus.h>
32 // KF
33 #include <KActionCollection>
34 #include <KLocalizedString>
35 #include <KSharedConfig>
36 #include <KPluginFactory>
37 // Qt
38 #include <QAction>
39 #include <QMessageBox>
40 #include <QMimeType>
41 #include <QMimeDatabase>
42 #include <QThread>
43 
44 namespace KDevelop
45 {
46 
isSupportedMimeType(const QMimeType & mimeType)47 bool isSupportedMimeType(const QMimeType& mimeType)
48 {
49     const QString mime = mimeType.name();
50     return (mime == QLatin1String("text/x-c++src") || mime == QLatin1String("text/x-csrc"));
51 }
52 
CompileAnalyzer(IPlugin * plugin,const QString & toolName,const QString & toolIconName,const QString & fileActionId,const QString & allActionId,const QString & modelId,ProblemModel::Features modelFeatures,QObject * parent)53 CompileAnalyzer::CompileAnalyzer(IPlugin* plugin,
54                                  const QString& toolName, const QString& toolIconName,
55                                  const QString& fileActionId, const QString& allActionId,
56                                  const QString& modelId,
57                                  ProblemModel::Features modelFeatures,
58                                  QObject* parent)
59     : QObject(parent)
60     , m_core(plugin->core())
61     , m_toolName(toolName)
62     , m_toolIcon(QIcon::fromTheme(toolIconName))
63     , m_modelId(modelId)
64     , m_model(new CompileAnalyzeProblemModel(toolName, this))
65 {
66     m_model->setFeatures(modelFeatures);
67 
68     ProblemModelSet* problemModelSet = core()->languageController()->problemModelSet();
69     problemModelSet->addModel(m_modelId, m_toolName, m_model);
70 
71     auto actionCollection = plugin->actionCollection();
72 
73     m_checkFileAction = new QAction(m_toolIcon,
74                                     i18nc("@action", "Analyze Current File with %1", m_toolName), this);
75     connect(m_checkFileAction, &QAction::triggered, this, &CompileAnalyzer::runToolOnFile);
76     actionCollection->addAction(fileActionId, m_checkFileAction);
77 
78     m_checkProjectAction = new QAction(m_toolIcon,
79                                        i18nc("@action", "Analyze Current Project with %1", m_toolName), this);
80     connect(m_checkProjectAction, &QAction::triggered, this, &CompileAnalyzer::runToolOnAll);
81     actionCollection->addAction(allActionId, m_checkProjectAction);
82 
83     connect(core()->documentController(), &KDevelop::IDocumentController::documentClosed,
84             this, &CompileAnalyzer::updateActions);
85     connect(core()->documentController(), &KDevelop::IDocumentController::documentActivated,
86             this, &CompileAnalyzer::updateActions);
87 
88     connect(core()->projectController(), &KDevelop::IProjectController::projectOpened,
89             this, &CompileAnalyzer::updateActions);
90     connect(core()->projectController(), &KDevelop::IProjectController::projectClosed,
91             this, &CompileAnalyzer::handleProjectClosed);
92 
93     connect(m_model, &CompileAnalyzeProblemModel::rerunRequested,
94             this, &CompileAnalyzer::handleRerunRequest);
95 
96     updateActions();
97 }
98 
~CompileAnalyzer()99 CompileAnalyzer::~CompileAnalyzer()
100 {
101     killJob();
102 
103     ProblemModelSet* problemModelSet = core()->languageController()->problemModelSet();
104     problemModelSet->removeModel(m_modelId);
105 }
106 
isOutputToolViewPreferred() const107 bool CompileAnalyzer::isOutputToolViewPreferred() const
108 {
109     return false;
110 }
111 
core() const112 ICore* CompileAnalyzer::core() const
113 {
114     return m_core;
115 }
116 
updateActions()117 void CompileAnalyzer::updateActions()
118 {
119     m_checkFileAction->setEnabled(false);
120     m_checkProjectAction->setEnabled(false);
121 
122     if (isRunning()) {
123         return;
124     }
125 
126     IDocument* activeDocument = core()->documentController()->activeDocument();
127     if (!activeDocument) {
128         return;
129     }
130 
131     auto currentProject = core()->projectController()->findProjectForUrl(activeDocument->url());
132     if (!currentProject) {
133         return;
134     }
135 
136     if (!currentProject->buildSystemManager()) {
137         return;
138     }
139 
140     if (isSupportedMimeType(activeDocument->mimeType())) {
141         m_checkFileAction->setEnabled(true);
142     }
143     m_checkProjectAction->setEnabled(true);
144 }
145 
handleProjectClosed(IProject * project)146 void CompileAnalyzer::handleProjectClosed(IProject* project)
147 {
148     if (project != m_model->project()) {
149         return;
150     }
151 
152     killJob();
153     m_model->reset();
154 }
155 
runTool(bool allFiles)156 void CompileAnalyzer::runTool(bool allFiles)
157 {
158     auto doc = core()->documentController()->activeDocument();
159     if (doc == nullptr) {
160         QMessageBox::critical(nullptr, m_toolName,
161                               i18n("No suitable active file, unable to deduce project."));
162         return;
163     }
164 
165     runTool(doc->url(), allFiles);
166 }
167 
runTool(const QUrl & url,bool allFiles)168 void CompileAnalyzer::runTool(const QUrl& url, bool allFiles)
169 {
170     KDevelop::IProject* project = core()->projectController()->findProjectForUrl(url);
171     if (!project) {
172         QMessageBox::critical(nullptr, m_toolName,
173                               i18n("Active file isn't in a project."));
174         return;
175     }
176 
177     m_model->reset(project, url, allFiles);
178 
179     const auto buildDir = project->buildSystemManager()->buildDirectory(project->projectItem());
180 
181     QString error;
182     const auto filePaths = Utils::filesFromCompilationDatabase(buildDir, url, allFiles, error);
183 
184     if (!error.isEmpty()) {
185         QMessageBox::critical(nullptr, m_toolName,
186                               i18n("Unable to start check for '%1':\n\n%2", url.toLocalFile(), error));
187         return;
188     }
189 
190     m_job = createJob(project, buildDir, url, filePaths);
191 
192     connect(m_job, &CompileAnalyzeJob::problemsDetected, m_model, &CompileAnalyzeProblemModel::addProblems);
193     connect(m_job, &KJob::finished, this, &CompileAnalyzer::result);
194 
195     core()->uiController()->registerStatus(new KDevelop::JobStatus(m_job, m_toolName));
196     core()->runController()->registerJob(m_job);
197 
198     updateActions();
199 
200     if (isOutputToolViewPreferred()) {
201         raiseOutputToolView();
202     } else {
203         raiseProblemsToolView();
204     }
205 }
206 
raiseProblemsToolView()207 void CompileAnalyzer::raiseProblemsToolView()
208 {
209     ProblemModelSet* problemModelSet = core()->languageController()->problemModelSet();
210     problemModelSet->showModel(m_modelId);
211 }
212 
raiseOutputToolView()213 void CompileAnalyzer::raiseOutputToolView()
214 {
215     core()->uiController()->findToolView(
216         i18ndc("kdevstandardoutputview", "@title:window", "Test"),
217         nullptr,
218         KDevelop::IUiController::FindFlags::Raise);
219 }
220 
isRunning() const221 bool CompileAnalyzer::isRunning() const
222 {
223     return (m_job != nullptr);
224 }
225 
killJob()226 void CompileAnalyzer::killJob()
227 {
228     if (m_job) {
229         m_job->kill(KJob::EmitResult);
230     }
231 }
232 
runToolOnFile()233 void CompileAnalyzer::runToolOnFile()
234 {
235     bool allFiles = false;
236     runTool(allFiles);
237 }
238 
runToolOnAll()239 void CompileAnalyzer::runToolOnAll()
240 {
241     bool allFiles = true;
242     runTool(allFiles);
243 }
244 
handleRerunRequest(const QUrl & url,bool allFiles)245 void CompileAnalyzer::handleRerunRequest(const QUrl& url, bool allFiles)
246 {
247     if (!isRunning()) {
248         runTool(url, allFiles);
249     }
250 }
251 
result(KJob * job)252 void CompileAnalyzer::result(KJob* job)
253 {
254     Q_UNUSED(job);
255 
256     if (!core()->projectController()->projects().contains(m_model->project())) {
257         m_model->reset();
258     } else {
259         m_model->finishAddProblems();
260 
261         if (m_job->status() == KDevelop::OutputExecuteJob::JobStatus::JobSucceeded ||
262             m_job->status() == KDevelop::OutputExecuteJob::JobStatus::JobCanceled) {
263             raiseProblemsToolView();
264         } else {
265             raiseOutputToolView();
266         }
267     }
268 
269     m_job = nullptr; // job automatically deletes itself later
270 
271     updateActions();
272 }
273 
fillContextMenuExtension(ContextMenuExtension & extension,Context * context,QWidget * parent)274 void CompileAnalyzer::fillContextMenuExtension(ContextMenuExtension &extension,
275                                                Context* context, QWidget* parent)
276 {
277     if (context->hasType(KDevelop::Context::EditorContext) && !isRunning()) {
278         IDocument* doc = core()->documentController()->activeDocument();
279 
280         auto project = core()->projectController()->findProjectForUrl(doc->url());
281         if (!project || !project->buildSystemManager()) {
282             return;
283         }
284         if (isSupportedMimeType(doc->mimeType())) {
285             auto action = new QAction(m_toolIcon, m_toolName, parent);
286             connect(action, &QAction::triggered, this, &CompileAnalyzer::runToolOnFile);
287             extension.addAction(KDevelop::ContextMenuExtension::AnalyzeFileGroup, action);
288         }
289         auto action = new QAction(m_toolIcon, m_toolName, parent);
290         connect(action, &QAction::triggered, this, &CompileAnalyzer::runToolOnAll);
291         extension.addAction(KDevelop::ContextMenuExtension::AnalyzeProjectGroup, action);
292     }
293 
294     if (context->hasType(KDevelop::Context::ProjectItemContext) && !isRunning()) {
295         auto projectItemContext = dynamic_cast<KDevelop::ProjectItemContext*>(context);
296         const auto items = projectItemContext->items();
297         if (items.size() != 1) {
298             return;
299         }
300 
301         const auto item = items.first();
302         const auto itemType = item->type();
303         if ((itemType != KDevelop::ProjectBaseItem::File) &&
304             (itemType != KDevelop::ProjectBaseItem::Folder) &&
305             (itemType != KDevelop::ProjectBaseItem::BuildFolder)) {
306             return;
307         }
308         if (itemType == KDevelop::ProjectBaseItem::File) {
309             const QMimeType mimetype = QMimeDatabase().mimeTypeForUrl(item->path().toUrl());
310             if (!isSupportedMimeType(mimetype)) {
311                 return;
312             }
313         }
314         if (!item->project()->buildSystemManager()) {
315             return;
316         }
317 
318         auto action = new QAction(m_toolIcon, m_toolName, parent);
319         connect(action, &QAction::triggered, this, [this, item]() {
320             runTool(item->path().toUrl());
321         });
322         extension.addAction(KDevelop::ContextMenuExtension::AnalyzeProjectGroup, action);
323     }
324 }
325 
326 }
327