analyst 0.14.2 → 0.15.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.
@@ -1,12 +1,25 @@
1
1
  module Analyst
2
2
 
3
3
  module Entities
4
- module Symbol
4
+ class Symbol < Entity
5
5
 
6
- def self.new(node, parent)
6
+ handles_node :sym
7
+
8
+ def value
7
9
  ast.children.first
8
10
  end
9
11
 
12
+ def name
13
+ end
14
+
15
+ def full_name
16
+ end
17
+
18
+ private
19
+
20
+ def content_node
21
+ end
22
+
10
23
  end
11
24
  end
12
25
  end
@@ -1,7 +1,9 @@
1
+ require_relative 'entity'
2
+
1
3
  module Analyst
2
4
 
3
5
  module Entities
4
- class Empty < Entity
6
+ class Unhandled < Entity
5
7
  def full_name
6
8
  ""
7
9
  end
@@ -1,5 +1,4 @@
1
1
  require 'fileutils'
2
- require 'pry'
3
2
 
4
3
  module Analyst
5
4
 
@@ -7,45 +6,49 @@ module Analyst
7
6
 
8
7
  extend Forwardable
9
8
 
10
- attr_reader :start_path
11
-
12
- def_delegators :root, :classes, :top_level_classes
13
-
14
- # TODO: Empty -> Unhandled (or something like that)
15
- PROCESSORS = Hash.new(Entities::Empty).merge!(
16
- :root => Entities::Root,
17
- :class => Entities::Class,
18
- :def => Entities::InstanceMethod,
19
- :defs => Entities::SingletonMethod,
20
- :begin => Entities::Begin,
21
- :module => Entities::Module,
22
- :send => Entities::MethodCall,
23
- :sclass => Entities::SingletonClass,
24
- :dstr => Entities::InterpolatedString
25
- # :def => :method_node_parser,
26
- # :send => :send_node_parser
27
- # TODO: make a method parser, which pushes the the context_stack so that things inside method bodies
28
- # are treated differently than those inside class or module bodies. same with Block (right?)
29
- )
30
-
31
- def self.process_node(node, parent)
32
- return if node.nil? # TODO: maybe a Entities:Nil would be appropriate? maybe?
33
- PROCESSORS[node.type].new(node, parent)
9
+ def_delegators :root, :classes, :top_level_classes, :constants,
10
+ :methods
11
+
12
+ def self.for_files(path_to_files)
13
+ file_paths = if File.directory?(path_to_files)
14
+ Dir.glob(File.join(path_to_files, "**", "*.rb"))
15
+ else
16
+ [path_to_files]
17
+ end
18
+
19
+ wrapped_asts = file_paths.map do |path|
20
+ ast = ::Parser::CurrentRuby.parse(File.open(path, 'r').read)
21
+ ::Parser::AST::Node.new(:analyst_file, [ast])
22
+ end
23
+
24
+ root_node = ::Parser::AST::Node.new(:analyst_root, wrapped_asts)
25
+ root = Entities::Root.new(root_node, file_paths)
26
+ new(root)
27
+ end
28
+
29
+ def self.for_source(source)
30
+ ast = ::Parser::CurrentRuby.parse(source)
31
+ wrapped_ast = ::Parser::AST::Node.new(:analyst_source, [ast])
32
+ root_node = ::Parser::AST::Node.new(:analyst_root, [wrapped_ast])
33
+ root = Entities::Root.new(root_node, [source])
34
+ new(root)
34
35
  end
35
36
 
36
- def initialize(ast)
37
- @ast = ast
37
+ def initialize(root)
38
+ @root = root
38
39
  end
39
40
 
40
41
  def inspect
41
42
  "\#<#{self.class}:#{object_id}>"
42
43
  end
43
44
 
45
+ def top_level_entities
46
+ root.contents
47
+ end
48
+
44
49
  private
45
50
 
46
- def root
47
- @root ||= self.class.process_node(@ast, nil)
48
- end
51
+ attr_reader :root
49
52
 
50
53
  end
51
54
 
@@ -0,0 +1,25 @@
1
+ require_relative 'entities/unhandled'
2
+
3
+ module Analyst
4
+
5
+ module Processor
6
+
7
+ PROCESSORS = Hash.new(Analyst::Entities::Unhandled)
8
+
9
+ def self.register_processor(type, processor)
10
+ if PROCESSORS.key? type
11
+ raise "(#{type}) nodes already registered by #{PROCESSORS[type]}"
12
+ end
13
+ PROCESSORS[type] = processor
14
+ end
15
+
16
+ def self.process_node(node, parent)
17
+ return if node.nil?
18
+ return unless node.respond_to?(:type)
19
+ PROCESSORS[node.type].new(node, parent)
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
@@ -1,3 +1,3 @@
1
1
  module Analyst
