From 327dde8b3cee2b1cdac8d60fc7f206edac7f9244 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Fri, 24 Jun 2011 03:19:47 -0400 Subject: [PATCH] First check-in of hash_syntax. While this will be a part of Laser, a small independent tool would make a lot of people happy. Long story short: auto-convert hash syntaxes. --- .gitignore | 2 + Rakefile | 26 +++++----- bin/hash_syntax | 5 ++ lib/hash_syntax.rb | 9 ++++ lib/hash_syntax/runner.rb | 43 +++++++++++++++++ lib/hash_syntax/token.rb | 23 +++++++++ lib/hash_syntax/transformer.rb | 70 +++++++++++++++++++++++++++ lib/hash_syntax/version.rb | 14 ++++++ spec/hash_syntax_spec.rb | 6 +-- spec/spec_helper.rb | 19 ++++++-- spec/transformer_spec.rb | 88 ++++++++++++++++++++++++++++++++++ 11 files changed, 285 insertions(+), 20 deletions(-) create mode 100755 bin/hash_syntax mode change 100644 => 100755 lib/hash_syntax.rb create mode 100644 lib/hash_syntax/runner.rb create mode 100644 lib/hash_syntax/token.rb create mode 100644 lib/hash_syntax/transformer.rb create mode 100644 lib/hash_syntax/version.rb create mode 100644 spec/transformer_spec.rb diff --git a/.gitignore b/.gitignore index c1e0daf..0b90377 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ rdoc pkg ## PROJECT::SPECIFIC + +.rvmrc diff --git a/Rakefile b/Rakefile index ef51606..78b74e7 100644 --- a/Rakefile +++ b/Rakefile @@ -1,34 +1,32 @@ require 'rubygems' require 'rake' +require './lib/hash_syntax/version' begin require 'jeweler' Jeweler::Tasks.new do |gem| gem.name = "hash_syntax" - gem.summary = %Q{TODO: one-line summary of your gem} - gem.description = %Q{TODO: longer description of your gem} + gem.summary = %Q{Converts Ruby files to and from Ruby 1.9's Hash syntax} + gem.description = %Q{The new label style for Ruby 1.9's literal hash keys +is somewhat controversial. This tool seamlessly converts Ruby files between +the old and the new syntaxes.} gem.email = "michael.j.edgar@dartmouth.edu" gem.homepage = "http://github.com/michaeledgar/hash_syntax" gem.authors = ["Michael Edgar"] - gem.add_development_dependency "rspec", ">= 1.2.9" + gem.add_dependency 'object_regex', '~> 1.0.1' + gem.add_dependency 'trollop', '~> 1.16.2' + gem.add_development_dependency 'rspec', '>= 2.3.0' gem.add_development_dependency "yard", ">= 0" - # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings + gem.version = HashSyntax::Version::STRING end Jeweler::GemcutterTasks.new rescue LoadError puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" end -require 'spec/rake/spectask' -Spec::Rake::SpecTask.new(:spec) do |spec| - spec.libs << 'lib' << 'spec' - spec.spec_files = FileList['spec/**/*_spec.rb'] -end - -Spec::Rake::SpecTask.new(:rcov) do |spec| - spec.libs << 'lib' << 'spec' - spec.pattern = 'spec/**/*_spec.rb' - spec.rcov = true +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList['spec/**/*_spec.rb'] end task :spec => :check_dependencies diff --git a/bin/hash_syntax b/bin/hash_syntax new file mode 100755 index 0000000..6fa1a11 --- /dev/null +++ b/bin/hash_syntax @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby -w + +require 'rubygems' +require 'hash_syntax' +HashSyntax::Runner.new.run! \ No newline at end of file diff --git a/lib/hash_syntax.rb b/lib/hash_syntax.rb old mode 100644 new mode 100755 index e69de29..ddb016b --- a/lib/hash_syntax.rb +++ b/lib/hash_syntax.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby -w +require 'ripper' +require 'object_regex' +require 'trollop' + +require 'hash_syntax/token' +require 'hash_syntax/runner' +require 'hash_syntax/transformer' +require 'hash_syntax/version' \ No newline at end of file diff --git a/lib/hash_syntax/runner.rb b/lib/hash_syntax/runner.rb new file mode 100644 index 0000000..4fc6469 --- /dev/null +++ b/lib/hash_syntax/runner.rb @@ -0,0 +1,43 @@ +module HashSyntax + class Runner + + def run! + options = gather_options + validate_options(options) + files = gather_files + files.each do |name| + transformed_file = Transformer.transform(File.read(name), options) + File.open(name, 'w') { |fp| fp.write(transformed_file) } + end + end + + private + + def gather_options + Trollop::options do + version HashSyntax::Version::STRING + banner <<-EOF +hash_syntax #{HashSyntax::Version::STRING} by Michael Edgar (adgar@carboni.ca) + +Automatically convert hash symbol syntaxes in your Ruby code. +EOF + opt :to_18, 'Convert to Ruby 1.8 syntax (:key => value)' + opt :to_19, 'Convert to Ruby 1.9 syntax (key: value)' + end + end + + def validate_options(opts) + Trollop::die 'Must specify --to_18 or --to_19' unless opts[:to_18] or opts[:to_19] + end + + AUTO_SUBDIRS = %w(app ext features lib spec test) + + def gather_files + if ARGV.empty? + AUTO_SUBDIRS.map { |dir| Dir["#{Dir.pwd}/#{dir}/**/*.rb"] }.flatten + else + ARGV + end + end + end +end \ No newline at end of file diff --git a/lib/hash_syntax/token.rb b/lib/hash_syntax/token.rb new file mode 100644 index 0000000..69b36c6 --- /dev/null +++ b/lib/hash_syntax/token.rb @@ -0,0 +1,23 @@ +module HashSyntax + Token = Struct.new(:type, :body, :line, :col) do + # Unpacks the token from Ripper and breaks it into its separate components. + # + # @param [Array, Symbol, String>] token the token + # from Ripper that we're wrapping + def initialize(token) + (self.line, self.col), self.type, self.body = token + end + + def width + body.size + end + + def reg_desc + if type == :on_op && body == '=>' + 'hashrocket' + else + type.to_s.sub(/^on_/, '') + end + end + end +end \ No newline at end of file diff --git a/lib/hash_syntax/transformer.rb b/lib/hash_syntax/transformer.rb new file mode 100644 index 0000000..dbfc7ab --- /dev/null +++ b/lib/hash_syntax/transformer.rb @@ -0,0 +1,70 @@ +module HashSyntax + module Transformer + MATCH_18 = ObjectRegex.new('symbeg (ident | kw) sp? hashrocket') + MATCH_19 = ObjectRegex.new('label') + + extend self + + def transform(input_text, options) + tokens = extract_tokens(input_text) + if options[:to_18] + transform_to_18(input_text, tokens, options) + elsif options[:to_19] + transform_to_19(input_text, tokens, options) + else + raise ArgumentError.new('Either :to_18 or :to_19 must be specified.') + end + end + + private + + def extract_tokens(text) + swizzle_parser_flags do + Ripper.lex(text).map { |token| Token.new(token) } + end + end + + def swizzle_parser_flags + old_w = $-w + old_v = $-v + old_d = $-d + $-w = $-v = $-d = false + yield + ensure + $-w = old_w + $-v = old_v + $-d = old_d + end + + def transform_to_18(input_text, tokens, options) + lines = input_text.lines.to_a # eagerly expand lines + matches = MATCH_19.all_matches(tokens) + line_adjustments = Hash.new(0) + matches.each do |label_list| + label = label_list.first + lines[label.line - 1][label.col + line_adjustments[label.line],label.width] = ":#{label.body[0..-2]} =>" + line_adjustments[label.line] += 3 # " =>" is inserted and is 3 chars + end + lines.join + end + + def transform_to_19(input_text, tokens, options) + lines = input_text.lines.to_a # eagerly expand lines + matches = MATCH_18.all_matches(tokens) + line_adjustments = Hash.new(0) + matches.each do |match_tokens| + symbeg, ident, *spacing_and_comments, rocket = match_tokens + lines[symbeg.line - 1][symbeg.col + line_adjustments[symbeg.line],1] = '' + lines[ident.line - 1].insert(ident.col + line_adjustments[ident.line] + ident.width - 1, ':') + lines[rocket.line - 1][rocket.col + line_adjustments[rocket.line],2] = '' + if spacing_and_comments.last != nil && spacing_and_comments.last.type == :on_sp + lines[rocket.line - 1][rocket.col + line_adjustments[rocket.line] - 1,1] = '' + line_adjustments[rocket.line] -= 3 # chomped " =>" + else + line_adjustments[rocket.line] -= 2 # only chomped the "=>" + end + end + lines.join + end + end +end \ No newline at end of file diff --git a/lib/hash_syntax/version.rb b/lib/hash_syntax/version.rb new file mode 100644 index 0000000..c4e11c6 --- /dev/null +++ b/lib/hash_syntax/version.rb @@ -0,0 +1,14 @@ +module HashSyntax + module Version + MAJOR = 1 + MINOR = 0 + PATCH = 0 + BUILD = 'pre1' + + if BUILD.empty? + STRING = [MAJOR, MINOR, PATCH].compact.join('.') + else + STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.') + end + end +end \ No newline at end of file diff --git a/spec/hash_syntax_spec.rb b/spec/hash_syntax_spec.rb index fababad..d475ec2 100644 --- a/spec/hash_syntax_spec.rb +++ b/spec/hash_syntax_spec.rb @@ -1,7 +1,7 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') +require_relative 'spec_helper' describe "HashSyntax" do - it "fails" do - fail "hey buddy, you should probably rename this file and start specing for real" + it "has a version" do + HashSyntax::Version::STRING.should be >= "1.0.0" end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9782e6f..1f7ce49 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,9 +1,22 @@ $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'hash_syntax' -require 'spec' -require 'spec/autorun' +require 'rspec' +require 'rspec/autorun' -Spec::Runner.configure do |config| +RSpec::Matchers.define :transform_to do |output, target| + match do |input| + @result = HashSyntax::Transformer.transform(input, target => true) + @result == output + end + + failure_message_for_should do |input| + "expected '#{input}' to correct to #{output}, not #{@result}" + end + + diffable +end + +RSpec.configure do |config| end diff --git a/spec/transformer_spec.rb b/spec/transformer_spec.rb new file mode 100644 index 0000000..4fa9b88 --- /dev/null +++ b/spec/transformer_spec.rb @@ -0,0 +1,88 @@ +require_relative 'spec_helper' + +describe HashSyntax::Transformer do + describe 'transforming from 1.8 to 1.9 syntax' do + it 'can transform a simple hash' do + 'x = {:foo => :bar}'.should transform_to('x = {foo: :bar}', :to_19) + end + + it 'transforms all hashes in a block of code' do + input = %q{ +with_jumps_redirected(:break => ensure_body[1], :redo => ensure_body[1], :next => ensure_body[1], + :return => ensure_body[1], :rescue => ensure_body[1], + :yield_fail => ensure_body[1]) do + rescue_target, yield_fail_target = + build_rescue_target(node, result, rescue_body, ensure_block, + current_rescue, current_yield_fail) + walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) +end +} + output = %q{ +with_jumps_redirected(break: ensure_body[1], redo: ensure_body[1], next: ensure_body[1], + return: ensure_body[1], rescue: ensure_body[1], + yield_fail: ensure_body[1]) do + rescue_target, yield_fail_target = + build_rescue_target(node, result, rescue_body, ensure_block, + current_rescue, current_yield_fail) + walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) +end +} + input.should transform_to(output, :to_19) + end + + it 'transforms all hashes in a block of code without minding tight spacing' do + input = %q{ +with_jumps_redirected(:break=>ensure_body[1], :redo=>ensure_body[1], :next=>ensure_body[1], + :return=>ensure_body[1], :rescue=>ensure_body[1], + :yield_fail=>ensure_body[1]) do + rescue_target, yield_fail_target = + build_rescue_target(node, result, rescue_body, ensure_block, + current_rescue, current_yield_fail) + walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) +end +} + output = %q{ +with_jumps_redirected(break:ensure_body[1], redo:ensure_body[1], next:ensure_body[1], + return:ensure_body[1], rescue:ensure_body[1], + yield_fail:ensure_body[1]) do + rescue_target, yield_fail_target = + build_rescue_target(node, result, rescue_body, ensure_block, + current_rescue, current_yield_fail) + walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) +end +} + input.should transform_to(output, :to_19) + end + + end + + describe 'transforming from 1.9 to 1.8 syntax' do + it 'can transform a simple hash' do + 'x = {foo: :bar}'.should transform_to('x = {:foo => :bar}', :to_18) + end + + it 'transforms all hashes in a block of code' do + input = %q{ +with_jumps_redirected(break: ensure_body[1], redo: ensure_body[1], next: ensure_body[1], + return: ensure_body[1], rescue: ensure_body[1], + yield_fail: ensure_body[1]) do + rescue_target, yield_fail_target = + build_rescue_target(node, result, rescue_body, ensure_block, + current_rescue, current_yield_fail) + walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) +end +} + output = %q{ +with_jumps_redirected(:break => ensure_body[1], :redo => ensure_body[1], :next => ensure_body[1], + :return => ensure_body[1], :rescue => ensure_body[1], + :yield_fail => ensure_body[1]) do + rescue_target, yield_fail_target = + build_rescue_target(node, result, rescue_body, ensure_block, + current_rescue, current_yield_fail) + walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) +end +} + input.should transform_to(output, :to_18) + end + end +end