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 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