rfmt 0.1.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/Cargo.lock +1748 -133
- data/README.md +360 -20
- data/exe/rfmt +15 -0
- data/ext/rfmt/Cargo.toml +46 -1
- data/ext/rfmt/extconf.rb +5 -5
- data/ext/rfmt/spec/config_spec.rb +39 -0
- data/ext/rfmt/spec/spec_helper.rb +16 -0
- data/ext/rfmt/src/ast/mod.rs +335 -0
- data/ext/rfmt/src/config/mod.rs +403 -0
- data/ext/rfmt/src/emitter/mod.rs +363 -0
- data/ext/rfmt/src/error/mod.rs +48 -0
- data/ext/rfmt/src/lib.rs +59 -36
- data/ext/rfmt/src/logging/logger.rs +128 -0
- data/ext/rfmt/src/logging/mod.rs +3 -0
- data/ext/rfmt/src/parser/mod.rs +9 -0
- data/ext/rfmt/src/parser/prism_adapter.rs +407 -0
- data/ext/rfmt/src/policy/mod.rs +36 -0
- data/ext/rfmt/src/policy/validation.rs +18 -0
- data/lib/rfmt/cache.rb +120 -0
- data/lib/rfmt/cli.rb +280 -0
- data/lib/rfmt/configuration.rb +95 -0
- data/lib/rfmt/prism_bridge.rb +255 -0
- data/lib/rfmt/prism_node_extractor.rb +81 -0
- data/lib/rfmt/rfmt.so +0 -0
- data/lib/rfmt/version.rb +1 -1
- data/lib/rfmt.rb +156 -5
- metadata +29 -7
- data/lib/rfmt/rfmt.bundle +0 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
|
|
3
|
+
/// Complete configuration structure matching .rfmt.yml format
|
|
4
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
5
|
+
pub struct Config {
|
|
6
|
+
#[serde(default)]
|
|
7
|
+
pub version: String,
|
|
8
|
+
|
|
9
|
+
#[serde(default)]
|
|
10
|
+
pub parser: ParserConfig,
|
|
11
|
+
|
|
12
|
+
#[serde(default)]
|
|
13
|
+
pub formatting: FormattingConfig,
|
|
14
|
+
|
|
15
|
+
#[serde(default)]
|
|
16
|
+
pub include: Vec<String>,
|
|
17
|
+
|
|
18
|
+
#[serde(default)]
|
|
19
|
+
pub exclude: Vec<String>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
23
|
+
pub struct ParserConfig {
|
|
24
|
+
pub version: String,
|
|
25
|
+
pub error_tolerance: bool,
|
|
26
|
+
pub encoding: String,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
30
|
+
pub struct FormattingConfig {
|
|
31
|
+
#[serde(default = "default_line_length")]
|
|
32
|
+
pub line_length: usize,
|
|
33
|
+
|
|
34
|
+
#[serde(rename = "indent_style", default)]
|
|
35
|
+
pub indent_style: IndentStyle,
|
|
36
|
+
|
|
37
|
+
#[serde(rename = "indent_width", default = "default_indent_width")]
|
|
38
|
+
pub indent_width: usize,
|
|
39
|
+
|
|
40
|
+
#[serde(rename = "quote_style", default)]
|
|
41
|
+
pub quote_style: QuoteStyle,
|
|
42
|
+
|
|
43
|
+
#[serde(default)]
|
|
44
|
+
pub style: StyleConfig,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn default_line_length() -> usize {
|
|
48
|
+
100
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn default_indent_width() -> usize {
|
|
52
|
+
2
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
56
|
+
#[serde(rename_all = "lowercase")]
|
|
57
|
+
pub enum IndentStyle {
|
|
58
|
+
#[default]
|
|
59
|
+
Spaces,
|
|
60
|
+
Tabs,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
64
|
+
pub struct StyleConfig {
|
|
65
|
+
#[serde(default)]
|
|
66
|
+
pub quotes: QuoteStyle,
|
|
67
|
+
|
|
68
|
+
#[serde(default)]
|
|
69
|
+
pub hash_syntax: HashSyntax,
|
|
70
|
+
|
|
71
|
+
#[serde(default)]
|
|
72
|
+
pub trailing_comma: TrailingComma,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
76
|
+
#[serde(rename_all = "lowercase")]
|
|
77
|
+
pub enum QuoteStyle {
|
|
78
|
+
#[default]
|
|
79
|
+
Double,
|
|
80
|
+
Single,
|
|
81
|
+
Consistent,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
85
|
+
pub enum HashSyntax {
|
|
86
|
+
#[default]
|
|
87
|
+
Ruby19,
|
|
88
|
+
HashRockets,
|
|
89
|
+
Consistent,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
93
|
+
#[serde(rename_all = "lowercase")]
|
|
94
|
+
pub enum TrailingComma {
|
|
95
|
+
Always,
|
|
96
|
+
Never,
|
|
97
|
+
#[default]
|
|
98
|
+
Multiline,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl Config {
|
|
102
|
+
/// Load configuration from a YAML file
|
|
103
|
+
#[cfg(test)]
|
|
104
|
+
pub fn load_file(path: &std::path::Path) -> crate::error::Result<Self> {
|
|
105
|
+
use crate::error::RfmtError;
|
|
106
|
+
|
|
107
|
+
let contents = std::fs::read_to_string(path).map_err(|e| RfmtError::ConfigError {
|
|
108
|
+
message: format!("Failed to read config file: {}", e),
|
|
109
|
+
})?;
|
|
110
|
+
|
|
111
|
+
let config: Config =
|
|
112
|
+
serde_yaml::from_str(&contents).map_err(|e| RfmtError::ConfigError {
|
|
113
|
+
message: format!("Failed to parse config file: {}", e),
|
|
114
|
+
})?;
|
|
115
|
+
|
|
116
|
+
config.validate()?;
|
|
117
|
+
|
|
118
|
+
Ok(config)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Validate configuration values
|
|
122
|
+
#[cfg(test)]
|
|
123
|
+
fn validate(&self) -> crate::error::Result<()> {
|
|
124
|
+
use crate::error::RfmtError;
|
|
125
|
+
|
|
126
|
+
if self.formatting.line_length < 40 || self.formatting.line_length > 500 {
|
|
127
|
+
return Err(RfmtError::ConfigError {
|
|
128
|
+
message: format!(
|
|
129
|
+
"line_length must be between 40 and 500, got {}",
|
|
130
|
+
self.formatting.line_length
|
|
131
|
+
),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if self.formatting.indent_width < 1 || self.formatting.indent_width > 8 {
|
|
136
|
+
return Err(RfmtError::ConfigError {
|
|
137
|
+
message: format!(
|
|
138
|
+
"indent_width must be between 1 and 8, got {}",
|
|
139
|
+
self.formatting.indent_width
|
|
140
|
+
),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Ok(())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Get the indent string based on configuration
|
|
148
|
+
#[cfg(test)]
|
|
149
|
+
pub fn indent_string(&self) -> String {
|
|
150
|
+
match self.formatting.indent_style {
|
|
151
|
+
IndentStyle::Spaces => " ".repeat(self.formatting.indent_width),
|
|
152
|
+
IndentStyle::Tabs => "\t".to_string(),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Check if a file path should be included based on include/exclude patterns
|
|
157
|
+
#[cfg(test)]
|
|
158
|
+
pub fn should_include(&self, path: &std::path::Path) -> bool {
|
|
159
|
+
use globset::{Glob, GlobSetBuilder};
|
|
160
|
+
|
|
161
|
+
let path_str = path.to_string_lossy();
|
|
162
|
+
|
|
163
|
+
// Check exclude patterns first
|
|
164
|
+
let mut exclude_builder = GlobSetBuilder::new();
|
|
165
|
+
for pattern in &self.exclude {
|
|
166
|
+
if let Ok(glob) = Glob::new(pattern) {
|
|
167
|
+
exclude_builder.add(glob);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if let Ok(exclude_set) = exclude_builder.build() {
|
|
172
|
+
if exclude_set.is_match(&*path_str) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check include patterns
|
|
178
|
+
let mut include_builder = GlobSetBuilder::new();
|
|
179
|
+
for pattern in &self.include {
|
|
180
|
+
if let Ok(glob) = Glob::new(pattern) {
|
|
181
|
+
include_builder.add(glob);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if let Ok(include_set) = include_builder.build() {
|
|
186
|
+
return include_set.is_match(&*path_str);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
false
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl Default for Config {
|
|
194
|
+
fn default() -> Self {
|
|
195
|
+
Self {
|
|
196
|
+
version: "1.0".to_string(),
|
|
197
|
+
parser: ParserConfig::default(),
|
|
198
|
+
formatting: FormattingConfig::default(),
|
|
199
|
+
include: vec!["**/*.rb".to_string(), "**/*.rake".to_string()],
|
|
200
|
+
exclude: vec![
|
|
201
|
+
"vendor/**/*".to_string(),
|
|
202
|
+
"tmp/**/*".to_string(),
|
|
203
|
+
"node_modules/**/*".to_string(),
|
|
204
|
+
],
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
impl Default for ParserConfig {
|
|
210
|
+
fn default() -> Self {
|
|
211
|
+
Self {
|
|
212
|
+
version: "latest".to_string(),
|
|
213
|
+
error_tolerance: true,
|
|
214
|
+
encoding: "UTF-8".to_string(),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
impl Default for FormattingConfig {
|
|
220
|
+
fn default() -> Self {
|
|
221
|
+
Self {
|
|
222
|
+
line_length: 100,
|
|
223
|
+
indent_style: IndentStyle::Spaces,
|
|
224
|
+
indent_width: 2,
|
|
225
|
+
quote_style: QuoteStyle::Double,
|
|
226
|
+
style: StyleConfig::default(),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
impl Default for StyleConfig {
|
|
232
|
+
fn default() -> Self {
|
|
233
|
+
Self {
|
|
234
|
+
quotes: QuoteStyle::Double,
|
|
235
|
+
hash_syntax: HashSyntax::Ruby19,
|
|
236
|
+
trailing_comma: TrailingComma::Multiline,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[cfg(test)]
|
|
242
|
+
mod tests {
|
|
243
|
+
use super::*;
|
|
244
|
+
use crate::error::RfmtError;
|
|
245
|
+
use std::io::Write;
|
|
246
|
+
use std::path::Path;
|
|
247
|
+
use tempfile::NamedTempFile;
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn test_default_config() {
|
|
251
|
+
let config = Config::default();
|
|
252
|
+
assert_eq!(config.version, "1.0");
|
|
253
|
+
assert_eq!(config.formatting.line_length, 100);
|
|
254
|
+
assert_eq!(config.formatting.indent_width, 2);
|
|
255
|
+
assert!(matches!(
|
|
256
|
+
config.formatting.indent_style,
|
|
257
|
+
IndentStyle::Spaces
|
|
258
|
+
));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn test_load_valid_config() {
|
|
263
|
+
let yaml = r#"
|
|
264
|
+
version: "1.0"
|
|
265
|
+
formatting:
|
|
266
|
+
line_length: 120
|
|
267
|
+
indent_width: 4
|
|
268
|
+
indent_style: tabs
|
|
269
|
+
quote_style: single
|
|
270
|
+
include:
|
|
271
|
+
- "**/*.rb"
|
|
272
|
+
exclude:
|
|
273
|
+
- "vendor/**/*"
|
|
274
|
+
"#;
|
|
275
|
+
|
|
276
|
+
let mut file = NamedTempFile::new().unwrap();
|
|
277
|
+
file.write_all(yaml.as_bytes()).unwrap();
|
|
278
|
+
file.flush().unwrap();
|
|
279
|
+
|
|
280
|
+
let config = Config::load_file(file.path()).unwrap();
|
|
281
|
+
assert_eq!(config.formatting.line_length, 120);
|
|
282
|
+
assert_eq!(config.formatting.indent_width, 4);
|
|
283
|
+
assert!(matches!(config.formatting.indent_style, IndentStyle::Tabs));
|
|
284
|
+
assert!(matches!(config.formatting.quote_style, QuoteStyle::Single));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[test]
|
|
288
|
+
fn test_validate_line_length_too_small() {
|
|
289
|
+
let yaml = r#"
|
|
290
|
+
formatting:
|
|
291
|
+
line_length: 30
|
|
292
|
+
"#;
|
|
293
|
+
|
|
294
|
+
let mut file = NamedTempFile::new().unwrap();
|
|
295
|
+
file.write_all(yaml.as_bytes()).unwrap();
|
|
296
|
+
file.flush().unwrap();
|
|
297
|
+
|
|
298
|
+
let result = Config::load_file(file.path());
|
|
299
|
+
assert!(result.is_err());
|
|
300
|
+
if let Err(RfmtError::ConfigError { message, .. }) = result {
|
|
301
|
+
assert!(message.contains("line_length"));
|
|
302
|
+
assert!(message.contains("40 and 500"));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn test_validate_line_length_too_large() {
|
|
308
|
+
let yaml = r#"
|
|
309
|
+
formatting:
|
|
310
|
+
line_length: 600
|
|
311
|
+
"#;
|
|
312
|
+
|
|
313
|
+
let mut file = NamedTempFile::new().unwrap();
|
|
314
|
+
file.write_all(yaml.as_bytes()).unwrap();
|
|
315
|
+
file.flush().unwrap();
|
|
316
|
+
|
|
317
|
+
let result = Config::load_file(file.path());
|
|
318
|
+
assert!(result.is_err());
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn test_validate_indent_width() {
|
|
323
|
+
let yaml = r#"
|
|
324
|
+
formatting:
|
|
325
|
+
indent_width: 10
|
|
326
|
+
"#;
|
|
327
|
+
|
|
328
|
+
let mut file = NamedTempFile::new().unwrap();
|
|
329
|
+
file.write_all(yaml.as_bytes()).unwrap();
|
|
330
|
+
file.flush().unwrap();
|
|
331
|
+
|
|
332
|
+
let result = Config::load_file(file.path());
|
|
333
|
+
assert!(result.is_err());
|
|
334
|
+
if let Err(RfmtError::ConfigError { message, .. }) = result {
|
|
335
|
+
assert!(message.contains("indent_width"));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn test_indent_string_spaces() {
|
|
341
|
+
let config = Config::default();
|
|
342
|
+
assert_eq!(config.indent_string(), " "); // 2 spaces
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[test]
|
|
346
|
+
fn test_indent_string_tabs() {
|
|
347
|
+
let mut config = Config::default();
|
|
348
|
+
config.formatting.indent_style = IndentStyle::Tabs;
|
|
349
|
+
assert_eq!(config.indent_string(), "\t");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn test_should_include_basic() {
|
|
354
|
+
let config = Config::default();
|
|
355
|
+
assert!(config.should_include(Path::new("lib/foo.rb")));
|
|
356
|
+
assert!(!config.should_include(Path::new("vendor/gem/foo.rb")));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#[test]
|
|
360
|
+
fn test_should_include_with_exclude() {
|
|
361
|
+
let mut config = Config::default();
|
|
362
|
+
config.exclude.push("test/**/*".to_string());
|
|
363
|
+
assert!(!config.should_include(Path::new("test/foo.rb")));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#[test]
|
|
367
|
+
fn test_invalid_yaml_syntax() {
|
|
368
|
+
let yaml = r#"
|
|
369
|
+
formatting:
|
|
370
|
+
line_length: not_a_number
|
|
371
|
+
"#;
|
|
372
|
+
|
|
373
|
+
let mut file = NamedTempFile::new().unwrap();
|
|
374
|
+
file.write_all(yaml.as_bytes()).unwrap();
|
|
375
|
+
file.flush().unwrap();
|
|
376
|
+
|
|
377
|
+
let result = Config::load_file(file.path());
|
|
378
|
+
assert!(result.is_err());
|
|
379
|
+
if let Err(RfmtError::ConfigError { message, .. }) = result {
|
|
380
|
+
assert!(message.contains("parse"));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#[test]
|
|
385
|
+
fn test_partial_config_uses_defaults() {
|
|
386
|
+
let yaml = r#"
|
|
387
|
+
formatting:
|
|
388
|
+
line_length: 80
|
|
389
|
+
"#;
|
|
390
|
+
|
|
391
|
+
let mut file = NamedTempFile::new().unwrap();
|
|
392
|
+
file.write_all(yaml.as_bytes()).unwrap();
|
|
393
|
+
file.flush().unwrap();
|
|
394
|
+
|
|
395
|
+
let config = Config::load_file(file.path()).unwrap();
|
|
396
|
+
assert_eq!(config.formatting.line_length, 80);
|
|
397
|
+
assert_eq!(config.formatting.indent_width, 2); // default
|
|
398
|
+
assert!(matches!(
|
|
399
|
+
config.formatting.indent_style,
|
|
400
|
+
IndentStyle::Spaces
|
|
401
|
+
)); // default
|
|
402
|
+
}
|
|
403
|
+
}
|