1 /* ide-diagnostics-manager.c
2  *
3  * Copyright 2016-2019 Christian Hergert <chergert@redhat.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * SPDX-License-Identifier: GPL-3.0-or-later
19  */
20 
21 #define G_LOG_DOMAIN "ide-diagnostics-manager"
22 
23 #include "config.h"
24 
25 #include <gtksourceview/gtksource.h>
26 #include <libide-plugins.h>
27 
28 #include "ide-buffer.h"
29 #include "ide-buffer-manager.h"
30 #include "ide-buffer-private.h"
31 #include "ide-diagnostic.h"
32 #include "ide-diagnostic-provider.h"
33 #include "ide-diagnostics.h"
34 #include "ide-diagnostics-manager.h"
35 #include "ide-diagnostics-manager-private.h"
36 
37 #define DEFAULT_DIAGNOSE_DELAY 333
38 #define DIAG_GROUP_MAGIC       0xF1282727
39 #define IS_DIAGNOSTICS_GROUP(g) ((g) && (g)->magic == DIAG_GROUP_MAGIC)
40 
41 typedef struct
42 {
43   /*
44    * Used to give ourself a modicum of assurance our structure hasn't
45    * been miss-used.
46    */
47   guint magic;
48 
49   /*
50    * This is our identifier for the diagnostics. We use this as the key in
51    * the hash table so that we can quickly find the target buffer. If the
52    * IdeBuffer:file property changes, we will have to fallback to the
53    * buffer to clear old entries.
54    */
55   GFile *file;
56 
57   /*
58    * This hash table uses the given provider as the key and the last
59    * reported IdeDiagnostics as the value.
60    */
61   GHashTable *diagnostics_by_provider;
62 
63   /*
64    * This extension set adapter is used to update the providers that are
65    * available based on the buffers current language. They may change
66    * at runtime due to the buffers language changing. When that happens
67    * we purge items from @diagnostics_by_provider and queue a diagnose
68    * request of the new provider.
69    */
70   IdeExtensionSetAdapter *adapter;
71 
72   /* The most recent bytes we received for a future diagnosis. */
73   GBytes *contents;
74 
75   /* The last language id we were notified about */
76   const gchar *lang_id;
77 
78   /*
79    * This is our sequence number for diagnostics. It is monotonically
80    * increasing with every diagnostic discovered.
81    */
82   guint sequence;
83 
84   /*
85    * If we are currently diagnosing, then this will be set to a
86    * number greater than zero.
87    */
88   guint in_diagnose;
89 
90   /*
91    * If we need a diagnose this bit will be set. If we complete a
92    * diagnosis and this bit is set, then we will automatically queue
93    * another diagnose upon completion.
94    */
95   guint needs_diagnose : 1;
96 
97   /*
98    * This bit is set if we know the file or buffer has diagnostics. This
99    * is useful when we've cleaned up our extensions and no longer have
100    * the diagnostics loaded in memory, but we know that it previously
101    * had diagnostics which have not been rectified.
102    */
103   guint has_diagnostics : 1;
104 
105   /*
106    * This bit is set when the group has been removed from the
107    * IdeDiagnosticsManager. That allows the providers to cleanup
108    * as necessary when their async operations complete.
109    */
110   guint was_removed : 1;
111 
112 } IdeDiagnosticsGroup;
113 
114 struct _IdeDiagnosticsManager
115 {
116   IdeObject parent_instance;
117 
118   /*
119    * This hashtable contains a mapping of GFile to the IdeDiagnosticsGroup
120    * for the file. When a buffer is renamed (the IdeBuffer:file property
121    * is changed) we need to update this entry so it reflects the new
122    * location.
123    */
124   GHashTable *groups_by_file;
125 
126   /*
127    * If any group has a queued diagnose in process, this will be set so
128    * we can coalesce the dispatch of everything at the same time.
129    */
130   guint queued_diagnose_source;
131 };
132 
133 enum {
134   PROP_0,
135   PROP_BUSY,
136   N_PROPS
137 };
138 
139 enum {
140   CHANGED,
141   N_SIGNALS
142 };
143 
144 
145 static gboolean ide_diagnostics_manager_clear_by_provider (IdeDiagnosticsManager *self,
146                                                            IdeDiagnosticProvider *provider);
147 static void     ide_diagnostics_manager_add_diagnostic    (IdeDiagnosticsManager *self,
148                                                            IdeDiagnosticProvider *provider,
149                                                            IdeDiagnostic         *diagnostic);
150 static void     ide_diagnostics_group_queue_diagnose      (IdeDiagnosticsGroup   *group,
151                                                            IdeDiagnosticsManager *self);
152 
153 
154 static GParamSpec *properties [N_PROPS];
155 static guint signals [N_SIGNALS];
156 
G_DEFINE_FINAL_TYPE(IdeDiagnosticsManager,ide_diagnostics_manager,IDE_TYPE_OBJECT)157 G_DEFINE_FINAL_TYPE (IdeDiagnosticsManager, ide_diagnostics_manager, IDE_TYPE_OBJECT)
158 
159 static void
160 free_diagnostics (gpointer data)
161 {
162   IdeDiagnostics *diagnostics = data;
163 
164   g_clear_object (&diagnostics);
165 }
166 
167 static inline guint
diagnostics_get_size(IdeDiagnostics * diags)168 diagnostics_get_size (IdeDiagnostics *diags)
169 {
170   return diags ? g_list_model_get_n_items (G_LIST_MODEL (diags)) : 0;
171 }
172 
173 static void
ide_diagnostics_group_finalize(IdeDiagnosticsGroup * group)174 ide_diagnostics_group_finalize (IdeDiagnosticsGroup *group)
175 {
176   g_assert (IDE_IS_MAIN_THREAD ());
177   g_assert (group != NULL);
178   g_assert (IS_DIAGNOSTICS_GROUP (group));
179 
180   group->magic = 0;
181 
182   g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
183   g_clear_pointer (&group->contents, g_bytes_unref);
184   ide_clear_and_destroy_object (&group->adapter);
185   g_clear_object (&group->file);
186 }
187 
188 static IdeDiagnosticsGroup *
ide_diagnostics_group_new(GFile * file)189 ide_diagnostics_group_new (GFile *file)
190 {
191   IdeDiagnosticsGroup *group;
192 
193   g_assert (IDE_IS_MAIN_THREAD ());
194   g_assert (G_IS_FILE (file));
195 
196   group = g_rc_box_new0 (IdeDiagnosticsGroup);
197   group->magic = DIAG_GROUP_MAGIC;
198   group->file = g_object_ref (file);
199 
200   return group;
201 }
202 
203 static IdeDiagnosticsGroup *
ide_diagnostics_group_ref(IdeDiagnosticsGroup * group)204 ide_diagnostics_group_ref (IdeDiagnosticsGroup *group)
205 {
206   g_assert (IDE_IS_MAIN_THREAD ());
207   g_assert (group != NULL);
208   g_assert (IS_DIAGNOSTICS_GROUP (group));
209 
210   return g_rc_box_acquire (group);
211 }
212 
213 static void
ide_diagnostics_group_unref(IdeDiagnosticsGroup * group)214 ide_diagnostics_group_unref (IdeDiagnosticsGroup *group)
215 {
216   g_assert (IDE_IS_MAIN_THREAD ());
217   g_assert (group != NULL);
218   g_assert (IS_DIAGNOSTICS_GROUP (group));
219 
220   g_rc_box_release_full (group, (GDestroyNotify)ide_diagnostics_group_finalize);
221 }
222 
223 static guint
ide_diagnostics_group_has_diagnostics(IdeDiagnosticsGroup * group)224 ide_diagnostics_group_has_diagnostics (IdeDiagnosticsGroup *group)
225 {
226   g_assert (IDE_IS_MAIN_THREAD ());
227   g_assert (group != NULL);
228   g_assert (IS_DIAGNOSTICS_GROUP (group));
229 
230   if (group->diagnostics_by_provider != NULL)
231     {
232       GHashTableIter iter;
233       gpointer value;
234 
235       g_hash_table_iter_init (&iter, group->diagnostics_by_provider);
236 
237       while (g_hash_table_iter_next (&iter, NULL, &value))
238         {
239           IdeDiagnostics *diagnostics = value;
240 
241           if (diagnostics_get_size (diagnostics) > 0)
242             return TRUE;
243         }
244     }
245 
246   return FALSE;
247 }
248 
249 static gboolean
ide_diagnostics_group_can_dispose(IdeDiagnosticsGroup * group)250 ide_diagnostics_group_can_dispose (IdeDiagnosticsGroup *group)
251 {
252   g_assert (IDE_IS_MAIN_THREAD ());
253   g_assert (group != NULL);
254   g_assert (IS_DIAGNOSTICS_GROUP (group));
255 
256   /*
257    * We can cleanup this group if we don't have a buffer loaded and
258    * the adapters have been unloaded and there are no diagnostics
259    * registered for the group.
260    */
261 
262   return group->adapter == NULL &&
263          group->has_diagnostics == FALSE;
264 }
265 
266 static void
ide_diagnostics_group_add(IdeDiagnosticsGroup * group,IdeDiagnosticProvider * provider,IdeDiagnostic * diagnostic)267 ide_diagnostics_group_add (IdeDiagnosticsGroup   *group,
268                            IdeDiagnosticProvider *provider,
269                            IdeDiagnostic         *diagnostic)
270 {
271   IdeDiagnostics *diagnostics;
272 
273   g_assert (IDE_IS_MAIN_THREAD ());
274   g_assert (group != NULL);
275   g_assert (IS_DIAGNOSTICS_GROUP (group));
276   g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
277   g_assert (diagnostic != NULL);
278 
279   if (group->diagnostics_by_provider == NULL)
280     group->diagnostics_by_provider = g_hash_table_new_full (NULL, NULL, NULL, free_diagnostics);
281 
282   diagnostics = g_hash_table_lookup (group->diagnostics_by_provider, provider);
283 
284   if (diagnostics == NULL)
285     {
286       diagnostics = ide_diagnostics_new ();
287       g_hash_table_insert (group->diagnostics_by_provider, provider, diagnostics);
288     }
289 
290   ide_diagnostics_add (diagnostics, diagnostic);
291 
292   group->has_diagnostics = TRUE;
293   group->sequence++;
294 }
295 
296 static void
ide_diagnostics_group_diagnose_cb(GObject * object,GAsyncResult * result,gpointer user_data)297 ide_diagnostics_group_diagnose_cb (GObject      *object,
298                                    GAsyncResult *result,
299                                    gpointer      user_data)
300 {
301   IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)object;
302   g_autoptr(IdeDiagnosticsManager) self = user_data;
303   g_autoptr(IdeDiagnostics) diagnostics = NULL;
304   g_autoptr(GError) error = NULL;
305   IdeDiagnosticsGroup *group;
306   gboolean changed;
307 
308   IDE_ENTRY;
309 
310   g_assert (IDE_IS_MAIN_THREAD ());
311   g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
312   g_assert (G_IS_ASYNC_RESULT (result));
313   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
314 
315   IDE_TRACE_MSG ("%s diagnosis completed", G_OBJECT_TYPE_NAME (provider));
316 
317   diagnostics = ide_diagnostic_provider_diagnose_finish (provider, result, &error);
318 
319   if (error != NULL &&
320       !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
321       !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
322     g_debug ("%s", error->message);
323 
324   /*
325    * This fetches the group our provider belongs to. Since the group is
326    * reference counted (and we only release it when our provider is
327    * finalized), we should be guaranteed we have a valid group.
328    */
329   group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
330 
331   if (group == NULL)
332     {
333       /* Warning and bail if we failed to get the diagnostic group.
334        * This shouldn't be happening, but I have definitely seen it
335        * so it is probably related to disposal.
336        */
337       g_warning ("Failed to locate group, possibly disposed.");
338       IDE_EXIT;
339     }
340 
341   g_assert (IS_DIAGNOSTICS_GROUP (group));
342 
343   /*
344    * Clear all of our old diagnostics no matter where they ended up.
345    */
346   changed = ide_diagnostics_manager_clear_by_provider (self, provider);
347 
348   /*
349    * The following adds diagnostics to the appropriate group, but tries the
350    * group we belong to first as our fast path. That will almost always be
351    * the case, except when a diagnostic came up for a header or something
352    * while parsing a given file.
353    */
354   if (diagnostics != NULL)
355     {
356       guint length = diagnostics_get_size (diagnostics);
357 
358       for (guint i = 0; i < length; i++)
359         {
360           g_autoptr(IdeDiagnostic) diagnostic = g_list_model_get_item (G_LIST_MODEL (diagnostics), i);
361           GFile *file = ide_diagnostic_get_file (diagnostic);
362 
363           if (file != NULL)
364             {
365               if (g_file_equal (file, group->file))
366                 ide_diagnostics_group_add (group, provider, diagnostic);
367               else
368                 ide_diagnostics_manager_add_diagnostic (self, provider, diagnostic);
369             }
370         }
371 
372       if (length > 0)
373         changed = TRUE;
374     }
375 
376   group->in_diagnose--;
377 
378   /*
379    * Ensure we increment our sequence number even when no diagnostics were
380    * reported. This ensures that the gutter gets cleared and line-flags
381    * cache updated.
382    */
383   group->sequence++;
384 
385   /*
386    * Since the individual groups have sequence numbers associated with changes,
387    * it's okay to emit this for every provider completion. That allows the UIs
388    * to update faster as each provider completes at the expensive of a little
389    * more CPU activity.
390    */
391   if (changed)
392     g_signal_emit (self, signals [CHANGED], 0);
393 
394   /*
395    * If there are no more diagnostics providers active and the group needs
396    * another diagnosis, then we can start the next one now.
397    *
398    * If we are completing this diagnosis and the buffer was already released
399    * (and other diagnose providers have unloaded), we might be able to clean
400    * up the group and be done with things.
401    */
402   if (group->was_removed == FALSE && group->in_diagnose == 0 && group->needs_diagnose)
403     {
404       ide_diagnostics_group_queue_diagnose (group, self);
405     }
406   else if (ide_diagnostics_group_can_dispose (group))
407     {
408       group->was_removed = TRUE;
409       g_hash_table_remove (self->groups_by_file, group->file);
410       IDE_EXIT;
411     }
412 
413   IDE_EXIT;
414 }
415 
416 static void
ide_diagnostics_group_diagnose_foreach(IdeExtensionSetAdapter * adapter,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)417 ide_diagnostics_group_diagnose_foreach (IdeExtensionSetAdapter *adapter,
418                                         PeasPluginInfo         *plugin_info,
419                                         PeasExtension          *exten,
420                                         gpointer                user_data)
421 {
422   IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
423   IdeDiagnosticsManager *self = user_data;
424   IdeDiagnosticsGroup *group;
425 
426   IDE_ENTRY;
427 
428   g_assert (IDE_IS_MAIN_THREAD ());
429   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
430   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
431   g_assert (plugin_info != NULL);
432   g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
433 
434   group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
435 
436   g_assert (group != NULL);
437   g_assert (IS_DIAGNOSTICS_GROUP (group));
438 
439   group->in_diagnose++;
440 
441 #ifdef IDE_ENABLE_TRACE
442   {
443     g_autofree gchar *uri = g_file_get_uri (group->file);
444     IDE_TRACE_MSG ("Beginning diagnose on %s with provider %s",
445                    uri, G_OBJECT_TYPE_NAME (provider));
446   }
447 #endif
448 
449   ide_diagnostic_provider_diagnose_async (provider,
450                                           group->file,
451                                           group->contents,
452                                           group->lang_id,
453                                           NULL,
454                                           ide_diagnostics_group_diagnose_cb,
455                                           g_object_ref (self));
456 
457   IDE_EXIT;
458 }
459 
460 static void
ide_diagnostics_group_diagnose(IdeDiagnosticsGroup * group,IdeDiagnosticsManager * self)461 ide_diagnostics_group_diagnose (IdeDiagnosticsGroup   *group,
462                                 IdeDiagnosticsManager *self)
463 {
464   IDE_ENTRY;
465 
466   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
467   g_assert (group != NULL);
468   g_assert (group->in_diagnose == FALSE);
469   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (group->adapter));
470 
471   group->needs_diagnose = FALSE;
472   group->has_diagnostics = FALSE;
473 
474   if (group->contents == NULL)
475     group->contents = g_bytes_new ("", 0);
476 
477   ide_extension_set_adapter_foreach (group->adapter,
478                                      ide_diagnostics_group_diagnose_foreach,
479                                      self);
480 
481   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
482 
483   IDE_EXIT;
484 }
485 
486 static gboolean
ide_diagnostics_manager_begin_diagnose(gpointer data)487 ide_diagnostics_manager_begin_diagnose (gpointer data)
488 {
489   IdeDiagnosticsManager *self = data;
490   GHashTableIter iter;
491   gpointer value;
492 
493   IDE_ENTRY;
494 
495   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
496 
497   self->queued_diagnose_source = 0;
498 
499   g_hash_table_iter_init (&iter, self->groups_by_file);
500 
501   while (g_hash_table_iter_next (&iter, NULL, &value))
502     {
503       IdeDiagnosticsGroup *group = value;
504 
505       g_assert (group != NULL);
506       g_assert (IS_DIAGNOSTICS_GROUP (group));
507 
508       if (group->needs_diagnose && group->adapter != NULL && group->in_diagnose == 0)
509         ide_diagnostics_group_diagnose (group, self);
510     }
511 
512   IDE_RETURN (G_SOURCE_REMOVE);
513 }
514 
515 static void
ide_diagnostics_group_queue_diagnose(IdeDiagnosticsGroup * group,IdeDiagnosticsManager * self)516 ide_diagnostics_group_queue_diagnose (IdeDiagnosticsGroup   *group,
517                                       IdeDiagnosticsManager *self)
518 {
519   g_assert (group != NULL);
520   g_assert (IS_DIAGNOSTICS_GROUP (group));
521   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
522 
523   /*
524    * This checks to see if we are diagnosing and if not queues a diagnose.
525    * If a diagnosis is already running, we don't need to do anything now
526    * because the completion of the diagnose will tick off the next diagnose
527    * upon seening group->needs_diagnose==TRUE.
528    */
529 
530   group->needs_diagnose = TRUE;
531 
532   if (group->in_diagnose == 0 && self->queued_diagnose_source == 0)
533     self->queued_diagnose_source = g_timeout_add_full (G_PRIORITY_LOW,
534                                                        DEFAULT_DIAGNOSE_DELAY,
535                                                        ide_diagnostics_manager_begin_diagnose,
536                                                        self, NULL);
537 }
538 
539 static void
ide_diagnostics_manager_finalize(GObject * object)540 ide_diagnostics_manager_finalize (GObject *object)
541 {
542   IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)object;
543 
544   g_clear_handle_id (&self->queued_diagnose_source, g_source_remove);
545   g_clear_pointer (&self->groups_by_file, g_hash_table_unref);
546 
547   G_OBJECT_CLASS (ide_diagnostics_manager_parent_class)->finalize (object);
548 }
549 
550 static void
ide_diagnostics_manager_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)551 ide_diagnostics_manager_get_property (GObject    *object,
552                                       guint       prop_id,
553                                       GValue     *value,
554                                       GParamSpec *pspec)
555 {
556   IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)object;
557 
558   switch (prop_id)
559     {
560     case PROP_BUSY:
561       g_value_set_boolean (value, ide_diagnostics_manager_get_busy (self));
562       break;
563 
564     default:
565       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
566     }
567 }
568 
569 static void
ide_diagnostics_manager_class_init(IdeDiagnosticsManagerClass * klass)570 ide_diagnostics_manager_class_init (IdeDiagnosticsManagerClass *klass)
571 {
572   GObjectClass *object_class = G_OBJECT_CLASS (klass);
573 
574   object_class->finalize = ide_diagnostics_manager_finalize;
575   object_class->get_property = ide_diagnostics_manager_get_property;
576 
577   properties [PROP_BUSY] =
578     g_param_spec_boolean ("busy",
579                           "Busy",
580                           "If the diagnostics manager is busy",
581                           FALSE,
582                           (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
583 
584   g_object_class_install_properties (object_class, N_PROPS, properties);
585 
586   /**
587    * IdeDiagnosticsManager::changed:
588    * @self: an #IdeDiagnosticsManager
589    *
590    * This signal is emitted when the diagnostics have changed for any
591    * file managed by the IdeDiagnosticsManager.
592    *
593    * Since: 3.32
594    */
595   signals [CHANGED] =
596     g_signal_new ("changed",
597                   G_TYPE_FROM_CLASS (klass),
598                   G_SIGNAL_RUN_LAST,
599                   0, NULL, NULL, NULL, G_TYPE_NONE, 0);
600 }
601 
602 static void
ide_diagnostics_manager_init(IdeDiagnosticsManager * self)603 ide_diagnostics_manager_init (IdeDiagnosticsManager *self)
604 {
605   self->groups_by_file = g_hash_table_new_full (g_file_hash,
606                                                 (GEqualFunc)g_file_equal,
607                                                 NULL,
608                                                 (GDestroyNotify)ide_diagnostics_group_unref);
609 }
610 
611 static void
ide_diagnostics_manager_add_diagnostic(IdeDiagnosticsManager * self,IdeDiagnosticProvider * provider,IdeDiagnostic * diagnostic)612 ide_diagnostics_manager_add_diagnostic (IdeDiagnosticsManager *self,
613                                         IdeDiagnosticProvider *provider,
614                                         IdeDiagnostic         *diagnostic)
615 {
616   IdeDiagnosticsGroup *group;
617   GFile *file;
618 
619   g_assert (IDE_IS_MAIN_THREAD ());
620   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
621   g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
622   g_assert (diagnostic != NULL);
623 
624   /*
625    * This is our slow path for adding a diagnostic to the system. We have
626    * to locate the proper group for the diagnostic and then insert it
627    * into that group.
628    */
629 
630   if (NULL == (file = ide_diagnostic_get_file (diagnostic)))
631     return;
632 
633   group = g_hash_table_lookup (self->groups_by_file, file);
634 
635   if (group == NULL)
636     {
637       group = ide_diagnostics_group_new (file);
638       g_hash_table_insert (self->groups_by_file, group->file, group);
639     }
640 
641   g_assert (group != NULL);
642   g_assert (IS_DIAGNOSTICS_GROUP (group));
643 
644   ide_diagnostics_group_add (group, provider, diagnostic);
645 }
646 
647 static IdeDiagnosticsGroup *
ide_diagnostics_manager_find_group(IdeDiagnosticsManager * self,GFile * file)648 ide_diagnostics_manager_find_group (IdeDiagnosticsManager *self,
649                                     GFile                 *file)
650 {
651   IdeDiagnosticsGroup *group;
652 
653   g_assert (IDE_IS_MAIN_THREAD ());
654   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
655   g_assert (G_IS_FILE (file));
656 
657   if (!(group = g_hash_table_lookup (self->groups_by_file, file)))
658     {
659       group = ide_diagnostics_group_new (file);
660       g_hash_table_insert (self->groups_by_file, group->file, group);
661     }
662 
663   g_assert (group != NULL);
664   g_assert (IS_DIAGNOSTICS_GROUP (group));
665 
666   return group;
667 }
668 
669 static IdeDiagnosticsGroup *
ide_diagnostics_manager_find_group_from_adapter(IdeDiagnosticsManager * self,IdeExtensionSetAdapter * adapter)670 ide_diagnostics_manager_find_group_from_adapter (IdeDiagnosticsManager  *self,
671                                                  IdeExtensionSetAdapter *adapter)
672 {
673   GHashTableIter iter;
674   gpointer value;
675 
676   g_assert (IDE_IS_MAIN_THREAD ());
677   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
678   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
679 
680   g_hash_table_iter_init (&iter, self->groups_by_file);
681 
682   while (g_hash_table_iter_next (&iter, NULL, &value))
683     {
684       IdeDiagnosticsGroup *group = value;
685 
686       g_assert (group != NULL);
687       g_assert (IS_DIAGNOSTICS_GROUP (group));
688 
689       if (group->adapter == adapter)
690         return group;
691     }
692 
693   g_assert_not_reached ();
694 
695   return NULL;
696 }
697 
698 static void
ide_diagnostics_manager_provider_invalidated(IdeDiagnosticsManager * self,IdeDiagnosticProvider * provider)699 ide_diagnostics_manager_provider_invalidated (IdeDiagnosticsManager *self,
700                                               IdeDiagnosticProvider *provider)
701 {
702   IdeDiagnosticsGroup *group;
703 
704   IDE_ENTRY;
705 
706   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
707   g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
708 
709   group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
710 
711   ide_diagnostics_group_queue_diagnose (group, self);
712 
713   IDE_EXIT;
714 }
715 
716 static void
ide_diagnostics_manager_extension_added(IdeExtensionSetAdapter * adapter,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)717 ide_diagnostics_manager_extension_added (IdeExtensionSetAdapter *adapter,
718                                          PeasPluginInfo         *plugin_info,
719                                          PeasExtension          *exten,
720                                          gpointer                user_data)
721 {
722   IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
723   IdeDiagnosticsManager *self = user_data;
724   IdeDiagnosticsGroup *group;
725 
726   IDE_ENTRY;
727 
728   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
729   g_assert (plugin_info != NULL);
730   g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
731   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
732 
733   group = ide_diagnostics_manager_find_group_from_adapter (self, adapter);
734 
735   /*
736    * We will need access to the group upon completion of the diagnostics,
737    * so we add a reference to the group and allow it to be automatically
738    * cleaned up when the provider finalizes.
739    */
740   g_object_set_data_full (G_OBJECT (provider),
741                           "IDE_DIAGNOSTICS_GROUP",
742                           ide_diagnostics_group_ref (group),
743                           (GDestroyNotify)ide_diagnostics_group_unref);
744 
745   /*
746    * We insert a dummy entry into the hashtable upon creation so
747    * that when an async diagnosis completes we can use the presence
748    * of this key to know if we've been unloaded.
749    */
750   g_hash_table_insert (group->diagnostics_by_provider, provider, NULL);
751 
752   /*
753    * We need to keep track of when the provider has been invalidated so
754    * that we can queue another request to fetch the diagnostics.
755    */
756   g_signal_connect_object (provider,
757                            "invalidated",
758                            G_CALLBACK (ide_diagnostics_manager_provider_invalidated),
759                            self,
760                            G_CONNECT_SWAPPED);
761 
762   ide_diagnostic_provider_load (provider);
763 
764   ide_diagnostics_group_queue_diagnose (group, self);
765 
766   IDE_EXIT;
767 }
768 
769 static gboolean
ide_diagnostics_manager_clear_by_provider(IdeDiagnosticsManager * self,IdeDiagnosticProvider * provider)770 ide_diagnostics_manager_clear_by_provider (IdeDiagnosticsManager *self,
771                                            IdeDiagnosticProvider *provider)
772 {
773   GHashTableIter iter;
774   gpointer value;
775   gboolean changed = FALSE;
776 
777   g_assert (IDE_IS_MAIN_THREAD ());
778   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
779   g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
780 
781   g_hash_table_iter_init (&iter, self->groups_by_file);
782 
783   while (g_hash_table_iter_next (&iter, NULL, &value))
784     {
785       IdeDiagnosticsGroup *group = value;
786 
787       g_assert (group != NULL);
788       g_assert (IS_DIAGNOSTICS_GROUP (group));
789 
790       if (group->diagnostics_by_provider != NULL)
791         {
792           g_hash_table_remove (group->diagnostics_by_provider, provider);
793 
794           /*
795            * If we caused this hashtable to become empty, we can release the
796            * hashtable. The hashtable is guaranteed to not be empty if there
797            * are other providers loaded for this group.
798            */
799           if (g_hash_table_size (group->diagnostics_by_provider) == 0)
800             g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
801 
802           /*
803            * TODO: If this provider is not part of this group, we can possibly
804            *       dispose of the group if there are no diagnostics.
805            */
806 
807           changed = TRUE;
808         }
809     }
810 
811   return changed;
812 }
813 
814 static void
ide_diagnostics_manager_extension_removed(IdeExtensionSetAdapter * adapter,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)815 ide_diagnostics_manager_extension_removed (IdeExtensionSetAdapter *adapter,
816                                            PeasPluginInfo         *plugin_info,
817                                            PeasExtension          *exten,
818                                            gpointer                user_data)
819 {
820   IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
821   IdeDiagnosticsManager *self = user_data;
822 
823   IDE_ENTRY;
824 
825   g_assert (IDE_IS_MAIN_THREAD ());
826   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
827   g_assert (plugin_info != NULL);
828   g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
829   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
830 
831   g_signal_handlers_disconnect_by_func (provider,
832                                         G_CALLBACK (ide_diagnostics_manager_provider_invalidated),
833                                         self);
834 
835   /*
836    * The goal of the following is to reomve our diagnostics from any file
837    * that has been loaded. It is possible for diagnostic providers to effect
838    * files outside the buffer they are loaded for and this ensures that we
839    * clean those up.
840    */
841   ide_diagnostics_manager_clear_by_provider (self, provider);
842 
843   /* Clear the diagnostics group */
844   g_object_set_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP", NULL);
845 
846   IDE_EXIT;
847 }
848 
849 /**
850  * ide_diagnostics_manager_get_busy:
851  *
852  * Gets if the diagnostics manager is currently executing a diagnosis.
853  *
854  * Returns: %TRUE if the #IdeDiagnosticsManager is busy diagnosing.
855  *
856  * Since: 3.32
857  */
858 gboolean
ide_diagnostics_manager_get_busy(IdeDiagnosticsManager * self)859 ide_diagnostics_manager_get_busy (IdeDiagnosticsManager *self)
860 {
861   GHashTableIter iter;
862   gpointer value;
863 
864   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
865   g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), FALSE);
866 
867   g_hash_table_iter_init (&iter, self->groups_by_file);
868 
869   while (g_hash_table_iter_next (&iter, NULL, &value))
870     {
871       IdeDiagnosticsGroup *group = value;
872 
873       g_assert (group != NULL);
874       g_assert (IS_DIAGNOSTICS_GROUP (group));
875 
876       if (group->in_diagnose > 0)
877         return TRUE;
878     }
879 
880   return FALSE;
881 }
882 
883 /**
884  * ide_diagnostics_manager_get_diagnostics_for_file:
885  * @self: An #IdeDiagnosticsManager
886  * @file: a #GFile to retrieve diagnostics for
887  *
888  * This function collects all of the diagnostics that have been collected
889  * for @file and returns them as a new #IdeDiagnostics to the caller.
890  *
891  * The #IdeDiagnostics structure will contain zero items if there are
892  * no diagnostics discovered. Therefore, this function will never return
893  * a %NULL value.
894  *
895  * Returns: (transfer full): A new #IdeDiagnostics.
896  *
897  * Since: 3.32
898  */
899 IdeDiagnostics *
ide_diagnostics_manager_get_diagnostics_for_file(IdeDiagnosticsManager * self,GFile * file)900 ide_diagnostics_manager_get_diagnostics_for_file (IdeDiagnosticsManager *self,
901                                                   GFile                 *file)
902 {
903   g_autoptr(IdeDiagnostics) ret = NULL;
904   IdeDiagnosticsGroup *group;
905 
906   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
907   g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), NULL);
908   g_return_val_if_fail (G_IS_FILE (file), NULL);
909 
910   ret = ide_diagnostics_new ();
911 
912   group = g_hash_table_lookup (self->groups_by_file, file);
913 
914   if (group != NULL && group->diagnostics_by_provider != NULL)
915     {
916       GHashTableIter iter;
917       gpointer value;
918 
919       g_hash_table_iter_init (&iter, group->diagnostics_by_provider);
920 
921       while (g_hash_table_iter_next (&iter, NULL, &value))
922         {
923           IdeDiagnostics *diagnostics = value;
924           guint length;
925 
926           if (diagnostics == NULL)
927             continue;
928 
929           length = g_list_model_get_n_items (G_LIST_MODEL (diagnostics));
930 
931           for (guint i = 0; i < length; i++)
932             {
933               g_autoptr(IdeDiagnostic) diagnostic = NULL;
934 
935               diagnostic = g_list_model_get_item (G_LIST_MODEL (diagnostics), i);
936               ide_diagnostics_add (ret, diagnostic);
937             }
938         }
939     }
940 
941   return g_steal_pointer (&ret);
942 }
943 
944 guint
ide_diagnostics_manager_get_sequence_for_file(IdeDiagnosticsManager * self,GFile * file)945 ide_diagnostics_manager_get_sequence_for_file (IdeDiagnosticsManager *self,
946                                                GFile                 *file)
947 {
948   IdeDiagnosticsGroup *group;
949 
950   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
951   g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), 0);
952   g_return_val_if_fail (G_IS_FILE (file), 0);
953 
954   group = g_hash_table_lookup (self->groups_by_file, file);
955 
956   if (group != NULL)
957     {
958       g_assert (IS_DIAGNOSTICS_GROUP (group));
959       g_assert (G_IS_FILE (group->file));
960       g_assert (g_file_equal (group->file, file));
961 
962       return group->sequence;
963     }
964 
965   return 0;
966 }
967 
968 /**
969  * ide_diagnostics_manager_rediagnose:
970  * @self: an #IdeDiagnosticsManager
971  * @buffer: an #IdeBuffer
972  *
973  * Requests that the diagnostics be reloaded for @buffer.
974  *
975  * You may want to call this if you changed something that a buffer depends on,
976  * and want to seamlessly update its diagnostics with that updated information.
977  *
978  * Since: 3.32
979  */
980 void
ide_diagnostics_manager_rediagnose(IdeDiagnosticsManager * self,IdeBuffer * buffer)981 ide_diagnostics_manager_rediagnose (IdeDiagnosticsManager *self,
982                                     IdeBuffer             *buffer)
983 {
984   IdeDiagnosticsGroup *group;
985   GFile *file;
986 
987   g_return_if_fail (IDE_IS_MAIN_THREAD ());
988   g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
989   g_return_if_fail (IDE_IS_BUFFER (buffer));
990 
991   file = ide_buffer_get_file (buffer);
992   group = ide_diagnostics_manager_find_group (self, file);
993 
994   ide_diagnostics_group_queue_diagnose (group, self);
995 }
996 
997 /**
998  * ide_diagnostics_manager_from_context:
999  * @context: an #IdeContext
1000  *
1001  * Gets the diagnostics manager for the context.
1002  *
1003  * Returns: (transfer none): an #IdeDiagnosticsManager
1004  *
1005  * Since: 3.32
1006  */
1007 IdeDiagnosticsManager *
ide_diagnostics_manager_from_context(IdeContext * context)1008 ide_diagnostics_manager_from_context (IdeContext *context)
1009 {
1010   IdeDiagnosticsManager *self;
1011 
1012   g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
1013 
1014   ide_object_lock (IDE_OBJECT (context));
1015   if (!(self = ide_context_peek_child_typed (context, IDE_TYPE_DIAGNOSTICS_MANAGER)))
1016     {
1017       g_autoptr(IdeDiagnosticsManager) created = NULL;
1018       created = ide_object_ensure_child_typed (IDE_OBJECT (context),
1019                                                IDE_TYPE_DIAGNOSTICS_MANAGER);
1020       self = ide_context_peek_child_typed (context, IDE_TYPE_DIAGNOSTICS_MANAGER);
1021     }
1022   ide_object_unlock (IDE_OBJECT (context));
1023 
1024   return self;
1025 }
1026 
1027 void
_ide_diagnostics_manager_file_closed(IdeDiagnosticsManager * self,GFile * file)1028 _ide_diagnostics_manager_file_closed (IdeDiagnosticsManager *self,
1029                                       GFile                 *file)
1030 {
1031   IdeDiagnosticsGroup *group;
1032   gboolean has_diagnostics;
1033 
1034   IDE_ENTRY;
1035 
1036   g_return_if_fail (IDE_IS_MAIN_THREAD ());
1037   g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
1038   g_return_if_fail (G_IS_FILE (file));
1039 
1040   /*
1041    * The goal here is to cleanup everything we can about this group that
1042    * is part of a loaded buffer. We might want to keep the group around
1043    * in case it is useful from other providers.
1044    */
1045 
1046   group = ide_diagnostics_manager_find_group (self, file);
1047 
1048   g_assert (group != NULL);
1049   g_assert (IS_DIAGNOSTICS_GROUP (group));
1050 
1051   /* Clear some state we've been tracking */
1052   g_clear_pointer (&group->contents, g_bytes_unref);
1053   group->lang_id = NULL;
1054   group->needs_diagnose = FALSE;
1055 
1056   /*
1057    * We track if we have diagnostics now so that after we unload the
1058    * the providers, we can save that bit for later.
1059    */
1060   has_diagnostics = ide_diagnostics_group_has_diagnostics (group);
1061 
1062   /*
1063    * Force our diagnostic providers to unload. This will cause them
1064    * extension-removed signal to be called for each provider which
1065    * in turn will perform per-provider cleanup including the removal
1066    * of its diagnostics from all groups. (A provider can in practice
1067    * affect another group since a .c file could create a diagnostic
1068    * for a .h).
1069    */
1070   ide_clear_and_destroy_object (&group->adapter);
1071 
1072   /*
1073    * Even after unloading the diagnostic providers, we might still have
1074    * diagnostics that were created from other files (this could happen when
1075    * one diagnostic is created for a header from a source file). So we don't
1076    * want to wipe out the hashtable unless everything was unloaded. The other
1077    * provider will cleanup during its own destruction.
1078    */
1079   if (group->diagnostics_by_provider != NULL &&
1080       g_hash_table_size (group->diagnostics_by_provider) == 0)
1081     g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
1082 
1083   group->has_diagnostics = has_diagnostics;
1084 
1085   IDE_EXIT;
1086 }
1087 
1088 void
_ide_diagnostics_manager_file_changed(IdeDiagnosticsManager * self,GFile * file,GBytes * contents,const gchar * lang_id)1089 _ide_diagnostics_manager_file_changed (IdeDiagnosticsManager *self,
1090                                        GFile                 *file,
1091                                        GBytes                *contents,
1092                                        const gchar           *lang_id)
1093 {
1094   IdeDiagnosticsGroup *group;
1095 
1096   g_return_if_fail (IDE_IS_MAIN_THREAD ());
1097   g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
1098   g_return_if_fail (G_IS_FILE (file));
1099 
1100   group = ide_diagnostics_manager_find_group (self, file);
1101 
1102   g_clear_pointer (&group->contents, g_bytes_unref);
1103 
1104   group->lang_id = g_intern_string (lang_id);
1105   group->contents = contents ? g_bytes_ref (contents) : NULL;
1106 
1107   ide_diagnostics_group_queue_diagnose (group, self);
1108 }
1109 
1110 void
_ide_diagnostics_manager_language_changed(IdeDiagnosticsManager * self,GFile * file,const gchar * lang_id)1111 _ide_diagnostics_manager_language_changed (IdeDiagnosticsManager *self,
1112                                            GFile                 *file,
1113                                            const gchar           *lang_id)
1114 {
1115   IdeDiagnosticsGroup *group;
1116 
1117   g_return_if_fail (IDE_IS_MAIN_THREAD ());
1118   g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
1119 
1120   group = ide_diagnostics_manager_find_group (self, file);
1121   group->lang_id = g_intern_string (lang_id);
1122 
1123   if (group->adapter != NULL)
1124     ide_extension_set_adapter_set_value (group->adapter, lang_id);
1125 
1126   ide_diagnostics_group_queue_diagnose (group, self);
1127 }
1128 
1129 void
_ide_diagnostics_manager_file_opened(IdeDiagnosticsManager * self,GFile * file,const gchar * lang_id)1130 _ide_diagnostics_manager_file_opened (IdeDiagnosticsManager *self,
1131                                       GFile                 *file,
1132                                       const gchar           *lang_id)
1133 {
1134   IdeDiagnosticsGroup *group;
1135 
1136   IDE_ENTRY;
1137 
1138   g_assert (IDE_IS_MAIN_THREAD ());
1139   g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
1140   g_assert (G_IS_FILE (file));
1141 
1142   group = ide_diagnostics_manager_find_group (self, file);
1143 
1144   if (group->diagnostics_by_provider == NULL)
1145     group->diagnostics_by_provider =
1146       g_hash_table_new_full (NULL, NULL, NULL, free_diagnostics);
1147 
1148   group->lang_id = g_intern_string (lang_id);
1149 
1150   if (group->adapter == NULL)
1151     {
1152       group->adapter = ide_extension_set_adapter_new (IDE_OBJECT (self),
1153                                                       peas_engine_get_default (),
1154                                                       IDE_TYPE_DIAGNOSTIC_PROVIDER,
1155                                                       "Diagnostic-Provider-Languages",
1156                                                       lang_id);
1157 
1158       g_signal_connect_object (group->adapter,
1159                                "extension-added",
1160                                G_CALLBACK (ide_diagnostics_manager_extension_added),
1161                                self,
1162                                0);
1163 
1164       g_signal_connect_object (group->adapter,
1165                                "extension-removed",
1166                                G_CALLBACK (ide_diagnostics_manager_extension_removed),
1167                                self,
1168                                0);
1169 
1170       ide_extension_set_adapter_foreach (group->adapter,
1171                                          ide_diagnostics_manager_extension_added,
1172                                          self);
1173     }
1174 
1175   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (group->adapter));
1176   g_assert (g_hash_table_lookup (self->groups_by_file, file) == group);
1177 
1178   ide_diagnostics_group_queue_diagnose (group, self);
1179 
1180   IDE_EXIT;
1181 }
1182 
1183