yard 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of yard might be problematic. Click here for more details.

@@ -0,0 +1,246 @@
1
+ require 'stringio'
2
+ require File.dirname(__FILE__) + '/ruby_lex'
3
+ require File.dirname(__FILE__) + '/namespace'
4
+ require File.dirname(__FILE__) + '/code_object'
5
+ require File.dirname(__FILE__) + '/handlers/all_handlers'
6
+
7
+ module YARD
8
+ ##
9
+ # Responsible for parsing a source file into the namespace
10
+ class SourceParser
11
+ attr_reader :file
12
+
13
+ def self.parse(content)
14
+ new.parse(content)
15
+ end
16
+
17
+ def self.parse_string(content)
18
+ new.parse(StringIO.new(content))
19
+ end
20
+
21
+ attr_accessor :current_namespace
22
+
23
+ def initialize
24
+ @current_namespace = NameStruct.new(Namespace.root)
25
+ end
26
+
27
+ ##
28
+ # Creates a new SourceParser that parses a file and returns
29
+ # analysis information about it.
30
+ #
31
+ # @param [String, TokenList, StatementList] content the source file to parse
32
+ def parse(content = __FILE__)
33
+ case content
34
+ when String
35
+ @file = content
36
+ statements = StatementList.new(IO.read(content))
37
+ when TokenList
38
+ statements = StatementList.new(content)
39
+ when StatementList
40
+ statements = content
41
+ else
42
+ if content.respond_to? :read
43
+ statements = StatementList.new(content.read)
44
+ else
45
+ raise ArgumentError, "Invalid argument for SourceParser::parse: #{content.inspect}:#{content.class}"
46
+ end
47
+ end
48
+
49
+ top_level_parse(statements)
50
+ end
51
+
52
+ private
53
+ def top_level_parse(statements)
54
+ statements.each do |stmt|
55
+ find_handlers(stmt).each do |handler|
56
+ handler.new(self, stmt).process
57
+ end
58
+ end
59
+ end
60
+
61
+ def find_handlers(stmt)
62
+ CodeObjectHandler.subclasses.find_all {|sub| sub.handles? stmt.tokens }
63
+ end
64
+ end
65
+
66
+ class StatementList < Array
67
+ include RubyToken
68
+
69
+ # The following list of tokens will require a block to be opened
70
+ # if used at the beginning of a statement.
71
+ @@open_block_tokens = [TkCLASS, TkDEF, TkMODULE, TkUNTIL,
72
+ TkIF, TkUNLESS, TkWHILE, TkFOR, TkCASE]
73
+
74
+ ##
75
+ # Creates a new statement list
76
+ #
77
+ # @param [TokenList, String] content the tokens to create the list from
78
+ def initialize(content)
79
+ if content.is_a? TokenList
80
+ @tokens = content
81
+ elsif content.is_a? String
82
+ parse_tokens(content)
83
+ else
84
+ raise ArgumentError, "Invalid content for StatementList: #{content.inspect}:#{content.class}"
85
+ end
86
+
87
+ parse_statements
88
+ end
89
+
90
+ private
91
+ def parse_tokens(content)
92
+ @tokens = TokenList.new
93
+ lex = RubyLex.new(content)
94
+ while tk = lex.token do @tokens << tk end
95
+ end
96
+
97
+ def parse_statements
98
+ while stmt = next_statement do self << stmt end
99
+ end
100
+
101
+ # MUST REFACTOR THIS CODE
102
+ # WARNING WARNING WARNING WARNING
103
+ # MUST REFACTOR THIS CODE |
104
+ # OR CHILDREN WILL DIE V
105
+ # WARNING WARNING WARNING WARNING
106
+ # THIS IS MEANT TO BE UGLY.
107
+ def next_statement
108
+ statement, block, comments = TokenList.new, nil, nil
109
+ stmt_number, level = 0, 0
110
+ new_statement, open_block = true, false
111
+ last_tk, before_last_tk = nil, nil
112
+ open_parens = 0
113
+
114
+ while tk = @tokens.shift
115
+ #p tk.class
116
+ open_parens += 1 if [TkLPAREN, TkLBRACK].include? tk.class
117
+ open_parens -= 1 if [TkRPAREN, TkRBRACK].include?(tk.class) if open_parens > 0
118
+
119
+ # raise block.to_s + " TOKEN #{tk.inspect}" if open_parens < 0
120
+
121
+ # Get the initial comments
122
+ if statement.empty?
123
+ # Two new-lines in a row will destroy any comment blocks
124
+ if tk.class == TkCOMMENT && last_tk.class == TkNL &&
125
+ (before_last_tk && (before_last_tk.class == TkNL || before_last_tk.class == TkSPACE))
126
+ comments = nil
127
+ elsif tk.class == TkCOMMENT
128
+ # Remove the "#" and up to 1 space before the text
129
+ # Since, of course, the convention is to have "# text"
130
+ # and not "#text", which I deem ugly (you heard it here first)
131
+ comments ||= []
132
+ comments << (tk.text[/^#+\s{0,1}(\s*[^\s#].+)/, 1] || "")
133
+ comments.pop if comments.size == 1 && comments.first =~ /^\s*$/
134
+ end
135
+ end
136
+
137
+ # Ignore any initial comments or whitespace
138
+ unless statement.empty? && [TkSPACE, TkNL, TkCOMMENT].include?(tk.class)
139
+ # Decrease if end or '}' is seen
140
+ level -= 1 if [TkEND, TkRBRACE].include?(tk.class)
141
+
142
+ # If the level is greater than 0, add the code to the block text
143
+ # otherwise it's part of the statement text
144
+ if stmt_number > 0
145
+ block ||= TokenList.new
146
+ block << tk
147
+ elsif stmt_number == 0 && tk.class != TkNL && tk.class != TkCOMMENT
148
+ statement << tk
149
+ end
150
+
151
+ # puts "#{tk.line_no} #{level} #{tk} \t#{tk.text} #{tk.lex_state}"
152
+
153
+ # Increase level if we have a 'do' or block opening
154
+ if tk.class == TkLBRACE
155
+ level += 1
156
+ elsif [TkDO, TkfLBRACE, TkBEGIN].include?(tk.class)
157
+ #p "#{tk.line_no} #{level} #{tk} \t#{tk.text} #{tk.lex_state}"
158
+ level += 1
159
+ open_block = false # Cancel our wish to open a block for the if, we're doing it now
160
+ end
161
+
162
+ # Vouch to open a block when this statement would otherwise end
163
+ open_block = true if (new_statement || (last_tk && last_tk.lex_state == EXPR_BEG)) && @@open_block_tokens.include?(tk.class)
164
+
165
+ # Check if this token creates a new statement or not
166
+ #puts "#{open_parens} open brackets for: #{statement.to_s}"
167
+ if open_parens == 0 && ([TkSEMICOLON, TkNL, TkEND_OF_SCRIPT].include?(tk.class) ||
168
+ (statement.first.class == TkDEF && tk.class == TkRPAREN))
169
+ # Make sure we don't have any running expressions
170
+ # This includes things like
171
+ #
172
+ # class <
173
+ # Foo
174
+ #
175
+ # if a ||
176
+ # b
177
+ if [EXPR_END, EXPR_ARG].include? last_tk.lex_state
178
+ stmt_number += 1
179
+ new_statement = true
180
+ #p "NEW STATEMENT #{statement.to_s}"
181
+
182
+ # The statement started with a if/while/begin, so we must go to the next level now
183
+ if open_block
184
+ open_block = false
185
+ level += 1
186
+ end
187
+ end
188
+ elsif tk.class != TkSPACE
189
+ new_statement = false
190
+ end
191
+
192
+ # Else keyword is kind of weird
193
+ if tk.is_a? RubyToken::TkELSE
194
+ new_statement = true
195
+ stmt_number += 1
196
+ open_block = false
197
+ end
198
+
199
+ # We're done if we've ended a statement and we're at level 0
200
+ break if new_statement && level == 0
201
+ end
202
+
203
+ before_last_tk = last_tk
204
+ last_tk = tk # Save last token
205
+ end
206
+
207
+ # Return the code block with starting token and initial comments
208
+ # If there is no code in the block, return nil
209
+ comments = comments.compact if comments
210
+ statement.empty? ? nil : Statement.new(statement, block, comments)
211
+ end
212
+ end
213
+
214
+ class TokenList < Array
215
+ def to_s
216
+ collect {|t| t.text }.join
217
+ end
218
+ end
219
+
220
+ class Statement
221
+ attr_reader :tokens, :comments, :block
222
+
223
+ def initialize(tokens, block = nil, comments = nil)
224
+ @tokens = clean_tokens(tokens)
225
+ @block = block
226
+ @comments = comments
227
+ end
228
+
229
+ private
230
+ def clean_tokens(tokens)
231
+ last_tk = nil
232
+ tokens.reject do |tk|
233
+ tk.is_a?(RubyToken::TkNL) ||
234
+ (last_tk.is_a?(RubyToken::TkSPACE) &&
235
+ last_tk.class == tk.class) && last_tk = tk
236
+ end
237
+ end
238
+ end
239
+
240
+ class NameStruct
241
+ attr_accessor :object, :attributes
242
+ def initialize(object)
243
+ @object, @attributes = object, { :visibility => :public, :scope => :instance }
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,162 @@
1
+ module YARD
2
+ ##
3
+ # Holds all the registered meta tags. If you want to extend YARD and add
4
+ # a new meta tag, you can do it in one of two ways.
5
+ #
6
+ # == Method #1
7
+ # Write your own +tagname_tag+ method that takes the raw text as a parameter.
8
+ # Example:
9
+ # def mytag_tag(text)
10
+ # Tag.parse_tag("mytag", text)
11
+ # end
12
+ #
13
+ # This will allow you to use @mytag TEXT to add meta data to classes through
14
+ # the docstring. {Tag} has a few convenience factory methods to create
15
+ #
16
+ # == Method #2
17
+ # Use {TagLibrary::define_tag!} to define a new tag by passing the tag name
18
+ # and the factory method to use when creating the tag. These definitions will
19
+ # be auto expanded into ruby code similar to what is shown in method #1. If you
20
+ # do not provide a factory method to use, it will default to {Tag::parse_tag}
21
+ # Example:
22
+ # define_tag! :param, :with_types_and_name
23
+ # define_tag! :author
24
+ #
25
+ # The first line will expand to the code:
26
+ # def param_tag(text) Tag.parse_tag_with_types_and_name(text) end
27
+ #
28
+ # The second line will expand to:
29
+ # def author_tag(text) Tag.parse_tag(text) end
30
+ #
31
+ # @see TagLibrary::define_tag!
32
+ module TagLibrary
33
+ class << self
34
+ ##
35
+ # Convenience method to define a new tag using one of {Tag}'s factory methods, or the
36
+ # regular {Tag::parse_tag} factory method if none is supplied.
37
+ #
38
+ # @param tag the tag name to create
39
+ # @param meth the {Tag} factory method to call when creating the tag
40
+ def self.define_tag!(tag, meth = "")
41
+ meth = meth.to_s
42
+ send_name = meth.empty? ? "" : "_" + meth
43
+ class_eval "def #{tag}_tag(text) Tag.parse_tag#{send_name}(#{tag.inspect}, text) end"
44
+ end
45
+
46
+ define_tag! :param, :with_types_and_name
47
+ define_tag! :yieldparam, :with_types_and_name
48
+ define_tag! :yield
49
+ define_tag! :return, :with_types
50
+ define_tag! :deprecated
51
+ define_tag! :author
52
+ define_tag! :raise, :with_name
53
+ define_tag! :see
54
+ define_tag! :since
55
+ define_tag! :version
56
+ end
57
+ end
58
+
59
+ class Tag
60
+ attr_reader :tag_name, :text, :types, :name
61
+
62
+ class << self
63
+ ##
64
+ # Parses tag text and creates a new tag with descriptive text
65
+ #
66
+ # @param tag_name the name of the tag to parse
67
+ # @param [String] text the raw tag text
68
+ # @return [Tag] a tag object with the tag_name and text values filled
69
+ def parse_tag(tag_name, text)
70
+ new(tag_name, text)
71
+ end
72
+
73
+ ##
74
+ # Parses tag text and creates a new tag with a key name and descriptive text
75
+ #
76
+ # @param tag_name the name of the tag to parse
77
+ # @param [String] text the raw tag text
78
+ # @return [Tag] a tag object with the tag_name, name and text values filled
79
+ def parse_tag_with_name(tag_name, text)
80
+ name, text = *extract_name_from_text(text)
81
+ new(tag_name, text, nil, name)
82
+ end
83
+
84
+ ##
85
+ # Parses tag text and creates a new tag with formally declared types and
86
+ # descriptive text
87
+ #
88
+ # @param tag_name the name of the tag to parse
89
+ # @param [String] text the raw tag text
90
+ # @return [Tag] a tag object with the tag_name, types and text values filled
91
+ def parse_tag_with_types(tag_name, text)
92
+ types, text = *extract_types_from_text(text)
93
+ new(tag_name, text, types)
94
+ end
95
+
96
+ ##
97
+ # Parses tag text and creates a new tag with formally declared types, a key
98
+ # name and descriptive text
99
+ #
100
+ # @param tag_name the name of the tag to parse
101
+ # @param [String] text the raw tag text
102
+ # @return [Tag] a tag object with the tag_name, name, types and text values filled
103
+ def parse_tag_with_types_and_name(tag_name, text)
104
+ types, text = *extract_types_from_text(text)
105
+ name, text = *extract_name_from_text(text)
106
+ new(tag_name, text, types, name)
107
+ end
108
+
109
+ ##
110
+ # Extracts the name from raw tag text returning the name and remaining value
111
+ #
112
+ # @param [String] text the raw tag text
113
+ # @return [Array] an array holding the name as the first element and the
114
+ # value as the second element
115
+ def extract_name_from_text(text)
116
+ text.strip.split(" ", 2)
117
+ end
118
+
119
+ ##
120
+ # Extracts the type signatures from the raw tag text
121
+ #
122
+ # @param [String] text the raw tag text
123
+ # @return [Array] an array holding the value as the first element and
124
+ # the array of types as the second element
125
+ def extract_types_from_text(text)
126
+ types, text = [], text.strip
127
+ if text =~ /^\s*\[(.+?)\]\s*(.*)/
128
+ text, types = $2, $1.split(",").collect {|e| e.strip }
129
+ end
130
+ [types, text]
131
+ end
132
+ end
133
+
134
+ ##
135
+ # Creates a new tag object with a tag name and text. Optionally, formally declared types
136
+ # and a key name can be specified.
137
+ #
138
+ # Types are mainly for meta tags that rely on type information, such as +param+, +return+, etc.
139
+ #
140
+ # Key names are for tags that declare meta data for a specific key or name, such as +param+,
141
+ # +raise+, etc.
142
+ #
143
+ # @param tag_name the tag name to create the tag for
144
+ # @param [String] text the descriptive text for this tag
145
+ # @param [Array<String>] types optional type list of formally declared types
146
+ # for the tag
147
+ # @param [String] name optional key name which the tag refers to
148
+ def initialize(tag_name, text, types = nil, name = nil)
149
+ @tag_name, @text, @types, @name = tag_name.to_s, text, types, name
150
+ end
151
+
152
+ ##
153
+ # Convenience method to access the first type specified. This should mainly
154
+ # be used for tags that only specify one type.
155
+ #
156
+ # @see #types
157
+ # @return (String) the first of the list of specified types
158
+ def type
159
+ types.first
160
+ end
161
+ end
162
+ end
data/lib/tag_type.rb ADDED
@@ -0,0 +1,155 @@
1
+ module YARD
2
+ ##
3
+ # Represents a tag and its respective tag name and value.
4
+ #
5
+ # @abstract Override this class to using the class name format of
6
+ # 'tagnameTag' to add a new tag to the registered tag library
7
+ class BaseTag
8
+ class << self
9
+ ##
10
+ # Returns the tag name that the class responds to. If the class name
11
+ # does not accurately represent the tag name, this method should be overrided
12
+ # by subclasses to return the correct tag name.
13
+ #
14
+ # @return [String] the tag name the class represents
15
+ def tag_name
16
+ @tag_name ||= self.to_s.split("::").last.gsub(/^Base.+|Tag$/, '').downcase
17
+ end
18
+
19
+ ##
20
+ # Return all valid tags in the tag library
21
+ #
22
+ # @return [Array<String>] a list of valid tags that are registered
23
+ # as being handled
24
+ def tag_library
25
+ @@tag_library || {}
26
+ end
27
+
28
+ ##
29
+ # @override
30
+ def inherited(subclass)
31
+ @@tag_library ||= {}
32
+ @@tag_library[subclass.tag_name] = subclass unless subclass.tag_name.empty?
33
+ end
34
+
35
+ ##
36
+ # Parses tag text from a doc string and returns a new tag
37
+ def parse_tag(text)
38
+ new(text)
39
+ end
40
+ end
41
+
42
+ ## All tags have an optional field for text data
43
+ attr_reader :text
44
+
45
+ def initialize(text)
46
+ @text = text
47
+ end
48
+
49
+ ##
50
+ # @see BaseTag::tag_name
51
+ def tag_name
52
+ self.class.tag_name
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Similar to the {BaseTag}, this class allows for a tag to
58
+ # formally specify type data if it depends on a specific object type.
59
+ #
60
+ class BaseTypeTag < BaseTag
61
+ ##
62
+ # Attribute reader for all specified types
63
+ #
64
+ # @return [String] the object types that the tag formally specifies
65
+ attr_reader :types
66
+
67
+ ##
68
+ # Extracts the type signatures from the raw tag text
69
+ #
70
+ # @param [String] text the raw tag text
71
+ # @return [Array] an array holding the value as the first element and
72
+ # the array of types as the second element
73
+ def self.extract_types_and_text(text)
74
+ types, value = [], text.strip
75
+ if text =~ /^\s*\[(.+?)\]\s*(.*)/
76
+ value = $2
77
+ types = $1.split(",").collect {|e| e.strip }
78
+ end
79
+ [value, types]
80
+ end
81
+
82
+ ##
83
+ # Extracts the name from raw tag text returning the name and remaining value
84
+ #
85
+ # @param [String] text the raw tag text
86
+ # @return [Array] an array holding the name as the first element and the
87
+ # value as the second element
88
+ def self.extract_name_and_text(text)
89
+ text.strip.split(" ", 2)
90
+ end
91
+
92
+ ##
93
+ # Create a new object with formally specified types
94
+ #
95
+ # @param [String] text the text data
96
+ # @param [Array<String>] types the types that are formally specified
97
+ # by the tag.
98
+ def initialize(text, types = [])
99
+ super(text)
100
+ @types = types || []
101
+ end
102
+
103
+ ##
104
+ # Convenience method to access the first type specified. This should mainly
105
+ # be used for tags that only specify one type.
106
+ #
107
+ # @return (String) the first of the list of specified types
108
+ def type
109
+ types.first
110
+ end
111
+ end
112
+
113
+ class ParamTag < BaseTypeTag
114
+ attr_reader :name
115
+
116
+ ##
117
+ # Parses a typed tag's value section and returns a new {ParamTag} object
118
+ #
119
+ # @param [String] text the raw tag value to parse type specifications from
120
+ # @return [ParamTag] the new typed tag object with the type values
121
+ # parsed from raw text
122
+ def self.parse_tag(text)
123
+ desc, types = *extract_types_and_text(text)
124
+ name, desc = *extract_name_and_text(desc)
125
+ new(name, desc, types)
126
+ end
127
+
128
+ ##
129
+ # Create a new +param+ tag with a description and declared types
130
+ #
131
+ # @param [String] name the parameter name specified by the tag
132
+ # @param [String] text a description of the parameter
133
+ # @param [Array<String>] types the optional types that the parameter accepts
134
+ def initialize(name, text, types = [])
135
+ super(text, types)
136
+ @name = name
137
+ end
138
+ end
139
+
140
+ class ReturnTag < BaseTypeTag
141
+ def self.parse_tag(text)
142
+ new *extract_types_and_text(text)
143
+ end
144
+
145
+ def initialize(text, types = []) super end
146
+ end
147
+
148
+ class DeprecatedTag < BaseTag
149
+ def initialize(text) super end
150
+ end
151
+
152
+ class AuthorTag < BaseTag
153
+ def initialize(text) super end
154
+ end
155
+ end