Module: Ibex::CLIFormatting

Defined in:
lib/ibex/cli/formatting.rb,
sig/ibex/cli/formatting.rbs

Overview

CLI coordination for deterministic, semantics-preserving grammar formatting. rubocop:disable Metrics/ModuleLength -- transaction lifecycle stays cohesive and auditable.

Instance Method Summary collapse

Instance Method Details

#backup_formatting_targets(targets) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[formatting_target] targets) -> void

Parameters:

  • targets (Array[formatting_target])


238
239
240
241
242
243
244
# File 'lib/ibex/cli/formatting.rb', line 238

def backup_formatting_targets(targets)
  targets.each do |target|
    backup = "#{target.fetch(:stage)}.backup"
    target[:backup] = backup
    File.link(target.fetch(:target), backup)
  end
end

#check_formatting_results(results) ⇒ Integer

RBS:

  • (Array[formatting_result] results) -> Integer

Parameters:

  • results (Array[formatting_result])

Returns:

  • (Integer)


141
142
143
144
145
# File 'lib/ibex/cli/formatting.rb', line 141

def check_formatting_results(results)
  changed = results.reject { |result| result.fetch(:source) == result.fetch(:formatted) }
  changed.each { |result| @stderr.puts("#{result.fetch(:label)}: needs formatting") }
  changed.empty? ? 0 : 1
end

#cleanup_formatting_artifacts(targets, preserve_installed_backups:) ⇒ formatting_cleanup_result

RBS:

  • (Array[formatting_target] targets, preserve_installed_backups: bool) -> formatting_cleanup_result

Parameters:

  • targets (Array[formatting_target])
  • preserve_installed_backups: (Boolean)

Returns:

  • (formatting_cleanup_result)


270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/ibex/cli/formatting.rb', line 270

def cleanup_formatting_artifacts(targets, preserve_installed_backups:)
  errors = [] #: Array[String]
  remaining = [] #: Array[String]
  targets.each do |target|
    artifacts = [target[:stage]] #: Array[String?]
    backup = target[:backup]
    if preserve_installed_backups && target.fetch(:installed) && backup
      remaining << backup
    else
      artifacts << backup
    end
    artifacts.compact.each do |path|
      error = remove_formatting_artifact(path)
      next unless error

      errors << error
      remaining << path
    end
  end
  { errors: errors, remaining: remaining }
end

#control_byte?(byte) ⇒ Boolean

RBS:

  • (Integer byte) -> bool

Parameters:

  • byte (Integer)

Returns:

  • (Boolean)


381
382
383
# File 'lib/ibex/cli/formatting.rb', line 381

def control_byte?(byte)
  byte < 0x20 || byte == 0x7f
end

#duplicate_formatting_target_pair(targets) ⇒ [ formatting_target, formatting_target ]?

RBS:

  • (Array[formatting_target] targets) -> [formatting_target, formatting_target]?

Parameters:

  • targets (Array[formatting_target])

Returns:

  • ([ formatting_target, formatting_target ], nil)


183
184
185
186
187
188
189
190
191
# File 'lib/ibex/cli/formatting.rb', line 183

def duplicate_formatting_target_pair(targets)
  targets.each_with_index do |left, index|
    targets.drop(index + 1).each do |right|
      same_path = left.fetch(:target) == right.fetch(:target)
      return [left, right] if same_path || File.identical?(left.fetch(:target), right.fetch(:target))
    end
  end
  nil
end

#formatting_control_bytes?(value) ⇒ Boolean

RBS:

  • (String value) -> bool

Parameters:

  • value (String)

Returns:

  • (Boolean)


376
377
378
# File 'lib/ibex/cli/formatting.rb', line 376

def formatting_control_bytes?(value)
  value.b.each_byte.any? { |byte| control_byte?(byte) }
end

#formatting_label(value) ⇒ String

RBS:

  • (String value) -> String

Parameters:

  • value (String)

Returns:

  • (String)


367
368
369
370
371
372
373
# File 'lib/ibex/cli/formatting.rb', line 367

def formatting_label(value)
  escaped = String.new(encoding: Encoding::BINARY)
  value.b.each_byte do |byte|
    escaped << (control_byte?(byte) ? format("\\x%02X", byte) : byte)
  end
  escaped.force_encoding(value.encoding)
end

#formatting_option_parser(settings) ⇒ OptionParser

RBS:

  • (formatting_settings settings) -> OptionParser

Parameters:

  • settings (formatting_settings)

Returns:

  • (OptionParser)


77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/ibex/cli/formatting.rb', line 77

