analyst 1.0.1 → 1.2.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: eab9524eb5763fcc6b6c96491d3eda6670cda02f
4
- data.tar.gz: ad946eb37111ca6329f1b089a59d99c1234a4740
3
+ metadata.gz: 5de0cf0eb364543fc02960acae4d770d645b8f1c
4
+ data.tar.gz: 05116346c0e6798512e8ade4548887a1c763ddda
5
5
  SHA512:
6
- metadata.gz: 84bd02e46b5745ae1f4b1847b61b3f003b014cb5370c71fdcd9bd9fdf56e6559441e70a11567d3932789061d2aed3dc9e04fadf61e395cd41fd14c2bc459982e
7
- data.tar.gz: 3c539573aa32661008537ed3353a704a9f12b2f31d7166a770b6badabda94efa115ccd88d8d716d86141f83f22563202a557747b354529349ba05739652ce572
6
+ metadata.gz: 32aef9e181c88cea65fc48ed60684e171f8d89bd5e525fa6164d03b4bc6c54fc83409bafd0ebafde1b815379c0697da3e1c9b5511b57917d5c906e9f671e5104
7
+ data.tar.gz: 153b549fe20f35e0b659a79feed8cb64550e5570e5ca3e2adb210d12d96e986dafdfce24c60c4ee48cb3f67c78976beed8f0f390f00e104b983153067006fdc2
@@ -8,8 +8,6 @@ require_relative "analyst/version"
8
8
  require_relative "analyst/entities/mixins/has_methods"
9
9
  require_relative "analyst/entities/entity"
10
10
  require_relative "analyst/entities/root"
11
- require_relative "analyst/entities/file"
12
- require_relative "analyst/entities/source"
13
11
  require_relative "analyst/entities/code_block"
14
12
  require_relative "analyst/entities/module"
15
13
  require_relative "analyst/entities/class"
@@ -8,7 +8,7 @@ module Analyst
8
8
  private
9
9
 
10
10
  def contents
11
- @contents ||= ast.children.map { |child| process_node(child) }
11
+ @contents ||= process_nodes(ast.children)
12
12
  end
13
13
  end
14
14
  end
@@ -36,3 +36,4 @@ module Analyst
36
36
  end
37
37
  end
38
38
  end
39
+
@@ -9,7 +9,7 @@ module Analyst
9
9
  def_delegators :parent, :name, :full_name
10
10
 
11
11
  def contents
12
- @contents ||= ast.children.map { |child| process_node(child) }
12
+ @contents ||= process_nodes(ast.children)
13
13
  end
14
14
 
15
15
  end
@@ -12,7 +12,7 @@ module Analyst
12
12
  private
13
13
 
14
14
  def contents
15
- @contents ||= ast.children.map { |child| process_node(child) }.compact
15
+ @contents ||= process_nodes(ast.children).compact
16
16
  end
17
17
  end
18
18
  end
@@ -28,10 +28,19 @@ module Analyst
28
28
  # and passing that node here would return [:CoolModule, :SubMod, :SweetClass]
29
29
  # TODO: should really be nested Entities::Constants all the way down.
30
30
  # ((cbase) can probably use the same Entity, or maybe it's a subclass of Constant)
31
+ #
32
+ # Note: if any node besides (const) or (cbase) is encountered, that part gets named
33
+ # '<`source`>' where source is the source code for that node.
34
+ # e.g. `@thing.class::Sub::Mod` parses to:
35
+ # (const
36
+ # (const
37
+ # (send
38
+ # (ivar :@thing) :class) :Sub) :Mod)
39
+ # and the corresponding Entities::Constant gets named "<`@thing.class`>::Sub::Mod"
31
40
  def const_node_array(node)
32
41
  return [] if node.nil?
33
42
  return [''] if node.type == :cbase
34
- raise "expected (const) or (cbase) node or nil, got (#{node.type})" unless node.type == :const
43
+ return ["<`#{node.location.expression.source}`>"] unless node.type == :const
35
44
  const_node_array(node.children.first) << node.children[1]
36
45
  end
37
46
 
@@ -11,6 +11,10 @@ module Analyst
11
11
  Analyst::Processor.register_processor(type, self)
12
12
  end
13
13
 
