dte test coverage


Directory: ./
File: src/editorconfig/editorconfig.c
Date: 2025-09-07 23:01:39
Exec Total Coverage
Lines: 104 114 91.2%
Functions: 12 12 100.0%
Branches: 53 94 56.4%

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