def formatting_option_parser(settings)
  OptionParser.new do |options|
    options.banner = "Usage: ibex fmt [--check | --write] [options] grammar.y [...]"
    options.on("--check", "report files that need formatting") { settings[:check] = true }
    options.on("--write", "atomically format files in place") { settings[:write] = true }
    options.on("--mode=MODE", %w[default extended], "grammar mode") { |value| settings[:mode] = value.to_sym }
    options.on("--stdin-filename=FILE", "diagnostic filename for standard input") do |value|
      settings[:stdin_filename] = value
    end
    options.on("--help", "show help") { settings[:help] = true }
  end
end

#formatting_options(arguments) ⇒ formatting_settings

RBS:

  • (Array[String] arguments) -> formatting_settings

Parameters:

  • arguments (Array[String])

Returns:

  • (formatting_settings)


70
71
72
73
74
# File 'lib/ibex/cli/formatting.rb', line 70

def formatting_options(arguments)
  settings = { paths: [], mode: :default, check: false, write: false } #: formatting_settings
  settings[:paths] = formatting_option_parser(settings).parse(arguments)
  settings
end

#formatting_targets(results) ⇒ Array[formatting_target]

RBS:

  • (Array[formatting_result] results) -> Array[formatting_target]

Parameters:

  • results (Array[formatting_result])

Returns:

  • (Array[formatting_target])


159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ibex/cli/formatting.rb', line 159

def formatting_targets(results)
  results.map do |result|
    target = File.realpath(result.fetch(:path))
    {
      path: result.fetch(:path), label: result.fetch(:label), target: target,
      formatted: result.fetch(:formatted), mode: File.stat(target).mode & 0o7777,
      directory: File.dirname(target), basename: File.basename(target),
      changed: result.fetch(:source) != result.fetch(:formatted), installed: false
    }
  end
rescue SystemCallError => e
  raise Ibex::Error, formatting_label(e.message)
end

#formatting_transaction_error(error, rollback_errors, cleanup, sync_errors) ⇒ Ibex::Error

RBS:

  • (StandardError error, Array[String] rollback_errors, formatting_cleanup_result cleanup, Array[String] sync_errors) -> Ibex::Error

Parameters:

  • error (StandardError)
  • rollback_errors (Array[String])
  • cleanup (formatting_cleanup_result)
  • sync_errors (Array[String])

Returns:



310
311
312
313
314
315
316
317
318
319
320
# File 'lib/ibex/cli/formatting.rb', line 310

def formatting_transaction_error(error, rollback_errors, cleanup, sync_errors)
  details = ["fmt write failed: #{formatting_label(error.message)}"] #: Array[String]
  details << "rollback failed: #{rollback_errors.join('; ')}" unless rollback_errors.empty?
  details << "cleanup failed: #{cleanup.fetch(:errors).join('; ')}" unless cleanup.fetch(:errors).empty?
  unless cleanup.fetch(:remaining).empty?
    paths = cleanup.fetch(:remaining).map { |path| formatting_label(path) }
    details << "preserved artifacts: #{paths.join(', ')}"
  end
  details << "rollback directory sync failed: #{sync_errors.join('; ')}" unless sync_errors.empty?
  Ibex::Error.new(details.join("; "))
end

#install_formatting_targets(targets) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[formatting_target] targets) -> void

Parameters:

  • targets (Array[formatting_target])


247
248
249
250
251
252
# File 'lib/ibex/cli/formatting.rb', line 247

def install_formatting_targets(targets)
  targets.each do |target|
    File.rename(target.fetch(:stage), target.fetch(:target))
    target[:installed] = true
  end
end

#prepare_formatting_results(settings) ⇒ [ Array[formatting_result], bool ]

RBS:

  • (formatting_settings settings) -> [Array[formatting_result], bool]

Parameters:

  • settings (formatting_settings)

Returns:

  • ([ Array[formatting_result], bool ])


125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/ibex/cli/formatting.rb', line 125

def prepare_formatting_results(settings)
  failed = false
  results = [] #: Array[formatting_result]
  settings.fetch(:paths).each do |path|
    source = path == "-" ? @stdin.read : File.binread(path)
    label = path == "-" ? settings[:stdin_filename] || "(stdin)" : formatting_label(path)
    formatted = Frontend::Formatter.format(source, file: label, mode: settings.fetch(:mode))
    results << { path: path, label: label, source: source, formatted: formatted }
  rescue Ibex::Error, SystemCallError => e
    @stderr.puts(formatting_label(e.message))
    failed = true
  end
  [results, failed]
end

#remove_formatting_artifact(path) ⇒ String?

RBS:

  • (String path) -> String?

Parameters:

  • path (String)

Returns:

  • (String, nil)


293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/ibex/cli/formatting.rb', line 293

def remove_formatting_artifact(path)
  attempts = 0
  begin
    attempts += 1
    File.unlink(path)
    nil
  rescue Errno::ENOENT
    nil
  rescue StandardError => e
    retry if attempts < 2

    "#{formatting_label(path)}: #{formatting_label(e.message)}"
  end
end

#report_formatting_cleanup_warning(cleanup, sync_errors) ⇒ void

This method returns an undefined value.

RBS:

  • (formatting_cleanup_result cleanup, Array[String] sync_errors) -> void

Parameters:

  • cleanup (formatting_cleanup_result)
  • sync_errors (Array[String])


323
324
325
326
327
328
329
330
331
332
333
# File 'lib/ibex/cli/formatting.rb', line 323

def report_formatting_cleanup_warning(cleanup, sync_errors)
  details = cleanup.fetch(:errors).dup
  details.concat(sync_errors)
  unless cleanup.fetch(:remaining).empty?
    paths = cleanup.fetch(:remaining).map { |path| formatting_label(path) }
    details << "remaining artifacts: #{paths.join(', ')}"
  end
  return if details.empty?

  @stderr.puts("fmt cleanup warning (formatted targets committed): #{details.join('; ')}")
end

#rollback_formatting_targets(targets) ⇒ Array[String]

RBS:

  • (Array[formatting_target] targets) -> Array[String]

Parameters:

  • targets (Array[formatting_target])

Returns:

  • (Array[String])


255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/ibex/cli/formatting.rb', line 255

def rollback_formatting_targets(targets)
  errors = [] #: Array[String]
  targets.reverse_each do |target|
    next unless target.fetch(:installed)

    File.rename(target.fetch(:backup), target.fetch(:target))
    target[:installed] = false
  rescue StandardError => e
    errors << "#{target.fetch(:label)}: #{formatting_label(e.message)}"
  end
  errors
end

#run_format_command(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/ibex/cli/formatting.rb', line 47

def run_format_command(arguments)
  settings = formatting_options(arguments)
  if settings[:help]
    @stdout.puts(formatting_option_parser(settings))
    return 0
  end

  validate_formatting_settings!(settings)
  results, failed = prepare_formatting_results(settings)
  if settings.fetch(:check)
    check_status = check_formatting_results(results)
    return 1 if failed

    return check_status
  end
  return 1 if failed
  return write_formatting_results(results) if settings.fetch(:write)

  @stdout.write(results.fetch(0).fetch(:formatted))
  0
end

#stage_formatting_targets(targets) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[formatting_target] targets) -> void

Parameters:

  • targets (Array[formatting_target])


214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/ibex/cli/formatting.rb', line 214

def stage_formatting_targets(targets)
  targets.each do |target|
    temporary = Tempfile.create(
      [".ibex-fmt-", ".stage"], target.fetch(:directory)
    )
    target[:stage] = temporary.path.dup
    temporary.binmode
    temporary.write(target.fetch(:formatted))
    temporary.flush
    temporary.fsync
    temporary.chmod(target.fetch(:mode))
    temporary.fsync
    temporary.close
  rescue StandardError
    begin
      temporary&.close
    rescue StandardError
      nil
    end
    raise
  end
end

#sync_formatting_directories(targets) ⇒ Array[String]

RBS:

  • (Array[formatting_target] targets) -> Array[String]

Parameters:

  • targets (Array[formatting_target])

Returns:

  • (Array[String])


349
350
351
352
353
354
355
356
357
# File 'lib/ibex/cli/formatting.rb', line 349

def sync_formatting_directories(targets)
  errors = [] #: Array[String]
  targets.map { |target| target.fetch(:directory) }.uniq.each do |directory|
    sync_formatting_directory(directory)
  rescue SystemCallError, IOError => e
    errors << "#{formatting_label(directory)}: #{formatting_label(e.message)}"
  end
  errors
end

#sync_formatting_directories!(targets) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[formatting_target] targets) -> void

Parameters:

  • targets (Array[formatting_target])


336
337
338
339
340
341
# File 'lib/ibex/cli/formatting.rb', line 336

def sync_formatting_directories!(targets)
  errors = sync_formatting_directories(targets)
  return if errors.empty?

  raise Ibex::Error, "directory sync failed: #{errors.join('; ')}"
end

#sync_formatting_directories_best_effort(targets) ⇒ Array[String]

RBS:

  • (Array[formatting_target] targets) -> Array[String]

Parameters:

  • targets (Array[formatting_target])

Returns:

  • (Array[String])


344
345
346
# File 'lib/ibex/cli/formatting.rb', line 344

def sync_formatting_directories_best_effort(targets)
  sync_formatting_directories(targets)
end

#sync_formatting_directory(directory) ⇒ void

This method returns an undefined value.

RBS:

  • (String directory) -> void