14
+ def self.process(ast, parent)
15
+ new(ast, parent)
16
+ end
17
+
14
18
  def initialize(ast, parent)
15
19
  @parent = parent
16
20
  @ast = ast
@@ -83,19 +87,15 @@ module Analyst
83
87
  end
84
88
 
85
89
  def file_path
86
- parent.file_path
90
+ ast.location.expression.source_buffer.name
87
91
  end
88
92
 
89
93
  def line_number
90
- ast.loc.line
94
+ ast.location.line
91
95
  end
92
96
 
93
97
  def source
94
- origin_source[source_range]
95
- end
96
-
97
- def origin_source
98
- parent.origin_source
98
+ ast.location.expression.source
99
99
  end
100
100
 
101
101
  def full_name
@@ -110,10 +110,6 @@ module Analyst
110
110
 
111
111
  private
112
112
 
113
- def source_range
114
- Range.new(ast.loc.expression.begin_pos, ast.loc.expression.end_pos)
115
- end
116
-
117
113
  def contents_of_type(klass)
118
114
  contents.select { |entity| entity.is_a? klass }
119
115
  end
@@ -13,10 +13,7 @@ module Analyst
13
13
  end
14
14
 
15
15
  def arguments
16
- @arguments ||= begin
17
- args = ast.children[2..-1]
18
- args.map { |arg| process_node(arg) }
19
- end
16
+ @arguments ||= process_nodes(ast.children[2..-1])
20
17
  end
21
18
 
22
19
  private
@@ -6,42 +6,25 @@ module Analyst
6
6
 
7
7
  handles_node :analyst_root
8
8
 
9
- def initialize(ast, source_data)
10
- @source_data = source_data
11
- super(ast, nil)
12
- end
13
-
14
9
  def full_name
15
10
  ""
16
11
  end
17
12
 
18
- def source_data_for(entity)
19
- source_data[actual_contents.index(entity)]
20
- end
21
-
22
- def file_path
23
- throw "Entity tree malformed - Source or File should have caught this call"
24
- end
25
-
26
- def origin_source
27
- throw "Entity tree malformed - Source or File hsould have caught this call"
28
- end
29
-
30
13
  def inspect
31
14
  "\#<#{self.class}>"
32
15
  end
33
16
 
34
17
  def contents
35
- # skip all top-level entities, cuz they're all Files and Sources
36
- @contents ||= actual_contents.map(&:contents).flatten
18
+ @contents ||= actual_contents.map do |child|
19
+ # skip top-level CodeBlocks
20
+ child.is_a?(Entities::CodeBlock) ? child.contents : child
21
+ end.flatten
37
22
  end
38
23
 
39
24
  private
40
25
 
41
- attr_reader :source_data
42
-
43
26
  def actual_contents
44
- @actual_contents ||= ast.children.map { |child| process_node(child) }
27
+ @actual_contents ||= process_nodes(ast.children)
45
28
  end
46
29
 
47
30
  end
@@ -1,5 +1,3 @@
1
- require 'fileutils'
2
-
3
1
  module Analyst
4
2
 
5
3
  class Parser
@@ -18,26 +16,42 @@ module Analyst
18
16
  end
19
17
  end.flatten
20
18
 
21
- wrapped_asts = file_paths.map do |path|
22
- ast = ::Parser::CurrentRuby.parse(File.open(path, 'r').read)
23
- ::Parser::AST::Node.new(:analyst_file, [ast])
24
- end
19
+ asts = file_paths.map do |path|
20
+ File.open(path) do |file|
21
+ parse_source(file.read, path)
22
+ end
23
+ end.compact
25
24
 
26
- root_node = ::Parser::AST::Node.new(:analyst_root, wrapped_asts)
27
- root = Entities::Root.new(root_node, file_paths)
28
- new(root)
25
+ new(asts)
29
26
  end
30
27
 
31
28
  def self.for_source(source)
