rfmt 0.2.2 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9981b614b859c7c49e597686b89c79279ba977911b9a62d691aa0b88408eb0f
4
- data.tar.gz: 1461e9bcf3ff3b298855963fdf2e6cdd0aca1b3e29bf6f5b9037e62889903469
3
+ metadata.gz: d42c0061474a8e7ef36fc5cbce1d7bfdad2bc5a9dbbff9b1a2c2b700b373c6ca
4
+ data.tar.gz: f12d17ab726936fbdf69e4e5aaf3bca55fadf49230aa641adf0cba61c363f5ca
5
5
  SHA512:
6
- metadata.gz: 0b6aa80fa2df44397d1bdc81a12b860e1446a611f565e3283af8a821e691ca5122041f9622d48e2577740e878d740a1dba95df0a8c6acbea71c8301993f6fb46
7
- data.tar.gz: a1f330f17114d738523adf1fdcc9ccc42590b028a754e32304d2d4724f9966d54e5a7467bd50c8884111bc3ff16780b9b9c7591a0a22d8cd1eb17ffa50a856ed
6
+ metadata.gz: 51948c8616f667de54435f87f7e154209c27777e82a84ada9f73d97c8928f68a063da72c1d7268035ca7d8d6e3eec658e4f6d874770658501edaad68a512ecdf
7
+ data.tar.gz: e9a794b23d30ba314aa2b0f5fcacb6bbaff7408430bab6623169ef7d76fad53f32f58789e2a0a828a3b9e38dc074ca22bb0c4ff93f023e7c1a8c000081ba66e5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.4] - 2025-11-25
4
+
5
+ ### Fixed
6
+ - Fixed if-else expression formatting
7
+
8
+ ## [0.2.3] - 2025-01-25
9
+
10
+ ### Added
11
+ - Configuration file (rfmt.yml) is now automatically loaded and applied during formatting
12
+ - Automatic config file discovery in current directory, parent directories, and home directory
13
+ - Support for custom indent_width and other formatting options via rfmt.yml
14
+
15
+ ### Changed
16
+ - Default config file name changed from .rfmt.yml to rfmt.yml (hidden file to regular file)
17
+ - Backward compatibility maintained: .rfmt.yml is still supported with lower priority
18
+ - Config file search order: rfmt.yml > rfmt.yaml > .rfmt.yml > .rfmt.yaml
19
+ - README updated to remove exaggerated expressions and focus on factual, data-driven descriptions
20
+
3
21
  ## [0.2.2] - 2025-01-25
4
22
 
5
23
  ### Fixed
data/Cargo.lock CHANGED
@@ -1219,7 +1219,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
1219
1219
 
1220
1220
  [[package]]
1221
1221
  name = "rfmt"