Parameters:

  • directory (String)


360
361
362
363
364
# File 'lib/ibex/cli/formatting.rb', line 360

def sync_formatting_directory(directory)
  File.open(directory, File::RDONLY, &:fsync)
rescue Errno::EINVAL, Errno::ENOTSUP, Errno::EBADF
  nil
end

#transactionally_write_formatted(targets) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[formatting_target] targets) -> void

Parameters:

  • targets (Array[formatting_target])


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/ibex/cli/formatting.rb', line 194

def transactionally_write_formatted(targets)
  begin
    stage_formatting_targets(targets)
    backup_formatting_targets(targets)
    sync_formatting_directories!(targets)
    install_formatting_targets(targets)
    sync_formatting_directories!(targets)
  rescue StandardError => e
    rollback_errors = rollback_formatting_targets(targets)
    cleanup = cleanup_formatting_artifacts(targets, preserve_installed_backups: true)
    sync_errors = sync_formatting_directories_best_effort(targets)
    raise formatting_transaction_error(e, rollback_errors, cleanup, sync_errors)
  end

  cleanup = cleanup_formatting_artifacts(targets, preserve_installed_backups: false)
  sync_errors = sync_formatting_directories_best_effort(targets)
  report_formatting_cleanup_warning(cleanup, sync_errors)
end

#validate_formatting_operation!(settings, paths) ⇒ void

This method returns an undefined value.

RBS:

  • (formatting_settings settings, Array[String] paths) -> void

Parameters:

  • settings (formatting_settings)
  • paths (Array[String])


100
101
102
103
104
105
106
107
# File 'lib/ibex/cli/formatting.rb', line 100

def validate_formatting_operation!(settings, paths)
  if settings.fetch(:check) && settings.fetch(:write)
    raise Ibex::Error, "(cli):1:1: fmt --check and --write cannot be combined"
  end
  return if settings.fetch(:check) || settings.fetch(:write) || paths.length == 1

  raise Ibex::Error, "(cli):1:1: fmt writes to stdout only for exactly one input"
end

#validate_formatting_settings!(settings) ⇒ void

This method returns an undefined value.

RBS:

  • (formatting_settings settings) -> void

Parameters:

  • settings (formatting_settings)


91
92
93
94
95
96
97
# File 'lib/ibex/cli/formatting.rb', line 91

def validate_formatting_settings!(settings)
  paths = settings.fetch(:paths)
  raise Ibex::Error, "(cli):1:1: fmt requires at least one grammar file" if paths.empty?

  validate_formatting_operation!(settings, paths)
  validate_formatting_stdin!(settings, paths)
end

#validate_formatting_stdin!(settings, paths) ⇒ void

This method returns an undefined value.

RBS:

  • (formatting_settings settings, Array[String] paths) -> void

Parameters:

  • settings (formatting_settings)
  • paths (Array[String])


110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/ibex/cli/formatting.rb', line 110

def validate_formatting_stdin!(settings, paths)
  raise Ibex::Error, "(cli):1:1: fmt standard input may be specified only once" if paths.count("-") > 1

  if paths.include?("-") && (settings.fetch(:check) || settings.fetch(:write))
    raise Ibex::Error, "(cli):1:1: fmt --check and --write require file inputs"
  end
  if settings[:stdin_filename] && formatting_control_bytes?(settings.fetch(:stdin_filename))
    raise Ibex::Error, "(cli):1:1: --stdin-filename must not contain control characters"
  end
  return unless settings[:stdin_filename] && paths != ["-"]

  raise Ibex::Error, "(cli):1:1: --stdin-filename requires fmt -"
end

#validate_unique_formatting_targets!(targets) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[formatting_target] targets) -> void

Parameters:

  • targets (Array[formatting_target])


174
175
176
177
178
179
180
# File 'lib/ibex/cli/formatting.rb', line 174

def validate_unique_formatting_targets!(targets)
  collision = duplicate_formatting_target_pair(targets)
  return unless collision

  labels = collision.map { |target| target.fetch(:label) }
  raise Ibex::Error, "(cli):1:1: fmt inputs resolve to the same target: #{labels.join(', ')}"
end

#write_formatting_results(results) ⇒ Integer

RBS:

  • (Array[formatting_result] results) -> Integer

Parameters:

  • results (Array[formatting_result])

Returns:

  • (Integer)


148
149
150
151
152
153
154
155
156
# File 'lib/ibex/cli/formatting.rb', line 148

def write_formatting_results(results)
  targets = formatting_targets(results)
  validate_unique_formatting_targets!(targets)
  targets = targets.select { |target| target.fetch(:changed) }
  return 0 if targets.empty?

  transactionally_write_formatted(targets)
  0
end