32
- ast = ::Parser::CurrentRuby.parse(source)
33
- wrapped_ast = ::Parser::AST::Node.new(:analyst_source, [ast])
34
- root_node = ::Parser::AST::Node.new(:analyst_root, [wrapped_ast])
35
- root = Entities::Root.new(root_node, [source])
36
- new(root)
29
+ ast = parse_source(source)
30
+ new([ast].compact)
31
+ end
32
+
33
+ def self.parse_source(source, filename='(string)')
34
+ parser = ::Parser::CurrentRuby.new
35
+ parser.diagnostics.all_errors_are_fatal = true
36
+ parser.diagnostics.ignore_warnings = true
37
+
38
+ buffer = ::Parser::Source::Buffer.new(filename)
39
+ buffer.source = source
40
+ parser.parse(buffer)
41
+ rescue ::Parser::SyntaxError => e
42
+ $stderr.puts "Error during parsing; #{filename == '(string)' ? 'string' : 'file'} will be skipped:"
43
+ $stderr.puts format_diagnostic_msg(e.diagnostic)
44
+ end
45
+ private_class_method :parse_source
46
+
47
+ def self.format_diagnostic_msg(diagnostic)
48
+ diagnostic.render.map { |line| " #{line}" }.join("\n")
37
49
  end
50
+ private_class_method :format_diagnostic_msg
38
51
 
39
- def initialize(root)
40
- @root = root
52
+ def initialize(asts)
53
+ root_node = ::Parser::AST::Node.new(:analyst_root, asts)
54
+ @root = Processor.process_node(root_node, nil)
41
55
  end
42
56
 
43
57
  def inspect
@@ -16,7 +16,7 @@ module Analyst
16
16
  def self.process_node(node, parent)
17
17
  return if node.nil?
18
18
  return unless node.respond_to?(:type)
19
- PROCESSORS[node.type].new(node, parent)
19
+ PROCESSORS[node.type].process(node, parent)
20
20
  end
21
21
 
22
22
  end
@@ -1,3 +1,3 @@
1
1
  module Analyst
2
- VERSION = "1.0.1"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Analyst::Entities::Class do
4
4
 
