ryan 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []