1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// SPDX-FileCopyrightText: Contributors to ecformat project <https://codeberg.org/BaumiCoder/ecformat>
//
// SPDX-License-Identifier: BlueOak-1.0.0
//! Module for the indentation related properties of EditorConfig,
//! i.e., `indent_style`, `indent_size` and `tab_width`
use ec4rs::property::{self, IndentSize, IndentStyle, TabWidth};
use itertools::Itertools;
use line_ending::LineEnding;
use snafu::ensure;
use crate::{errors, files};
use super::{PropertyHandler, charset, end_of_line};
/// Handles the indentation related properties for a single file,
/// i.e., `indent_style`, `indent_size` and `tab_width`
pub struct IndentationHandler {
charset: property::Charset,
line_ending: LineEnding,
indent_style: Option<property::IndentStyle>,
indent_size: Option<usize>,
tab_width: Option<usize>,
}
impl PropertyHandler for IndentationHandler {
fn check(&self, file_path: &std::path::Path) -> anyhow::Result<()> {
let content = files::read_file(file_path, &self.charset)?;
let lines_with_wrong_indentation = self
.get_lines_with_wrong_indentation(&content)
.collect_vec();
ensure!(
lines_with_wrong_indentation.is_empty(),
errors::IndentationSnafu {
lines_with_wrong_indentation
}
);
Ok(())
}
fn fix(&self, file_path: &std::path::Path) -> anyhow::Result<()> {
let mut content = files::read_file(file_path, &self.charset)?;
if self.fix_indentations(&mut content) {
files::overwrite_file(file_path, &self.charset, &content)?;
}
Ok(())
}
}
impl IndentationHandler {
/// Creates a [`IndentationHandler`] for the given properties,
/// if a handler is necessary for these properties.
pub fn build(properties: &ec4rs::Properties) -> Option<IndentationHandler> {
let indent_style = properties.get::<property::IndentStyle>().ok();
let mut tab_width = match properties.get::<property::TabWidth>() {
Ok(TabWidth::Value(tab_width)) => Some(tab_width),
Err(_) => None,
};
let indent_size = match properties.get::<property::IndentSize>() {
Ok(IndentSize::Value(indent_size)) => Some(indent_size),
Ok(IndentSize::UseTabWidth) => tab_width,
Err(_) => None,
};
if indent_style.is_some() || indent_size.is_some() {
if tab_width.is_none() {
// Specification 0.17.2 says that tab_width uses indent_size as fallback
tab_width = indent_size;
}
Some(IndentationHandler {
charset: charset::get_charset(properties),
line_ending: end_of_line::get_line_ending(properties),
indent_style,
indent_size,
tab_width,
})
} else {
// Only a tab_width has only visual implications in an editor
None
}
}
/// Returns an iterator over the indices of lines with a wrong indentation
fn get_lines_with_wrong_indentation(&self, content: &str) -> impl Iterator<Item = usize> {
content
.split(self.line_ending.as_str())
.enumerate()
.filter_map(|(index, line)| {
if self.check_line_indentation(line) {
None
} else {
Some(index)
}
})
}
/// Checks the indentation of a given line
/// and returns true if it respects the EditorConfig properties and false otherwise.
fn check_line_indentation(&self, line: &str) -> bool {
let indent = line
.chars()
.position(Self::is_no_indentation_char)
.map(|i| line.chars().take(i));
if let Some(indent_chars) = indent {
if let Some(tab_width) = self.tab_width {
let line_indent_size = Self::get_indentation_size(indent_chars.clone(), tab_width);
if let Some(indent_size) = self.indent_size
&& !line_indent_size.is_multiple_of(indent_size)
{
// line has wrong indentation size
false
} else {
match self.indent_style {
Some(IndentStyle::Spaces) => {
// Only spaces for the indentation allowed
indent_chars.clone().all(|c| c == ' ')
}
Some(IndentStyle::Tabs) => {
let mut tabs = 0;
let mut spaces = 0;
for c in indent_chars {
if c == '\t' {
tabs += 1;
} else {
spaces += 1;
}
}
let expected_tabs = line_indent_size / tab_width;
let expected_spaces = line_indent_size % tab_width;
// Use the correct number of tabs and spaces
tabs == expected_tabs || spaces == expected_spaces
}
None => true,
}
}
} else {
// Without a tab_width value, also indent_size is not set
// which allows only limited validations.
match self.indent_style {
Some(IndentStyle::Spaces) => {
// Only spaces for the indentation allowed
indent_chars.clone().all(|c| c == ' ')
}
Some(IndentStyle::Tabs) => {
// Without a tab_width anything is allowed,
// e.g., smaller indentations can be without any tabs
true
}
None => {
debug_assert!(
false,
"Without indent_style and indent_size a handler was created"
);
true
}
}
}
} else {
// line is empty or only contains indentation characters
true
}
}
/// Fix the indentation of the lines, where it is necessary.
/// Returns true if there were lines to fix (i.e., content was changed).
fn fix_indentations(&self, content: &mut String) -> bool {
let mut lines_with_wrong_indentation =
self.get_lines_with_wrong_indentation(content).peekable();
let wrong_indentations = lines_with_wrong_indentation.peek().is_some();
if wrong_indentations {
let mut lines = content
.split(self.line_ending.as_str())
.map(String::from)
.collect_vec();
for index in lines_with_wrong_indentation {
lines[index] = self.fix_line_indentation(lines[index].as_str());
}
*content = lines.join(self.line_ending.as_str());
}
wrong_indentations
}
/// Fixes the indentation of a given line
/// and returns the fixed line.
fn fix_line_indentation(&self, line: &str) -> String {
let new_indent: String;
let after_indent = line.chars().position(Self::is_no_indentation_char);
if let Some(index_after_indent) = after_indent {
let indent_chars = line.chars().take(index_after_indent);
// Without tab_width set, use a default to allow an indent_style only fix
let tab_width = self.tab_width.unwrap_or(4);
let line_indent_size = Self::get_indentation_size(indent_chars.clone(), tab_width);
if let Some(indent_size) = self.indent_size {
let new_indent_size = if line_indent_size.is_multiple_of(indent_size) {
indent_size
} else {
// Heuristic: Round the indentation level up
let indent_levels = (line_indent_size / indent_size) + 1;
indent_levels * indent_size
};
let indent_style = self.indent_style.unwrap_or_else(|| {
// Heuristic: If a tab is included it is tabs style
if indent_chars.clone().contains(&'\t') {
IndentStyle::Tabs
} else {
IndentStyle::Spaces
}
});
new_indent =
IndentationHandler::get_indentation(indent_style, new_indent_size, tab_width);
} else {
// Without indent_size only fix the indent_style and respect the tab_width
new_indent = IndentationHandler::get_indentation(
self.indent_style
.expect("Without indent style and size no wrong indents possible"),
line_indent_size, // keep size unchanged as indent_size is not set
tab_width,
);
}
new_indent + &line[index_after_indent..]
} else {
unreachable!("Without an indentation it cannot be wrong")
}
}
/// Produces an indentation in the given style
/// and with the given size (in columns), respecting the given tab width.
fn get_indentation(
indent_style: property::IndentStyle,
size: usize,
tab_width: usize,
) -> String {
match indent_style {
IndentStyle::Tabs => {
let tabs = size / tab_width;
let spaces = size % tab_width;
// The example in the specification 0.17.2 talks about
// "indented with one tab, and one space".
// Therefore, assume that tabs should come first.
// (Although `check_line_indentation()` accepts any order)
"\t".repeat(tabs) + " ".repeat(spaces).as_str()
}
IndentStyle::Spaces => " ".repeat(size),
}
}
/// Returns true if the given character cannot be part of an indentation.
fn is_no_indentation_char(character: char) -> bool {
// Specification 0.17.2 only mentions "spaces" and "tabs" for indentations
// and does not mention the bigger group of whitespace characters for indentations.
character != ' ' && character != '\t'
}
/// Returns the size of the given indentation chars in number of columns
/// with respect to the given tab_width.
fn get_indentation_size(indent_chars: impl Iterator<Item = char>, tab_width: usize) -> usize {
indent_chars
.map(|c| if c == '\t' { tab_width } else { 1 })
.sum()
}
}
#[cfg(test)]
mod tests;