dte test coverage


Directory: ./
File: src/editorconfig/editorconfig.c
Date: 2025-05-08 15:05:54
Exec Total Coverage
Lines: 101 111 91.0%
Functions: 11 11 100.0%
Branches: 54 96 56.2%

Line Branch Exec Source
1 #include <stddef.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include "editorconfig.h"
5 #include "ini.h"
6 #include "match.h"
7 #include "util/debug.h"
8 #include "util/path.h"
9 #include "util/readfile.h"
10 #include "util/string.h"
11 #include "util/string-view.h"
12 #include "util/strtonum.h"
13 #include "util/xstring.h"
14
15 enum {
16 MAX_FILESIZE = 32u << 20, // 32 MiB
17 };
18
19 typedef struct {
20 const char *const pathname;
21 StringView config_file_dir;
22 EditorConfigOptions options;
23 bool match;
24 } UserData;
25
26 typedef enum {
27 ECONF_CHARSET,
28 ECONF_END_OF_LINE,
29 ECONF_INDENT_SIZE,
30 ECONF_INDENT_STYLE,
31 ECONF_INSERT_FINAL_NL,
32 ECONF_MAX_LINE_LENGTH,
33 ECONF_TAB_WIDTH,
34 ECONF_TRIM_TRAILING_WS,
35 ECONF_UNKNOWN_PROPERTY,
36 } PropertyType;
37
38 #define CMP(s, val) if (mem_equal(name->data, s, STRLEN(s))) return val;
39
40 9 static PropertyType lookup_property(const StringView *name)
41 {
42
4/8
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→6) taken 2 times.
✓ Branch 2 (2→9) taken 3 times.
✓ Branch 3 (2→14) taken 3 times.
✓ Branch 4 (2→17) taken 1 times.
✗ Branch 5 (2→20) not taken.
✗ Branch 6 (2→23) not taken.
✗ Branch 7 (2→26) not taken.
9 switch (name->length) {
43 case 7: CMP("charset", ECONF_CHARSET); break;
44
1/2
✓ Branch 0 (7→8) taken 2 times.
✗ Branch 1 (7→26) not taken.
2 case 9: CMP("tab_width", ECONF_TAB_WIDTH); break;
45 3 case 11:
46
1/2
✗ Branch 0 (10→11) not taken.
✓ Branch 1 (10→26) taken 3 times.
3 CMP("indent_size", ECONF_INDENT_SIZE);
47 CMP("end_of_line", ECONF_END_OF_LINE);
48 break;
49
1/2
✓ Branch 0 (15→16) taken 3 times.
✗ Branch 1 (15→26) not taken.
3 case 12: CMP("indent_style", ECONF_INDENT_STYLE); break;
50
1/2
✓ Branch 0 (18→19) taken 1 times.
✗ Branch 1 (18→26) not taken.
1 case 15: CMP("max_line_length", ECONF_MAX_LINE_LENGTH); break;
51 case 20: CMP("insert_final_newline", ECONF_INSERT_FINAL_NL); break;
52 case 24: CMP("trim_trailing_whitespace", ECONF_TRIM_TRAILING_WS); break;
53 }
54 return ECONF_UNKNOWN_PROPERTY;
55 }
56
57 3 static EditorConfigIndentStyle lookup_indent_style(const StringView *val)
58 {
59
1/2
✗ Branch 0 (3→4) not taken.
✓ Branch 1 (3→7) taken 3 times.
3 if (strview_equal_cstring_icase(val, "space")) {
60 return INDENT_STYLE_SPACE;
61 } else if (strview_equal_cstring_icase(val, "tab")) {
62 return INDENT_STYLE_TAB;
63 }
64 return INDENT_STYLE_UNSPECIFIED;
65 }
66
67 5 static unsigned int parse_indent_digit(const StringView *val)
68 {
69 5 const unsigned char *data = val->data;
70
1/2
✓ Branch 0 (2→3) taken 5 times.
✗ Branch 1 (2→5) not taken.
5 unsigned int indent = (val->length == 1) ? data[0] - '0' : 0;
71
1/2
✗ Branch 0 (3→4) not taken.
✓ Branch 1 (3→5) taken 5 times.
5 return (indent <= 8) ? indent : 0; // Valid indent widths are 1-8
72 }
73
74 3 static void parse_indent_size(EditorConfigOptions *options, const StringView *val)
75 {
76 3 bool tab = strview_equal_cstring_icase(val, "tab");
77 3 options->indent_size_is_tab = tab;
78
1/2
✓ Branch 0 (3→4) taken 3 times.
✗ Branch 1 (3→5) not taken.
3 options->indent_size = tab ? 0 : parse_indent_digit(val);
79 3 }
80
81 1 static unsigned int parse_max_line_length(const StringView *val)
82 {
83 1 unsigned int maxlen = 0;
84 1 size_t ndigits = buf_parse_uint(val->data, val->length, &maxlen);
85
1/2
✓ Branch 0 (3→4) taken 1 times.
✗ Branch 1 (3→5) not taken.
1 return (ndigits == val->length) ? maxlen : 0;
86 }
87
88 9 static void editorconfig_option_set (
89 EditorConfigOptions *options,
90 const StringView *name,
91 const StringView *val
92 ) {
93
4/6
✓ Branch 0 (3→4) taken 3 times.
✓ Branch 1 (3→6) taken 3 times.
✓ Branch 2 (3→7) taken 2 times.
✓ Branch 3 (3→8) taken 1 times.
✗ Branch 4 (3→10) not taken.
✗ Branch 5 (3→12) not taken.
9 switch (lookup_property(name)) {
94 3 case ECONF_INDENT_STYLE:
95 3 options->indent_style = lookup_indent_style(val);
96 3 break;
97 3 case ECONF_INDENT_SIZE:
98 3 parse_indent_size(options, val);
99 3 break;
100 2 case ECONF_TAB_WIDTH:
101 2 options->tab_width = parse_indent_digit(val);
102 2 break;
103 1 case ECONF_MAX_LINE_LENGTH:
104 1 options->max_line_length = parse_max_line_length(val);
105 1 break;
106 case ECONF_CHARSET:
107 case ECONF_END_OF_LINE:
108 case ECONF_INSERT_FINAL_NL:
109 case ECONF_TRIM_TRAILING_WS:
110 case ECONF_UNKNOWN_PROPERTY:
111 break;
112 default:
113 BUG("unhandled property type");
114 }
115 9 }
116
117 // See: https://editorconfig.org/#wildcards and ec_pattern_match()
118 296 static bool is_ec_special_char(char c)
119 {
120
1/2
✓ Branch 0 (3→4) taken 296 times.
✗ Branch 1 (3→8) not taken.
296 return c == '*' || c == ',' || c == '-' || c == '?' || c == '['
121
4/8
✓ Branch 0 (2→3) taken 296 times.
✗ Branch 1 (2→8) not taken.
✓ Branch 2 (4→5) taken 296 times.
✗ Branch 3 (4→8) not taken.
✓ Branch 4 (5→6) taken 296 times.
✗ Branch 5 (5→8) not taken.
✓ Branch 6 (6→7) taken 296 times.
✗ Branch 7 (6→8) not taken.
592 || c == ']' || c == '{' || c == '}' || c == '\\';
122 }
123
124 12 static bool section_matches_path(StringView section, StringView dir, const char *path)
125 {
126 12 BUG_ON(section.length == 0);
127 12 String pattern = string_new(dir.length + section.length + 16);
128
129 // Add `dir` prefix to `pattern`, escaping any special characters
130
2/2
✓ Branch 0 (10→6) taken 296 times.
✓ Branch 1 (10→11) taken 12 times.
308 for (size_t i = 0, n = dir.length; i < n; i++) {
131 296 const char c = dir.data[i];
132
1/2
✗ Branch 0 (6→7) not taken.
✓ Branch 1 (6→8) taken 296 times.
296 if (is_ec_special_char(c)) {
133 string_append_byte(&pattern, '\\');
134 }
135 296 string_append_byte(&pattern, c);
136 }
137
138
2/2
✓ Branch 0 (11→12) taken 10 times.
✓ Branch 1 (11→13) taken 2 times.
12 if (!strview_memchr(&section, '/')) {
139 // Section contains no slashes; insert "**/" between `dir` and `section`
140 10 string_append_literal(&pattern, "**/");
141
1/2
✓ Branch 0 (13→14) taken 2 times.
✗ Branch 1 (13→15) not taken.
2 } else if (section.data[0] != '/') {
142 // Section contains slashes, but not at the start; insert '/' between
143 // `dir` and `section`
144 2 string_append_byte(&pattern, '/');
145 }
146
147 // Append the section heading (from the .editorconfig file) to the
148 // prefix constructed above and test whether the resulting `pattern`
149 // matches against `path`
150 12 string_append_strview(&pattern, &section);
151 12 bool r = ec_pattern_match(pattern.buffer, pattern.len, path);
152 12 string_free(&pattern);
153 12 return r;
154 }
155
156 4 static bool is_root_key(const IniParser *ini)
157 {
158 4 return strview_equal_cstring_icase(&ini->name, "root")
159
2/4
✓ Branch 0 (3→4) taken 4 times.
✗ Branch 1 (3→7) not taken.
✗ Branch 2 (5→6) not taken.
✓ Branch 3 (5→7) taken 4 times.
4 && strview_equal_cstring_icase(&ini->value, "true");
160 }
161
162 4 static void editorconfig_parse(const char *buf, size_t size, UserData *data)
163 {
164 8 IniParser ini = {
165 .input = buf,
166 .input_len = size,
167
3/4
✓ Branch 0 (2→3) taken 4 times.
✗ Branch 1 (2→6) not taken.
✓ Branch 2 (4→5) taken 2 times.
✓ Branch 3 (4→6) taken 2 times.
4 .pos = (size >= 3 && mem_equal(buf, "\xEF\xBB\xBF", 3)) ? 3 : 0, // Skip UTF-8 BOM
168 };
169
170
2/2
✓ Branch 0 (19→7) taken 30 times.
✓ Branch 1 (19→20) taken 4 times.
34 while (ini_parse(&ini)) {
171
2/2
✓ Branch 0 (7→8) taken 4 times.
✓ Branch 1 (7→12) taken 26 times.
30 if (ini.section.length == 0) {
172
1/2
✓ Branch 0 (9→10) taken 4 times.
✗ Branch 1 (9→11) not taken.
4 if (is_root_key(&ini)) {
173 // root=true, clear all previous values
174 4 data->options = editorconfig_options_init();
175 }
176 4 continue;
177 }
178
179
2/2
✓ Branch 0 (12→13) taken 12 times.
✓ Branch 1 (12→15) taken 14 times.
26 if (ini.name_count == 1) {
180 // If name_count is 1, it indicates that the name/value pair is
181 // the first in the section and therefore requires a new pattern
182 // to be built and tested for a match
183 12 StringView dir = data->config_file_dir;
184 12 data->match = section_matches_path(ini.section, dir, data->pathname);
185 } else {
186 // Otherwise, the section is the same as was passed for the first
187 // name/value pair in the section and the value of data->match
188 // can just be reused
189 26 }
190
191
2/2
✓ Branch 0 (15→16) taken 9 times.
✓ Branch 1 (15→17) taken 17 times.
26 if (data->match) {
192 9 editorconfig_option_set(&data->options, &ini.name, &ini.value);
193 }
194 }
195 4 }
196
197 2 int get_editorconfig_options(const char *pathname, EditorConfigOptions *opts)
198 {
199 2 BUG_ON(!path_is_absolute(pathname));
200 2 UserData data = {
201 .pathname = pathname,
202 .config_file_dir = STRING_VIEW_INIT,
203 .match = false
204 };
205
206 2 static const char ecfilename[16] = "/.editorconfig";
207 2 char buf[8192];
208 2 memcpy(buf, ecfilename, sizeof ecfilename);
209
210 2 const char *ptr = pathname + 1;
211 2 size_t dir_len = 1;
212
213 // Iterate up directory tree, looking for ".editorconfig" at each level
214 22 while (1) {
215 12 char *text;
216 12 ssize_t len = read_file(buf, &text, MAX_FILESIZE);
217
2/2
✓ Branch 0 (6→7) taken 4 times.
✓ Branch 1 (6→9) taken 8 times.
12 if (len >= 0) {
218 4 data.config_file_dir = string_view(buf, dir_len);
219 4 editorconfig_parse(text, len, &data);
220 4 free(text);
221 }
222
223 12 const char *slash = strchr(ptr, '/');
224
2/2
✓ Branch 0 (9→10) taken 10 times.
✓ Branch 1 (9→11) taken 2 times.
12 if (!slash) {
225 break;
226 }
227
228 10 dir_len = slash - pathname;
229 10 memcpy(buf, pathname, dir_len);
230 10 memcpy(buf + dir_len, ecfilename, sizeof ecfilename);
231 10 ptr = slash + 1;
232 }
233
234 // Set indent_size to "tab" if indent_size is not specified and
235 // indent_style is set to "tab"
236 2 EditorConfigOptions *o = &data.options;
237
3/4
✓ Branch 0 (11→12) taken 1 times.
✓ Branch 1 (11→14) taken 1 times.
✗ Branch 2 (12→13) not taken.
✓ Branch 3 (12→14) taken 1 times.
2 if (o->indent_size == 0 && o->indent_style == INDENT_STYLE_TAB) {
238 o->indent_size_is_tab = true;
239 }
240
241 // Set indent_size to tab_width if indent_size is "tab" and
242 // tab_width is specified
243
1/4
✗ Branch 0 (14→15) not taken.
✓ Branch 1 (14→17) taken 2 times.
✗ Branch 2 (15→16) not taken.
✗ Branch 3 (15→17) not taken.
2 if (o->indent_size_is_tab && o->tab_width > 0) {
244 o->indent_size = o->tab_width;
245 }
246
247 // Set tab_width to indent_size if indent_size is specified as
248 // something other than "tab" and tab_width is unspecified
249
4/6
✓ Branch 0 (17→18) taken 1 times.
✓ Branch 1 (17→21) taken 1 times.
✓ Branch 2 (18→19) taken 1 times.
✗ Branch 3 (18→21) not taken.
✓ Branch 4 (19→20) taken 1 times.
✗ Branch 5 (19→21) not taken.
2 if (o->indent_size != 0 && o->tab_width == 0 && !o->indent_size_is_tab) {
250 1 o->tab_width = o->indent_size;
251 }
252
253 2 *opts = data.options;
254 2 return 0;
255 }
256