Skip to content

Commit f2bd262

Browse files
committed
feat(plugin): introduce YardPlugin as an example plugin
1 parent 0fa6793 commit f2bd262

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed

lib/rdoc/yard_plugin.rb

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Yard type parser is inspired by the following code:
2+
# https://github.com/lsegal/yard-types-parser/blob/master/lib/yard_types_parser.rb
3+
4+
require_relative 'base_plugin'
5+
require 'strscan'
6+
7+
module RDoc
8+
class YardPlugin < BasePlugin
9+
listens_to :rdoc_store_complete do |env, store|
10+
store.all_classes_and_modules.each do |cm|
11+
cm.each_method do |meth|
12+
puts "Parsing #{meth.name}"
13+
parsed_comment = Parser.new(meth.comment.text).parse
14+
# meth.params = parsed_comment.param.map(&:to_s).join("\n")
15+
meth.comment.text = parsed_comment.plain.join("\n")
16+
end
17+
end
18+
end
19+
20+
class Parser
21+
ParamData = Struct.new(:type, :name, :desc, keyword_init: true) do
22+
def append_desc(line)
23+
self[:desc] += line
24+
end
25+
26+
def to_s
27+
"Name: #{self[:name]}, Type: #{self[:type].map(&:to_s).join(' or ')}, Desc: #{self[:desc]}"
28+
end
29+
end
30+
ReturnData = Struct.new(:type, :desc, keyword_init: true)
31+
RaiseData = Struct.new(:type, :desc, keyword_init: true)
32+
ParsedComment = Struct.new(:param, :return, :raise, :plain)
33+
34+
TAG_PARSING_REGEXES = {
35+
param: /
36+
@param\s+
37+
(?: # Match either of the following:
38+
\[(?<type1>[^\]]+)\]\s+(?<name1>\S+)\s*(?<desc1>.*)? | # [Type] name desc
39+
(?<name2>\S+)\s+\[(?<type2>[^\]]+)\]\s*(?<desc2>.*)? # name [Type] desc
40+
)
41+
/x,
42+
return: /@return\s+\[(?<type>[^\]]+)\]\s*(?<desc>.*)?/,
43+
raise: /@raise\s+\[(?<type>[^\]]+)\]\s*(?<desc>.*)?/
44+
}
45+
def initialize(comment)
46+
@comment = comment
47+
@parsed_comment = ParsedComment.new([], nil, [], [])
48+
@mode = :initial
49+
@base_indentation_level = 0 # @comment.lines.first[/^#\s*/].size
50+
end
51+
52+
def parse
53+
@comment.each_line do |line|
54+
current_indentation_level = line[/^#\s*/]&.size || 0
55+
if current_indentation_level >= @base_indentation_level + 2
56+
# Append to the previous tag
57+
data = @mode == :param ? @parsed_comment[@mode].last : @parsed_comment[@mode]
58+
data.append_desc(line)
59+
else
60+
if (tag, matchdata = matching_any_tag(line))
61+
if tag == :param
62+
type = matchdata[:type1] || matchdata[:type2]
63+
name = matchdata[:name1] || matchdata[:name2]
64+
desc = matchdata[:desc1] || matchdata[:desc2]
65+
parsed_type = TypeParser.parse(type)
66+
@parsed_comment[:param] << ParamData.new(type: parsed_type, name: name, desc: desc)
67+
@mode = :param
68+
elsif tag == :return
69+
type = matchdata[:type]
70+
desc = matchdata[:desc]
71+
parsed_type = TypeParser.parse(type)
72+
@parsed_comment[:return] = ReturnData.new(type: parsed_type, desc: desc)
73+
@mode = :return
74+
elsif tag == :raise
75+
type = matchdata[:type]
76+
desc = matchdata[:desc]
77+
parsed_type = TypeParser.parse(type)
78+
@parsed_comment[:raise] << RaiseData.new(type: parsed_type, desc: desc)
79+
@mode = :raise
80+
end
81+
else
82+
@parsed_comment[:plain] << line
83+
end
84+
end
85+
@base_indentation_level = current_indentation_level
86+
end
87+
88+
@parsed_comment
89+
end
90+
91+
private
92+
93+
def matching_any_tag(line)
94+
TAG_PARSING_REGEXES.each do |tag, regex|
95+
matchdata = line.match(regex)
96+
return [tag, matchdata] if matchdata
97+
end
98+
nil
99+
end
100+
end
101+
102+
class Type
103+
attr_reader :name
104+
105+
def initialize(name)
106+
@name = name
107+
end
108+
109+
def to_s
110+
@name
111+
end
112+
end
113+
114+
class CollectionType < Type
115+
attr_reader :type
116+
117+
def initialize(name, type)
118+
super(name)
119+
@type = type
120+
end
121+
122+
def to_s
123+
"#{@name}<#{@type}>"
124+
end
125+
end
126+
127+
class FixedCollectionType < Type
128+
attr_reader :type
129+
130+
def initialize(name, type)
131+
super(name)
132+
@type = type
133+
end
134+
135+
def to_s
136+
"#{@name}(#{@type})"
137+
end
138+
end
139+
140+
class HashCollectionType < Type
141+
attr_reader :key_type, :value_type
142+
143+
def initialize(name, key_type, value_type)
144+
super(name)
145+
@key_type = key_type
146+
@value_type = value_type
147+
end
148+
149+
def to_s
150+
"#{@name}<#{@key_type} => #{@value_type}>"
151+
end
152+
end
153+
154+
class TypeParser
155+
TOKENS = {
156+
collection_start: /</,
157+
collection_end: />/,
158+
fixed_collection_start: /\(/,
159+
fixed_collection_end: /\)/,
160+
type_name: /#\w+|((::)?\w+)+/,
161+
literal: /(?:
162+
'(?:\\'|[^'])*' |
163+
"(?:\\"|[^"])*" |
164+
:[a-zA-Z_][a-zA-Z0-9_]*|
165+
\b(?:true|false|nil)\b |
166+
\b\d+(?:\.\d+)?\b
167+
)/x,
168+
type_next: /[,;]/,
169+
whitespace: /\s+/,
170+
hash_collection_start: /\{/,
171+
hash_collection_next: /=>/,
172+
hash_collection_end: /\}/,
173+
parse_end: nil
174+
}
175+
176+
def self.parse(string)
177+
new(string).parse
178+
end
179+
180+
def initialize(string)
181+
@scanner = StringScanner.new(string)
182+
end
183+
184+
def parse
185+
types = []
186+
type = nil
187+
fixed = false
188+
name = nil
189+
loop do
190+
found = false
191+
TOKENS.each do |token_type, match|
192+
if (match.nil? && @scanner.eos?) || (match && token = @scanner.scan(match))
193+
found = true
194+
case token_type
195+
when :type_name, :literal
196+
raise SyntaxError, "expecting END, got name '#{token}'" if name
197+
name = token
198+
when :type_next
199+
raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
200+
unless type
201+
type = Type.new(name)
202+
end
203+
types << type
204+
type = nil
205+
name = nil
206+
when :fixed_collection_start, :collection_start
207+
name ||= "Array"
208+
klass = token_type == :collection_start ? CollectionType : FixedCollectionType
209+
type = klass.new(name, parse)
210+
when :hash_collection_start
211+
name ||= "Hash"
212+
type = HashCollectionType.new(name, parse, parse)
213+
when :hash_collection_next, :hash_collection_end, :fixed_collection_end, :collection_end, :parse_end
214+
raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
215+
unless type
216+
type = Type.new(name)
217+
end
218+
types << type
219+
return types
220+
end
221+
end
222+
end
223+
raise SyntaxError, "invalid character at #{@scanner.peek(1)}" unless found
224+
end
225+
end
226+
end
227+
end
228+
end

0 commit comments

Comments
 (0)