2
- VERSION = "0.14.2"
2
+ VERSION = "0.15.0"
3
3
  end
data/lib/analyst.rb CHANGED
@@ -1,56 +1,45 @@
1
- require 'ephemeral'
2
- require 'poro_plus'
3
1
  require 'fileutils'
4
2
  require 'haml'
3
+ require 'parser/current'
5
4
 
6
- require_relative "analyst/analyzer"
5
+ require_relative "analyst/parser"
6
+ require_relative "analyst/processor"
7
+ require_relative "analyst/version"
7
8
  require_relative "analyst/entities/entity"
8
- require_relative "analyst/entities/empty"
9
9
  require_relative "analyst/entities/root"
10
- require_relative "analyst/entities/begin"
10
+ require_relative "analyst/entities/file"
11
+ require_relative "analyst/entities/source"
12
+ require_relative "analyst/entities/code_block"
11
13
  require_relative "analyst/entities/module"
12
14
  require_relative "analyst/entities/class"
13
15
  require_relative "analyst/entities/interpolated_string"
16
+ require_relative "analyst/entities/constant"
17
+ require_relative "analyst/entities/conditional"
14
18
  require_relative "analyst/entities/method"
15
19
  require_relative "analyst/entities/method_call"
16
20
  require_relative "analyst/entities/singleton_class"
21
+ require_relative "analyst/entities/array"
17
22
  require_relative "analyst/entities/hash"
18
23
  require_relative "analyst/entities/pair"
19
24
  require_relative "analyst/entities/symbol"
20
25
  require_relative "analyst/entities/string"
21
- require_relative "analyst/parser"
22
- require_relative "analyst/version"
23
26
 
24
27
  module Analyst
25
- def self.new(path_to_files)
26
- Analyst::Parser.new(FileProcessor.new(path_to_files).ast)
27
- end
28
-
29
- class FileProcessor
30
28
 
31
- attr_reader :path_to_files
29
+ module ClassMethods
32
30
 
33
- def initialize(path_to_files)
34
- @path_to_files = path_to_files
31
+ def for_files(path_to_files)
32
+ Analyst::Parser.for_files(path_to_files)
35
33
  end
36
34
 
37
- def source_files
38
- if File.directory?(path_to_files)
39
- return Dir.glob(File.join(path_to_files, "**", "*.rb"))
40
- else
41
- return [path_to_files]
42
- end
43
- end
35
+ alias :for_file :for_files
44
36
 
45
- def ast
46
- ::Parser::AST::Node.new(
47
- :root, source_files.map do |file|
48
- content = File.open(file, "r").read
49
- ::Parser::CurrentRuby.parse(content)
50
- end
51
- )
37
+ def for_source(source)
38
+ Analyst::Parser.for_source(source)
52
39
  end
53
40
 
54
41
  end
55
42
 
43
+ extend ClassMethods
44
+
56
45
  end
data/spec/class_spec.rb CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Analyst::Entities::Class do
4
4
 
5
- let(:parser) { Analyst.new("./spec/fixtures/music.rb") }
5
+ let(:parser) { Analyst.for_file("./spec/fixtures/music.rb") }
6
6
  let(:artist) { parser.classes.detect { |klass| klass.full_name == "Artist" } }
7
7
  let(:singer) { parser.classes.detect { |klass| klass.full_name == "Singer" } }
