class_source 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/class_source.rb +11 -1
- data/lib/class_source/class_method_index.rb +44 -0
- data/lib/class_source/collator.rb +34 -18
- data/lib/class_source/declarations.rb +1 -0
- data/lib/class_source/index.rb +27 -4
- data/lib/class_source/locator.rb +26 -6
- data/lib/class_source/method_index.rb +7 -35
- data/lib/class_source/{guesser.rb → scanner.rb} +3 -1
- data/lib/class_source/version.rb +1 -1
- metadata +8 -7
data/lib/class_source.rb
CHANGED
@@ -1,11 +1,21 @@
|
|
1
1
|
require 'class_source/declarations'
|
2
2
|
require 'class_source/index'
|
3
3
|
require 'class_source/method_index'
|
4
|
+
require 'class_source/class_method_index'
|
4
5
|
require 'class_source/locator'
|
5
|
-
require 'class_source/
|
6
|
+
require 'class_source/scanner'
|
6
7
|
require 'class_source/collator'
|
7
8
|
|
9
|
+
# extend your class with ClassSource in order to inspect its source
|
8
10
|
module ClassSource
|
11
|
+
# Returns a proxy for inspecting the source of a class
|
12
|
+
# To view the source as a string, use
|
13
|
+
# ExampleClass.__source__.to_s
|
14
|
+
#
|
15
|
+
# To view the source as a hash of file/line locations and strings, use
|
16
|
+
# ExampleClass.__source__.all
|
17
|
+
#
|
18
|
+
# @return[ClassSource::Index] an index of all source code used to construct the class
|
9
19
|
def __source__(options={})
|
10
20
|
Index.new(self, options)
|
11
21
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ClassSource
|
2
|
+
# An index of all class methods available for a class
|
3
|
+
class ClassMethodIndex
|
4
|
+
def initialize(target_class)
|
5
|
+
@target_class = target_class
|
6
|
+
end
|
7
|
+
|
8
|
+
# @return [Array] An array of method names unique to or overridden in this class, not inherited from its ancestors or singleton_class ancestors.
|
9
|
+
def unique
|
10
|
+
uniquely_named + overridden - extended
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Array] An array of method names for all methods included into a class via class extension.
|
14
|
+
def extended
|
15
|
+
@target_class.singleton_class.ancestors.map do |mod|
|
16
|
+
mod.instance_methods.select { |m| mod.instance_method(m).source_location == @target_class.method(m).source_location }
|
17
|
+
end.flatten
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Array] An array of method names introduced for the first time in the current class
|
21
|
+
def uniquely_named
|
22
|
+
@target_class.singleton_methods(false)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Array] An array of method names with new source in this class vs its ancestors
|
26
|
+
def overridden
|
27
|
+
(@target_class.methods - uniquely_named).select do |m|
|
28
|
+
!ancestral_sources(m).include?(@target_class.method(m).source_location)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Array] An array of ancestral sources for a given method
|
33
|
+
def ancestral_sources(method)
|
34
|
+
superclasses.map { |mod| mod.respond_to?(method) && mod.method(method).source_location }.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
# All ancestors of the target class
|
38
|
+
# @return[Array] An array of classes and modules
|
39
|
+
def superclasses
|
40
|
+
@target_class.ancestors - [@target_class]
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -1,27 +1,30 @@
|
|
1
1
|
require 'ruby_parser'
|
2
2
|
|
3
3
|
module ClassSource
|
4
|
+
# Responsible for collating class source information into a clear readable hash of source values.
|
4
5
|
class Collator
|
5
6
|
def initialize(target_class, index)
|
6
7
|
@klass = target_class
|
7
8
|
@source = index
|
8
9
|
end
|
9
10
|
|
11
|
+
# @return [Hash] A hash with keys of [file_path, line_number] tuples pointing to values of source code segments
|
10
12
|
def to_hash(options = {})
|
11
|
-
|
13
|
+
return sources_without_nesting if options[:include_nested] == false
|
14
|
+
@source.locations(options).inject({}) do |results, location|
|
12
15
|
results[ location ] = source_helper(location)
|
13
16
|
results
|
14
17
|
end
|
15
18
|
|
16
|
-
|
17
|
-
|
19
|
+
end
|
20
|
+
|
21
|
+
# @private
|
22
|
+
# @return [Hash] A hash of sources filtered for the source of any nested classes
|
23
|
+
def sources_without_nesting
|
24
|
+
@source.locations.inject({}) do |clean_sources, location|
|
25
|
+
source = source_helper(location)
|
18
26
|
if nested_class_line_ranges[location.first]
|
19
|
-
|
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("")
|
27
|
+
clean_sources[location] = source_without_nesting(location, source)
|
25
28
|
else
|
26
29
|
clean_sources[location] = source
|
27
30
|
end
|
@@ -29,26 +32,38 @@ module ClassSource
|
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
35
|
+
# @private
|
36
|
+
# @return [Hash] A source string with the contained nested class values removed
|
37
|
+
def source_without_nesting(location, source)
|
38
|
+
complete_file = full_file(location)
|
39
|
+
target_range = (location.last - 1)..(location.last + source.lines.count - 2)
|
40
|
+
complete_file.lines.to_a.select.with_index do |line, index|
|
41
|
+
target_range.include?(index) &&
|
42
|
+
nested_class_line_ranges[location.first].all? { |range| !range.include?(index) }
|
43
|
+
end.join("")
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return a hash of nested data within the class source based on existing class constants
|
47
|
+
# @private
|
32
48
|
def nested_class_line_ranges
|
33
49
|
nested_classes = @klass.constants.select { |c| @klass.const_get(c).is_a?(Class) }.map {|c| @klass.const_get(c) }
|
34
50
|
return @nested_class_ranges if @nested_class_ranges
|
35
|
-
@nested_class_ranges = {}
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
@nested_class_ranges[file] ||= []
|
40
|
-
@nested_class_ranges[file] << ((line - 1)..(line + source.lines.count - 2))
|
51
|
+
@nested_class_ranges = nested_classes.inject({}) do |ranges, nested_klass|
|
52
|
+
nested_klass.__source__.all.each do |(file, line), source|
|
53
|
+
ranges[file] ||= []
|
54
|
+
ranges[file] << ((line - 1)..(line + source.lines.count - 2))
|
41
55
|
end
|
56
|
+
ranges
|
42
57
|
end
|
43
|
-
|
44
|
-
@nested_class_ranges
|
45
58
|
end
|
46
59
|
|
60
|
+
# A helper to return the full text of a file
|
61
|
+
# @private
|
47
62
|
def full_file(location)
|
48
63
|
File.read(location.first)
|
49
64
|
end
|
50
65
|
|
51
|
-
# source_helper and valid_expression? are
|
66
|
+
# source_helper and valid_expression? are from the method_source gem
|
52
67
|
# (c) 2011 John Mair (banisterfiend)
|
53
68
|
def source_helper(source_location)
|
54
69
|
return nil if !source_location.is_a?(Array)
|
@@ -68,6 +83,7 @@ module ClassSource
|
|
68
83
|
end
|
69
84
|
|
70
85
|
|
86
|
+
# (see #source_helper)
|
71
87
|
def valid_expression?(code)
|
72
88
|
RubyParser.new.parse(code)
|
73
89
|
rescue Racc::ParseError, SyntaxError
|
data/lib/class_source/index.rb
CHANGED
@@ -1,38 +1,61 @@
|
|
1
1
|
module ClassSource
|
2
|
+
# An index of all source code available for a class
|
2
3
|
class Index
|
4
|
+
# @param [Hash] options, may include a :file => 'path_to_expected_source' param for tricky classes.
|
5
|
+
# Classes with no methods will need this hint to be sourced correctly.
|
3
6
|
def initialize(target_class, options = {})
|
4
7
|
@target_class = target_class
|
5
8
|
@options = options
|
6
9
|
end
|
7
10
|
|
11
|
+
# This returns a string containing all the class' source code.
|
12
|
+
# Order is not guaranteed to match evaluation order.
|
13
|
+
# @param [Hash] options may contain a key :include_nested => false
|
14
|
+
# to return the source without the body of any nested classes
|
15
|
+
# @return [String] the joined value of all source code for the class
|
8
16
|
def to_s(options={})
|
9
17
|
all(options).values.join("")
|
10
18
|
end
|
11
19
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
20
|
+
# Returns a hash of source code fragments indexed by location
|
21
|
+
# @param (see #to_s)
|
22
|
+
# @return [Hash] a hash of source code fragments with keys being a tuple in the form of [file_path, line_number]
|
16
23
|
def all(options={})
|
17
24
|
@collator ||= Collator.new(@target_class, self).to_hash(options)
|
18
25
|
end
|
19
26
|
|
27
|
+
# Returns an array of source code locations
|
28
|
+
# @return [Array] an array of tuples in the form of [file_path, line_number]
|
20
29
|
def locations(options={})
|
21
30
|
locator.to_a
|
22
31
|
end
|
23
32
|
|
33
|
+
# Convenience method for comparing sources as string values
|
34
|
+
# @private
|
35
|
+
def ==(value)
|
36
|
+
to_s == value
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns an index of all methods found for the class
|
40
|
+
# @private
|
24
41
|
def methods
|
25
42
|
@method_details ||= MethodIndex.new(@target_class)
|
26
43
|
end
|
27
44
|
|
45
|
+
# Returns an index of all methods found for the class
|
46
|
+
# @private
|
28
47
|
def class_methods
|
29
48
|
methods.klass
|
30
49
|
end
|
31
50
|
|
51
|
+
# Returns an instance of the source locator object that searches out source locations
|
52
|
+
# @private
|
32
53
|
def locator
|
33
54
|
@locator ||= Locator.new(@target_class, @options)
|
34
55
|
end
|
35
56
|
|
57
|
+
# Returns an array of file containing relevant source code
|
58
|
+
# @private
|
36
59
|
def files
|
37
60
|
locator.files
|
38
61
|
end
|
data/lib/class_source/locator.rb
CHANGED
@@ -2,28 +2,45 @@ require 'tempfile'
|
|
2
2
|
require 'yaml'
|
3
3
|
|
4
4
|
module ClassSource
|
5
|
+
# A helper class responsible for tracing the evaluation of files to discover class declarations points
|
5
6
|
class Locator
|
6
7
|
def initialize(target_class, options={})
|
7
8
|
@klass = target_class
|
8
9
|
@options=options
|
9
10
|
end
|
10
11
|
|
12
|
+
# @return [Array] An array of [file_path, line_number] tuples describing where the class was declared.
|
11
13
|
def to_a
|
12
14
|
source_locations
|
13
15
|
end
|
14
16
|
|
17
|
+
# @return [ClassSource::MethodIndex] A pointer to the method index for tracking down files.
|
18
|
+
# @private
|
15
19
|
def methods
|
16
20
|
MethodIndex.new(@klass)
|
17
21
|
end
|
18
22
|
|
23
|
+
# @return [Array] An array of file paths where the class was declared.
|
19
24
|
def files(options={})
|
20
25
|
@source_files ||= methods.locations.map(&:first).uniq
|
21
26
|
return @source_files + [@options[:file]] if @options[:file]
|
22
27
|
@source_files
|
23
28
|
end
|
24
29
|
|
30
|
+
# @return (see #to_a)
|
31
|
+
# @private
|
25
32
|
def source_locations(options={})
|
26
33
|
return @locations if @locations
|
34
|
+
evaluate_code_in_a_fork(options)
|
35
|
+
@locations = if !Declarations[@klass.name].nil?
|
36
|
+
Declarations[@klass.name].uniq
|
37
|
+
else
|
38
|
+
Scanner.new(@klass, files).locations || []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @private
|
43
|
+
def evaluate_code_in_a_fork(options)
|
27
44
|
t = Tempfile.new('class_creation_events')
|
28
45
|
fork do
|
29
46
|
declarations = files(options).inject({}) do |declarations, source_file|
|
@@ -32,15 +49,12 @@ module ClassSource
|
|
32
49
|
YAML.dump(declarations, t)
|
33
50
|
end
|
34
51
|
Process.wait
|
35
|
-
Declarations.save YAML.load_file(t.path)
|
36
52
|
t.close
|
37
|
-
|
38
|
-
Declarations[@klass.name].uniq
|
39
|
-
else
|
40
|
-
Guesser.new(@klass, files).locations || []
|
41
|
-
end
|
53
|
+
Declarations.save YAML.load_file(t.path)
|
42
54
|
end
|
43
55
|
|
56
|
+
# Traces the evaluation of a file looking for class declarations
|
57
|
+
# @private
|
44
58
|
def trace_declarations(source_file, declarations)
|
45
59
|
set_trace_func lambda { |event, file, line, id, binding, classname|
|
46
60
|
defined_class = standard_class_declared(event, binding) || dynamic_class_declared(id, classname, file, line)
|
@@ -53,17 +67,23 @@ module ClassSource
|
|
53
67
|
declarations
|
54
68
|
end
|
55
69
|
|
70
|
+
# A heuristic for seeing that a class has been declared dynamically (e.g. using Class.new)
|
71
|
+
# @private
|
56
72
|
def dynamic_class_declared(id, classname, file, line)
|
57
73
|
return unless id == :new && classname == Class
|
58
74
|
File.read(file).lines.to_a[line-1][/[A-Z][\w_:]*/, 0]
|
59
75
|
end
|
60
76
|
|
77
|
+
# A fast way to spot a normal class declaration (e.g. class MyNewClass)
|
78
|
+
# @private
|
61
79
|
def standard_class_declared(event, binding)
|
62
80
|
return unless event == 'class'
|
63
81
|
event_class = eval( "Module.nesting", binding )
|
64
82
|
event_class.first
|
65
83
|
end
|
66
84
|
|
85
|
+
# Need one of these, re-evaluating code is a noisy business
|
86
|
+
# @private
|
67
87
|
def silence_warnings
|
68
88
|
old_verbose, $VERBOSE = $VERBOSE, nil
|
69
89
|
yield
|
@@ -1,9 +1,12 @@
|
|
1
1
|
module ClassSource
|
2
|
+
# An index of all the methods in the target class
|
2
3
|
class MethodIndex
|
3
4
|
def initialize(target_class)
|
4
5
|
@target_class = target_class
|
5
6
|
end
|
6
7
|
|
8
|
+
|
9
|
+
# @return [Array] An array of [file_path, line_number] tuples for all unique methods of the class
|
7
10
|
def locations
|
8
11
|
@locations ||= (unique.map do |m|
|
9
12
|
@target_class.instance_method(m).source_location
|
@@ -12,6 +15,7 @@ module ClassSource
|
|
12
15
|
end).compact
|
13
16
|
end
|
14
17
|
|
18
|
+
# @return [Array] An array of method names unique to or overridden in this class, not inherited from its ancestors or singleton_class ancestors.
|
15
19
|
def unique
|
16
20
|
uniquely_named_methods = all(:include_inherited_methods => false)
|
17
21
|
overridden_methods = (all - uniquely_named_methods).select do |m|
|
@@ -21,6 +25,7 @@ module ClassSource
|
|
21
25
|
end
|
22
26
|
|
23
27
|
|
28
|
+
# @return [Array] An array of method names for all instance methods in the class
|
24
29
|
def all(options={})
|
25
30
|
include_inherited_methods = options.has_key?(:include_inherited_methods) ? options[:include_inherited_methods] : true
|
26
31
|
target = options[:target] || @target_class
|
@@ -29,44 +34,11 @@ module ClassSource
|
|
29
34
|
target.protected_instance_methods(include_inherited_methods)
|
30
35
|
end
|
31
36
|
|
37
|
+
# @return [ClassSource::ClassMethodIndex] A index of class methods
|
38
|
+
# @private
|
32
39
|
def klass
|
33
40
|
ClassMethodIndex.new(@target_class)
|
34
41
|
end
|
35
42
|
|
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
43
|
end
|
72
44
|
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
module ClassSource
|
2
|
-
class
|
2
|
+
# A helper class for scanning files looking for potential class declarations
|
3
|
+
class Scanner
|
3
4
|
def initialize(klass, source_files)
|
4
5
|
@source_files = source_files
|
5
6
|
@klass = klass
|
6
7
|
end
|
7
8
|
|
9
|
+
# @return [Array] An array of [file_name, line_number] tuples where the classes name was detected
|
8
10
|
def locations
|
9
11
|
return if @source_files.empty?
|
10
12
|
@source_files.map do |file|
|
data/lib/class_source/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: class_source
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-09-
|
12
|
+
date: 2011-09-22 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
requirement: &
|
16
|
+
requirement: &2152640600 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2152640600
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: ruby_parser
|
27
|
-
requirement: &
|
27
|
+
requirement: &2152640180 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2152640180
|
36
36
|
description: Expose source files and full source code for each class via a simple
|
37
37
|
API
|
38
38
|
email:
|
@@ -41,12 +41,13 @@ executables: []
|
|
41
41
|
extensions: []
|
42
42
|
extra_rdoc_files: []
|
43
43
|
files:
|
44
|
+
- lib/class_source/class_method_index.rb
|
44
45
|
- lib/class_source/collator.rb
|
45
46
|
- lib/class_source/declarations.rb
|
46
|
-
- lib/class_source/guesser.rb
|
47
47
|
- lib/class_source/index.rb
|
48
48
|
- lib/class_source/locator.rb
|
49
49
|
- lib/class_source/method_index.rb
|
50
|
+
- lib/class_source/scanner.rb
|
50
51
|
- lib/class_source/version.rb
|
51
52
|
- lib/class_source.rb
|
52
53
|
- spec/class_source_spec.rb
|