├── .document ├── .gitignore ├── LICENSE ├── README.md ├── Rakefile ├── bin └── hash_syntax ├── hash_syntax.gemspec ├── lib ├── hash_syntax.rb └── hash_syntax │ ├── runner.rb │ ├── token.rb │ ├── transformer.rb │ └── version.rb └── spec ├── hash_syntax_spec.rb ├── spec.opts ├── spec_helper.rb └── transformer_spec.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | ## PROJECT::SPECIFIC 22 | 23 | .rvmrc 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Michael Edgar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hash_syntax 2 | 3 | In Ruby 1.9, you can write a hash with symbol keys in two ways: 4 | 5 | { :foo => bar } 6 | { foo: bar } 7 | 8 | Some have [expressed discontent](http://logicalfriday.com/2011/06/20/i-dont-like-the-ruby-1-9-hash-syntax/) 9 | at this syntax change. Luckily, it's purely syntax sugar: there's no reason other than preference to 10 | use one or the other (assuming you are targeting 1.9). That means we can freely convert between them! 11 | That's what hash_syntax does: it scans Ruby code and turns the code into all 1.8 syntax or all 1.9 syntax. 12 | 13 | ## Using hash_syntax 14 | 15 | To convert a whole project to 1.8 syntax: 16 | 17 | hash_syntax --to-18 18 | 19 | To convert a whole project to 1.9 syntax: 20 | 21 | hash_syntax --to-19 22 | 23 | With no arguments, hash_syntax will scan the following paths using `Dir[]` and operate on 24 | each matching file: 25 | 26 | * `app/**/*.rb` 27 | * `ext/**/*.rb` 28 | * `features/**/*.rb` 29 | * `lib/**/*.rb` 30 | * `spec/**/*.rb` 31 | * `test/**/*.rb` 32 | 33 | If you wish to convert individual files, you can name them explicitly: 34 | 35 | hash_syntax --to-19 lib/foo.rb 36 | 37 | That's all there is to it! 38 | 39 | ## How it works 40 | 41 | `hash_syntax` uses the [`object_regex` library](http://carboni.ca/blog/p/Regex-Search-on-Arbitrary-Sequences) 42 | ([source](https://github.com/michaeledgar/object_regex/)) to perform regex searches on the Ruby token 43 | stream of a given file. A Ruby 1.8 symbol hash key which can be converted to 1.9's syntax can be described as: 44 | 45 | symbeg (ident | kw) sp? hashrocket 46 | 47 | By using each token's unique name, (with one tweak: all other operators are `op` and the hashrocket is `hashrocket`), 48 | object_regex can search using this pattern and find it in the Ruby source. Conveniently, you cannot have a line break 49 | between the symbol and the hashrocket; otherwise, the regex would be a bit more complicated (also needing to consider 50 | comments!). Each match is replaced inline as text, and `hash_syntax` notes how much the line has shrunk, in case further 51 | replacements happen on the same line. A better option would be to replace the actual tokens in the stream and reconstruct 52 | the source from the token stream. 53 | 54 | Ruby 1.9 symbol tokens are just a single `label` token; they are easy to find in the source. 55 | 56 | ## Installation 57 | 58 | gem install hash_syntax 59 | 60 | ## Note on Patches/Pull Requests 61 | 62 | * Fork the project. 63 | * Make your feature addition or bug fix. 64 | * Add tests for it. This is important so I don't break it in a 65 | future version unintentionally. 66 | * Commit, do not mess with rakefile, version, or history. 67 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 68 | * Send me a pull request. Bonus points for topic branches. 69 | 70 | ## Copyright 71 | 72 | Copyright (c) 2011 Michael Edgar. See LICENSE for details. 73 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | require './lib/hash_syntax/version' 5 | begin 6 | require 'jeweler' 7 | Jeweler::Tasks.new do |gem| 8 | gem.name = "hash_syntax" 9 | gem.summary = %Q{Converts Ruby files to and from Ruby 1.9's Hash syntax} 10 | gem.description = %Q{The new label style for Ruby 1.9's literal hash keys 11 | is somewhat controversial. This tool seamlessly converts Ruby files between 12 | the old and the new syntaxes.} 13 | gem.email = "michael.j.edgar@dartmouth.edu" 14 | gem.homepage = "http://github.com/michaeledgar/hash_syntax" 15 | gem.authors = ["Michael Edgar"] 16 | gem.add_dependency 'object_regex', '~> 1.0.1' 17 | gem.add_dependency 'trollop', '~> 1.16.2' 18 | gem.add_development_dependency 'rspec', '>= 2.3.0' 19 | gem.add_development_dependency "yard", ">= 0" 20 | gem.version = HashSyntax::Version::STRING 21 | end 22 | Jeweler::GemcutterTasks.new 23 | rescue LoadError 24 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 25 | end 26 | 27 | require 'rspec/core/rake_task' 28 | RSpec::Core::RakeTask.new(:spec) do |spec| 29 | spec.pattern = FileList['spec/**/*_spec.rb'] 30 | end 31 | 32 | task :spec => :check_dependencies 33 | 34 | task :default => :spec 35 | 36 | begin 37 | require 'yard' 38 | YARD::Rake::YardocTask.new 39 | rescue LoadError 40 | task :yardoc do 41 | abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /bin/hash_syntax: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -w 2 | 3 | require 'rubygems' 4 | require 'hash_syntax' 5 | HashSyntax::Runner.new.run! -------------------------------------------------------------------------------- /hash_syntax.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{hash_syntax} 8 | s.version = "1.0.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = [%q{Michael Edgar}] 12 | s.date = %q{2011-06-24} 13 | s.description = %q{The new label style for Ruby 1.9's literal hash keys 14 | is somewhat controversial. This tool seamlessly converts Ruby files between 15 | the old and the new syntaxes.} 16 | s.email = %q{michael.j.edgar@dartmouth.edu} 17 | s.executables = [%q{hash_syntax}] 18 | s.extra_rdoc_files = [ 19 | "LICENSE", 20 | "README.rdoc" 21 | ] 22 | s.files = [ 23 | ".document", 24 | "LICENSE", 25 | "README.rdoc", 26 | "Rakefile", 27 | "bin/hash_syntax", 28 | "hash_syntax.gemspec", 29 | "lib/hash_syntax.rb", 30 | "lib/hash_syntax/runner.rb", 31 | "lib/hash_syntax/token.rb", 32 | "lib/hash_syntax/transformer.rb", 33 | "lib/hash_syntax/version.rb", 34 | "spec/hash_syntax_spec.rb", 35 | "spec/spec.opts", 36 | "spec/spec_helper.rb", 37 | "spec/transformer_spec.rb" 38 | ] 39 | s.homepage = %q{http://github.com/michaeledgar/hash_syntax} 40 | s.require_paths = [%q{lib}] 41 | s.rubygems_version = %q{1.8.5} 42 | s.summary = %q{Converts Ruby files to and from Ruby 1.9's Hash syntax} 43 | 44 | if s.respond_to? :specification_version then 45 | s.specification_version = 3 46 | 47 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 48 | s.add_runtime_dependency(%q, ["~> 1.0.1"]) 49 | s.add_runtime_dependency(%q, ["~> 1.16.2"]) 50 | s.add_development_dependency(%q, [">= 2.3.0"]) 51 | s.add_development_dependency(%q, [">= 0"]) 52 | else 53 | s.add_dependency(%q, ["~> 1.0.1"]) 54 | s.add_dependency(%q, ["~> 1.16.2"]) 55 | s.add_dependency(%q, [">= 2.3.0"]) 56 | s.add_dependency(%q, [">= 0"]) 57 | end 58 | else 59 | s.add_dependency(%q, ["~> 1.0.1"]) 60 | s.add_dependency(%q, ["~> 1.16.2"]) 61 | s.add_dependency(%q, [">= 2.3.0"]) 62 | s.add_dependency(%q, [">= 0"]) 63 | end 64 | end 65 | 66 | -------------------------------------------------------------------------------- /lib/hash_syntax.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -w 2 | require 'ripper' 3 | require 'object_regex' 4 | require 'trollop' 5 | 6 | require 'hash_syntax/token' 7 | require 'hash_syntax/runner' 8 | require 'hash_syntax/transformer' 9 | require 'hash_syntax/version' -------------------------------------------------------------------------------- /lib/hash_syntax/runner.rb: -------------------------------------------------------------------------------- 1 | module HashSyntax 2 | class Runner 3 | 4 | def run! 5 | options = gather_options 6 | validate_options(options) 7 | files = gather_files 8 | files.each do |name| 9 | transformed_file = Transformer.transform(File.read(name), options) 10 | File.open(name, 'w') { |fp| fp.write(transformed_file) } 11 | end 12 | end 13 | 14 | private 15 | 16 | def gather_options 17 | Trollop::options do 18 | version HashSyntax::Version::STRING 19 | banner <<-EOF 20 | hash_syntax #{HashSyntax::Version::STRING} by Michael Edgar (adgar@carboni.ca) 21 | 22 | Automatically convert hash symbol syntaxes in your Ruby code. 23 | EOF 24 | opt :"to-18", 'Convert to Ruby 1.8 syntax (:key => value)', :short => '-o' 25 | opt :"to-19", 'Convert to Ruby 1.9 syntax (key: value)', :short => '-n' 26 | end 27 | end 28 | 29 | def validate_options(opts) 30 | Trollop::die 'Must specify --to-18 or --to-19' unless opts[:"to-18"] or opts[:"to-19"] 31 | end 32 | 33 | AUTO_SUBDIRS = %w(app ext features lib spec test) 34 | 35 | def gather_files 36 | if ARGV.empty? 37 | AUTO_SUBDIRS.map { |dir| Dir["#{Dir.pwd}/#{dir}/**/*.rb"] }.flatten 38 | else 39 | ARGV 40 | end 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /lib/hash_syntax/token.rb: -------------------------------------------------------------------------------- 1 | module HashSyntax 2 | Token = Struct.new(:type, :body, :line, :col) do 3 | # Unpacks the token from Ripper and breaks it into its separate components. 4 | # 5 | # @param [Array, Symbol, String>] token the token 6 | # from Ripper that we're wrapping 7 | def initialize(token) 8 | (self.line, self.col), self.type, self.body = token 9 | end 10 | 11 | def width 12 | body.size 13 | end 14 | 15 | def reg_desc 16 | if type == :on_op && body == '=>' 17 | 'hashrocket' 18 | else 19 | type.to_s.sub(/^on_/, '') 20 | end 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/hash_syntax/transformer.rb: -------------------------------------------------------------------------------- 1 | module HashSyntax 2 | module Transformer 3 | MATCH_18 = ObjectRegex.new('symbeg (ident | kw) sp? hashrocket') 4 | MATCH_19 = ObjectRegex.new('label') 5 | 6 | extend self 7 | 8 | def transform(input_text, options) 9 | tokens = extract_tokens(input_text) 10 | if options[:"to-18"] 11 | transform_to_18(input_text, tokens, options) 12 | elsif options[:"to-19"] 13 | transform_to_19(input_text, tokens, options) 14 | else 15 | raise ArgumentError.new('Either :to_18 or :to_19 must be specified.') 16 | end 17 | end 18 | 19 | private 20 | 21 | def extract_tokens(text) 22 | swizzle_parser_flags do 23 | Ripper.lex(text).map { |token| Token.new(token) } 24 | end 25 | end 26 | 27 | def swizzle_parser_flags 28 | old_w = $-w 29 | old_v = $-v 30 | old_d = $-d 31 | $-w = $-v = $-d = false 32 | yield 33 | ensure 34 | $-w = old_w 35 | $-v = old_v 36 | $-d = old_d 37 | end 38 | 39 | def transform_to_18(input_text, tokens, options) 40 | lines = input_text.lines.to_a # eagerly expand lines 41 | matches = MATCH_19.all_matches(tokens) 42 | line_adjustments = Hash.new(0) 43 | matches.each do |label_list| 44 | label = label_list.first 45 | lines[label.line - 1][label.col + line_adjustments[label.line],label.width] = ":#{label.body[0..-2]} =>" 46 | line_adjustments[label.line] += 3 # " =>" is inserted and is 3 chars 47 | end 48 | lines.join 49 | end 50 | 51 | def transform_to_19(input_text, tokens, options) 52 | lines = input_text.lines.to_a # eagerly expand lines 53 | matches = MATCH_18.all_matches(tokens) 54 | line_adjustments = Hash.new(0) 55 | matches.each do |match_tokens| 56 | symbeg, ident, *spacing_and_comments, rocket = match_tokens 57 | lines[symbeg.line - 1][symbeg.col + line_adjustments[symbeg.line],1] = '' 58 | lines[ident.line - 1].insert(ident.col + line_adjustments[ident.line] + ident.width - 1, ':') 59 | lines[rocket.line - 1][rocket.col + line_adjustments[rocket.line],2] = '' 60 | if spacing_and_comments.last != nil && spacing_and_comments.last.type == :on_sp 61 | lines[rocket.line - 1][rocket.col + line_adjustments[rocket.line] - 1,1] = '' 62 | line_adjustments[rocket.line] -= 3 # chomped " =>" 63 | else 64 | line_adjustments[rocket.line] -= 2 # only chomped the "=>" 65 | end 66 | end 67 | lines.join 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /lib/hash_syntax/version.rb: -------------------------------------------------------------------------------- 1 | module HashSyntax 2 | module Version 3 | MAJOR = 1 4 | MINOR = 0 5 | PATCH = 0 6 | BUILD = '' 7 | 8 | if BUILD.empty? 9 | STRING = [MAJOR, MINOR, PATCH].compact.join('.') 10 | else 11 | STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.') 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /spec/hash_syntax_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "HashSyntax" do 4 | it "has a version" do 5 | HashSyntax::Version::STRING.should be >= "1.0.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'hash_syntax' 4 | require 'rspec' 5 | require 'rspec/autorun' 6 | 7 | RSpec::Matchers.define :transform_to do |output, target| 8 | match do |input| 9 | @result = HashSyntax::Transformer.transform(input, target => true) 10 | @result == output 11 | end 12 | 13 | failure_message_for_should do |input| 14 | "expected '#{input}' to correct to #{output}, not #{@result}" 15 | end 16 | 17 | diffable 18 | end 19 | 20 | RSpec.configure do |config| 21 | 22 | end 23 | -------------------------------------------------------------------------------- /spec/transformer_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe HashSyntax::Transformer do 4 | describe 'transforming from 1.8 to 1.9 syntax' do 5 | it 'can transform a simple hash' do 6 | 'x = {:foo => :bar}'.should transform_to('x = {foo: :bar}', :to_19) 7 | end 8 | 9 | it 'transforms all hashes in a block of code' do 10 | input = %q{ 11 | with_jumps_redirected(:break => ensure_body[1], :redo => ensure_body[1], :next => ensure_body[1], 12 | :return => ensure_body[1], :rescue => ensure_body[1], 13 | :yield_fail => ensure_body[1]) do 14 | rescue_target, yield_fail_target = 15 | build_rescue_target(node, result, rescue_body, ensure_block, 16 | current_rescue, current_yield_fail) 17 | walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) 18 | end 19 | } 20 | output = %q{ 21 | with_jumps_redirected(break: ensure_body[1], redo: ensure_body[1], next: ensure_body[1], 22 | return: ensure_body[1], rescue: ensure_body[1], 23 | yield_fail: ensure_body[1]) do 24 | rescue_target, yield_fail_target = 25 | build_rescue_target(node, result, rescue_body, ensure_block, 26 | current_rescue, current_yield_fail) 27 | walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) 28 | end 29 | } 30 | input.should transform_to(output, :to_19) 31 | end 32 | 33 | it 'transforms all hashes in a block of code without minding tight spacing' do 34 | input = %q{ 35 | with_jumps_redirected(:break=>ensure_body[1], :redo=>ensure_body[1], :next=>ensure_body[1], 36 | :return=>ensure_body[1], :rescue=>ensure_body[1], 37 | :yield_fail=>ensure_body[1]) do 38 | rescue_target, yield_fail_target = 39 | build_rescue_target(node, result, rescue_body, ensure_block, 40 | current_rescue, current_yield_fail) 41 | walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) 42 | end 43 | } 44 | output = %q{ 45 | with_jumps_redirected(break:ensure_body[1], redo:ensure_body[1], next:ensure_body[1], 46 | return:ensure_body[1], rescue:ensure_body[1], 47 | yield_fail:ensure_body[1]) do 48 | rescue_target, yield_fail_target = 49 | build_rescue_target(node, result, rescue_body, ensure_block, 50 | current_rescue, current_yield_fail) 51 | walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) 52 | end 53 | } 54 | input.should transform_to(output, :to_19) 55 | end 56 | 57 | end 58 | 59 | describe 'transforming from 1.9 to 1.8 syntax' do 60 | it 'can transform a simple hash' do 61 | 'x = {foo: :bar}'.should transform_to('x = {:foo => :bar}', :to_18) 62 | end 63 | 64 | it 'transforms all hashes in a block of code' do 65 | input = %q{ 66 | with_jumps_redirected(break: ensure_body[1], redo: ensure_body[1], next: ensure_body[1], 67 | return: ensure_body[1], rescue: ensure_body[1], 68 | yield_fail: ensure_body[1]) do 69 | rescue_target, yield_fail_target = 70 | build_rescue_target(node, result, rescue_body, ensure_block, 71 | current_rescue, current_yield_fail) 72 | walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) 73 | end 74 | } 75 | output = %q{ 76 | with_jumps_redirected(:break => ensure_body[1], :redo => ensure_body[1], :next => ensure_body[1], 77 | :return => ensure_body[1], :rescue => ensure_body[1], 78 | :yield_fail => ensure_body[1]) do 79 | rescue_target, yield_fail_target = 80 | build_rescue_target(node, result, rescue_body, ensure_block, 81 | current_rescue, current_yield_fail) 82 | walk_body_with_rescue_target(result, body, body_block, rescue_target, yield_fail_target) 83 | end 84 | } 85 | input.should transform_to(output, :to_18) 86 | end 87 | end 88 | end 89 | --------------------------------------------------------------------------------