dte test coverage


Directory: ./
File: src/tag.c
Date: 2025-05-08 15:05:54
Exec Total Coverage
Lines: 81 185 43.8%
Functions: 8 15 53.3%
Branches: 17 108 15.7%

Line Branch Exec Source
1 #include "feature.h"
2 #include <errno.h>
3 #include <stdlib.h>
4 #include <string.h>
5 #include <sys/stat.h>
6 #include <sys/types.h>
7 #include <unistd.h>
8 #include "tag.h"
9 #include "command/error.h"
10 #include "util/debug.h"
11 #include "util/numtostr.h"
12 #include "util/path.h"
13 #include "util/time-util.h"
14 #include "util/xmalloc.h"
15 #include "util/xreadwrite.h"
16 #include "util/xstring.h"
17
18 static bool tag_is_local_to_file(const Tag *tag, const char *path)
19 {
20 return tag->local && !!path && strview_equal_cstring(&tag->filename, path);
21 }
22
23 static int visibility_cmp(const Tag *a, const Tag *b, const char *filename)
24 {
25 if (!a->local && !b->local) {
26 return 0;
27 }
28
29 // Is tag visibility limited to the current file?
30 bool a_this_file = tag_is_local_to_file(a, filename);
31 bool b_this_file = tag_is_local_to_file(b, filename);
32
33 // Tags local to other file than current are not interesting
34 if (a->local && !a_this_file) {
35 // a is not interesting
36 if (b->local && !b_this_file) {
37 // b is equally uninteresting
38 return 0;
39 }
40 // b is more interesting, sort it before a
41 return 1;
42 }
43 if (b->local && !b_this_file) {
44 // b is not interesting
45 return -1;
46 }
47
48 // Both are NOT UNinteresting
49
50 if (a->local && a_this_file) {
51 if (b->local && b_this_file) {
52 return 0;
53 }
54 // a is more interesting because it's a local symbol
55 return -1;
56 }
57 if (b->local && b_this_file) {
58 // b is more interesting because it's a local symbol
59 return 1;
60 }
61 return 0;
62 }
63
64 static int kind_cmp(const Tag *a, const Tag *b)
65 {
66 if (a->kind == b->kind) {
67 return 0;
68 }
69
70 // Struct member (m) is not very interesting
71 if (a->kind == 'm') {
72 return 1;
73 }
74 if (b->kind == 'm') {
75 return -1;
76 }
77
78 // Global variable (v) is not very interesting
79 if (a->kind == 'v') {
80 return 1;
81 }
82 if (b->kind == 'v') {
83 return -1;
84 }
85
86 // Struct (s), union (u)
87 return 0;
88 }
89
90 static int tag_cmp_r(const void *ap, const void *bp, void *userdata)
91 {
92 const Tag *const *a = ap;
93 const Tag *const *b = bp;
94 const char *filename = userdata;
95 int r = visibility_cmp(*a, *b, filename);
96 return r ? r : kind_cmp(*a, *b);
97 }
98
99 // Find "tags" file from directory path and its parent directories
100 1 static int open_tag_file(ErrorBuffer *ebuf, char *path)
101 {
102 1 static const char tags[] = "tags";
103
1/2
✓ Branch 0 (12→3) taken 1 times.
✗ Branch 1 (12→13) not taken.
1 while (*path) {
104 1 size_t len = strlen(path);
105 1 char *slash = strrchr(path, '/');
106
1/2
✓ Branch 0 (3→4) taken 1 times.
✗ Branch 1 (3→5) not taken.
1 if (slash != path + len - 1) {
107 1 path[len++] = '/';
108 }
109 1 memcpy(path + len, tags, sizeof(tags));
110 1 int fd = xopen(path, O_RDONLY | O_CLOEXEC, 0);
111
1/2
✗ Branch 0 (6→7) not taken.
✓ Branch 1 (6→15) taken 1 times.
1 if (fd >= 0) {
112 return fd;
113 }
114 if (errno != ENOENT) {
115 error_msg(ebuf, "failed to open '%s': %s", path, strerror(errno));
116 return -1;
117 }
118 *slash = '\0';
119 }
120
121 error_msg(ebuf, "no tags file");
122 return -1;
123 }
124
125 static bool tag_file_changed (
126 const TagFile *tf,
127 const char *filename,
128 const struct stat *st
129 ) {
130 return tf->mtime != st->st_mtime || !streq(tf->filename, filename);
131 }
132
133 // Note: does not free `tf` itself
134 9 void tag_file_free(TagFile *tf)
135 {
136 9 free(tf->filename);
137 9 free(tf->buf);
138 9 *tf = (TagFile){.filename = NULL};
139 9 }
140
141 1 bool load_tag_file(TagFile *tf, ErrorBuffer *ebuf)
142 {
143 1 char path[8192];
144
1/2
✗ Branch 0 (3→4) not taken.
✓ Branch 1 (3→5) taken 1 times.
1 if (unlikely(!getcwd(path, sizeof(path) - STRLEN("/tags")))) {
145 return error_msg_errno(ebuf, "getcwd");
146 }
147
148 1 int fd = open_tag_file(ebuf, path);
149
1/2
✓ Branch 0 (6→7) taken 1 times.
✗ Branch 1 (6→34) not taken.
1 if (fd < 0) {
150 return false;
151 }
152
153 1 struct stat st;
154
1/2
✗ Branch 0 (8→9) not taken.
✓ Branch 1 (8→12) taken 1 times.
1 if (unlikely(fstat(fd, &st) != 0)) {
155 const char *str = strerror(errno);
156 xclose(fd);
157 return error_msg(ebuf, "fstat: %s", str);
158 }
159
160
1/2
✗ Branch 0 (12→13) not taken.
✓ Branch 1 (12→15) taken 1 times.
1 if (unlikely(st.st_size <= 0)) {
161 xclose(fd);
162 return error_msg(ebuf, "empty tags file");
163 }
164
165
1/2
✗ Branch 0 (15→16) not taken.
✓ Branch 1 (15→22) taken 1 times.
1 if (tf->filename) {
166 if (!tag_file_changed(tf, path, &st)) {
167 xclose(fd);
168 return true;
169 }
170 tag_file_free(tf);
171 BUG_ON(tf->filename);
172 }
173
174 1 char *buf = malloc(st.st_size);
175
1/2
✗ Branch 0 (22→23) not taken.
✓ Branch 1 (22→26) taken 1 times.
1 if (unlikely(!buf)) {
176 xclose(fd);
177 return error_msg(ebuf, "malloc: %s", strerror(ENOMEM));
178 }
179
180 1 ssize_t size = xread_all(fd, buf, st.st_size);
181 1 int err = errno;
182 1 xclose(fd);
183
1/2
✗ Branch 0 (28→29) not taken.
✓ Branch 1 (28→31) taken 1 times.
1 if (size < 0) {
184 free(buf);
185 return error_msg(ebuf, "read: %s", strerror(err));
186 }
187
188 2 *tf = (TagFile) {
189 1 .filename = xstrdup(path),
190 1 .dirname_len = (xstrrchr(path, '/') - path) + 1, // Includes last slash
191 .buf = buf,
192 .size = size,
193 1 .mtime = st.st_mtime,
194 };
195
196 1 return true;
197 }
198
199 1 static void free_tags_cb(Tag *tag)
200 {
201 1 free_tag(tag);
202 1 free(tag);
203 1 }
204
205 1 static void free_tags(PointerArray *tags)
206 {
207 1 ptr_array_free_cb(tags, FREE_FUNC(free_tags_cb));
208 1 }
209
210 #if !HAVE_QSORT_R
211 static const char *current_filename_global; // NOLINT(*-non-const-global-variables)
212 static int tag_cmp(const void *t1, const void *t2)
213 {
214 return tag_cmp_r(t1, t2, (char*)current_filename_global);
215 }
216 #endif
217
218 1 static void tag_file_find_tags (
219 const TagFile *tf,
220 const char *filename,
221 const StringView *name,
222 PointerArray *tags
223 ) {
224 1 Tag *tag = xmalloc(sizeof(*tag));
225 1 size_t pos = 0;
226
2/2
✓ Branch 0 (7→4) taken 1 times.
✓ Branch 1 (7→8) taken 1 times.
2 while (next_tag(tf->buf, tf->size, &pos, name, true, tag)) {
227 1 ptr_array_append(tags, tag);
228 1 tag = xmalloc(sizeof(*tag));
229 }
230 1 free(tag);
231
232
1/2
✓ Branch 0 (8→9) taken 1 times.
✗ Branch 1 (8→10) not taken.
1 if (tags->count < 2) {
233 1 return;
234 }
235
236 if (filename) {
237 BUG_ON(!path_is_absolute(filename));
238 size_t n = tf->dirname_len;
239 BUG_ON(n == 0);
240 if (strncmp(filename, tf->filename, n) == 0) {
241 filename += n;
242 } else {
243 // Filename doesn't start with directory prefix of tag file
244 filename = NULL;
245 }
246 }
247
248 void **ptrs = tags->ptrs;
249 BUG_ON(!ptrs);
250
251 #if HAVE_QSORT_R
252 qsort_r(ptrs, tags->count, sizeof(*ptrs), tag_cmp_r, (char*)filename);
253 #else
254 current_filename_global = filename;
255 qsort(ptrs, tags->count, sizeof(*ptrs), tag_cmp);
256 current_filename_global = NULL;
257 #endif
258 }
259
260 // Note: this moves ownership of tag->pattern to the generated Message
261 // and assigns NULL to the old pointer
262 1 void add_message_for_tag(MessageArray *messages, Tag *tag, const StringView *dir)
263 {
264 1 BUG_ON(dir->length == 0);
265 1 BUG_ON(dir->data[0] != '/');
266
267 1 static const char prefix[] = "Tag ";
268 1 size_t prefix_len = sizeof(prefix) - 1;
269 1 size_t msg_len = prefix_len + tag->name.length;
270 1 Message *m = xmalloc(sizeof(*m) + msg_len + 1);
271
272 1 memcpy(m->msg, prefix, prefix_len);
273 1 memcpy(m->msg + prefix_len, tag->name.data, tag->name.length);
274 1 m->msg[msg_len] = '\0';
275
276 1 char *filename = path_join_sv(dir, &tag->filename, false);
277 1 m->loc = new_file_location(filename, 0, 0, 0);
278
279
1/2
✓ Branch 0 (9→10) taken 1 times.
✗ Branch 1 (9→11) not taken.
1 if (tag->pattern) {
280 1 m->loc->pattern = tag->pattern; // Message takes ownership
281 1 tag->pattern = NULL;
282 } else {
283 m->loc->line = tag->lineno;
284 }
285
286 1 add_message(messages, m);
287 1 }
288
289 1 size_t tag_lookup (
290 TagFile *tf,
291 MessageArray *messages,
292 ErrorBuffer *ebuf,
293 const StringView *name,
294 const char *filename
295 ) {
296 1 BUG_ON(!tf->filename);
297
298 // Filename helps to find correct tags
299 1 PointerArray tags = PTR_ARRAY_INIT;
300 1 tag_file_find_tags(tf, filename, name, &tags);
301
302 1 size_t ntags = tags.count;
303
1/2
✗ Branch 0 (5→6) not taken.
✓ Branch 1 (5→8) taken 1 times.
1 if (ntags == 0) {
304 error_msg(ebuf, "Tag '%.*s' not found", (int)name->length, name->data);
305 return 0;
306 }
307
308 // Note that `dirname_len` always includes a trailing slash, but the
309 // call to path_join_sv() in add_message_for_tag() handles that fine
310 1 BUG_ON(tf->dirname_len == 0);
311 1 StringView tagfile_dir = string_view(tf->filename, tf->dirname_len);
312
313
2/2
✓ Branch 0 (13→11) taken 1 times.
✓ Branch 1 (13→14) taken 1 times.
2 for (size_t i = 0; i < ntags; i++) {
314 1 Tag *tag = tags.ptrs[i];
315 1 add_message_for_tag(messages, tag, &tagfile_dir);
316 }
317
318 1 free_tags(&tags);
319 1 return ntags;
320 }
321
322 void collect_tags(TagFile *tf, PointerArray *a, const StringView *prefix)
323 {
324 ErrorBuffer ebuf;
325 ebuf.print_to_stderr = false;
326 if (!load_tag_file(tf, &ebuf)) {
327 return;
328 }
329
330 Tag tag;
331 size_t pos = 0;
332 StringView prev = STRING_VIEW_INIT;
333 while (next_tag(tf->buf, tf->size, &pos, prefix, false, &tag)) {
334 BUG_ON(tag.name.length == 0);
335 if (prev.length == 0 || !strview_equal(&tag.name, &prev)) {
336 ptr_array_append(a, xstrcut(tag.name.data, tag.name.length));
337 prev = tag.name;
338 }
339 free_tag(&tag);
340 }
341 }
342
343 String dump_tags(TagFile *tf, ErrorBuffer *ebuf)
344 {
345 if (!load_tag_file(tf, ebuf)) {
346 return string_new(0);
347 }
348
349 const struct timespec ts = {.tv_sec = tf->mtime};
350 char sizestr[FILESIZE_STR_MAX];
351 char tstr[TIME_STR_BUFSIZE];
352 String buf = string_new(tf->size);
353
354 string_sprintf (
355 &buf,
356 "Tags file\n---------\n\n"
357 "%s %s\n%s %s\n%s %s\n\n"
358 "Tag entries\n-----------\n\n",
359 " Path:", tf->filename,
360 " Modified:", timespec_to_str(&ts, tstr) ? tstr : "-",
361 " Size:", filesize_to_str(tf->size, sizestr)
362 );
363
364 const StringView prefix = STRING_VIEW_INIT;
365 size_t pos = 0;
366 Tag tag;
367
368 while (next_tag(tf->buf, tf->size, &pos, &prefix, false, &tag)) {
369 string_append_buf(&buf, tag.name.data, tag.name.length);
370 string_append_cstring(&buf, " ");
371 string_append_buf(&buf, tag.filename.data, tag.filename.length);
372 if (tag.kind) {
373 string_sprintf(&buf, " kind:%c", tag.kind);
374 }
375 if (tag.local) {
376 string_append_cstring(&buf, " LOCAL");
377 }
378 if (tag.pattern) {
379 string_sprintf(&buf, " /%s/", tag.pattern);
380 } else {
381 string_sprintf(&buf, " lineno:%lu", tag.lineno);
382 }
383 string_append_byte(&buf, '\n');
384 free_tag(&tag);
385 }
386
387 return buf;
388 }
389