8
8
 
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Analyst::Entities::Constant do
4
+
5
+ describe "#name" do
6
+ context "with top-level scoping" do
7
+ let(:source) { "::Food::Pizza::Hawaiian" }
8
+
9
+ it "correctly reports the name" do
10
+ analyzer = Analyst.for_source(source)
11
+ expect(analyzer.constants.first.name).to eq source
12
+ end
13
+ end
14
+
15
+ context "without a scope" do
16
+ let(:source) { "Body::Organs::Pancreas" }
17
+
18
+ it "correctly reports the name" do
19
+ analyzer = Analyst.for_source(source)
20
+ expect(analyzer.constants.first.name).to eq source
21
+ end
22
+
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,118 @@
1
+ require 'spec_helper'
2
+
3
+ describe Analyst::Entities::Entity do
4
+
5
+ let(:parser) { Analyst.for_file("./spec/fixtures/music.rb") }
6
+ let(:singer) { parser.classes.detect{ |klass| klass.name == "Singer" }}
7
+
8
+ describe "#constants" do
9
+ it "lists all constants from recursive search" do
10
+ constants = singer.constants.map(&:full_name)
11
+ expected = %w[SUPER_ATTRS HIPSTER_THRESHOLD HIPSTER_THRESHOLD Song Performance::Equipment::Microphone]
12
+ # TODO: once (casgn) is done, expected should contain one more
13
+ # SUPER_ATTRS and one more HIPSTER_THRESHOLD
14
+
15
+ expect(constants).to match_array expected
16
+ end
17
+
18
+ it "finds constants inside of Arrays" do
19
+ code = <<-CODE
20
+ class Mail
21
+ def things
22
+ [Stamp, Envelope, Postcard]
23
+ end
24
+ end
25
+ CODE
26
+
27
+ found = Analyst.for_source(code).constants.map(&:full_name)
28
+ expect(found).to match_array %w[Stamp Envelope Postcard]
29
+ end
30
+
31
+ it "finds constants inside of method calls" do
32
+ code = <<-CODE
33
+ class Mail
34
+ def deliver
35
+ ship_to(PostOffice.nearest_to(recipient))
36
+ end
37
+ end
38
+ CODE
39
+
40
+ found = Analyst.for_source(code).constants.map(&:full_name)
41
+ expect(found).to match_array %w[PostOffice]
42
+ end
43
+
44
+ it "finds constants inside of parenthetical expressions" do
45
+ code = "def fn; A + (B || C); end"
46
+ found = Analyst.for_source(code).constants.map(&:full_name)
47
+ expect(found).to match_array %w[A B C]
48
+ end
49
+ end
50
+
51
+ describe "#conditionals" do
52
+ it "lists all conditionals from recursive search" do
53
+ # TODO: test recursive search frd -- i.e. conditionals inside of conditionals
54
+ conditionals = singer.imethods.map(&:conditionals).flatten
55
+ expect(conditionals.count).to eq(1)
56
+ end
57
+ end
58
+
59
+ describe "(private) #source_range" do
60
+ let(:code) do <<-CODE
61
+ class Foo
62
+ attr_accessor :bar
63
+ end
64
+
65
+ class Baz
66
+ def initialize
67
+ puts "Fresh Baz!"
68
+ end
69
+ end
70
+ CODE
71
+ end
72
+
73
+ let(:parser) { Analyst.for_source(code) }
74
+
75
+ context "returns the source code location" do
76
+ let(:baz) { parser.classes.detect {|klass| klass.name == "Baz"} }
77
+ let(:source_range) { baz.send :source_range }
78
+
79
+ it "includes the start position" do
80
+ expect(source_range.begin).to eq code.index("class Baz")
81
+ end
82
+
83
+ it "includes the end position" do
84
+ expect(source_range.end).to eq code.size - 1
85
+ end
86
+ end
87
+ end
88
+
89
+ describe "#file_path" do
90
+ let(:parser) { Analyst.for_files("./spec/fixtures") }
91
+ let(:singer) { parser.classes.detect { |klass| klass.name == "Singer" } }
92
+
93
+ it "reports the path of the source file" do
94
+ expect(singer.file_path).to eq "./spec/fixtures/music.rb"
95
+ end
96
+ end
97
+
98
+ describe "#location" do
99
+ let(:parser) { Analyst.for_files("./spec/fixtures") }
100
+ let(:singer) { parser.classes.detect { |klass| klass.name == "Singer" } }
101
+ let(:songs) { singer.imethods.detect { |meth| meth.name == "songs" } }
102
+
103
+ it "reports the location of the source for the entity" do
104
+ expect(songs.location).to eq "./spec/fixtures/music.rb:41"
105
+ end
106
+ end
107
+
108
+ describe "#source" do
109
+ let(:test_method) { singer.imethods.first }
110
+
111
+ it "correctly maps to the source" do
112
+ method_text = "def status\n if self.album_sales > HIPSTER_THRESHOLD\n \"sellout\"\n else\n \"cool\"\n end\n end\n"
113
+
114
+ expect(test_method.source).to eq(method_text)
115
+ end
116
+ end
117
+
118
+ end
@@ -16,22 +16,35 @@ end
16
16
  class Singer < Artist
17
17
 
18
18
  SUPER_ATTRS = { singing: 10, dancing: 10, looks: 10 }
