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
- #backup_formatting_targets(targets) ⇒ void
- #check_formatting_results(results) ⇒ Integer
- #cleanup_formatting_artifacts(targets, preserve_installed_backups:) ⇒ formatting_cleanup_result
- #control_byte?(byte) ⇒ Boolean
- #duplicate_formatting_target_pair(targets) ⇒ [ formatting_target, formatting_target ]?
- #formatting_control_bytes?(value) ⇒ Boolean
- #formatting_label(value) ⇒ String
- #formatting_option_parser(settings) ⇒ OptionParser
- #formatting_options(arguments) ⇒ formatting_settings
- #formatting_targets(results) ⇒ Array[formatting_target]
- #formatting_transaction_error(error, rollback_errors, cleanup, sync_errors) ⇒ Ibex::Error
- #install_formatting_targets(targets) ⇒ void
- #prepare_formatting_results(settings) ⇒ [ Array[formatting_result], bool ]
- #remove_formatting_artifact(path) ⇒ String?
- #report_formatting_cleanup_warning(cleanup, sync_errors) ⇒ void
- #rollback_formatting_targets(targets) ⇒ Array[String]
- #run_format_command(arguments) ⇒ Integer
- #stage_formatting_targets(targets) ⇒ void
- #sync_formatting_directories(targets) ⇒ Array[String]
- #sync_formatting_directories!(targets) ⇒ void
- #sync_formatting_directories_best_effort(targets) ⇒ Array[String]
- #sync_formatting_directory(directory) ⇒ void
- #transactionally_write_formatted(targets) ⇒ void
- #validate_formatting_operation!(settings, paths) ⇒ void
- #validate_formatting_settings!(settings) ⇒ void
- #validate_formatting_stdin!(settings, paths) ⇒ void
- #validate_unique_formatting_targets!(targets) ⇒ void
- #write_formatting_results(results) ⇒ Integer
Instance Method Details
#backup_formatting_targets(targets) ⇒ void
This method returns an undefined value.
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
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
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
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 ]?
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
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
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
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 || . = "Usage: ibex fmt [--check | --write] [options] grammar.y [...]" .on("--check", "report files that need formatting") { settings[:check] = true } .on("--write", "atomically format files in place") { settings[:write] = true } .on("--mode=MODE", %w[default extended], "grammar mode") { |value| settings[:mode] = value.to_sym } .on("--stdin-filename=FILE", "diagnostic filename for standard input") do |value| settings[:stdin_filename] = value end .on("--help", "show help") { settings[:help] = true } end end |
#formatting_options(arguments) ⇒ formatting_settings
70 71 72 73 74 |
# File 'lib/ibex/cli/formatting.rb', line 70 def (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]
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.) end |
#formatting_transaction_error(error, rollback_errors, cleanup, sync_errors) ⇒ Ibex::Error
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.)}"] #: 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.
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 ]
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.)) failed = true end [results, failed] end |
#remove_formatting_artifact(path) ⇒ String?
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.)}" end end |
#report_formatting_cleanup_warning(cleanup, sync_errors) ⇒ void
This method returns an undefined value.
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]
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.)}" end errors end |
#run_format_command(arguments) ⇒ 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 = (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.
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]
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.)}" end errors end |
#sync_formatting_directories!(targets) ⇒ void
This method returns an undefined value.
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]
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.
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.
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.
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.
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.
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.
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
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 |