rfmt 1.3.0-arm64-darwin
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 +7 -0
- data/CHANGELOG.md +223 -0
- data/LICENSE.txt +21 -0
- data/README.md +397 -0
- data/exe/rfmt +21 -0
- data/lib/rfmt/3.1/rfmt.bundle +0 -0
- data/lib/rfmt/3.2/rfmt.bundle +0 -0
- data/lib/rfmt/3.3/rfmt.bundle +0 -0
- data/lib/rfmt/cache.rb +112 -0
- data/lib/rfmt/cli.rb +308 -0
- data/lib/rfmt/configuration.rb +95 -0
- data/lib/rfmt/prism_bridge.rb +390 -0
- data/lib/rfmt/prism_node_extractor.rb +115 -0
- data/lib/rfmt/version.rb +5 -0
- data/lib/rfmt.rb +172 -0
- data/lib/ruby_lsp/rfmt/addon.rb +20 -0
- data/lib/ruby_lsp/rfmt/formatter_runner.rb +26 -0
- metadata +68 -0
data/lib/rfmt/cache.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Rfmt
|
|
7
|
+
# Cache system for formatted files
|
|
8
|
+
# Uses mtime (modification time) to determine if formatting is needed
|
|
9
|
+
class Cache
|
|
10
|
+
class CacheError < StandardError; end
|
|
11
|
+
|
|
12
|
+
DEFAULT_CACHE_DIR = File.expand_path('~/.cache/rfmt').freeze
|
|
13
|
+
CACHE_VERSION = '1'
|
|
14
|
+
|
|
15
|
+
attr_reader :cache_dir
|
|
16
|
+
|
|
17
|
+
def initialize(cache_dir: DEFAULT_CACHE_DIR)
|
|
18
|
+
@cache_dir = cache_dir
|
|
19
|
+
@cache_data = {}
|
|
20
|
+
ensure_cache_dir
|
|
21
|
+
load_cache
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Check if file needs formatting
|
|
25
|
+
# Returns true if file mtime has changed or not in cache
|
|
26
|
+
def needs_formatting?(file_path)
|
|
27
|
+
return true unless File.exist?(file_path)
|
|
28
|
+
|
|
29
|
+
current_mtime = File.mtime(file_path).to_i
|
|
30
|
+
cached_mtime = @cache_data.dig(file_path, 'mtime')
|
|
31
|
+
|
|
32
|
+
current_mtime != cached_mtime
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Mark file as formatted with current mtime
|
|
36
|
+
def mark_formatted(file_path)
|
|
37
|
+
return unless File.exist?(file_path)
|
|
38
|
+
|
|
39
|
+
@cache_data[file_path] = {
|
|
40
|
+
'mtime' => File.mtime(file_path).to_i,
|
|
41
|
+
'formatted_at' => Time.now.to_i,
|
|
42
|
+
'version' => CACHE_VERSION
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Save cache to disk
|
|
47
|
+
def save
|
|
48
|
+
cache_file = File.join(@cache_dir, 'cache.json')
|
|
49
|
+
File.write(cache_file, JSON.pretty_generate(@cache_data))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Clear all cache data
|
|
53
|
+
def clear
|
|
54
|
+
@cache_data = {}
|
|
55
|
+
save
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Remove cache for specific file
|
|
59
|
+
def invalidate(file_path)
|
|
60
|
+
@cache_data.delete(file_path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get cache statistics
|
|
64
|
+
def stats
|
|
65
|
+
{
|
|
66
|
+
total_files: @cache_data.size,
|
|
67
|
+
cache_dir: @cache_dir,
|
|
68
|
+
cache_size_bytes: cache_size
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Prune old cache entries (files that no longer exist)
|
|
73
|
+
def prune
|
|
74
|
+
before_count = @cache_data.size
|
|
75
|
+
@cache_data.delete_if { |file_path, _| !File.exist?(file_path) }
|
|
76
|
+
after_count = @cache_data.size
|
|
77
|
+
pruned = before_count - after_count
|
|
78
|
+
|
|
79
|
+
save if pruned.positive?
|
|
80
|
+
pruned
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def ensure_cache_dir
|
|
86
|
+
FileUtils.mkdir_p(@cache_dir)
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
raise CacheError, "Failed to create cache directory: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def load_cache
|
|
92
|
+
cache_file = File.join(@cache_dir, 'cache.json')
|
|
93
|
+
return unless File.exist?(cache_file)
|
|
94
|
+
|
|
95
|
+
content = File.read(cache_file)
|
|
96
|
+
@cache_data = JSON.parse(content)
|
|
97
|
+
rescue JSON::ParserError => e
|
|
98
|
+
warn "Warning: Failed to parse cache file, starting with empty cache: #{e.message}"
|
|
99
|
+
@cache_data = {}
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
warn "Warning: Failed to load cache, starting with empty cache: #{e.message}"
|
|
102
|
+
@cache_data = {}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def cache_size
|
|
106
|
+
cache_file = File.join(@cache_dir, 'cache.json')
|
|
107
|
+
return 0 unless File.exist?(cache_file)
|
|
108
|
+
|
|
109
|
+
File.size(cache_file)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/rfmt/cli.rb
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
# Check for verbose flag before loading rfmt to set debug mode early
|
|
6
|
+
ENV['RFMT_DEBUG'] = '1' if ARGV.include?('-v') || ARGV.include?('--verbose')
|
|
7
|
+
|
|
8
|
+
require 'rfmt'
|
|
9
|
+
require 'rfmt/configuration'
|
|
10
|
+
require 'rfmt/cache'
|
|
11
|
+
|
|
12
|
+
module Rfmt
|
|
13
|
+
# Cache management commands
|
|
14
|
+
class CacheCommands < Thor
|
|
15
|
+
desc 'clear', 'Clear all cache data'
|
|
16
|
+
option :cache_dir, type: :string, desc: 'Cache directory (default: ~/.cache/rfmt)'
|
|
17
|
+
def clear
|
|
18
|
+
cache_opts = options[:cache_dir] ? { cache_dir: options[:cache_dir] } : {}
|
|
19
|
+
cache = Cache.new(**cache_opts)
|
|
20
|
+
cache.clear
|
|
21
|
+
say 'Cache cleared', :green
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc 'stats', 'Show cache statistics'
|
|
25
|
+
option :cache_dir, type: :string, desc: 'Cache directory (default: ~/.cache/rfmt)'
|
|
26
|
+
def stats
|
|
27
|
+
cache_opts = options[:cache_dir] ? { cache_dir: options[:cache_dir] } : {}
|
|
28
|
+
cache = Cache.new(**cache_opts)
|
|
29
|
+
stats = cache.stats
|
|
30
|
+
say "Cache directory: #{stats[:cache_dir]}", :blue
|
|
31
|
+
say "Total files in cache: #{stats[:total_files]}", :blue
|
|
32
|
+
say "Cache size: #{(stats[:cache_size_bytes] / 1024.0).round(2)} KB", :blue
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc 'prune', 'Remove cache entries for files that no longer exist'
|
|
36
|
+
option :cache_dir, type: :string, desc: 'Cache directory (default: ~/.cache/rfmt)'
|
|
37
|
+
def prune
|
|
38
|
+
cache_opts = options[:cache_dir] ? { cache_dir: options[:cache_dir] } : {}
|
|
39
|
+
cache = Cache.new(**cache_opts)
|
|
40
|
+
pruned = cache.prune
|
|
41
|
+
say "Pruned #{pruned} stale cache entries", :green
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Command Line Interface for rfmt
|
|
46
|
+
class CLI < Thor
|
|
47
|
+
class_option :config, type: :string, desc: 'Path to configuration file'
|
|
48
|
+
class_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
|
|
49
|
+
|
|
50
|
+
default_command :format
|
|
51
|
+
|
|
52
|
+
desc 'format [FILES]', 'Format Ruby files (default command)'
|
|
53
|
+
option :write, type: :boolean, default: true, desc: 'Write formatted output'
|
|
54
|
+
option :check, type: :boolean, desc: "Check if files are formatted (don't write)"
|
|
55
|
+
option :diff, type: :boolean, desc: 'Show diff of changes'
|
|
56
|
+
option :diff_format, type: :string, default: 'unified', desc: 'Diff format: unified, side_by_side, or color'
|
|
57
|
+
option :parallel, type: :boolean, default: true, desc: 'Process files in parallel'
|
|
58
|
+
option :jobs, type: :numeric, desc: 'Number of parallel jobs (default: CPU count)'
|
|
59
|
+
option :cache, type: :boolean, default: true, desc: 'Use cache to skip unchanged files'
|
|
60
|
+
option :cache_dir, type: :string, desc: 'Cache directory (default: ~/.cache/rfmt)'
|
|
61
|
+
def format(*files)
|
|
62
|
+
config = load_config
|
|
63
|
+
files = files.empty? ? config.files_to_format : files.flatten
|
|
64
|
+
|
|
65
|
+
if files.empty?
|
|
66
|
+
say 'No files to format', :yellow
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Initialize cache
|
|
71
|
+
cache = if options[:cache]
|
|
72
|
+
cache_opts = options[:cache_dir] ? { cache_dir: options[:cache_dir] } : {}
|
|
73
|
+
Cache.new(**cache_opts)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Filter files using cache
|
|
77
|
+
if cache
|
|
78
|
+
original_count = files.size
|
|
79
|
+
files = files.select { |file| cache.needs_formatting?(file) }
|
|
80
|
+
skipped = original_count - files.size
|
|
81
|
+
say "ℹ Skipped #{skipped} unchanged file(s) (cached)", :cyan if skipped.positive? && options[:verbose]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if files.empty?
|
|
85
|
+
say '✓ All files are already formatted (cached)', :green
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Show progress message
|
|
90
|
+
if files.size == 1
|
|
91
|
+
say "Processing #{files.first}...", :blue
|
|
92
|
+
else
|
|
93
|
+
say "Processing #{files.size} file(s)...", :blue
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
results = if options[:parallel] && files.size > 1
|
|
97
|
+
format_files_parallel(files)
|
|
98
|
+
else
|
|
99
|
+
format_files_sequential(files)
|
|
100
|
+
end
|
|
101
|
+
handle_results(results, cache)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
desc 'check [FILES]', 'Check if files need formatting'
|
|
105
|
+
def check(*files)
|
|
106
|
+
invoke :format, files, check: true, write: false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
desc 'version', 'Show version'
|
|
110
|
+
def version
|
|
111
|
+
say "rfmt #{Rfmt::VERSION}"
|
|
112
|
+
say "Rust extension: #{Rfmt.rust_version}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
desc 'config', 'Show current configuration'
|
|
116
|
+
def config_cmd
|
|
117
|
+
config = load_config
|
|
118
|
+
require 'json'
|
|
119
|
+
say JSON.pretty_generate(config.config)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
desc 'cache SUBCOMMAND', 'Manage cache'
|
|
123
|
+
subcommand 'cache', CacheCommands
|
|
124
|
+
|
|
125
|
+
desc 'init', 'Initialize rfmt configuration'
|
|
126
|
+
option :force, type: :boolean, desc: 'Overwrite existing configuration'
|
|
127
|
+
option :path, type: :string, default: '.rfmt.yml', desc: 'Configuration file path'
|
|
128
|
+
def init
|
|
129
|
+
config_file = options[:path] || '.rfmt.yml'
|
|
130
|
+
|
|
131
|
+
# Use Rfmt::Config module for consistent behavior
|
|
132
|
+
result = Rfmt::Config.init(config_file, force: options[:force] || false)
|
|
133
|
+
|
|
134
|
+
if result
|
|
135
|
+
say "Created #{config_file}", :green
|
|
136
|
+
else
|
|
137
|
+
say "Configuration file already exists at #{config_file}. Use --force to overwrite.", :yellow
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def load_config
|
|
144
|
+
if options[:config]
|
|
145
|
+
Configuration.new(file: options[:config])
|
|
146
|
+
else
|
|
147
|
+
Configuration.discover
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def format_files_sequential(files)
|
|
152
|
+
files.map do |file|
|
|
153
|
+
format_single_file(file)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def format_files_parallel(files)
|
|
158
|
+
require 'parallel'
|
|
159
|
+
|
|
160
|
+
# Determine number of processes to use
|
|
161
|
+
process_count = options[:jobs] || Parallel.processor_count
|
|
162
|
+
|
|
163
|
+
say "Processing #{files.size} files with #{process_count} parallel jobs...", :blue if options[:verbose]
|
|
164
|
+
|
|
165
|
+
Parallel.map(files, in_processes: process_count) do |file|
|
|
166
|
+
format_single_file(file)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def format_single_file(file)
|
|
171
|
+
start_time = Time.now
|
|
172
|
+
source = File.read(file)
|
|
173
|
+
|
|
174
|
+
formatted = Rfmt.format(source)
|
|
175
|
+
changed = source != formatted
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
file: file,
|
|
179
|
+
changed: changed,
|
|
180
|
+
original: source,
|
|
181
|
+
formatted: formatted,
|
|
182
|
+
duration: Time.now - start_time,
|
|
183
|
+
error: nil
|
|
184
|
+
}
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
{
|
|
187
|
+
file: file,
|
|
188
|
+
error: e.message,
|
|
189
|
+
duration: Time.now - start_time
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def handle_results(results, cache = nil)
|
|
194
|
+
failed_count = 0
|
|
195
|
+
changed_count = 0
|
|
196
|
+
error_count = 0
|
|
197
|
+
|
|
198
|
+
results.each do |result|
|
|
199
|
+
if result[:error]
|
|
200
|
+
say "Error in #{result[:file]}: #{result[:error]}", :red
|
|
201
|
+
error_count += 1
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if result[:changed]
|
|
206
|
+
changed_count += 1
|
|
207
|
+
|
|
208
|
+
if options[:check]
|
|
209
|
+
say "#{result[:file]} needs formatting", :yellow
|
|
210
|
+
failed_count += 1
|
|
211
|
+
show_diff(result[:file], result[:original], result[:formatted]) if options[:diff]
|
|
212
|
+
elsif options[:diff]
|
|
213
|
+
show_diff(result[:file], result[:original], result[:formatted])
|
|
214
|
+
elsif options[:write]
|
|
215
|
+
File.write(result[:file], result[:formatted])
|
|
216
|
+
# Always show formatted files (not just in verbose mode)
|
|
217
|
+
say "✓ Formatted #{result[:file]}", :green
|
|
218
|
+
|
|
219
|
+
# Update cache after successful write
|
|
220
|
+
cache&.mark_formatted(result[:file])
|
|
221
|
+
else
|
|
222
|
+
puts result[:formatted]
|
|
223
|
+
end
|
|
224
|
+
else
|
|
225
|
+
# Show already formatted files in non-check mode
|
|
226
|
+
say "✓ #{result[:file]} already formatted", :cyan unless options[:check]
|
|
227
|
+
|
|
228
|
+
# Update cache even if no changes (file was checked)
|
|
229
|
+
cache&.mark_formatted(result[:file])
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Save cache to disk
|
|
234
|
+
cache&.save
|
|
235
|
+
|
|
236
|
+
# Summary - always show a summary message
|
|
237
|
+
if error_count.positive?
|
|
238
|
+
say "\n✗ Failed: #{error_count} error(s) occurred", :red
|
|
239
|
+
elsif options[:check] && failed_count.positive?
|
|
240
|
+
say "\n✗ Check failed: #{failed_count} file(s) need formatting", :yellow
|
|
241
|
+
elsif changed_count.positive?
|
|
242
|
+
# Success message with appropriate details
|
|
243
|
+
say "\n✓ Success! Formatted #{changed_count} file(s)", :green
|
|
244
|
+
elsif results.size == 1
|
|
245
|
+
say "\n✓ Success! File is already formatted", :green
|
|
246
|
+
else
|
|
247
|
+
say "\n✓ Success! All #{results.size} files are already formatted", :green
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Detailed summary in verbose mode
|
|
251
|
+
if options[:verbose]
|
|
252
|
+
say "Total: #{results.size} file(s) processed", :blue
|
|
253
|
+
say "Changed: #{changed_count} file(s)", :yellow if changed_count.positive?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
exit(1) if (options[:check] && failed_count.positive?) || error_count.positive?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def show_diff(file, original, formatted)
|
|
260
|
+
require 'diffy'
|
|
261
|
+
|
|
262
|
+
say "\n#{'=' * 80}", :blue
|
|
263
|
+
say "Diff for #{file}:", :yellow
|
|
264
|
+
say '=' * 80, :blue
|
|
265
|
+
|
|
266
|
+
case options[:diff_format]
|
|
267
|
+
when 'unified'
|
|
268
|
+
diff = Diffy::Diff.new(original, formatted, context: 3)
|
|
269
|
+
puts diff.to_s(:color)
|
|
270
|
+
when 'side_by_side'
|
|
271
|
+
diff = Diffy::Diff.new(original, formatted, context: 3)
|
|
272
|
+
# Side-by-side is not well supported in terminal, use unified with more context
|
|
273
|
+
puts diff.to_s(:color)
|
|
274
|
+
when 'color'
|
|
275
|
+
show_colored_line_diff(original, formatted)
|
|
276
|
+
else
|
|
277
|
+
diff = Diffy::Diff.new(original, formatted, context: 3)
|
|
278
|
+
puts diff.to_s(:color)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
say "#{'=' * 80}\n", :blue
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def show_colored_line_diff(original, formatted)
|
|
285
|
+
require 'diff/lcs'
|
|
286
|
+
|
|
287
|
+
original_lines = original.split("\n")
|
|
288
|
+
formatted_lines = formatted.split("\n")
|
|
289
|
+
|
|
290
|
+
diffs = Diff::LCS.sdiff(original_lines, formatted_lines)
|
|
291
|
+
|
|
292
|
+
diffs.each_with_index do |diff, idx|
|
|
293
|
+
line_num = idx + 1
|
|
294
|
+
case diff.action
|
|
295
|
+
when '-'
|
|
296
|
+
say "#{line_num}: - #{diff.old_element}", :red
|
|
297
|
+
when '+'
|
|
298
|
+
say "#{line_num}: + #{diff.new_element}", :green
|
|
299
|
+
when '='
|
|
300
|
+
say "#{line_num}: #{diff.old_element}", :white
|
|
301
|
+
when '!'
|
|
302
|
+
say "#{line_num}: - #{diff.old_element}", :red
|
|
303
|
+
say "#{line_num}: + #{diff.new_element}", :green
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Rfmt
|
|
6
|
+
# Configuration management for rfmt
|
|
7
|
+
class Configuration
|
|
8
|
+
class ConfigError < StandardError; end
|
|
9
|
+
|
|
10
|
+
DEFAULT_CONFIG = {
|
|
11
|
+
'version' => '1.0',
|
|
12
|
+
'formatting' => {
|
|
13
|
+
'line_length' => 100,
|
|
14
|
+
'indent_width' => 2,
|
|
15
|
+
'indent_style' => 'spaces'
|
|
16
|
+
},
|
|
17
|
+
'include' => ['**/*.rb'],
|
|
18
|
+
'exclude' => ['vendor/**/*', 'tmp/**/*', 'node_modules/**/*']
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
CONFIG_FILES = ['rfmt.yml', 'rfmt.yaml', '.rfmt.yml', '.rfmt.yaml'].freeze
|
|
22
|
+
|
|
23
|
+
attr_reader :config
|
|
24
|
+
|
|
25
|
+
def initialize(options = {})
|
|
26
|
+
@config = load_configuration(options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Discover configuration file in current directory
|
|
30
|
+
def self.discover
|
|
31
|
+
config_file = CONFIG_FILES.find { |file| File.exist?(file) }
|
|
32
|
+
config_file ? new(file: config_file) : new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get list of files to format based on include/exclude patterns
|
|
36
|
+
def files_to_format(base_path: '.')
|
|
37
|
+
include_patterns = @config['include']
|
|
38
|
+
exclude_patterns = @config['exclude']
|
|
39
|
+
|
|
40
|
+
included_files = include_patterns.flat_map { |pattern| Dir.glob(File.join(base_path, pattern)) }
|
|
41
|
+
excluded_files = exclude_patterns.flat_map { |pattern| Dir.glob(File.join(base_path, pattern)) }
|
|
42
|
+
|
|
43
|
+
(included_files - excluded_files).select { |f| File.file?(f) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get formatting configuration
|
|
47
|
+
def formatting_config
|
|
48
|
+
@config['formatting']
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def load_configuration(options)
|
|
54
|
+
config = deep_dup(DEFAULT_CONFIG)
|
|
55
|
+
|
|
56
|
+
# Load from file if specified
|
|
57
|
+
if (file = options[:file] || options['file'])
|
|
58
|
+
file_config = YAML.load_file(file)
|
|
59
|
+
config = deep_merge(config, file_config)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Override with options
|
|
63
|
+
options.delete(:file)
|
|
64
|
+
options.delete('file')
|
|
65
|
+
config = deep_merge(config, options) unless options.empty?
|
|
66
|
+
|
|
67
|
+
validate_config!(config)
|
|
68
|
+
config
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_config!(config)
|
|
72
|
+
line_length = config.dig('formatting', 'line_length')
|
|
73
|
+
raise ConfigError, 'line_length must be positive' if line_length && line_length <= 0
|
|
74
|
+
|
|
75
|
+
indent_width = config.dig('formatting', 'indent_width')
|
|
76
|
+
raise ConfigError, 'indent_width must be positive' if indent_width && indent_width <= 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def deep_merge(hash1, hash2)
|
|
80
|
+
hash1.merge(hash2) do |_key, old_val, new_val|
|
|
81
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
82
|
+
deep_merge(old_val, new_val)
|
|
83
|
+
else
|
|
84
|
+
new_val
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def deep_dup(hash)
|
|
90
|
+
hash.transform_values do |value|
|
|
91
|
+
value.is_a?(Hash) ? deep_dup(value) : value.dup
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|