19
+ HIPSTER_THRESHOLD = 10
19
20
 
20
21
  class << self
21
22
  def sellouts
22
- where(:album_sales > 10)
23
+ where(:album_sales > HIPSTER_THRESHOLD)
23
24
  end
24
25
  end
25
26
 
26
27
  def self.superstar
27
28
  first = %w[Michael Beyonce Cee-lo Devin]
28
29
  last = %w[Jackson Knowles Green Townsend]
29
-
30
30
  new("#{first.sample} #{last.sample}", SUPER_ATTRS.dup)
31
31
  end
32
32
 
33
+ def status
34
+ if self.album_sales > HIPSTER_THRESHOLD
35
+ "sellout"
36
+ else
37
+ "cool"
38
+ end
39
+ end
40
+
41
+ def songs
42
+ Song.where(artist: self)
43
+ end
44
+
33
45
  def sing
34
- "♬ Hang the DJ! ♬"
46
+ mic = Performance::Equipment::Microphone
47
+ mic.shout "♬ Hang the DJ! ♬"
35
48
  end
36
49
  end
37
50
 
@@ -65,6 +78,9 @@ module Performances
65
78
  module Equipment
66
79
  class Amp
67
80
  end
81
+
82
+ class Microphone
83
+ end
68
84
  end
69
85
  end
70
86
 
data/spec/parser_spec.rb CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe "Parser" do
4
4
 
5
- let(:parser) { Analyst.new("./spec/fixtures/music.rb") }
5
+ let(:parser) { Analyst.for_file("./spec/fixtures/music.rb") }
6
6
 
7
7
  describe "#top_level_classes" do
8
8
 
@@ -17,7 +17,8 @@ describe "Parser" do
17
17
  it "lists all classes from recursive search" do
18
18
  all_classes = %w[Artist Singer Song
19
19
  Instruments::Stringed Instruments::Guitar
20
- Performances::Equipment::Amp]
20
+ Performances::Equipment::Amp
21
+ Performances::Equipment::Microphone]
21
22
 
22
23
  class_names = parser.classes.map(&:full_name)
23
24
 
data/spec/spec_helper.rb CHANGED
@@ -6,3 +6,6 @@ require 'rubygems'
6
6
  require 'rspec'
7
7
  require 'analyst'
8
8
  require 'pry'
9
+ require 'byebug'
10
+ require 'awesome_print'
11
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: analyst
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.2
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Coraline Ada Ehmke
@@ -9,24 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-10-30 00:00:00.000000000 Z
12
+ date: 2014-11-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: ephemeral
15
+ name: haml
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: 2.4.0
20
+ version: '0'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: 2.4.0
27
+ version: '0'
28
28
  - !ruby/object:Gem::Dependency
29
- name: poro_plus
29
+ name: parser
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - ">="
@@ -40,7 +40,7 @@ dependencies:
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
42
  - !ruby/object:Gem::Dependency
43
- name: haml
43
+ name: rainbow
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - ">="
@@ -54,7 +54,7 @@ dependencies:
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
56
  - !ruby/object:Gem::Dependency
57
- name: parser
57
+ name: rouge
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - ">="
@@ -68,7 +68,7 @@ dependencies:
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
70
  - !ruby/object:Gem::Dependency
71
- name: rainbow
71
+ name: terminal-table
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
74
  - - ">="
@@ -82,7 +82,7 @@ dependencies:
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
84
  - !ruby/object:Gem::Dependency
85
- name: rouge
85
+ name: thor
86
86
  requirement: !ruby/object:Gem::Requirement
87
87
  requirements:
88
88
  - - ">="
@@ -96,27 +96,27 @@ dependencies:
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
98
  - !ruby/object:Gem::Dependency
99
- name: terminal-table
99
+ name: bundler
100
100
  requirement: !ruby/object:Gem::Requirement
101
101
  requirements:
102
- - - ">="
102
+ - - "~>"
103
103
  - !ruby/object:Gem::Version
104
- version: '0'
105
- type: :runtime
104
+ version: '1.6'
105
+ type: :development
106
106
  prerelease: false
107
107
  version_requirements: !ruby/object:Gem::Requirement
108
108
  requirements:
109
- - - ">="
109
+ - - "~>"
110
110
  - !ruby/object:Gem::Version
111
- version: '0'
111
+ version: '1.6'
112
112
  - !ruby/object:Gem::Dependency
113
- name: thor
113
+ name: rake
114
114
  requirement: !ruby/object:Gem::Requirement
115
115
  requirements:
