wool 0.5.1

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.
Files changed (88) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/LICENSE +45 -0
  4. data/README.rdoc +17 -0
  5. data/Rakefile +77 -0
  6. data/TODO.md +17 -0
  7. data/VERSION +1 -0
  8. data/bin/wool +4 -0
  9. data/features/step_definitions/wool_steps.rb +39 -0
  10. data/features/support/env.rb +14 -0
  11. data/features/support/testdata/1_input +1 -0
  12. data/features/support/testdata/1_output +1 -0
  13. data/features/support/testdata/2_input +4 -0
  14. data/features/support/testdata/2_output +4 -0
  15. data/features/support/testdata/3_input +8 -0
  16. data/features/support/testdata/3_output +11 -0
  17. data/features/support/testdata/4_input +5 -0
  18. data/features/support/testdata/4_output +5 -0
  19. data/features/wool.feature +24 -0
  20. data/lib/wool.rb +40 -0
  21. data/lib/wool/advice/advice.rb +42 -0
  22. data/lib/wool/advice/comment_advice.rb +37 -0
  23. data/lib/wool/analysis/annotations.rb +34 -0
  24. data/lib/wool/analysis/annotations/next_annotation.rb +26 -0
  25. data/lib/wool/analysis/annotations/parent_annotation.rb +20 -0
  26. data/lib/wool/analysis/annotations/scope_annotation.rb +37 -0
  27. data/lib/wool/analysis/lexical_analysis.rb +165 -0
  28. data/lib/wool/analysis/protocol_registry.rb +32 -0
  29. data/lib/wool/analysis/protocols.rb +82 -0
  30. data/lib/wool/analysis/scope.rb +13 -0
  31. data/lib/wool/analysis/sexp_analysis.rb +98 -0
  32. data/lib/wool/analysis/signature.rb +16 -0
  33. data/lib/wool/analysis/symbol.rb +10 -0
  34. data/lib/wool/analysis/visitor.rb +36 -0
  35. data/lib/wool/analysis/wool_class.rb +47 -0
  36. data/lib/wool/rake/task.rb +42 -0
  37. data/lib/wool/runner.rb +156 -0
  38. data/lib/wool/scanner.rb +160 -0
  39. data/lib/wool/support/module_extensions.rb +84 -0
  40. data/lib/wool/third_party/trollop.rb +845 -0
  41. data/lib/wool/warning.rb +145 -0
  42. data/lib/wool/warnings/comment_spacing.rb +30 -0
  43. data/lib/wool/warnings/extra_blank_lines.rb +29 -0
  44. data/lib/wool/warnings/extra_whitespace.rb +15 -0
  45. data/lib/wool/warnings/line_length.rb +113 -0
  46. data/lib/wool/warnings/misaligned_unindentation.rb +16 -0
  47. data/lib/wool/warnings/operator_spacing.rb +63 -0
  48. data/lib/wool/warnings/rescue_exception.rb +41 -0
  49. data/lib/wool/warnings/semicolon.rb +24 -0
  50. data/lib/wool/warnings/useless_double_quotes.rb +37 -0
  51. data/spec/advice_specs/advice_spec.rb +69 -0
  52. data/spec/advice_specs/comment_advice_spec.rb +38 -0
  53. data/spec/advice_specs/spec_helper.rb +1 -0
  54. data/spec/analysis_specs/annotations_specs/next_prev_annotation_spec.rb +47 -0
  55. data/spec/analysis_specs/annotations_specs/parent_annotation_spec.rb +41 -0
  56. data/spec/analysis_specs/annotations_specs/spec_helper.rb +5 -0
  57. data/spec/analysis_specs/lexical_analysis_spec.rb +179 -0
  58. data/spec/analysis_specs/protocol_registry_spec.rb +58 -0
  59. data/spec/analysis_specs/protocols_spec.rb +49 -0
  60. data/spec/analysis_specs/scope_spec.rb +20 -0
  61. data/spec/analysis_specs/sexp_analysis_spec.rb +134 -0
  62. data/spec/analysis_specs/spec_helper.rb +2 -0
  63. data/spec/analysis_specs/visitor_spec.rb +53 -0
  64. data/spec/analysis_specs/wool_class_spec.rb +54 -0
  65. data/spec/rake_specs/spec_helper.rb +1 -0
  66. data/spec/rake_specs/task_spec.rb +67 -0
  67. data/spec/runner_spec.rb +171 -0
  68. data/spec/scanner_spec.rb +75 -0
  69. data/spec/spec.opts +1 -0
  70. data/spec/spec_helper.rb +93 -0
  71. data/spec/support_specs/module_extensions_spec.rb +91 -0
  72. data/spec/support_specs/spec_helper.rb +1 -0
  73. data/spec/warning_spec.rb +95 -0
  74. data/spec/warning_specs/comment_spacing_spec.rb +57 -0
  75. data/spec/warning_specs/extra_blank_lines_spec.rb +70 -0
  76. data/spec/warning_specs/extra_whitespace_spec.rb +33 -0
  77. data/spec/warning_specs/line_length_spec.rb +165 -0
  78. data/spec/warning_specs/misaligned_unindentation_spec.rb +35 -0
  79. data/spec/warning_specs/operator_spacing_spec.rb +101 -0
  80. data/spec/warning_specs/rescue_exception_spec.rb +105 -0
  81. data/spec/warning_specs/semicolon_spec.rb +58 -0
  82. data/spec/warning_specs/spec_helper.rb +1 -0
  83. data/spec/warning_specs/useless_double_quotes_spec.rb +62 -0
  84. data/spec/wool_spec.rb +8 -0
  85. data/status_reports/2010/12/2010-12-14.md +163 -0
  86. data/test/third_party_tests/test_trollop.rb +1181 -0
  87. data/wool.gemspec +173 -0
  88. metadata +235 -0
