class_source 0.0.1 → 0.0.2
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.
- 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
|