ryan 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 043ec6bf0cba3ff0f096fc4b47e2c0580b50f5ca
4
- data.tar.gz: d65251331f9a6da616765fc2b854da2f16b9a368
3
+ metadata.gz: 291fea61132de8ec31f0e70fda31c930a41fc86a
4
+ data.tar.gz: 9ec06895b0fbd8e8f24cb4333ef443d1e88ffdac
5
5
  SHA512:
6
- metadata.gz: 6e1a7162edfa879a93cf71d59aff18c73696dadae7a1cde638df7b5b5e2a6aaef592a42cd957b3d6f8fbc5866674b9420ec7d22dc3adfdb152cfd519c031819f
7
- data.tar.gz: 022145f0844c32f77eccfb3234295a03e08492e23ec292f575c7ae27a3d2fd826142c55a1d9a1c7fabbac4728725cfbed25da2e1edb08fd347b81074a710f8f0
6
+ metadata.gz: df0bd56e2f47a7887c2dc5d84c5076665c5343a34cd3971a15efac6be4f323802e2dd6f7b55756d74a27d4ae41784805730d86e66336e967c5d374dc014d68bd
7
+ data.tar.gz: 309d0f95cf4f7e952d6af7054300402cfb0b8282580179126fddbd8d74beb9df188062526396ccf0dc8ac7f6fb1b003970799716d3397d1c8f4fd234143f15c9
@@ -0,0 +1 @@
1
+ ryan
@@ -0,0 +1 @@
1
+ ruby-2.2.2
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in ryan.gemspec
4
4
  gemspec
5
+
6
+ gem 'byebug'
data/README.md CHANGED
@@ -1,3 +1,100 @@
1
1
  # Ryan
2
2
 