116
116
  - - ">="
117
117
  - !ruby/object:Gem::Version
118
118
  version: '0'
119
- type: :runtime
119
+ type: :development
120
120
  prerelease: false
121
121
  version_requirements: !ruby/object:Gem::Requirement
122
122
  requirements:
@@ -124,21 +124,21 @@ dependencies:
124
124
  - !ruby/object:Gem::Version
125
125
  version: '0'
126
126
  - !ruby/object:Gem::Dependency
127
- name: bundler
127
+ name: rspec
128
128
  requirement: !ruby/object:Gem::Requirement
129
129
  requirements:
130
- - - "~>"
130
+ - - ">="
131
131
  - !ruby/object:Gem::Version
132
- version: '1.6'
132
+ version: '0'
133
133
  type: :development
134
134
  prerelease: false
135
135
  version_requirements: !ruby/object:Gem::Requirement
136
136
  requirements:
137
- - - "~>"
137
+ - - ">="
138
138
  - !ruby/object:Gem::Version
139
- version: '1.6'
139
+ version: '0'
140
140
  - !ruby/object:Gem::Dependency
141
- name: rake
141
+ name: simplecov
142
142
  requirement: !ruby/object:Gem::Requirement
143
143
  requirements:
144
144
  - - ">="
@@ -152,7 +152,7 @@ dependencies:
152
152
  - !ruby/object:Gem::Version
153
153
  version: '0'
154
154
  - !ruby/object:Gem::Dependency
155
- name: rspec
155
+ name: pry
156
156
  requirement: !ruby/object:Gem::Requirement
157
157
  requirements:
158
158
  - - ">="
@@ -166,7 +166,7 @@ dependencies:
166
166
  - !ruby/object:Gem::Version
167
167
  version: '0'
168
168
  - !ruby/object:Gem::Dependency
169
- name: simplecov
169
+ name: byebug
170
170
  requirement: !ruby/object:Gem::Requirement
171
171
  requirements:
172
172
  - - ">="
@@ -180,7 +180,7 @@ dependencies:
180
180
  - !ruby/object:Gem::Version
181
181
  version: '0'
182
182
  - !ruby/object:Gem::Dependency
183
- name: pry
183
+ name: awesome_print
184
184
  requirement: !ruby/object:Gem::Requirement
185
185
  requirements:
186
186
  - - ">="
@@ -210,12 +210,14 @@ files:
210
210
  - Rakefile
211
211
  - analyst.gemspec
212
212
  - lib/analyst.rb
213
- - lib/analyst/analyzer.rb
214
213
  - lib/analyst/association.rb
215
- - lib/analyst/entities/begin.rb
214
+ - lib/analyst/entities/array.rb
216
215
  - lib/analyst/entities/class.rb
217
- - lib/analyst/entities/empty.rb
216
+ - lib/analyst/entities/code_block.rb
217
+ - lib/analyst/entities/conditional.rb
218
+ - lib/analyst/entities/constant.rb
218
219
  - lib/analyst/entities/entity.rb
220
+ - lib/analyst/entities/file.rb
219
221
  - lib/analyst/entities/hash.rb
220
222
  - lib/analyst/entities/interpolated_string.rb
221
223
  - lib/analyst/entities/method.rb
@@ -224,11 +226,16 @@ files:
224
226
  - lib/analyst/entities/pair.rb
225
227
  - lib/analyst/entities/root.rb
226
228
  - lib/analyst/entities/singleton_class.rb
229
+ - lib/analyst/entities/source.rb
227
230
  - lib/analyst/entities/string.rb
228
231
  - lib/analyst/entities/symbol.rb
232
+ - lib/analyst/entities/unhandled.rb
229
233
  - lib/analyst/parser.rb
234
+ - lib/analyst/processor.rb
230
235
  - lib/analyst/version.rb
231
236
  - spec/class_spec.rb
237
+ - spec/constant_spec.rb
238
+ - spec/entity_spec.rb
232
239
  - spec/fixtures/music.rb
233
240
  - spec/method_spec.rb
234
241
  - spec/parser_spec.rb
@@ -259,6 +266,8 @@ specification_version: 4
259
266
  summary: A nice API for interacting with parsed Ruby source code.
260
267
  test_files:
261
268
  - spec/class_spec.rb
269
+ - spec/constant_spec.rb
270
+ - spec/entity_spec.rb
262
271
  - spec/fixtures/music.rb
263
272
  - spec/method_spec.rb
264
273
  - spec/parser_spec.rb