1222
- version = "0.2.2"
1222
+ version = "0.2.4"
1223
1223
  dependencies = [
1224
1224
  "anyhow",
1225
1225
  "clap",
data/README.md CHANGED
@@ -65,15 +65,36 @@ Built-in logging system:
65
65
  - Performance metrics and timing data
66
66
  - Debug context information
67
67
 
68
- ### Editor Integration
68
+ ## Performance Benchmarks
69
+
70
+ Performance comparison with RuboCop on a Rails project (111 files, 3,231 lines):
71
+
72
+ ### Benchmark Results (Rails Project)
73
+
74
+ | Scenario | rfmt | RuboCop | Speedup |
75
+ |----------|------|---------|---------|
76
+ | **Single File** | ~190ms | ~1.35s | **7.3x faster** |
77
+ | **Directory (14 files)** | 168ms | 1.67s | **10.0x faster** |
78
+ | **Full Project (111 files)** | 173ms | 10.09s | **58.5x faster** |
79
+ | **Check Mode (CI/CD)** | 172ms | 1.55s | **9.0x faster** |
80
+
81
+ ### Key Metrics
82
+
83
+ - Single file: Formats in ~190ms
84
+ - Scaling: 58x faster on full project (111 files)
85
+ - CI/CD: Check time reduced from 10.09s to 0.173s (98% reduction)
86
+ - Variance: Low standard deviation across runs
69
87
 
70
- Compatible editors:
88
+ ### Throughput Comparison
71
89
 
72
- - Visual Studio Code
73
- - RubyMine / IntelliJ IDEA
74
- - Vim / Neovim
75
- - Emacs
76
- - Sublime Text
90
+ | Directory | rfmt | RuboCop | Difference |
91
+ |-----------|------|---------|------------|
92
+ | app/models (14 files) | 83.5 files/s | 8.4 files/s | **10x throughput** |
93
+ | test/ (30 files) | 168.1 files/s | 18.1 files/s | **9.3x throughput** |
94
+
95
+ *Benchmark environment: Apple Silicon (arm64), macOS Darwin 23.6.0, Ruby 3.4.5*
96
+
97
+ See [detailed benchmark report](docspriv/benchmark_report.md) for full data.
77
98
 
78
99
  ## Installation
79
100
 
@@ -168,7 +189,7 @@ Create a configuration file with default settings:
168
189
  rfmt init
169
190
  ```
170
191
 
171
- This creates a `.rfmt.yml` file with default settings:
192
+ This creates a `rfmt.yml` file with default settings:
172
193
 
173
194
  ```yaml
174
195
  version: "1.0"
@@ -206,9 +227,9 @@ rfmt init --force
206
227
 
207
228
  rfmt automatically searches for configuration files in this order:
208
229
 
209
- 1. Current directory (`.rfmt.yml` or `.rfmt.yaml`)
230
+ 1. Current directory (`rfmt.yml`, `rfmt.yaml`, `.rfmt.yml`, or `.rfmt.yaml`)
210
231
  2. Parent directories (up to root)
211
- 3. User home directory (`~/.rfmt.yml` or `~/.rfmt.yaml`)
232
+ 3. User home directory (`rfmt.yml`, `rfmt.yaml`, `.rfmt.yml`, or `.rfmt.yaml`)
212
233
  4. Default settings (if no file found)
213
234
 
214
235
  #### Ruby API for Configuration
@@ -217,11 +238,11 @@ rfmt automatically searches for configuration files in this order:
217
238
  require 'rfmt'
218
239
 
219
240
  # Generate configuration file
220
- Rfmt::Config.init('.rfmt.yml', force: false)
241
+ Rfmt::Config.init('rfmt.yml', force: false)
221
242
 
222
243
  # Find configuration file
223
244
  config_path = Rfmt::Config.find
224
- # => "/Users/username/project/.rfmt.yml"
245
+ # => "/Users/username/project/rfmt.yml"
225
246
 
226
247
  # Check if configuration exists
227
248
  Rfmt::Config.exists?
@@ -312,37 +333,6 @@ class User < ApplicationRecord
312
333
  end
313
334
  ```
314
335
 
315
- ## Performance Benchmarks
316
-
317
- Performance comparison with RuboCop on a Rails project (111 files, 3,231 lines):
318
-
319
- ### Benchmark Results (Rails Project)
320
-
321
- | Scenario | rfmt | RuboCop | Speedup |
322
- |----------|------|---------|---------|
323
- | **Single File** | ~190ms | ~1.35s | **7.3x faster** |
324
- | **Directory (14 files)** | 168ms | 1.67s | **10.0x faster** |
325
- | **Full Project (111 files)** | 173ms | 10.09s | **58.5x faster** |
326
- | **Check Mode (CI/CD)** | 172ms | 1.55s | **9.0x faster** |
327
-
328
- ### Key Metrics
329
-
330
- - Single file: Formats in ~190ms
331
- - Scaling: 58x faster on full project (111 files)
332
- - CI/CD: Check time reduced from 10.09s to 0.173s (98% reduction)
333
- - Variance: Low standard deviation across runs
334
-
335
- ### Throughput Comparison
336
-
337
- | Directory | rfmt | RuboCop | Difference |
338
- |-----------|------|---------|------------|
339
- | app/models (14 files) | 83.5 files/s | 8.4 files/s | **10x throughput** |
340
- | test/ (30 files) | 168.1 files/s | 18.1 files/s | **9.3x throughput** |
341
-
342
- *Benchmark environment: Apple Silicon (arm64), macOS Darwin 23.6.0, Ruby 3.4.5*
343
-
344
- See [detailed benchmark report](docspriv/benchmark_report.md) for full data.
345
-
346
336
  ## Documentation
347
337
 
348
338
  Documentation is available in the [docs](docs/) directory. See [User Guide](docs/user_guide.md) or [Contributing Guide](CONTRIBUTING.md) for details.
@@ -358,7 +348,7 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
358
348
  | Feature | rfmt | RuboCop |
359
349
  |---------|------|---------|
360
350
  | **Primary Purpose** | Code formatting | Linting + formatting |
361
- | **Speed** | 58x faster (tested benchmark) | Ruby-based |
351
+ | **Speed** | 58x faster (tested benchmark) | Baseline |
362
352
  | **Configuration** | Minimal | Extensive |
363
353
  | **Code Quality Checks** | No | Yes |
364
354
  | **Bug Detection** | No | Yes |
@@ -377,7 +367,6 @@ Everyone interacting in the rfmt project's codebases, issue trackers, chat rooms
377
367
 
378
368
  - 📖 [Documentation](docs/)
379
369
  - 🐛 [Issues](https://github.com/fujitanisora/rfmt/issues)
380
- - 💬 [Discussions](https://github.com/fujitanisora/rfmt/discussions)
381
370
  - 📧 Email: fujitanisora0414@gmail.com
382
371
 
383
372
  ## Acknowledgments
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -40,6 +40,7 @@ pub enum NodeType {
40
40
  // Expressions
41
41
  CallNode,
42
42
  IfNode,
43
+ ElseNode,
43
44
  UnlessNode,
44
45
 
45
46
  // Literals
@@ -83,6 +84,7 @@ impl NodeType {
83
84
  "def_node" => Self::DefNode,
84
85
  "call_node" => Self::CallNode,
85
86
  "if_node" => Self::IfNode,
87
+ "else_node" => Self::ElseNode,
86
88
  "unless_node" => Self::UnlessNode,
87
89
  "string_node" => Self::StringNode,
88
90
  "integer_node" => Self::IntegerNode,
@@ -99,8 +99,45 @@ pub enum TrailingComma {
99
99
  }
100
100
 
101
101
  impl Config {
102
+ /// Discover configuration file in current directory or parent directories
103
+ /// Searches in order: rfmt.yml, rfmt.yaml, .rfmt.yml, .rfmt.yaml
104
+ pub fn discover() -> crate::error::Result<Self> {
105
+ let config_files = ["rfmt.yml", "rfmt.yaml", ".rfmt.yml", ".rfmt.yaml"];
106
+
107
+ // Try current directory and parent directories
108
+ if let Ok(mut current_dir) = std::env::current_dir() {
109
+ loop {
110
+ for filename in &config_files {
111
+ let config_path = current_dir.join(filename);
112
+ if config_path.exists() {
113
+ log::info!("Found config file: {:?}", config_path);
114
+ return Self::load_file(&config_path);
115
+ }
116
+ }
117
+
118
+ // Move to parent directory
119
+ if !current_dir.pop() {
120
+ break;
121
+ }
122
+ }
123
+ }
124
+
125
+ // Try home directory
126
+ if let Some(home_dir) = dirs::home_dir() {
127
+ for filename in &config_files {
128
+ let config_path = home_dir.join(filename);
129
+ if config_path.exists() {
130
+ log::debug!("Found config file in home: {:?}", config_path);
131
+ return Self::load_file(&config_path);
132
+ }
133
+ }
134
+ }
135
+
136
+ log::info!("No config file found, using defaults");
137
+ Ok(Config::default())
138
+ }
139
+
102
140
  /// Load configuration from a YAML file
103
- #[cfg(test)]
104
141
  pub fn load_file(path: &std::path::Path) -> crate::error::Result<Self> {
105
142
  use crate::error::RfmtError;
106
143
 
@@ -119,7 +156,6 @@ impl Config {
119
156
  }
120
157
 
121
158
  /// Validate configuration values
122
- #[cfg(test)]
123
159
  fn validate(&self) -> crate::error::Result<()> {
124
160
  use crate::error::RfmtError;
125
161
 
@@ -145,7 +181,6 @@ impl Config {
145
181
  }
146
182
 
147
183
  /// Get the indent string based on configuration
148
- #[cfg(test)]
149
184
  pub fn indent_string(&self) -> String {
150
185
  match self.formatting.indent_style {
151
186
  IndentStyle::Spaces => " ".repeat(self.formatting.indent_width),
@@ -113,6 +113,8 @@ impl Emitter {
113
113
  NodeType::ClassNode => self.emit_class(node, indent_level)?,
114
114
  NodeType::ModuleNode => self.emit_module(node, indent_level)?,
115
115
  NodeType::DefNode => self.emit_method(node, indent_level)?,
116
+ NodeType::IfNode => self.emit_if_unless(node, indent_level, false, "if")?,
117
+ NodeType::UnlessNode => self.emit_if_unless(node, indent_level, false, "unless")?,
116
118
  _ => self.emit_generic(node, indent_level)?,
117
119
  }
118
120
  Ok(())
@@ -304,6 +306,122 @@ impl Emitter {
304
306
  Ok(())
305
307
  }
306
308
 
309
+ /// Emit if/unless/elsif/else node
310
+ /// is_elsif: true if this is an elsif clause (don't emit 'end')
311
+ /// keyword: "if" or "unless"
312
+ fn emit_if_unless(&mut self, node: &Node, indent_level: usize, is_elsif: bool, keyword: &str) -> Result<()> {
313
+ // Check if this is a postfix if (modifier form)
314
+ // In postfix if, the statements come before the if keyword in source
315
+ let is_postfix = if let (Some(predicate), Some(statements)) =
316
+ (node.children.first(), node.children.get(1)) {
317
+ statements.location.start_offset < predicate.location.start_offset
318
+ } else {
319
+ false
320
+ };
321
+
322
+ // Postfix if/unless: "statement if/unless condition"
323
+ if is_postfix && !is_elsif {
324
+ self.emit_comments_before(node.location.start_line, indent_level)?;
325
+ self.emit_indent(indent_level)?;
326
+
327
+ // Emit statement
328
+ if let Some(statements) = node.children.get(1) {
329
+ if matches!(statements.node_type, NodeType::StatementsNode) {
330
+ // Extract the statement text (without extra indentation)
331
+ if !self.source.is_empty() {
332
+ let start = statements.location.start_offset;
333
+ let end = statements.location.end_offset;
334
+ if let Some(text) = self.source.get(start..end) {
335
+ write!(self.buffer, "{}", text.trim())?;
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ write!(self.buffer, " {} ", keyword)?;
342
+
343
+ // Emit condition
344
+ if let Some(predicate) = node.children.first() {
345
+ if !self.source.is_empty() {
346
+ let start = predicate.location.start_offset;
347
+ let end = predicate.location.end_offset;
348
+ if let Some(text) = self.source.get(start..end) {
349
+ write!(self.buffer, "{}", text)?;
350
+ }
351
+ }
352
+ }
353
+
354
+ return Ok(());
355
+ }
356
+
357
+ // Normal if/unless/elsif
358
+ if !is_elsif {
359
+ self.emit_comments_before(node.location.start_line, indent_level)?;
360
+ }
361
+
362
+ // Emit 'if'/'unless' or 'elsif' keyword
363
+ self.emit_indent(indent_level)?;
364
+ if is_elsif {
365
+ write!(self.buffer, "elsif ")?;
366
+ } else {
367
+ write!(self.buffer, "{} ", keyword)?;
368
+ }
369
+
370
+ // Emit predicate (condition) - first child
371
+ if let Some(predicate) = node.children.first() {
372
+ // Extract predicate from source
373
+ if !self.source.is_empty() {
374
+ let start = predicate.location.start_offset;
375
+ let end = predicate.location.end_offset;
376
+ if let Some(text) = self.source.get(start..end) {
377
+ write!(self.buffer, "{}", text)?;
378
+ }
379
+ }
380
+ }
381
+
382
+ self.buffer.push('\n');
383
+
384
+ // Emit then clause (second child is StatementsNode)
385
+ if let Some(statements) = node.children.get(1) {
386
+ if matches!(statements.node_type, NodeType::StatementsNode) {
387
+ self.emit_statements(statements, indent_level + 1)?;
388
+ self.buffer.push('\n');
389
+ }
390
+ }
391
+
392
+ // Check for elsif/else (third child)
393
+ if let Some(consequent) = node.children.get(2) {
394
+ match &consequent.node_type {
395
+ NodeType::IfNode => {
396
+ // This is an elsif clause (only valid for if, not unless)
397
+ self.emit_if_unless(consequent, indent_level, true, "if")?;
398
+ }
399
+ NodeType::ElseNode => {
400
+ // This is an else clause
401
+ self.emit_indent(indent_level)?;
402
+ write!(self.buffer, "else\n")?;
403
+
404
+ // Emit else body (first child of ElseNode)
405
+ if let Some(else_statements) = consequent.children.first() {
406
+ if matches!(else_statements.node_type, NodeType::StatementsNode) {
407
+ self.emit_statements(else_statements, indent_level + 1)?;
408
+ self.buffer.push('\n');
409
+ }
410
+ }
411
+ }
412
+ _ => {}
413
+ }
414
+ }
415
+
416
+ // Only emit 'end' for the outermost if (not for elsif)
417
+ if !is_elsif {
418
+ self.emit_indent(indent_level)?;
419
+ write!(self.buffer, "end")?;
420
+ }
421
+
422
+ Ok(())
423
+ }
424
+
307
425
  /// Emit generic node by extracting from source
308
426
  fn emit_generic(&mut self, node: &Node, indent_level: usize) -> Result<()> {
309
427
  // Emit any comments before this node
@@ -18,7 +18,6 @@ pub enum RfmtError {
18
18
  },
19
19
 
20
20
  #[error("Configuration error: {message}")]
21
- #[cfg(test)]
22
21
  ConfigError { message: String },
23
22
  }
24
23
 
@@ -36,7 +35,6 @@ impl RfmtError {
36
35
  RfmtError::PrismError(_) => "PrismError",
37
36
  RfmtError::FormatError(_) => "FormatError",
38
37
  RfmtError::UnsupportedFeature { .. } => "UnsupportedFeature",
39
- #[cfg(test)]
40
38
  RfmtError::ConfigError { .. } => "ConfigError",
41
39
  };
42
40
 
data/ext/rfmt/src/lib.rs CHANGED
@@ -14,6 +14,7 @@ use magnus::{define_module, function, prelude::*, Error, Ruby};
14
14
  use parser::{PrismAdapter, RubyParser};
15
15
 
16
16
  fn format_ruby_code(ruby: &Ruby, source: String, json: String) -> Result<String, Error> {
17
+ log::info!("format_ruby_code called");
17
18
  let policy = SecurityPolicy::default();
18
19
 
19
20
  policy
@@ -25,7 +26,10 @@ fn format_ruby_code(ruby: &Ruby, source: String, json: String) -> Result<String,
25
26
  let parser = PrismAdapter::new();
26
27
  let ast = parser.parse(&json).map_err(|e| e.to_magnus_error(ruby))?;
27
28
 
28
- let config = Config::default();
29
+ // Load configuration from file or use defaults
30
+ log::info!("Attempting to discover config file...");
31
+ let config = Config::discover().map_err(|e| e.to_magnus_error(ruby))?;
32
+ log::info!("Config loaded successfully, line_length: {}", config.formatting.line_length);
29
33
  let mut emitter = Emitter::with_source(config, source);
30
34
 
31
35
  let formatted = emitter.emit(&ast).map_err(|e| e.to_magnus_error(ruby))?;
@@ -22,7 +22,11 @@ impl RfmtLogger {
22
22
  }
23
23
 
24
24
  pub fn init() {
25
- let logger = Self::new(LevelFilter::Info);
25
+ let level = std::env::var("RFMT_LOG")
26
+ .ok()
27
+ .and_then(|s| s.parse().ok())
28
+ .unwrap_or(LevelFilter::Info);
29
+ let logger = Self::new(level);
26
30
  log::set_boxed_logger(Box::new(logger)).expect("Failed to initialize logger");
27
31
  log::set_max_level(LevelFilter::Trace);
28
32
  }
@@ -18,7 +18,7 @@ module Rfmt
18
18
  'exclude' => ['vendor/**/*', 'tmp/**/*', 'node_modules/**/*']
19
19
  }.freeze
20
20
 
21
- CONFIG_FILES = ['.rfmt.yml', '.rfmt.yaml', 'rfmt.yml', 'rfmt.yaml'].freeze
21
+ CONFIG_FILES = ['rfmt.yml', 'rfmt.yaml', '.rfmt.yml', '.rfmt.yaml'].freeze
22
22
 
23
23
  attr_reader :config
24
24
 
@@ -159,6 +159,8 @@ module Rfmt
159
159
  node.statements,
160
160
  node.consequent
161
161
  ].compact
162
+ when Prism::ElseNode
163
+ [node.statements].compact
162
164
  when Prism::ArrayNode
163
165
  node.elements || []
164
166
  when Prism::HashNode
data/lib/rfmt/rfmt.so CHANGED
Binary file
data/lib/rfmt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rfmt
4
- VERSION = '0.2.2'
4
+ VERSION = '0.2.4'
5
5
  end
data/lib/rfmt.rb CHANGED
@@ -95,10 +95,10 @@ module Rfmt
95
95
  YAML
96
96
 
97
97
  # Generate a default configuration file
98
- # @param path [String] Path where to create the config file (default: .rfmt.yml)
98
+ # @param path [String] Path where to create the config file (default: rfmt.yml)
99
99
  # @param force [Boolean] Overwrite existing file if true
100
100
  # @return [Boolean] true if file was created, false if already exists
101
- def self.init(path = '.rfmt.yml', force: false)
101
+ def self.init(path = 'rfmt.yml', force: false)
102
102
  if File.exist?(path) && !force
103
103
  warn "Configuration file already exists: #{path}"
104
104
  warn 'Use force: true to overwrite'
@@ -116,7 +116,7 @@ module Rfmt
116
116
  current_dir = Dir.pwd
117
117
 
118
118
  loop do
119
- ['.rfmt.yml', '.rfmt.yaml'].each do |filename|
119
+ ['rfmt.yml', 'rfmt.yaml', '.rfmt.yml', '.rfmt.yaml'].each do |filename|
120
120
  config_path = File.join(current_dir, filename)
121
121
  return config_path if File.exist?(config_path)
122
122
  end
@@ -134,7 +134,7 @@ module Rfmt
134
134
  nil
135
135
  end
136
136
  if home_dir
137
- ['.rfmt.yml', '.rfmt.yaml'].each do |filename|
137
+ ['rfmt.yml', 'rfmt.yaml', '.rfmt.yml', '.rfmt.yaml'].each do |filename|
138
138
  config_path = File.join(home_dir, filename)
139
139
  return config_path if File.exist?(config_path)
140
140
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfmt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora