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 |