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 +4 -4
- data/lib/analyst.rb +0 -2
- data/lib/analyst/entities/array.rb +1 -1
- data/lib/analyst/entities/class.rb +1 -0
- data/lib/analyst/entities/code_block.rb +1 -1
- data/lib/analyst/entities/conditional.rb +1 -1
- data/lib/analyst/entities/constant.rb +10 -1
- data/lib/analyst/entities/entity.rb +7 -11
- data/lib/analyst/entities/method_call.rb +1 -4
- data/lib/analyst/entities/root.rb +5 -22
- data/lib/analyst/parser.rb +30 -16
- data/lib/analyst/processor.rb +1 -1
- data/lib/analyst/version.rb +1 -1
- data/spec/entities/class_spec.rb +1 -1
- data/spec/entities/entity_spec.rb +18 -34
- data/spec/fixtures/{music.rb → music/music.rb} +0 -0
- data/spec/fixtures/syntax_errors/bad.rb +6 -0
- data/spec/fixtures/syntax_errors/good.rb +7 -0
- data/spec/parser_spec.rb +58 -1
- metadata +8 -7
- data/lib/analyst/entities/file.rb +0 -46
- data/lib/analyst/entities/source.rb +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5de0cf0eb364543fc02960acae4d770d645b8f1c
|
4
|
+
data.tar.gz: 05116346c0e6798512e8ade4548887a1c763ddda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 32aef9e181c88cea65fc48ed60684e171f8d89bd5e525fa6164d03b4bc6c54fc83409bafd0ebafde1b815379c0697da3e1c9b5511b57917d5c906e9f671e5104
|
7
|
+
data.tar.gz: 153b549fe20f35e0b659a79feed8cb64550e5570e5ca3e2adb210d12d96e986dafdfce24c60c4ee48cb3f67c78976beed8f0f390f00e104b983153067006fdc2
|
data/lib/analyst.rb
CHANGED
@@ -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"
|
@@ -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
|
-
|
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
|
-
|
90
|
+
ast.location.expression.source_buffer.name
|
87
91
|
end
|
88
92
|
|
89
93
|
def line_number
|
90
|
-
ast.
|
94
|
+
ast.location.line
|
91
95
|
end
|
92
96
|
|
93
97
|
def source
|
94
|
-
|
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
|
@@ -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
|
-
|
36
|
-
|
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
|
27
|
+
@actual_contents ||= process_nodes(ast.children)
|
45
28
|
end
|
46
29
|
|
47
30
|
end
|
data/lib/analyst/parser.rb
CHANGED
@@ -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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
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 =
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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(
|
40
|
-
|
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
|
data/lib/analyst/processor.rb
CHANGED
data/lib/analyst/version.rb
CHANGED
data/spec/entities/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.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 "
|
66
|
-
let(:
|
67
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
100
|
-
|
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
|
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
|
File without changes
|
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.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
|
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:
|
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
|