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 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/guesser'
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
- full_sources = @source.locations(options).inject({}) do |results, location|
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
- return full_sources unless options[:include_nested] == false
17
- full_sources.inject({}) do |clean_sources, (location, source)|
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
- 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("")
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
- 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))
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 lifted from method_source
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
@@ -1,4 +1,5 @@
1
1
  module ClassSource
2
+ # A helper class for tracking class declaration points
2
3
  class Declarations
3
4
  def self.[](key)
4
5
  @declarations ||= {}
@@ -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
- def ==(value)
13
- to_s == value
14
- end
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
@@ -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
- @locations = if !Declarations[@klass.name].nil?
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 Guesser
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|
@@ -1,3 +1,3 @@
1
1
  module ClassSource
2
- VERSION="0.0.1"
2
+ VERSION="0.0.2"
3
3
  end
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.1
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-21 00:00:00.000000000Z
12
+ date: 2011-09-22 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &2153572280 !ruby/object:Gem::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: *2153572280
24
+ version_requirements: *2152640600
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: ruby_parser
27
- requirement: &2153571860 !ruby/object:Gem::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: *2153571860
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