dte test coverage


Directory: ./
File: src/ctags.c
Date: 2025-11-12 12:04:10
Coverage Exec Excl Total
Lines: 100.0% 67 0 67
Functions: 100.0% 5 0 5
Branches: 85.5% 53 0 62

Line Branch Exec Source
1 #include <stdlib.h>
2 #include "ctags.h"
3 #include "util/arith.h"
4 #include "util/ascii.h"
5 #include "util/debug.h"
6 #include "util/str-util.h"
7 #include "util/strtonum.h"
8 #include "util/xmalloc.h"
9 #include "util/xstring.h"
10
11 // Convert an ex(1) style pattern from a tags(5) file to a basic POSIX
12 // regex ("BRE"), so that it can be compiled with regcomp(3)
13 19 static size_t regex_from_ex_pattern (
14 const char *ex_str,
15 size_t len,
16 char **regex_str // out param
17 ) {
18 19 BUG_ON(len == 0);
19 19 const char open_delim = ex_str[0];
20 19 BUG_ON(open_delim != '/' && open_delim != '?');
21 19 char *buf = xmalloc(xmul(2, len));
22
23 // The pattern isn't a real regex; special chars need to be escaped
24
2/2
✓ Branch 20 → 9 taken 890 times.
✓ Branch 20 → 21 taken 1 time.
891 for (size_t i = 1, j = 0; i < len; i++) {
25 890 char c = ex_str[i];
26
2/2
✓ Branch 9 → 10 taken 889 times.
✓ Branch 9 → 21 taken 1 time.
890 if (c == '\0') {
27 break;
28
2/2
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 14 taken 886 times.
889 } else if (c == '\\') {
29
1/2
✓ Branch 11 → 12 taken 3 times.
✗ Branch 11 → 21 not taken.
3 if (unlikely(++i >= len)) {
30 break;
31 }
32 3 c = ex_str[i];
33
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 19 taken 2 times.
3 if (c == '\\') {
34 // Escape "\\" as "\\" (any other "\x" becomes just "x")
35 1 buf[j++] = '\\';
36 }
37
4/4
✓ Branch 14 → 15 taken 859 times.
✓ Branch 14 → 16 taken 27 times.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 858 times.
886 } else if (c == '*' || c == '[' || c == ']') {
38 28 buf[j++] = '\\';
39
2/2
✓ Branch 17 → 18 taken 17 times.
✓ Branch 17 → 19 taken 841 times.
858 } else if (c == open_delim) {
40 17 buf[j] = '\0';
41 17 *regex_str = buf;
42 17 return i + 1;
43 }
44 872 buf[j++] = c;
45 }
46
47 // End of string reached without a matching end delimiter; invalid input
48 2 free(buf);
49 2 return 0;
50 }
51
52 21 static size_t parse_ex_cmd(Tag *tag, const char *buf, size_t size)
53 {
54
1/2
✓ Branch 2 → 3 taken 21 times.
✗ Branch 2 → 12 not taken.
21 if (unlikely(size == 0)) {
55 return 0;
56 }
57
58 21 size_t n;
59
2/2
✓ Branch 3 → 4 taken 19 times.
✓ Branch 3 → 5 taken 2 times.
21 if (buf[0] == '/' || buf[0] == '?') {
60 19 n = regex_from_ex_pattern(buf, size, &tag->pattern);
61 } else {
62 2 n = buf_parse_ulong(buf, size, &tag->lineno);
63 }
64
65
2/2
✓ Branch 6 → 7 taken 19 times.
✓ Branch 6 → 12 taken 2 times.
21 if (n == 0) {
66 return 0;
67 }
68
69
4/4
✓ Branch 7 → 8 taken 17 times.
✓ Branch 7 → 11 taken 2 times.
✓ Branch 9 → 10 taken 16 times.
✓ Branch 9 → 11 taken 1 time.
19 bool trailing_comment = (n + 1 < size) && mem_equal(buf + n, ";\"", 2);
70 19 return n + (trailing_comment ? 2 : 0);
71 }
72
73 21 bool parse_ctags_line(Tag *tag, const char *line, size_t line_len)
74 {
75 21 size_t pos = 0;
76 21 *tag = (Tag){.name = get_delim(line, &pos, line_len, '\t')};
77
2/4
✓ Branch 3 → 4 taken 21 times.
✗ Branch 3 → 25 not taken.
✓ Branch 4 → 5 taken 21 times.
✗ Branch 4 → 25 not taken.
21 if (tag->name.length == 0 || pos >= line_len) {
78 return false;
79 }
80
81 21 tag->filename = get_delim(line, &pos, line_len, '\t');
82
2/4
✓ Branch 6 → 7 taken 21 times.
✗ Branch 6 → 25 not taken.
✓ Branch 7 → 8 taken 21 times.
✗ Branch 7 → 25 not taken.
21 if (tag->filename.length == 0 || pos >= line_len) {
83 return false;
84 }
85
86 21 size_t len = parse_ex_cmd(tag, line + pos, line_len - pos);
87
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 12 taken 19 times.
21 if (len == 0) {
88 2 BUG_ON(tag->pattern);
89 return false;
90 }
91
92 19 pos += len;
93
2/2
✓ Branch 12 → 13 taken 18 times.
✓ Branch 12 → 25 taken 1 time.
19 if (pos >= line_len) {
94 return true;
95 }
96
97 /*
98 * Extension fields (key:[value]):
99 *
100 * file: visibility limited to this file
101 * struct:NAME tag is member of struct NAME
102 * union:NAME tag is member of union NAME
103 * typeref:struct:NAME::MEMBER_TYPE MEMBER_TYPE is type of the tag
104 */
105
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 24 taken 17 times.
18 if (line[pos++] != '\t') {
106 // free `pattern` allocated by parse_ex_cmd()
107 1 free_tag(tag);
108 1 tag->pattern = NULL;
109 1 return false;
110 }
111
112
2/2
✓ Branch 24 → 16 taken 38 times.
✓ Branch 24 → 25 taken 17 times.
55 while (pos < line_len) {
113 38 StringView field = get_delim(line, &pos, line_len, '\t');
114
3/4
✓ Branch 17 → 18 taken 17 times.
✓ Branch 17 → 20 taken 21 times.
✓ Branch 18 → 19 taken 17 times.
✗ Branch 18 → 20 not taken.
38 if (field.length == 1 && ascii_isalpha(field.data[0])) {
115 17 tag->kind = field.data[0];
116
2/2
✓ Branch 21 → 22 taken 7 times.
✓ Branch 21 → 23 taken 14 times.
21 } else if (strview_equal_cstring(field, "file:")) {
117 7 tag->local = true;
118 }
119 // TODO: struct/union/typeref
120 }
121
122 return true;
123 }
124
125 18 bool next_tag (
126 const char *buf,
127 size_t buf_len,
128 size_t *posp, // in-out param
129 StringView prefix,
130 bool exact,
131 Tag *tag // out param
132 ) {
133
2/2
✓ Branch 14 → 3 taken 55 times.
✓ Branch 14 → 15 taken 4 times.
59 for (size_t pos = *posp; pos < buf_len; ) {
134 55 StringView line = buf_slice_next_line(buf, &pos, buf_len);
135 55 if (
136
1/2
✓ Branch 4 → 5 taken 55 times.
✗ Branch 4 → 13 not taken.
55 line.length > 0 // Line is non-empty
137
2/2
✓ Branch 5 → 6 taken 37 times.
✓ Branch 5 → 13 taken 18 times.
55 && line.data[0] != '!' // and not a comment
138
2/2
✓ Branch 7 → 8 taken 15 times.
✓ Branch 7 → 13 taken 22 times.
37 && strview_has_sv_prefix(line, prefix) // and starts with `prefix`
139
4/4
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 13 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 13 taken 1 time.
15 && (!exact || line.data[prefix.length] == '\t') // and matches `prefix` exactly, if applicable
140
1/2
✓ Branch 11 → 12 taken 14 times.
✗ Branch 11 → 13 not taken.
14 && parse_ctags_line(tag, line.data, line.length) // and is a valid tags(5) entry
141 ) {
142 // Advance the position; `tag` has been filled by parse_ctags_line()
143 14 *posp = pos;
144 14 return true;
145 }
146 }
147
148 // No matching tags remaining
149 4 return false;
150 }
151
152 // NOTE: tag itself is not freed
153 22 void free_tag(Tag *tag)
154 {
155 22 free(tag->pattern);
156 22 }
157