3
- Reviews and rewrites rspec files using the style I like
3
+ A wrapper around the awesome [RubyParser](https://github.com/seattlerb/ruby_parser) gem that provides an OO interface for
4
+ reading Ruby code.
5
+
6
+ ## Installation
7
+
8
+ ```ruby
9
+ gem 'ryan'
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ Give Ryan a Ruby file to play with. Test it out in an IRB session with `bin/console`
15
+
16
+ ```ruby
17
+ ryan = Ryan.new FIXTURE_ROOT.join('report.rb')
18
+ ryan.name
19
+ #=> "Report"
20
+ ryan.class?
21
+ #=> true
22
+ ryan.module?
23
+ #=> false
24
+ ryan.initialization_args
25
+ #=> [:message]
26
+ ryan.funcs.length
27
+ #=> 12
28
+ ryan.funcs.reject(&:private?).length
29
+ #=> 10
30
+ ryan.funcs.first.name
31
+ #=> :enqueue
32
+ ```
33
+
34
+ ### Assignments
35
+
36
+ ```ruby
37
+ func = ryan.func_by_name(:call)
38
+ #=> #<Ryan::InstanceFunc:0x007fd49c10d8d0 @sexp=...>
39
+ func.assignments.length
40
+ #=> 1
41
+ func.assignments.first.name
42
+ #=> :@duder
43
+ ```
44
+
45
+ ### Conditions
46
+
47
+ ```ruby
48
+ #=> "assigns @duder"
49
+ func.conditions.length
50
+ #=> 3
51
+ condition = func.conditions.last
52
+ #=> #<Ryan::Condition:0x007fd49c10d8d0 @sexp=...>
53
+ condition.statement
54
+ #=> "report.save"
55
+ condition.full_statement
56
+ #=> "if report.save then\n UserMailer.spam(user).deliver_now if user.wants_mail?\n report.perform\nelse\n ..."
57
+ condition.if_sexp
58
+ #=> s(:call, s(:call, nil, :report), :perform)
59
+ condition.if_text
60
+ #=> "returns report.perform"
61
+ condition.else_text
62
+ #=> ""
63
+ ```
64
+
65
+ ### Condition Parts
66
+
67
+ ```ruby
68
+ # Get the parts of the current condition, which will be the any elsif's
69
+ condition.parts.length
70
+ #=> 1
71
+ part = condition.parts.first
72
+ #=> #<Ryan::Condition:0x007fd49ca5a028 @sexp=...>
73
+ part.if_text
74
+ #=> "returns report.force_perform"
75
+ part.else_text
76
+ #=> "returns report.failure"
77
+ ```
78
+
79
+ ### Nested Conditions
80
+
81
+ ```ruby
82
+ # Find conditions nested inside this condition
83
+ condition.nested_conditions.length
84
+ #=> 1
85
+ nested_condition = condition.nested_conditions.last
86
+ #=> #<Ryan::Condition:0x007fd49ca8bbf0 @sexp=...>
87
+ nested_condition.if_sexp
88
+ #=> s(:call, s(:call, s(:const, :UserMailer), :spam, s(:call, nil, :user)), :deliver_now)
89
+ nested_condition.if_text
90
+ #=> "returns UserMailer.spam(user).deliver_now"
91
+ ```
92
+
93
+ ## Development
94
+
95
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests.
96
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
97
+
98
+ ## Contributing
99
+
100
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ridiculous/ryan.
data/Rakefile CHANGED
@@ -1 +1,10 @@
1
- require "bundler/gem_tasks"
1
+ $LOAD_PATH.unshift './lib'
2
+ require 'ryan'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec) do |spec|
6
+ spec.pattern = 'spec/lib/**/*_spec.rb'
7
+ end
8
+
9
+ desc 'Run specs'
10
+ task default: :spec
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/setup"
4
4
  require "ryan"
5
+ require_relative "../spec/spec_helper"
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -1,5 +1,26 @@
1
- require 'ryan/version'
1
+ require 'ruby_parser'
2
+ require 'forwardable'
2
3
 
3
- module Ryan
4
- # todo
4
+ class Ryan
5
+ autoload :Version, 'ryan/version'
6
+ autoload :Const, 'ryan/const'
7
+ autoload :Condition, 'ryan/condition'
8
+ autoload :Func, 'ryan/func'
9
+ autoload :ClassFunc, 'ryan/class_func'
10
+ autoload :InstanceFunc, 'ryan/instance_func'
11
+ autoload :Assignment, 'ryan/assignment'
12
+ autoload :SexpDecorator, 'ryan/sexp_decorator'
13
+
14
+ attr_reader :sexp, :const
15
+
16
+ extend Forwardable
17
+
18
+ def_delegators :const,
19
+ :name, :funcs, :type, :initialization_args, :func_by_name, :class?, :module?
20
+
21
+ # @param [Pathname, String] file
22
+ def initialize(file)
23
+ @sexp = RubyParser.new.parse File.read(file)
24
+ @const = Const.new(sexp)
25
+ end
5
26
  end
@@ -0,0 +1,23 @@
1
+ class Ryan
2
+ class Assignment
3
+ attr_reader :sexp
4
+
5
+ def initialize(sexp)
6
+ @sexp = sexp
7
+ end
8
+
9
+ def to_s
10
+ "assigns #{name}"
11
+ end
12
+
13
+ # @example s(:iasgn, :@duder, s(:if, ...)
14
+ # @example s(:op_asgn_or, s(:ivar, :@report), ...)
15
+ def name
16
+ if sexp.first == :iasgn
17
+ sexp[1]
18
+ elsif sexp.first == :op_asgn_or
19
+ sexp[1][1]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ class Ryan::ClassFunc < Ryan::Func
2
+ def name
3
+ sexp[2]
4
+ end
5
+
6
+ def args
7
+ map_args(sexp[3]) if sexp[3].first == :args
8
+ end
9
+
10
+ def class?
11
+ true
12
+ end
13
+ end
@@ -0,0 +1,111 @@
1
+ require 'set'
2
+ require 'ruby2ruby'
3
+
4
+ class Ryan::Condition
5
+
6
+ attr_reader :nested_conditions, :sexp, :parts
7
+
8
+ def initialize(sexp)
9
+ @sexp = sexp
10
+ @nested_conditions = create_nested_conditions
11
+ @parts = load_parts
12
+ end
13
+
14
+ def full_statement
15
+ @full_statement ||= Ruby2Ruby.new.process(sexp.deep_clone)
16
+ end
17
+
18
+ def statement
19
+ edit_statement Ruby2Ruby.new.process(sexp[1].deep_clone)
20
+ end
21
+
22
+ def if_text
23
+ edit_return_text Ruby2Ruby.new.process(if_sexp.deep_clone)
24
+ end
25
+
26
+ def else_text
27
+ edit_return_text Ruby2Ruby.new.process(else_sexp.deep_clone)
28
+ end
29
+
30
+ def if_sexp
31
+ if sexp[2].nil?
32
+ sexp.last
33
+ elsif sexp[2].first == :if
34
+ nil
35
+ elsif sexp[2].first == :block
36
+ sexp[2].last
37
+ elsif sexp[2].first == :rescue
38
+ sexp[2][1].last
39
+ else
40
+ sexp[2]
41
+ end
42
+ end
43
+
44
+ def else_sexp
45
+ if sexp.compact[3].nil?
46
+ nil
47
+ elsif sexp[3].first == :block
48
+ sexp[3].last
49
+ elsif sexp[3].first == :if
50
+ nil
51
+ else
52
+ sexp[3]
53
+ end
54
+ end
55
+
56
+ #
57
+ # Private
58
+ #
59
+
60
+ def edit_return_text(txt)
61
+ txt = txt.to_s
62
+ txt.sub! /^return\b/, 'returns'
63
+ txt.sub! /^returns\s*$/, 'returns nil'
64
+ if txt.include?(' = ')
65
+ txt = "assigns #{txt}"
66
+ elsif !txt.empty? and txt !~ /^return/
67
+ txt = "returns #{txt.strip}"
68
+ end
69
+ txt.strip
70
+ end
71
+
72
+ def edit_statement(txt)
73
+ txt = txt[/(.+)\n?/, 1] # take first line
74
+ txt.prepend 'unless ' if sexp[2].nil? # this is an unless statement
75
+ txt
76
+ end
77
+
78
+ # @description handles elsif
79
+ def load_parts
80
+ condition_parts = Set.new
81
+ sexp.each_sexp do |s|
82
+ if s.first == :if
83
+ condition_parts << self.class.new(s)
84
+ end
85
+ end
86
+ condition_parts.to_a
87
+ end
88
+
89
+ def create_nested_conditions
90
+ nc = Set.new
91
+ s = sexp.drop(1)
92
+ s.flatten.include?(:if) && s.deep_each do |exp|
93
+ Ryan::SexpDecorator.new(exp).each_sexp_condition do |node|
94
+ nc << self.class.new(node)
95
+ end
96
+ end
97
+ nc.to_a
98
+ end
99
+
100
+ #
101
+ # Set comparison operators
102
+ #
103
+
104
+ def eql?(other)
105
+ hash == other.hash
106
+ end
107
+
108
+ def hash
109
+ sexp.object_id
110
+ end
111
+ end
@@ -0,0 +1,97 @@
1
+ class Ryan::Const
2
+ CLASS_MOD_DEFS = { class: :class, module: :module }
3
+ CONSTANT_DEFS = { cdecl: :class }.merge CLASS_MOD_DEFS
4
+ PRIVATE_DEFS = [:private, :protected]
5
+
6
+ attr_reader :sexp
7
+
8
+ # @param [Sexp] sexp
9
+ def initialize(sexp)
10
+ sexp = sexp[2] if sexp[0] == :block
11
+ @sexp = sexp
12
+ end
13
+
14
+ # @description Extracts the class/mod definition from a Sexp object. Handles stuff that looks like:
15
+ # - s(:module, :Admin, s(:class, :Super, nil, s(:class, :UsersController, s(:colon3, :ApplicationController), s(:defn, :name, s(:args)
16
+ # - s(:class, :Report, nil, s(:cdecl, :DEFAULT_TZ, s(:str, "UTC")), s(:defs, s(:self), :enqueue,
17
+ # - s(:module, s(:colon2, s(:const, :Mixins), :Models))
18
+ # - s(:module, :Mixins, s(:module, :Helpers, s(:cdecl, :CSS, s(:str, "height:0"))))
19
+ # - s(:module, :Admin, s(:class, :Super, nil, s(:class, :UsersController, s(:colon3, :ApplicationController), s(:defn, :name, s(:args), ...))))
20
+ def name(sexp = @sexp, list = [])
21
+ if sexp[1].is_a?(Sexp) && sexp[1][0] == :colon2
22
+ parts = sexp[1].to_a.flatten
23
+ list.concat parts.drop(parts.size / 2)
24
+ elsif CONSTANT_DEFS.key?(sexp[0])
25
+ list << sexp[1].to_s
26
+ # skip when the second element is nil; it's just there sometimes and usually indicates the end of the definition
27
+ if !sexp[2].nil? || (sexp[3].is_a?(Sexp) and CLASS_MOD_DEFS.key?(sexp[3].first))
28
+ compact_sexp = sexp.compact[2]
29
+ if CLASS_MOD_DEFS.key?(compact_sexp.first)
30
+ name(compact_sexp, list)
31
+ end
32
+ end
33
+ end
34
+ list.join('::')
35
+ end
36
+
37
+ def initialization_args
38
+ funcs.find(-> { OpenStruct.new }) { |func| func.name == :initialize }.args.to_a.reject { |a| a.to_s.sub(/^\*/, '').empty? }
39
+ end
40
+
41
+ def func_by_name(name_as_symbol)
42
+ funcs.find { |x| x.name == name_as_symbol }
43
+ end
44
+
45
+ def funcs
46
+ @funcs ||= load_funcs.flatten
47
+ end
48
+
49
+ def class?
50
+ type == :class
51
+ end
52
+
53
+ def module?
54
+ type == :module
55
+ end
56
+
57
+ def type(sexp = @sexp, val = nil)
58
+ sexp = Array(sexp)
59
+ if sexp[1].is_a?(Sexp) && sexp[1][0] == :colon2
60
+ val = sexp[0]
61
+ elsif CONSTANT_DEFS.key?(sexp[0])
62
+ val = type(sexp.compact[2], sexp[0])
63
+ end
64
+ CONSTANT_DEFS[val]
65
+ end
66
+
67
+ private
68
+
69
+ def load_funcs(sexp_list = sexp)
70
+ marked_private = false
71
+ sexp_list.map { |node|
72
+ next unless node.is_a?(Sexp)
73
+ case node.first
74
+ when :defn
75
+ Ryan::InstanceFunc.new(node, marked_private)
76
+ when :defs
77
+ Ryan::ClassFunc.new(node, marked_private)
78
+ when :call
79
+ marked_private ||= PRIVATE_DEFS.include?(node[2])
80
+ nil
81
+ when ->(name) { CONSTANT_DEFS.key?(name) }
82
+ # @note handle diz kind stuff:
83
+ # s(:class, :VerificationCode, nil, s(:defs, s(:self), :find, s(:args, :*)))
84
+ # and
85
+ # s(:module, :ConditionParsers, s(:class, :ReturnValue, ...))
86
+ # and
87
+ # s(:class, :NotificationPresenter, nil, s(:call, nil, :extend, s(:const, :Forwardable)), s(:call, nil, :attr_reader, ...))
88
+ load_funcs(node.compact.drop(2))
89
+ else
90
+ # @todo uncomment with logger at debug level
91
+ # puts "got #{__method__} with #{node.first} and don't know how to handle it"
92
+ # @note :const seems to indicate module inclusion/extension
93
+ nil
94
+ end
95
+ }.compact
96
+ end
97
+ end
@@ -0,0 +1,41 @@
1
+ class Ryan::Func
2
+ attr_accessor :sexp, :_private
3
+
4
+ alias private? _private
5
+
6
+ def initialize(sexp, _private)
7
+ @sexp, @_private = sexp, _private
8
+ end
9
+
10
+ def conditions
11
+ @conditions ||= Ryan::SexpDecorator.new(sexp).each_sexp_condition.map &Ryan::Condition.method(:new)
12
+ end
13
+
14
+ def assignments
15
+ nodes = find_assignments(sexp)
16
+ sexp.find_nodes(:rescue).each do |node|
17
+ node = node[1] if node[1].first == :block
18
+ nodes.concat find_assignments(node)
19
+ end
20
+ nodes.map &Ryan::Assignment.method(:new)
21
+ end
22
+
23
+ def find_assignments(s_expression)
24
+ s_expression.find_nodes(:op_asgn_or) + s_expression.find_nodes(:iasgn)
25
+ end
26
+
27
+ # @note we drop(1) to get rid of :args (which should be the first item in the sexp)
28
+ # @note called from subclasses
29
+ def map_args(_sexp = sexp, list = [])
30
+ val = _sexp.first
31
+ return list.drop(1) unless val
32
+ case val
33
+ when Symbol
34
+ map_args(_sexp.drop(1), list << val)
35
+ when Sexp
36
+ map_args(_sexp.drop(1), list << val[1])
37
+ else
38
+ nil
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ class Ryan::InstanceFunc < Ryan::Func
2
+ def name
3
+ sexp[1]
4
+ end
5
+
6
+ def args
7
+ map_args(sexp[2]) if sexp[2].first == :args
8
+ end
9
+
10
+ def class?
11
+ false
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ require 'delegate'
2
+
3
+ class Ryan::SexpDecorator < DelegateClass(Sexp)
4
+ def each_sexp_condition
5
+ return enum_for(__method__) unless block_given?
6
+ yielded = []
7
+ each_sexp do |exp|
8
+ if exp.first == :if
9
+ yield exp
10
+ yielded << exp
11
+ else
12
+ nested_sexp = exp.enum_for(:deep_each).select { |s| s.first == :if }
13
+ nested_sexp.each do |sexp|
14
+ if yielded.find { |x| x.object_id == sexp.object_id or x.enum_for(:deep_each).find { |xx| xx.object_id == sexp.object_id } }.nil?
15
+ yield sexp
16
+ yielded << sexp
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
- module Ryan
2
- VERSION = '0.1.0'.freeze
1
+ class Ryan
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Ryan Buckley"]
10
10
  spec.email = ["arebuckley@gmail.com"]
11
11
 
12
- spec.summary = %q{Reviews and rewrites rspec files}
13
- spec.description = %q{Reviews and rewrites rspec files using the style I like}
12
+ spec.summary = %q{Ryan meet Ruby}
13
+ spec.description = %q{A wrapper around the RubyParser gem}
14
14
  spec.homepage = "https://github.com/ridiculous/ryan"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
@@ -20,4 +20,8 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.10"
22
22
  spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", ">= 3.2", "< 4"
24
+
25
+ spec.add_dependency 'ruby_parser', '>= 3.7.0', '< 4.0.0'
26
+ spec.add_dependency 'ruby2ruby', '~> 2.2'
23
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ryan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Buckley
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-07-11 00:00:00.000000000 Z
11
+ date: 2015-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,7 +38,61 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
- description: Reviews and rewrites rspec files using the style I like
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.2'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '4'
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '3.2'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '4'
61
+ - !ruby/object:Gem::Dependency
62
+ name: ruby_parser
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.7.0
68
+ - - "<"
69
+ - !ruby/object:Gem::Version
70
+ version: 4.0.0
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 3.7.0
78
+ - - "<"
79
+ - !ruby/object:Gem::Version
80
+ version: 4.0.0
81
+ - !ruby/object:Gem::Dependency
82
+ name: ruby2ruby
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '2.2'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '2.2'
95
+ description: A wrapper around the RubyParser gem
42
96
  email:
43
97
  - arebuckley@gmail.com
44
98
  executables: []
@@ -46,6 +100,8 @@ extensions: []
46
100
  extra_rdoc_files: []
47
101
  files:
48
102
  - ".gitignore"
103
+ - ".ruby-gemset"
104
+ - ".ruby-version"
49
105
  - ".travis.yml"
50
106
  - Gemfile
51
107
  - README.md
@@ -53,6 +109,13 @@ files:
53
109
  - bin/console
54
110
  - bin/setup
55
111
  - lib/ryan.rb
112
+ - lib/ryan/assignment.rb
113
+ - lib/ryan/class_func.rb
114
+ - lib/ryan/condition.rb
115
+ - lib/ryan/const.rb
116
+ - lib/ryan/func.rb
117
+ - lib/ryan/instance_func.rb
118
+ - lib/ryan/sexp_decorator.rb
56
119
  - lib/ryan/version.rb
57
120
  - ryan.gemspec
58
121
  homepage: https://github.com/ridiculous/ryan
@@ -74,8 +137,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
137
  version: '0'
75
138
  requirements: []
76
139
  rubyforge_project:
77
- rubygems_version: 2.4.6
140
+ rubygems_version: 2.4.8
78
141
  signing_key:
79
142
  specification_version: 4
80
- summary: Reviews and rewrites rspec files
143
+ summary: Ryan meet Ruby
81
144
  test_files: []