@@ -0,0 +1,34 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # This is the base module for all annotations that can run on ASTs.
4
+ # It includes all other annotation modules to provide all the
5
+ # annotations to the Sexp class. These annotations are run at initialize
6
+ # time for the Sexps and have access to a node and all of its child nodes,
7
+ # all of which have been annotated. Synthesized attributes are fair game,
8
+ # and adding inherited attributes to subnodes is also fair game.
9
+ #
10
+ # All annotations add O(V) to the parser runtime.
11
+ #
12
+ # This module also provides some helper methods to inject functionality into
13
+ # the Sexp class. Since that's what an annotation is, I don't consider
14
+ # this bad form!
15
+ module BasicAnnotation
16
+ def add_annotator(*args)
17
+ SexpAnalysis::Sexp.annotations.concat args.map(&:new)
18
+ end
19
+ alias_method :add_annotators, :add_annotator
20
+ def add_global_annotator(*args)
21
+ SexpAnalysis.global_annotations.concat args.map(&:new)
22
+ end
23
+ alias_method :add_global_annotators, :add_global_annotator
24
+ def add_property(*args)
25
+ SexpAnalysis::Sexp.__send__(:attr_accessor, *args)
26
+ end
27
+ alias_method :add_properties, :add_property
28
+ end
29
+ end
30
+ end
31
+
32
+ Dir[File.expand_path(File.join(File.dirname(__FILE__), 'annotations', '**', '*.rb'))].each do |file|
33
+ load file
34
+ end
@@ -0,0 +1,26 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # This is a simple inherited attribute applied to each node,
4
+ # giving a pointer to that node's next and previous AST node.
5
+ # That way AST traversal is easier.
6
+ module NextPrevAnnotation
7
+ extend BasicAnnotation
8
+ add_properties :next, :prev
9
+
10
+ # This is the annotator for the next and prev annotation.
11
+ class Annotator
12
+ def annotate!(root)
13
+ children = root.children
14
+ children.each_with_index do |elt, idx|
15
+ # ignore non-sexps. Primitives can't be annotated, sadly.
16
+ if SexpAnalysis::Sexp === elt
17
+ elt.next = children[idx+1]
18
+ elt.prev = children[idx-1] if idx >= 1
19
+ end
20
+ end
21
+ end
22
+ end
23
+ add_annotator Annotator
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # This is a simple inherited attribute applied to each node,
4
+ # giving a pointer to that node's parent. That way AST traversal
5
+ # is easier.
6
+ module ParentAnnotation
7
+ extend BasicAnnotation
8
+ add_property :parent
9
+
10
+ # This is the annotator for the parent annotation.
11
+ class Annotator
12
+ def annotate!(root)
13
+ root.parent = nil
14
+ root.children.select {|x| SexpAnalysis::Sexp === x}.each {|sexp| sexp.parent = root}
15
+ end
16
+ end
17
+ add_annotator Annotator
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # This is a *global* annotation, namely the one that determines the statically-known
4
+ # scope for each node in the AST, at the time of that node's execution. For
5
+ # example, every node should be able to say "hey scope, what's 'this' for this
6
+ # statement?", and be able to return its type (*NOT* its class, they're different).
7
+ module ScopeAnnotation
8
+ extend BasicAnnotation
9
+ add_property :scope
10
+
11
+ # This is the annotator for the parent annotation.
12
+ class Annotator
13
+ include Visitor
14
+ def annotate!(root)
15
+ @current_scope = Scope::GlobalScope
16
+ visit(root)
17
+ end
18
+
19
+ # Replaces the general node visit method with one that assigns
20
+ # the current scope to the visited node.
21
+ def default_visit(node)
22
+ node.scope = @current_scope
23
+ end
24
+
25
+ def visit_module(node)
26
+ path_node, body = node.children
27
+ end
28
+
29
+ def visit_class(node)
30
+ path_to_new_class, superclass, body = node.children
31
+ superclass = superclass ? superclass.eval_as_constant(@current_scope) : ClassRegistry['Object']
32
+ end
33
+ end
34
+ add_global_annotator Annotator
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,165 @@
1
+ module Wool
2
+ # This is a set of methods that get provided to Warnings so they can perform
3
+ # lexical analysis of their bodies. This module handles tokenizing only - not
4
+ # parse-trees.
5
+ module LexicalAnalysis
6
+ # This is a wrapper class around the tokens returned by Ripper. Since the
7
+ # tokens are just arrays, this class lets us use nice mnemonics with almost zero
8
+ # runtime overhead.
9
+ class Token < Struct.new(:type, :body, :line, :col)
10
+ # Unpacks the token from Ripper and breaks it into its separate components.
11
+ #
12
+ # @param [Array<Array<Integer, Integer>, Symbol, String>] token the token
13
+ # from Ripper that we're wrapping
14
+ def initialize(token)
15
+ pos, self.type, self.body = token
16
+ self.line, self.col = pos
17
+ end
18
+ end
19
+
20
+ # Lexes the given text.
21
+ #
22
+ # @param [String] body (self.body) The text to lex
23
+ # @return [Array<Array<Integer, Integer>, Symbol, String>] A set of tokens
24
+ # in Ripper's result format. Each token is an array of the form:
25
+ # [[1, token_position], token_type, token_text]. I'm not exactly clear on
26
+ # why the 1 is always there. At any rate - the result is an array of those
27
+ # tokens.
28
+ def lex(body = self.body)
29
+ Ripper.lex(body).map {|token| Token.new(token) }
30
+ end
31
+
32
+ # Returns the text between two token positions. The token positions are
33
+ # in [line, column] format. The body, left, and right tokens must be provided,
34
+ # and optionally, you can override the inclusiveness of the text-between operation.
35
+ # It defaults to :none, for including neither the left nor right tokens in the
36
+ # result. You can pass :none, :left, :right, or :both.
37
+ #
38
+ # @param [String] body (self.body) The first parameter is optional: the text
39
+ # to search. This defaults to the full text.
40
+ # @param [Token] left the left token to get the text between
41
+ # @param [Token] right the right token to get the text between
42
+ # @param [Symbol] inclusive should the :left, :right, :both, or :none tokens
43
+ # be included in the resulting text?
44
+ # @return the text between the two tokens within the text. This is necessary
45
+ # because the lexer provides [line, column] coordinates which is quite
46
+ # unfortunate.
47
+ def text_between_token_positions(text, left, right, inclusive = :none)
48
+ result = ""
49
+ lines = text.lines.to_a
50
+ left.line.upto(right.line) do |cur_line|
51
+ line = lines[cur_line - 1]
52
+ result << left.body if cur_line == left.line && (inclusive == :both || inclusive == :left)
53
+ left_bound = cur_line == left.line ? left.col + left.body.size : 0
54
+ right_bound = cur_line == right.line ? right.col - 1 : -1
55
+ result << line[left_bound..right_bound]
56
+ result << right.body if cur_line == right.line && (inclusive == :both || inclusive == :right)
57
+ end
58
+ result
59
+ end
60
+
61
+ # Searches for the given token using standard [body], target symbols syntax.
62
+ # Yields for each token found that matches the query, and returns all those
63
+ # who match.
64
+ #
65
+ # @param [String] body (self.body) The first parameter is optional: the text
66
+ # to search. This defaults to the full text.
67
+ # @param [Symbol] token The rest of the arguments are tokens to search
68
+ # for. Any number of tokens may be specified.
69
+ # @return [Array<Array>] All the matching tokens for the query
70
+ def select_token(*args)
71
+ body, list = _extract_token_search_args(args)
72
+ result = []
73
+ while (token = find_token(body, *list)) && token != nil
74
+ result << token if yield(*token)
75
+ _, body = split_on_token(body, *list)
76
+ body = body[token.body.size..-1]
77
+ end
78
+ return result
79
+ end
80
+
81
+ # Finds the first instance of a set of keywords in the body. If no text is
82
+ # given to scan, then the full content is scanned.
83
+ #
84
+ # @param [String] body (self.body) The first parameter is optional: the text
85
+ # to search. This defaults to the full text.
86
+ # @param [Symbol] keyword The rest of the arguments are keywords to search
87
+ # for. Any number of keywords may be specified.
88
+ # @return [Array] the token in the form returned by Ripper. See #lex.
89
+ def find_keyword(*args)
90
+ body, list = _extract_token_search_args(args)
91
+ list.map! {|x| x.to_s}
92
+ lexed = lex(body)
93
+ lexed.find.with_index do |tok, idx|
94
+ is_keyword = tok.type == :on_kw && list.include?(tok.body)
95
+ is_not_symbol = idx == 0 || lexed[idx-1].type != :on_symbeg
96
+ is_keyword && is_not_symbol
97
+ end
98
+ end
99
+
100
+ # Finds the first instance of a set of tokens in the body. If no text is
101
+ # given to scan, then the full content is scanned.
102
+ #
103
+ # @param [String] body (self.body) The first parameter is optional: the text
104
+ # to search. This defaults to the full text.
105
+ # @param [Symbol] token The rest of the arguments are tokens to search
106
+ # for. Any number of tokens may be specified.
107
+ # @return [Array] the token in the form returned by Ripper. See #lex.
108
+ def find_token(*args)
109
+ body, list = _extract_token_search_args(args)
110
+ lexed = lex(body)
111
+ lexed.find.with_index do |tok, idx|
112
+ is_token = list.include?(tok.type)
113
+ is_not_symbol = idx == 0 || lexed[idx-1].type != :on_symbeg
114
+ is_token && is_not_symbol
115
+ end
116
+ end
117
+
118
+ # Splits the body into two halfs based on the first appearance of a keyword.
119
+ #
120
+ # @example
121
+ # split_on_keyword('x = 5 unless y == 2', :unless)
122
+ # # => ['x = 5 ', 'unless y == 2']
123
+ # @param [String] body (self.body) The first parameter is optional: the text
124
+ # to search. This defaults to the full text.
125
+ # @param [Symbol] token The rest of the arguments are keywords to search
126
+ # for. Any number of keywords may be specified.
127
+ # @return [Array<String, String>] The body split by the keyword.
128
+ def split_on_keyword(*args)
129
+ body, keywords = _extract_token_search_args(args)
130
+ token = find_keyword(body, *keywords)
131
+ return _split_body_with_raw_token(body, token)
132
+ end
133
+
134
+ # Splits the body into two halfs based on the first appearance of a token.
135
+ #
136
+ # @example
137
+ # split_on_token('x = 5 unless y == 2', :on_kw)
138
+ # # => ['x = 5 ', 'unless y == 2']
139
+ # @param [String] body (self.body) The first parameter is optional: the text
140
+ # to search. This defaults to the full text.
141
+ # @param [Symbol] token The rest of the arguments are tokens to search
142
+ # for. Any number of tokens may be specified.
143
+ # @return [Array<String, String>] The body split by the token.
144
+ def split_on_token(*args)
145
+ body, tokens = _extract_token_search_args(args)
146
+ token = find_token(body, *tokens)
147
+ return _split_body_with_raw_token(body, token)
148
+ end
149
+
150
+ private
151
+
152
+ def _extract_token_search_args(args)
153
+ if args.first.is_a?(String)
154
+ return args[0], args[1..-1]
155
+ else
156
+ return self.body, args
157
+ end
158
+ end
159
+
160
+ def _split_body_with_raw_token(body, token)
161
+ max = token ? [0, token.col].max : body.size
162
+ return body[0,max], body[max..-1]
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,32 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # The ProtocolRegistry module handles negotiating instantiated protocols at
4
+ # compile-time, such as the automatically-generated protocols created by a
5
+ # class creation (as each class has a corresponding protocol, though some
6
+ # distinct classes may have equivalent protocols).
7
+ module ProtocolRegistry
8
+ extend ModuleExtensions
9
+ cattr_accessor_with_default :protocols, []
10
+ cattr_accessor_with_default :class_protocols, {}
11
+
12
+ def self.add_protocol(proto)
13
+ self.protocols << proto
14
+ end
15
+
16
+ def self.add_class_protocol(class_protocol)
17
+ add_protocol class_protocol
18
+ self.class_protocols[class_protocol.class_used.path] = class_protocol
19
+ end
20
+
21
+ def self.[](class_name)
22
+ return query(:class_path => class_name.gsub(/^::/, ''))
23
+ end
24
+
25
+ def self.query(query={})
26
+ if query[:class_path]
27
+ return [self.class_protocols[query[:class_path]]]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,82 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # This module contains a class hierarchy that represents the type
4
+ # hierarchy in the analyzed Ruby code. This is where method responsiveness
5
+ # can be checked. Some protocols are simple, such as a ClassProtocol,
6
+ # which responds to all the methods that the corresponding Class does.
7
+ # Some might be structural or dependent or generic! The key part is that
8
+ # method signatures can be listed and queried.
9
+ module Protocols
10
+ # The base class for all protocols.
11
+ class Base
12
+ include Comparable
13
+
14
+ # Returns the list of all known signatures that this protocol responds
15
+ # to.
16
+ #
17
+ # @return [Array<Signature>] the supported signatures for this protocol.
18
+ def signatures
19
+ raise NotImplementedError.new('You must implement #signatures yourself.')
20
+ end
21
+
22
+ # Compares the protocol to another protocol by comparing their signatures
23
+ # list (sorted).
24
+ def <=>(other)
25
+ self.signatures.sort <=> other.signatures.sort
26
+ end
27
+ end
28
+
29
+ # This is a simple protocol whose signature set is just the union of the
30
+ # signature sets of its constituent protocols.
31
+ class UnionProtocol < Base
32
+ # Initializes the Union protocol to a set of constituent protocols.
33
+ #
34
+ # @param [Array<Base>] constituents the set of constituent protocols that
35
+ # this protocol is a union of.
36
+ def initialize(constituents)
37
+ @protocols = constituents
38
+ end
39
+
40
+ # Returns the list of all known signatures that this protocol responds
41
+ # to, which is the union of all the constituent protocols.
42
+ #
43
+ # @return [Array<Signature>] the supported signatures for this protocol.
44
+ def signatures
45
+ @protocols.map(&:signatures).inject(:|)
46
+ end
47
+ end
48
+
49
+ # A protocol that has the same signatures as a given class.
50
+ class ClassProtocol < Base
51
+ attr_reader :class_used
52
+
53
+ # Initializes the class protocol with the given class.
54
+ #
55
+ # @param [WoolClass] klass the wool class whose protocol we are representing
56
+ def initialize(klass)
57
+ @class_used = klass
58
+ end
59
+
60
+ # Compares two ClassProtocols. Has a short-circuit check if two protocols
61
+ # refer to the same class or not. Otherwise, it has to check the signatures.
62
+ #
63
+ # @param [Protocols::Base] other the other protocol to compare against
64
+ # @return [-1, 0, 1] a standard comparison result.
65
+ def <=>(other)
66
+ if ClassProtocol === other && self.class_used == other.class_used
67
+ then return 0
68
+ else self.signatures <=> other.signatures
69
+ end
70
+ end
71
+
72
+ # Returns all the signatures that the class responds to, since this protocol
73
+ # has the same signatures.
74
+ #
75
+ # @return [Array<Signature>] the supported signatures for this protocol.
76
+ def signatures
77
+ @class_used.signatures
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,13 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # This class models a scope in Ruby. It has a constant table,
4
+ # a self pointer, and a parent pointer to the enclosing scope.
5
+ # It also has a local variable table.
6
+ class Scope
7
+ attr_reader :constants, :self_ptr, :parent
8
+ def initialize(parent, self_ptr, constants={})
9
+ @parent, @self_ptr, @constants = parent, self_ptr, constants
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,98 @@
1
+ module Wool
2
+ # This is a set of methods that get provided to Warnings so they can perform
3
+ # parse-tree analysis of their bodies.
4
+ module SexpAnalysis
5
+ extend ModuleExtensions
6
+
7
+ # Replaces the ParseTree Sexps by adding a few handy-dandy methods.
8
+ class Sexp < Array
9
+ extend ModuleExtensions
10
+
11
+ # Returns a mutable reference to the list of annotations that will run
12
+ # upon initializing a new Sexp.
13
+ #
14
+ # @return [Array[Class < Annotation]] the activated annotations, in the
15
+ # order they will run in.
16
+ cattr_accessor_with_default :annotations, []
17
+
18
+ # Initializes the Sexp with the contents of the array returned by Ripper.
19
+ #
20
+ # @param [Array<Object>] other the other
21
+ def initialize(other)
22
+ replace other
23
+ replace_children!
24
+ self.class.annotations.each {|annotator| annotator.annotate!(self) }
25
+ end
26
+
27
+ # @return [Array<Object>] the children of the node.
28
+ def children
29
+ (Array === self[0] ? self : self[1..-1]) || []
30
+ end
31
+
32
+ # @return [Symbol] the type of the node.
33
+ def type
34
+ self[0]
35
+ end
36
+
37
+ # Replaces the children with Sexp versions of them
38
+ def replace_children!
39
+ replace(map do |x|
40
+ case x
41
+ when Array
42
+ self.class.new(x)
43
+ else x
44
+ end
45
+ end)
46
+ end
47
+ private :replace_children!
48
+
49
+ # Evaluates the constant reference/path with the given scope
50
+ # as context.
51
+ def eval_as_constant(scope)
52
+ case type
53
+ # [:var_ref, [:@const, "B", [1, 17]]]
54
+ when :var_ref then scope.constants[self[1][1]]
55
+ # [:const_ref, [:@const, "M", [1, 17]]]
56
+ when :const_ref then scope.constants[self[1][1]]
57
+ # [:top_const_ref, [:@const, "M", [1, 2]]]
58
+ when :top_const_ref then Scope::GlobalScope.constants[self[1][1]]
59
+ # [:@const, "B", [1, 7]]
60
+ when :@const then scope.constants[self[1]]
61
+ # [:const_path_ref, [:var_ref, [:@const, "B", [1, 17]]], [:@const, "M", [1, 20]]]
62
+ when :const_path_ref
63
+ left, right = children
64
+ scope = left.eval_as_constant(scope).scope
65
+ right.eval_as_constant(scope)
66
+ end
67
+ end
68
+ end
69
+
70
+ # Global annotations are only run once, at the root.
71
+ cattr_accessor_with_default :global_annotations, []
72
+
73
+ # Parses the given text.
74
+ #
75
+ # @param [String] body (self.body) The text to parse
76
+ # @return [Sexp, NilClass] the sexp representing the input text.
77
+ def parse(body = self.body)
78
+ result = Sexp.new Ripper.sexp(body)
79
+ SexpAnalysis.global_annotations.each {|annotator| annotator.annotate!(result)}
80
+ result
81
+ end
82
+
83
+ # Finds all sexps of the given type in the given Sexp tree.
84
+ #
85
+ # @param [Symbol] type the type of sexp to search for
86
+ # @param [Sexp] tree (self.parse(self.body)) The tree to search in. Leave
87
+ # blank to search the entire body.
88
+ # @return [Array<Sexp>] all sexps in the input tree (or whole body) that
89
+ # are of the given type.
90
+ def find_sexps(type, tree = self.parse(self.body))
91
+ result = tree[0] == type ? [tree] : []
92
+ tree.each do |node|
93
+ result.concat find_sexps(type, node) if node.is_a?(Array)
94
+ end
95
+ result
96
+ end
97
+ end
98
+ end