Skip to content

Commit

Permalink
First check-in of hash_syntax.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Michael Edgar committed Jun 24, 2011
1 parent bf111f3 commit 327dde8
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -19,3 +19,5 @@ rdoc
pkg

## PROJECT::SPECIFIC

.rvmrc
26 changes: 12 additions & 14 deletions 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
Expand Down
5 changes: 5 additions & 0 deletions bin/hash_syntax
@@ -0,0 +1,5 @@
#!/usr/bin/env ruby -w

require 'rubygems'
require 'hash_syntax'
HashSyntax::Runner.new.run!
9 changes: 9 additions & 0 deletions lib/hash_syntax.rb 100644 → 100755
@@ -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'
43 changes: 43 additions & 0 deletions 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
23 changes: 23 additions & 0 deletions 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<Array<Integer, Integer>, 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
70 changes: 70 additions & 0 deletions 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
14 changes: 14 additions & 0 deletions 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
6 changes: 3 additions & 3 deletions 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
19 changes: 16 additions & 3 deletions 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
88 changes: 88 additions & 0 deletions 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

0 comments on commit 327dde8

Please sign in to comment.