analyst 1.0.1 → 1.2.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: 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