5
- let(:parser) { Analyst.for_file("./spec/fixtures/music.rb") }
5
+ let(:parser) { Analyst.for_file("./spec/fixtures/music/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
  let(:amp) { parser.classes.detect { |klass| klass.full_name == "Performances::Equipment::Amp" }}
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Analyst::Entities::Entity do
4
4
 
5
- let(:parser) { Analyst.for_file("./spec/fixtures/music.rb") }
5
+ let(:parser) { Analyst.for_file("./spec/fixtures/music/music.rb") }
6
6
  let(:singer) { parser.classes.detect{ |klass| klass.name == "Singer" }}
7
7
 
8
8
  describe "#constants" do
@@ -62,52 +62,36 @@ describe Analyst::Entities::Entity do
62
62
  end
63
63
  end
64
64
 
65
- describe "(private) #source_range" do
66
- let(:code) do <<-CODE
67
- class Foo
68
- attr_accessor :bar
69
- end
65
+ describe "#file_path" do
66
+ let(:parser) { Analyst.for_files("./spec/fixtures/music") }
67
+ let(:singer) { parser.classes.detect { |klass| klass.name == "Singer" } }
70
68
 
71
- class Baz
72
- def initialize
73
- puts "Fresh Baz!"
74
- end
75
- end
76
- CODE
69
+ it "reports the path of the source file" do
70
+ expect(singer.file_path).to eq "./spec/fixtures/music/music.rb"
77
71
  end
78
72
 
79
- let(:parser) { Analyst.for_source(code) }
80
-
81
- context "returns the source code location" do
82
- let(:baz) { parser.classes.detect {|klass| klass.name == "Baz"} }
83
- let(:source_range) { baz.send :source_range }
84
-
85
- it "includes the start position" do
86
- expect(source_range.begin).to eq code.index("class Baz")
87
- end
88
-
89
- it "includes the end position" do
90
- expect(source_range.end).to eq code.size - 1
91
- end
73
+ it "works for non-top-level Entities too" do
74
+ a_method = singer.methods.first
75
+ expect(a_method.file_path).to eq "./spec/fixtures/music/music.rb"
92
76
  end
93
- end
94
77
 
95
- describe "#file_path" do
96
- let(:parser) { Analyst.for_files("./spec/fixtures") }
97
- let(:singer) { parser.classes.detect { |klass| klass.name == "Singer" } }
78
+ context "when the source is a string" do
79
+ let(:parser) { Analyst.for_source("class Foo; end") }
80
+ let(:foo_class) { parser.classes.first }
98
81
 
99
- it "reports the path of the source file" do
100
- expect(singer.file_path).to eq "./spec/fixtures/music.rb"
82
+ it "returns '(string)'" do
83
+ expect(foo_class.file_path).to eq '(string)'
84
+ end
101
85
  end
102
86
  end
103
87
 
104
88
  describe "#location" do
105
- let(:parser) { Analyst.for_files("./spec/fixtures") }
89
+ let(:parser) { Analyst.for_files("./spec/fixtures/music") }
106
90
  let(:singer) { parser.classes.detect { |klass| klass.name == "Singer" } }
107
91
  let(:songs) { singer.imethods.detect { |meth| meth.name == "songs" } }
108
92
 
109
93
  it "reports the location of the source for the entity" do
110
- expect(songs.location).to eq "./spec/fixtures/music.rb:41"
94
+ expect(songs.location).to eq "./spec/fixtures/music/music.rb:41"
111
95
  end
112
96
  end
113
97
 
@@ -115,7 +99,7 @@ end
115
99
  let(:test_method) { singer.imethods.first }
116
100
 
117
101
  it "correctly maps to the source" do
118
- method_text = "def status\n if self.album_sales > HIPSTER_THRESHOLD\n \"sellout\"\n else\n \"cool\"\n end\n end\n"
102
+ method_text = "def status\n if self.album_sales > HIPSTER_THRESHOLD\n \"sellout\"\n else\n \"cool\"\n end\n end"
119
103
 
120
104
  expect(test_method.source).to eq(method_text)
121
105
  end
@@ -0,0 +1,6 @@
1
+ class Bad
2
+ def illegal+character
3
+ "you can't have a + in there, dummy!"
4
+ end
5
+ end
6
+
@@ -0,0 +1,7 @@
1
+ class Good
2
+ def beautiful_syntax
3
+ "there's nothing wrong with this file"
4
+ end
5
+ end
6
+
7
+
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe "Parser" do
4
4
 
5
- let(:parser) { Analyst.for_file("./spec/fixtures/music.rb") }
5
+ let(:parser) { Analyst.for_file("./spec/fixtures/music/music.rb") }
6
6
 
7
7
  describe "#top_level_classes" do
8
8
 
@@ -28,5 +28,62 @@ describe "Parser" do
28
28
  end
29
29
  end
30
30
 
31
+ describe "#constants" do
32
+ let(:code) { "Nice::Static::Constant; stupid.dynamic::Constant; @another.stupid::Constant" }
33
+ let(:parser) { Analyst.for_source(code) }
34
+
35
+ it "recognizes static constants" do
36
+ expect(parser.constants.map(&:name)).to include("Nice::Static::Constant")
37
+ end
38
+
39
+ it "recognizes dynamically-named constants" do
40
+ dynamic_constants = %w[<`stupid.dynamic`>::Constant <`@another.stupid`>::Constant]
41
+ expect(parser.constants.map(&:name)).to include(*dynamic_constants)
42
+ end
43
+ end
44
+
45
+ describe "::for_source" do
46
+ context "with syntax errors" do
47
+ let(:code) {<<-CODE
48
+ class Mail
49
+ def deliver
50
+ ship_to(PostOffice.nearest_to(recipient))
51
+ end
52
+ end
53
+
54
+ count_those_parentheses())
55
+ CODE
56
+ }
57
+
58
+ let(:parser) { Analyst.for_source(code) }
59
+
60
+ it "reports the error" do
61
+ error_line = Regexp.new(Regexp.quote("count_those_parentheses())"))
62
+ expect { parser }.to output(error_line).to_stderr
63
+ end
64
+
65
+ it "aborts parsing" do
66
+ allow($stderr).to receive(:puts) # suppress error reporting in spec output
67
+ expect(parser.classes).to be_empty
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "::for_files" do
73
+ context "with syntax errors" do
74
+ let(:parser) { Analyst.for_files("./spec/fixtures/syntax_errors/") }
75
+
76
+ it "reports the error" do
77
+ error_line = Regexp.new(Regexp.quote("def illegal+character"))
78
+ expect { parser }.to output(error_line).to_stderr
79
+ end
80
+
81
+ it "omits the bad files, but parses the good ones" do
82
+ allow($stderr).to receive(:puts) # suppress error reporting in spec output
83
+ expect(parser.classes.map(&:name)).to eq ['Good']
84
+ end
85
+ end
86
+ end
87
+
31
88
  end
32
89
 
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: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Coraline Ada Ehmke
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-12-01 00:00:00.000000000 Z
12
+ date: 2015-01-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: haml
@@ -190,7 +190,6 @@ files:
190
190
  - lib/analyst/entities/constant.rb
191
191
  - lib/analyst/entities/constant_assignment.rb
192
192
  - lib/analyst/entities/entity.rb
193
- - lib/analyst/entities/file.rb
194
193
  - lib/analyst/entities/hash.rb
195
194
  - lib/analyst/entities/interpolated_string.rb
196
195
  - lib/analyst/entities/method.rb
@@ -200,7 +199,6 @@ files:
200
199
  - lib/analyst/entities/pair.rb
201
200
  - lib/analyst/entities/root.rb
202
201
  - lib/analyst/entities/singleton_class.rb
203
- - lib/analyst/entities/source.rb
204
202
  - lib/analyst/entities/string.rb
205
203
  - lib/analyst/entities/symbol.rb
206
204
  - lib/analyst/entities/unhandled.rb
@@ -217,7 +215,9 @@ files:
217
215
  - spec/entities/method_call_spec.rb
218
216
  - spec/entities/singleton_class_spec.rb
219
217
  - spec/entities/string_spec.rb
220
- - spec/fixtures/music.rb
218
+ - spec/fixtures/music/music.rb
219
+ - spec/fixtures/syntax_errors/bad.rb
220
+ - spec/fixtures/syntax_errors/good.rb
221
221
  - spec/parser_spec.rb
222
222
  - spec/spec_helper.rb
223
223
  homepage: ''
@@ -255,7 +255,8 @@ test_files:
255
255
  - spec/entities/method_call_spec.rb
256
256
  - spec/entities/singleton_class_spec.rb
257
257
  - spec/entities/string_spec.rb
258
- - spec/fixtures/music.rb
258
+ - spec/fixtures/music/music.rb
259
+ - spec/fixtures/syntax_errors/bad.rb
260
+ - spec/fixtures/syntax_errors/good.rb
259
261
  - spec/parser_spec.rb
260
262
  - spec/spec_helper.rb
261
- has_rdoc:
@@ -1,46 +0,0 @@
1
- module Analyst
2
-
3
- module Entities
4
-
5
- class File < Entity
6
-
7
- handles_node :analyst_file
8
-
9
- def full_name
10
- ""
11
- end
12
-
13
- def file_path
14
- parent.source_data_for(self)
15
- end
16
-
17
- def location
18
- file_path
19
- end
20
-
21
- def origin_source
22
- ::File.open(file_path, 'r').read
23
- end
24
-
25
- def contents
26
- @contents ||= actual_contents.map do |child|
27
- # skip top-level CodeBlocks
28
- child.is_a?(Entities::CodeBlock) ? child.contents : child
29
- end.flatten
30
- end
31
-
32
- private
33
-
34
- def source_range
35
- 0..-1
36
- end
37
-
38
- def actual_contents
39
- @actual_contents ||= ast.children.map { |child| process_node(child) }
40
- end
41
-
42
- end
43
-
44
- end
45
-
46
- end
@@ -1,46 +0,0 @@
1
- module Analyst
2
-
3
- module Entities
4
-
5
- class Source < Entity
6
-
7
- handles_node :analyst_source
8
-
9
- def full_name
10
- ""
11
- end
12
-
13
- def file_path
14
- "$SOURCE$"
15
- end
16
-
17
- def location
18
- file_path
19
- end
20
-
21
- def origin_source
22
- parent.source_data_for(self)
23
- end
24
-
25
- def contents
26
- @contents ||= actual_contents.map do |child|
27
- # skip top-level CodeBlocks
28
- child.is_a?(Entities::CodeBlock) ? child.contents : child
29
- end.flatten
30
- end
31
-
32
- private
33
-
34
- def source_range
35
- 0..-1
36
- end
37
-
38
- def actual_contents
39
- @actual_contents ||= ast.children.map { |child| process_node(child) }
40
- end
41
-
42
- end
43
-
44
- end
45
-
46
- end