class_source 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/class_source/collator.rb +80 -0
- data/lib/class_source/declarations.rb +21 -0
- data/lib/class_source/guesser.rb +25 -0
- data/lib/class_source/index.rb +41 -0
- data/lib/class_source/locator.rb +74 -0
- data/lib/class_source/method_index.rb +72 -0
- data/lib/class_source/version.rb +3 -0
- data/lib/class_source.rb +13 -0
- data/spec/class_source_spec.rb +223 -0
- data/spec/fixtures/class_methods_class.rb +5 -0
- data/spec/fixtures/cloned_class.rb +5 -0
- data/spec/fixtures/duplicated_class.rb +5 -0
- data/spec/fixtures/dynamic_class.rb +14 -0
- data/spec/fixtures/eval_class.rb +11 -0
- data/spec/fixtures/example_code.rb +10 -0
- data/spec/fixtures/nested_example.rb +5 -0
- data/spec/fixtures/no_methods_class.rb +5 -0
- data/spec/fixtures/outer_class.rb +22 -0
- data/spec/fixtures/protected_method_class.rb +19 -0
- data/spec/fixtures/re_opened_class.rb +16 -0
- data/spec/fixtures/re_opened_class_2.rb +11 -0
- data/spec/fixtures/side_effect_class.rb +11 -0
- data/spec/fixtures/simple_class.rb +6 -0
- data/spec/fixtures/sub_class.rb +10 -0
- data/spec/fixtures/unnested_child.rb +3 -0
- data/spec/spec_helper.rb +47 -0
- metadata +113 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'ruby_parser'
|
2
|
+
|
3
|
+
module ClassSource
|
4
|
+
class Collator
|
5
|
+
def initialize(target_class, index)
|
6
|
+
@klass = target_class
|
7
|
+
@source = index
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_hash(options = {})
|
11
|
+
full_sources = @source.locations(options).inject({}) do |results, location|
|
12
|
+
results[ location ] = source_helper(location)
|
13
|
+
results
|
14
|
+
end
|
15
|
+
|
16
|
+
return full_sources unless options[:include_nested] == false
|
17
|
+
full_sources.inject({}) do |clean_sources, (location, source)|
|
18
|
+
if nested_class_line_ranges[location.first]
|
19
|
+
complete_file = full_file(location)
|
20
|
+
target_range = (location.last - 1)..(location.last + source.lines.count - 2)
|
21
|
+
clean_sources[location] = complete_file.lines.to_a.select.with_index do |line, index|
|
22
|
+
target_range.include?(index) &&
|
23
|
+
nested_class_line_ranges[location.first].all? { |range| !range.include?(index) }
|
24
|
+
end.join("")
|
25
|
+
else
|
26
|
+
clean_sources[location] = source
|
27
|
+
end
|
28
|
+
clean_sources
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def nested_class_line_ranges
|
33
|
+
nested_classes = @klass.constants.select { |c| @klass.const_get(c).is_a?(Class) }.map {|c| @klass.const_get(c) }
|
34
|
+
return @nested_class_ranges if @nested_class_ranges
|
35
|
+
@nested_class_ranges = {}
|
36
|
+
nested_classes.each do |klass|
|
37
|
+
# (klass.source_location.last-1)..(klass.source_location.last + klass.source.lines.count - 2)
|
38
|
+
klass.__source__.all.each do |(file, line), source|
|
39
|
+
@nested_class_ranges[file] ||= []
|
40
|
+
@nested_class_ranges[file] << ((line - 1)..(line + source.lines.count - 2))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@nested_class_ranges
|
45
|
+
end
|
46
|
+
|
47
|
+
def full_file(location)
|
48
|
+
File.read(location.first)
|
49
|
+
end
|
50
|
+
|
51
|
+
# source_helper and valid_expression? are lifted from method_source
|
52
|
+
# (c) 2011 John Mair (banisterfiend)
|
53
|
+
def source_helper(source_location)
|
54
|
+
return nil if !source_location.is_a?(Array)
|
55
|
+
|
56
|
+
file_name, line = source_location
|
57
|
+
File.open(file_name) do |file|
|
58
|
+
(line - 1).times { file.readline }
|
59
|
+
|
60
|
+
code = ""
|
61
|
+
loop do
|
62
|
+
val = file.readline
|
63
|
+
code << val
|
64
|
+
|
65
|
+
return code if valid_expression?(code)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def valid_expression?(code)
|
72
|
+
RubyParser.new.parse(code)
|
73
|
+
rescue Racc::ParseError, SyntaxError
|
74
|
+
false
|
75
|
+
else
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ClassSource
|
2
|
+
class Declarations
|
3
|
+
def self.[](key)
|
4
|
+
@declarations ||= {}
|
5
|
+
@declarations[key]
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.add(klass_name, locations)
|
9
|
+
@declarations ||= {}
|
10
|
+
@declarations[klass_name] ||= []
|
11
|
+
@declarations[klass_name] += locations
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.save(declarations)
|
15
|
+
declarations.each do |klass_name, locations|
|
16
|
+
add(klass_name, locations)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ClassSource
|
2
|
+
class Guesser
|
3
|
+
def initialize(klass, source_files)
|
4
|
+
@source_files = source_files
|
5
|
+
@klass = klass
|
6
|
+
end
|
7
|
+
|
8
|
+
def locations
|
9
|
+
return if @source_files.empty?
|
10
|
+
@source_files.map do |file|
|
11
|
+
[file, find_module_name(File.read(file))]
|
12
|
+
end.select { |result| !result.last.nil? }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def find_module_name(text)
|
18
|
+
submodule_name = @klass.name.split("::").last
|
19
|
+
text.lines.each.with_index do |line, index|
|
20
|
+
return index + 1 if line.match /\b#{submodule_name}\b/
|
21
|
+
end
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ClassSource
|
2
|
+
class Index
|
3
|
+
def initialize(target_class, options = {})
|
4
|
+
@target_class = target_class
|
5
|
+
@options = options
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s(options={})
|
9
|
+
all(options).values.join("")
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(value)
|
13
|
+
to_s == value
|
14
|
+
end
|
15
|
+
|
16
|
+
def all(options={})
|
17
|
+
@collator ||= Collator.new(@target_class, self).to_hash(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def locations(options={})
|
21
|
+
locator.to_a
|
22
|
+
end
|
23
|
+
|
24
|
+
def methods
|
25
|
+
@method_details ||= MethodIndex.new(@target_class)
|
26
|
+
end
|
27
|
+
|
28
|
+
def class_methods
|
29
|
+
methods.klass
|
30
|
+
end
|
31
|
+
|
32
|
+
def locator
|
33
|
+
@locator ||= Locator.new(@target_class, @options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def files
|
37
|
+
locator.files
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module ClassSource
|
5
|
+
class Locator
|
6
|
+
def initialize(target_class, options={})
|
7
|
+
@klass = target_class
|
8
|
+
@options=options
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_a
|
12
|
+
source_locations
|
13
|
+
end
|
14
|
+
|
15
|
+
def methods
|
16
|
+
MethodIndex.new(@klass)
|
17
|
+
end
|
18
|
+
|
19
|
+
def files(options={})
|
20
|
+
@source_files ||= methods.locations.map(&:first).uniq
|
21
|
+
return @source_files + [@options[:file]] if @options[:file]
|
22
|
+
@source_files
|
23
|
+
end
|
24
|
+
|
25
|
+
def source_locations(options={})
|
26
|
+
return @locations if @locations
|
27
|
+
t = Tempfile.new('class_creation_events')
|
28
|
+
fork do
|
29
|
+
declarations = files(options).inject({}) do |declarations, source_file|
|
30
|
+
trace_declarations(source_file, declarations)
|
31
|
+
end
|
32
|
+
YAML.dump(declarations, t)
|
33
|
+
end
|
34
|
+
Process.wait
|
35
|
+
Declarations.save YAML.load_file(t.path)
|
36
|
+
t.close
|
37
|
+
@locations = if !Declarations[@klass.name].nil?
|
38
|
+
Declarations[@klass.name].uniq
|
39
|
+
else
|
40
|
+
Guesser.new(@klass, files).locations || []
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def trace_declarations(source_file, declarations)
|
45
|
+
set_trace_func lambda { |event, file, line, id, binding, classname|
|
46
|
+
defined_class = standard_class_declared(event, binding) || dynamic_class_declared(id, classname, file, line)
|
47
|
+
break unless defined_class
|
48
|
+
defined_class_name = defined_class.is_a?(String) ? defined_class : defined_class.name
|
49
|
+
declarations[defined_class_name] ||= []
|
50
|
+
declarations[defined_class_name] << [ file, line ]
|
51
|
+
}
|
52
|
+
silence_warnings { load source_file }
|
53
|
+
declarations
|
54
|
+
end
|
55
|
+
|
56
|
+
def dynamic_class_declared(id, classname, file, line)
|
57
|
+
return unless id == :new && classname == Class
|
58
|
+
File.read(file).lines.to_a[line-1][/[A-Z][\w_:]*/, 0]
|
59
|
+
end
|
60
|
+
|
61
|
+
def standard_class_declared(event, binding)
|
62
|
+
return unless event == 'class'
|
63
|
+
event_class = eval( "Module.nesting", binding )
|
64
|
+
event_class.first
|
65
|
+
end
|
66
|
+
|
67
|
+
def silence_warnings
|
68
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
69
|
+
yield
|
70
|
+
ensure
|
71
|
+
$VERBOSE = old_verbose
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ClassSource
|
2
|
+
class MethodIndex
|
3
|
+
def initialize(target_class)
|
4
|
+
@target_class = target_class
|
5
|
+
end
|
6
|
+
|
7
|
+
def locations
|
8
|
+
@locations ||= (unique.map do |m|
|
9
|
+
@target_class.instance_method(m).source_location
|
10
|
+
end + klass.unique.map do |m|
|
11
|
+
@target_class.method(m).source_location
|
12
|
+
end).compact
|
13
|
+
end
|
14
|
+
|
15
|
+
def unique
|
16
|
+
uniquely_named_methods = all(:include_inherited_methods => false)
|
17
|
+
overridden_methods = (all - uniquely_named_methods).select do |m|
|
18
|
+
@target_class.instance_method(m).source_location != @target_class.superclass.instance_method(m).source_location
|
19
|
+
end
|
20
|
+
overridden_methods + uniquely_named_methods
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def all(options={})
|
25
|
+
include_inherited_methods = options.has_key?(:include_inherited_methods) ? options[:include_inherited_methods] : true
|
26
|
+
target = options[:target] || @target_class
|
27
|
+
target.public_instance_methods(include_inherited_methods) +
|
28
|
+
target.private_instance_methods(include_inherited_methods) +
|
29
|
+
target.protected_instance_methods(include_inherited_methods)
|
30
|
+
end
|
31
|
+
|
32
|
+
def klass
|
33
|
+
ClassMethodIndex.new(@target_class)
|
34
|
+
end
|
35
|
+
|
36
|
+
class ClassMethodIndex
|
37
|
+
def initialize(target_class)
|
38
|
+
@target_class = target_class
|
39
|
+
end
|
40
|
+
|
41
|
+
def unique
|
42
|
+
uniquely_named + overridden - extended
|
43
|
+
end
|
44
|
+
|
45
|
+
def extended
|
46
|
+
@target_class.singleton_class.ancestors.map do |mod|
|
47
|
+
mod.instance_methods.select { |m| mod.instance_method(m).source_location == @target_class.method(m).source_location }
|
48
|
+
end.flatten
|
49
|
+
end
|
50
|
+
|
51
|
+
def uniquely_named
|
52
|
+
@target_class.singleton_methods(false)
|
53
|
+
end
|
54
|
+
|
55
|
+
def overridden
|
56
|
+
(@target_class.methods - uniquely_named).select do |m|
|
57
|
+
!ancestral_sources(m).include?(@target_class.method(m).source_location)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def ancestral_sources(method)
|
62
|
+
superclasses.map { |mod| mod.respond_to?(method) && mod.method(method).source_location }.compact
|
63
|
+
end
|
64
|
+
|
65
|
+
def superclasses
|
66
|
+
@target_class.ancestors - [@target_class]
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
data/lib/class_source.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'class_source/declarations'
|
2
|
+
require 'class_source/index'
|
3
|
+
require 'class_source/method_index'
|
4
|
+
require 'class_source/locator'
|
5
|
+
require 'class_source/guesser'
|
6
|
+
require 'class_source/collator'
|
7
|
+
|
8
|
+
module ClassSource
|
9
|
+
def __source__(options={})
|
10
|
+
Index.new(self, options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ClassSource do
|
4
|
+
after { test_unload_all }
|
5
|
+
|
6
|
+
describe "for a simple class with only an initializer" do
|
7
|
+
before do
|
8
|
+
test_load 'SimpleClass'
|
9
|
+
end
|
10
|
+
it "knows the unique methods of the class" do
|
11
|
+
SimpleClass.__source__.methods.unique.should == [:initialize]
|
12
|
+
end
|
13
|
+
|
14
|
+
it "knows the method location for each of the unique methods of the class" do
|
15
|
+
SimpleClass.__source__.methods.locations.should == [[fixtures_path(:simple_class), 4]]
|
16
|
+
end
|
17
|
+
|
18
|
+
it "can pinpoint the opening of a simple class" do
|
19
|
+
SimpleClass.__source__.locations.should == [[fixtures_path(:simple_class), 3]]
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can return the full source of a simple class" do
|
23
|
+
SimpleClass.__source__.should == File.read(fixtures_path(:simple_class)).lines.to_a[2..-1].join("")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "for a class with public, private and protected methods" do
|
28
|
+
before { test_load 'ProtectedMethodClass' }
|
29
|
+
|
30
|
+
it "knows the unique methods of the class" do
|
31
|
+
ProtectedMethodClass.__source__.methods.unique.should =~ [:initialize, :talk, :think, :whisper]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "knows the method location for each of the unique methods of the class" do
|
35
|
+
ProtectedMethodClass.__source__.methods.locations.should =~ [
|
36
|
+
[fixtures_path(:protected_method_class), 2],
|
37
|
+
[fixtures_path(:protected_method_class), 6],
|
38
|
+
[fixtures_path(:protected_method_class), 11],
|
39
|
+
[fixtures_path(:protected_method_class), 16]
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
it "can pinpoint the opening" do
|
44
|
+
ProtectedMethodClass.__source__.locations.should == [[fixtures_path(:protected_method_class), 1]]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can return the full source" do
|
48
|
+
ProtectedMethodClass.__source__.should == File.read(fixtures_path(:protected_method_class)).lines.to_a[0..-1].join("")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "for a subclass of a user class" do
|
53
|
+
before { test_load 'ProtectedMethodClass', 'SubClass' }
|
54
|
+
|
55
|
+
it "knows the unique methods of the class" do
|
56
|
+
SubClass.__source__.methods.unique.should =~ [:talk, :think]
|
57
|
+
end
|
58
|
+
|
59
|
+
it "knows the method location for each of the unique methods of the class" do
|
60
|
+
SubClass.__source__.methods.locations.should =~ [
|
61
|
+
[fixtures_path(:sub_class), 2],
|
62
|
+
[fixtures_path(:sub_class), 6]
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "can pinpoint the opening" do
|
67
|
+
SubClass.__source__.locations.should == [[fixtures_path(:sub_class), 1]]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "can return the full source" do
|
71
|
+
SubClass.__source__.should == File.read(fixtures_path(:sub_class)).lines.to_a[0..-1].join("")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "for a nested class" do
|
76
|
+
before { test_load 'OuterClass' }
|
77
|
+
it "can return the full source" do
|
78
|
+
OuterClass::NestedClass.__source__.should == File.read(fixtures_path(:outer_class)).lines.to_a[12..16].join("")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "for a class with no unique methods" do
|
83
|
+
before { test_load 'NoMethodsClass' }
|
84
|
+
it "can return the full source if you pass a source file" do
|
85
|
+
NoMethodsClass.__source__(:file => fixtures_path(:no_methods_class)).should == File.read(fixtures_path(:no_methods_class)).lines.to_a[0..4].join("")
|
86
|
+
end
|
87
|
+
describe "for a nested class with no unique methods" do
|
88
|
+
it "can return the full source" do
|
89
|
+
NoMethodsClass::NestedClass.__source__(:file => fixtures_path(:no_methods_class)).should == File.read(fixtures_path(:no_methods_class)).lines.to_a[1..3].join("")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "for an outer class" do
|
95
|
+
before { test_load 'OuterClass' }
|
96
|
+
it "can return the full source" do
|
97
|
+
OuterClass.__source__.should == File.read(fixtures_path(:outer_class)).lines.to_a[0..-1].join("")
|
98
|
+
end
|
99
|
+
|
100
|
+
it "can also return the source minus any nested classes" do
|
101
|
+
source_lines = File.read(fixtures_path(:outer_class)).lines.to_a
|
102
|
+
non_nested_lines = (source_lines[0..3] + source_lines[8..11] + source_lines[17..-1]).join("")
|
103
|
+
OuterClass.__source__.to_s(:include_nested => false).should == non_nested_lines
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "for dynamically defined classes" do
|
108
|
+
before do
|
109
|
+
test_load 'DynamicClass'
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should have a single source location" do
|
113
|
+
DynamicClass.__source__.should have(1).locations
|
114
|
+
end
|
115
|
+
|
116
|
+
it "can return the full source" do
|
117
|
+
DynamicClass.__source__.should == File.read(fixtures_path(:dynamic_class)).lines.to_a[0..4].join("")
|
118
|
+
end
|
119
|
+
|
120
|
+
it "can return the source for a class named later in the file" do
|
121
|
+
LateNamedDynamicClass.send :extend, ClassSource
|
122
|
+
LateNamedDynamicClass.__source__.should == File.read(fixtures_path(:dynamic_class)).lines.to_a[11]
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "with no methods" do
|
126
|
+
it "knows the source" do
|
127
|
+
MethodlessDynamicClass.send :extend, ClassSource
|
128
|
+
MethodlessDynamicClass.__source__.should == "MethodlessDynamicClass = Class.new\n"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "for duplicated classes" do
|
134
|
+
before { test_load 'DuplicatedClass' }
|
135
|
+
it "should point to the duplication point" do
|
136
|
+
DuplicateClass.__source__.locations.should == [
|
137
|
+
[fixtures_path(:duplicated_class), 5]
|
138
|
+
]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "for cloned classes" do
|
143
|
+
before { test_load 'ClonedClass' }
|
144
|
+
it "should point to the cloning point" do
|
145
|
+
CloneClass.__source__.locations.should == [
|
146
|
+
[fixtures_path(:cloned_class), 5]
|
147
|
+
]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "for classes defined within eval" do
|
152
|
+
before { test_load 'EvalClass' }
|
153
|
+
it "should point to the evaluation point" do
|
154
|
+
EvalClass.__source__.locations.should == [
|
155
|
+
[fixtures_path(:eval_class), 11]
|
156
|
+
]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe "for classes that are reopened in separate files" do
|
161
|
+
before { test_load 'ReOpenedClass' }
|
162
|
+
it "should have more than one source file" do
|
163
|
+
ReOpenedClass.__source__.files.length.should == 2
|
164
|
+
end
|
165
|
+
|
166
|
+
it "should have more than one source location" do
|
167
|
+
ReOpenedClass.__source__.locations.length.should == 2
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should have the full source in its source" do
|
171
|
+
ReOpenedClass.__source__.all.should == {
|
172
|
+
[fixtures_path(:re_opened_class), 3] => File.read(fixtures_path(:re_opened_class)).lines.to_a[2..-1].join(""),
|
173
|
+
[fixtures_path(:re_opened_class_2), 1] => File.read(fixtures_path(:re_opened_class_2))
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should be able to display without nested classes if needed" do
|
178
|
+
reopened_source = File.read(fixtures_path(:re_opened_class)).lines.to_a
|
179
|
+
reopened_source_2 = File.read(fixtures_path(:re_opened_class_2)).lines.to_a
|
180
|
+
ReOpenedClass.__source__.all(:include_nested => false).should == {
|
181
|
+
[fixtures_path(:re_opened_class), 3] => (reopened_source[2..6] + reopened_source[15..-1]).join(""),
|
182
|
+
[fixtures_path(:re_opened_class_2), 1] => (reopened_source_2[0..4] + reopened_source_2[10..-1]).join("")
|
183
|
+
}
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "for a nested class that is reopened within the parent" do
|
188
|
+
before { test_load 'ReOpenedClass' }
|
189
|
+
it "should have more than one source location" do
|
190
|
+
ReOpenedClass::NestedClass.__source__.all.should == {
|
191
|
+
[fixtures_path(:re_opened_class), 8] => File.read(fixtures_path(:re_opened_class)).lines.to_a[7..10].join(""),
|
192
|
+
[fixtures_path(:re_opened_class), 12] => File.read(fixtures_path(:re_opened_class)).lines.to_a[11..14].join("")
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "for class with only class methods" do
|
198
|
+
before do
|
199
|
+
test_load 'ClassMethodsClass'
|
200
|
+
end
|
201
|
+
it "knows the unique class methods of the class" do
|
202
|
+
ClassMethodsClass.__source__.class_methods.unique.should == [:simple_method]
|
203
|
+
end
|
204
|
+
it "can find the source" do
|
205
|
+
ClassMethodsClass.__source__.should == File.read(fixtures_path(:class_methods_class)).lines.to_a[0..-1].join("")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe "it doesn't cause side effects for class-scoped code" do
|
210
|
+
before do
|
211
|
+
test_load 'SideEffectClass'
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should retain the same values as set" do
|
215
|
+
SideEffectClass.should be_active
|
216
|
+
SideEffectClass.deactivate!
|
217
|
+
SideEffectClass.should_not be_active
|
218
|
+
SideEffectClass.__source__.should == File.read(fixtures_path(:side_effect_class)).lines.to_a[0..-1].join("")
|
219
|
+
SideEffectClass.should_not be_active
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class OuterClass
|
2
|
+
def simple_method_1
|
3
|
+
end
|
4
|
+
|
5
|
+
class OtherNestedClass
|
6
|
+
def example_method
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def simple_method_2
|
11
|
+
end
|
12
|
+
|
13
|
+
class NestedClass
|
14
|
+
def example_method
|
15
|
+
puts "example"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def simple_method_3
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
SOURCE_PATH=File.expand_path(File.dirname(__FILE__), '../lib')
|
3
|
+
$LOAD_PATH << SOURCE_PATH
|
4
|
+
require 'class_source'
|
5
|
+
|
6
|
+
RSpec.configure do
|
7
|
+
def spec_path
|
8
|
+
@path ||= File.expand_path(File.dirname(__FILE__))
|
9
|
+
end
|
10
|
+
|
11
|
+
def fixtures_path(file_name)
|
12
|
+
"#{SOURCE_PATH}/fixtures/#{file_name}.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_load(*args)
|
16
|
+
name = args.shift
|
17
|
+
file_name = name.to_s.gsub(/([A-Z])/) { |match| "_" + match.downcase }.slice(1..-1)
|
18
|
+
raise "can't find #{fixtures_path(file_name)}" unless File.exist?(fixtures_path(file_name))
|
19
|
+
|
20
|
+
@loaded_fixtures ||= []
|
21
|
+
@loaded_fixtures << name
|
22
|
+
existing_classes = Object.constants
|
23
|
+
load fixtures_path(file_name)
|
24
|
+
new_classes = Object.constants - existing_classes - [name.to_sym]
|
25
|
+
@loaded_fixtures += new_classes.map &:to_s
|
26
|
+
allow_source_inspection(name)
|
27
|
+
|
28
|
+
test_load(*args) unless args.empty?
|
29
|
+
new_classes.each { |new_name| allow_source_inspection(new_name) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def allow_source_inspection(name, nesting = Object)
|
33
|
+
klass = nesting.const_get(name.to_sym)
|
34
|
+
|
35
|
+
klass.send :extend, ClassSource
|
36
|
+
klass.constants.select { |kc| klass.const_get(kc).is_a?(Class) }.each do |nested_class|
|
37
|
+
allow_source_inspection nested_class, klass
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_unload_all
|
42
|
+
@loaded_fixtures.each do |klass_name|
|
43
|
+
Object.send :remove_const, klass_name if Object.const_defined?(klass_name)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: class_source
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Austin Putman, austingfromboston
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-21 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &2152549200 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2152549200
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: ruby_parser
|
27
|
+
requirement: &2152548780 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2152548780
|
36
|
+
description: Expose source files and full source code for each class via a simple
|
37
|
+
API
|
38
|
+
email:
|
39
|
+
- austin@rawfingertips.com
|
40
|
+
executables: []
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- lib/class_source/collator.rb
|
45
|
+
- lib/class_source/declarations.rb
|
46
|
+
- lib/class_source/guesser.rb
|
47
|
+
- lib/class_source/index.rb
|
48
|
+
- lib/class_source/locator.rb
|
49
|
+
- lib/class_source/method_index.rb
|
50
|
+
- lib/class_source/version.rb
|
51
|
+
- lib/class_source.rb
|
52
|
+
- spec/class_source_spec.rb
|
53
|
+
- spec/fixtures/class_methods_class.rb
|
54
|
+
- spec/fixtures/cloned_class.rb
|
55
|
+
- spec/fixtures/duplicated_class.rb
|
56
|
+
- spec/fixtures/dynamic_class.rb
|
57
|
+
- spec/fixtures/eval_class.rb
|
58
|
+
- spec/fixtures/example_code.rb
|
59
|
+
- spec/fixtures/nested_example.rb
|
60
|
+
- spec/fixtures/no_methods_class.rb
|
61
|
+
- spec/fixtures/outer_class.rb
|
62
|
+
- spec/fixtures/protected_method_class.rb
|
63
|
+
- spec/fixtures/re_opened_class.rb
|
64
|
+
- spec/fixtures/re_opened_class_2.rb
|
65
|
+
- spec/fixtures/side_effect_class.rb
|
66
|
+
- spec/fixtures/simple_class.rb
|
67
|
+
- spec/fixtures/sub_class.rb
|
68
|
+
- spec/fixtures/unnested_child.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage: http://github.com/austinfromboston/class_source
|
71
|
+
licenses:
|
72
|
+
- MIT
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubyforge_project: class_source
|
91
|
+
rubygems_version: 1.8.6
|
92
|
+
signing_key:
|
93
|
+
specification_version: 3
|
94
|
+
summary: See where your classes are defined
|
95
|
+
test_files:
|
96
|
+
- spec/class_source_spec.rb
|
97
|
+
- spec/fixtures/class_methods_class.rb
|
98
|
+
- spec/fixtures/cloned_class.rb
|
99
|
+
- spec/fixtures/duplicated_class.rb
|
100
|
+
- spec/fixtures/dynamic_class.rb
|
101
|
+
- spec/fixtures/eval_class.rb
|
102
|
+
- spec/fixtures/example_code.rb
|
103
|
+
- spec/fixtures/nested_example.rb
|
104
|
+
- spec/fixtures/no_methods_class.rb
|
105
|
+
- spec/fixtures/outer_class.rb
|
106
|
+
- spec/fixtures/protected_method_class.rb
|
107
|
+
- spec/fixtures/re_opened_class.rb
|
108
|
+
- spec/fixtures/re_opened_class_2.rb
|
109
|
+
- spec/fixtures/side_effect_class.rb
|
110
|
+
- spec/fixtures/simple_class.rb
|
111
|
+
- spec/fixtures/sub_class.rb
|
112
|
+
- spec/fixtures/unnested_child.rb
|
113
|
+
- spec/spec_helper.rb
|