docrb 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class CommentParser
5
+ # TextBlock represents an array of characters to be built into a single
6
+ # text block.
7
+ class TextBlock
8
+ def initialize(text = nil)
9
+ @buffer = [text].compact
10
+ end
11
+
12
+ def <<(obj)
13
+ @buffer << obj
14
+ @text = nil
15
+ end
16
+
17
+ def empty?
18
+ @buffer.empty?
19
+ end
20
+
21
+ def match?(regexp)
22
+ regexp.match? text
23
+ end
24
+
25
+ def last
26
+ @buffer.last
27
+ end
28
+
29
+ def text
30
+ @text ||= @buffer.join
31
+ end
32
+
33
+ def subs!(index)
34
+ @text = nil
35
+ @buffer = @buffer[index...]
36
+ end
37
+
38
+ def to_h
39
+ { type: :text_block, contents: text.gsub(/\n /, " ") }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ # CommentParser implements a small parser for matching comment's contents to
5
+ # relevant references and annotations.
6
+ class CommentParser
7
+ COMMENT_METHOD_REF_REGEXP = /(?:([A-Z][a-zA-Z0-9_]*::)*([A-Z][a-zA-Z0-9_]*))?(::|\.|#)([A-Za-z_][a-zA-Z0-9_@]*[!?]?)(?:\([a-zA-Z0-9=_,\s*]+\))?/
8
+ COMMENT_SYMBOL_REGEXP = /:(!|[@$][a-z_][a-z0-9_]*|[a-z_][a-z0-9_]*|[a-z_][a-z0-9_]*[?!]?)/i
9
+ CAMELCASE_IDENTIFIER_REGEXP = /[A-Z][a-z]+(?:[A-Z][a-z]+)+/
10
+ INTERNAL_ANNOTATION_REGEXP = /^internal:/i
11
+ PUBLIC_ANNOTATION_REGEXP = /^public:/i
12
+ PRIVATE_ANNOTATION_REGEXP = /^private:/i
13
+ DEPRECATED_ANNOTATION_REGEXP = /^deprecated:/i
14
+ VISIBILITY_ANNOTATIONS = {
15
+ internal: INTERNAL_ANNOTATION_REGEXP,
16
+ public: PUBLIC_ANNOTATION_REGEXP,
17
+ private: PRIVATE_ANNOTATION_REGEXP,
18
+ depreacated: DEPRECATED_ANNOTATION_REGEXP
19
+ }.freeze
20
+ CARRIAGE = "\r"
21
+ LINE_BREAK = "\n"
22
+ SPACE = " "
23
+ DASH = "-"
24
+
25
+ autoload :TextBlock, "docrb/comment_parser/text_block"
26
+ autoload :FieldListParser, "docrb/comment_parser/field_list_parser"
27
+ autoload :CodeExampleParser, "docrb/comment_parser/code_example_parser"
28
+ autoload :FieldBlock, "docrb/comment_parser/field_block"
29
+ autoload :CodeExampleBlock, "docrb/comment_parser/code_example_block"
30
+
31
+ # Parses a given comment for a given object type.
32
+ #
33
+ # type: - Type of the object's to which the comment data belongs to
34
+ # comment: - A string containing the object's documentation.
35
+ #
36
+ # Returns a Hash containing the parsed content for the comment.
37
+ def self.parse(type:, comment:)
38
+ new(type).parse(comment)
39
+ end
40
+
41
+ # Internal: Initializes a new parser with a provided type
42
+ #
43
+ # type - Type of the object being documented
44
+ def initialize(type)
45
+ @type = type
46
+ @components = []
47
+ @current = TextBlock.new
48
+ @last_char = nil
49
+ @current_char = nil
50
+ @meta = {}
51
+ end
52
+
53
+ # Intenral: Loads the provided data into the parser and executes all steps
54
+ # required to extract relevant information and annotations.
55
+ #
56
+ # data - String containing the comment being parsed
57
+ #
58
+ # Returns a Hash containing the parsed content for the comment.
59
+ def parse(data)
60
+ load(data)
61
+ coalesce_field_list
62
+ detect_code_example
63
+
64
+ infer_visibility_from_doc if %i[def defs].include? @type
65
+
66
+ data = to_h
67
+ data[:contents] = detect_references(data[:contents])
68
+ data
69
+ end
70
+
71
+ # Internal: Attempts to infer an object visiblity based on the comment's
72
+ # prefix. Supported visibility options are `public:`, `private:`, and
73
+ # `internal:`. Annotations are case-insensitive.
74
+ # This method also removes the detected annotation from the comment block,
75
+ # to reduce clutter in the emitted documentation.
76
+ def infer_visibility_from_doc
77
+ return if @components.empty?
78
+
79
+ item = @components.first
80
+ return unless item.is_a? TextBlock
81
+ return unless (annotation = VISIBILITY_ANNOTATIONS.find { |_k, v| v.match?(item.text) })
82
+
83
+ item.subs! item.text.index(":") + 1
84
+ @meta[:doc_visibility_annotation] = annotation.first
85
+ end
86
+
87
+ # Internal: Commits the current text block being processed by #load
88
+ def commit_current!
89
+ @components << @current if @current && !@current.empty?
90
+ @current = TextBlock.new
91
+ end
92
+
93
+ # Internal: Loads a given stirng into the parser, by splitting it into
94
+ # text blocks.
95
+ def load(text)
96
+ text.each_char do |c|
97
+ @last_char = @current_char
98
+ @current_char = c
99
+ next if c == CARRIAGE
100
+
101
+ if @last_char == LINE_BREAK && (c == LINE_BREAK)
102
+ commit_current!
103
+ next
104
+ end
105
+ @current << c
106
+ end
107
+ commit_current!
108
+ end
109
+
110
+ # Internal: Attempts to find and split field lists from the loaded comment.
111
+ def coalesce_field_list
112
+ return if @field_list_coalesced
113
+
114
+ @field_list_coalesced = true
115
+
116
+ new_contents = []
117
+ has_field_list = false
118
+
119
+ @components.each do |block|
120
+ unless has_field_list
121
+ parser = FieldListParser.new(block.text)
122
+ if parser.detect
123
+ has_field_list = true
124
+ new_contents << FieldBlock.new(parser.result)
125
+ next
126
+ end
127
+ end
128
+ new_contents << block
129
+ end
130
+
131
+ @components = new_contents
132
+ end
133
+
134
+ # Internal: Attempts to find and extract code examples contained within the
135
+ # comment
136
+ def detect_code_example
137
+ return if @code_examples_detected
138
+
139
+ @code_examples_detected = true
140
+
141
+ @components = CodeExampleParser.process(@components)
142
+ end
143
+
144
+ # Internal: Attempts to detect references on a provided list of blocks
145
+ #
146
+ # on - List of blocks to process
147
+ #
148
+ # Returns an updated list of blocks with references transformed into
149
+ # specialised structures.
150
+ def detect_references(on)
151
+ new_contents = []
152
+
153
+ on.each do |block|
154
+ case block[:type]
155
+ when :text_block
156
+ block[:contents] = detect_text_references(block[:contents])
157
+ when :field_block
158
+ block[:contents] = detect_field_references(block[:contents])
159
+ end
160
+ new_contents << block
161
+ end
162
+
163
+ new_contents
164
+ end
165
+
166
+ # Internal: Attempts to detect references on a text object.
167
+ #
168
+ # text - Text data to have references transformed into specialised objects
169
+ #
170
+ # Returns an array containing the updated text data
171
+ def detect_text_references(text)
172
+ changed = true
173
+ text, changed = update_next_reference(text) while changed
174
+ text
175
+ end
176
+
177
+ # Internal: Attempts to detect field references on a given field object.
178
+ #
179
+ # Returns a new hash containing the field's data post-processed with
180
+ # reference annotations
181
+ def detect_field_references(field)
182
+ field
183
+ .transform_values do |v|
184
+ { type: :text_block, contents: detect_text_references(v) }
185
+ end
186
+ end
187
+
188
+ def process_comment_method_reference(contents, match)
189
+ class_path, target, invocation, name = match.to_a.slice(1...)
190
+ class_path&.gsub!(/::$/, "")
191
+ reference_type = invocation == "#" ? :method : :ambiguous
192
+ begin_at = match.begin(0)
193
+ end_at = match.end(0)
194
+ left_slice = contents[0...begin_at]
195
+ right_slice = contents[end_at...]
196
+ item = {
197
+ type: :ref,
198
+ ref_type: reference_type,
199
+ name:,
200
+ target:,
201
+ class_path:,
202
+ contents: match[0]
203
+ }
204
+ [
205
+ { type: :span, contents: left_slice },
206
+ item,
207
+ { type: :span, contents: right_slice }
208
+ ].reject { |i| i[:contents].empty? }
209
+ end
210
+
211
+ def process_simple_symbol(type, contents, match)
212
+ begin_at = match.begin(0)
213
+ end_at = match.end(0)
214
+ left_slice = contents[0...begin_at]
215
+ right_slice = contents[end_at...]
216
+ [
217
+ { type: :span, contents: left_slice },
218
+ { type:, contents: match[0] },
219
+ { type: :span, contents: right_slice }
220
+ ].reject { |i| i[:contents].empty? }
221
+ end
222
+
223
+ def update_string_reference(contents)
224
+ updated = false
225
+ if (match = COMMENT_METHOD_REF_REGEXP.match(contents))
226
+ contents = process_comment_method_reference(contents, match)
227
+ updated = true
228
+ elsif (match = COMMENT_SYMBOL_REGEXP.match(contents))
229
+ contents = process_simple_symbol(:sym_ref, contents, match)
230
+ updated = true
231
+ elsif (match = CAMELCASE_IDENTIFIER_REGEXP.match(contents))
232
+ contents = process_simple_symbol(:camelcase_identifier, contents, match)
233
+ updated = true
234
+ end
235
+ [contents, updated]
236
+ end
237
+
238
+ # Internal: Recursivelly updates a given value until all references are
239
+ # processed.
240
+ #
241
+ # contents - Contents to be processed
242
+ #
243
+ # Returns a new array containing the processed contents
244
+ def update_next_reference(contents)
245
+ return update_string_reference(contents) if contents.is_a? String
246
+
247
+ new_contents = []
248
+ changed = false
249
+ contents.each do |block|
250
+ if block[:type] == :span
251
+ updated, ok = update_next_reference(block[:contents])
252
+ if ok
253
+ changed = true
254
+ next new_contents.append(*updated)
255
+ end
256
+ end
257
+ new_contents << block
258
+ end
259
+
260
+ [new_contents, changed]
261
+ end
262
+
263
+ # Public: Returns a hash by merging contents and metadata obtained through
264
+ # the parsing process.
265
+ def to_h
266
+ @meta.merge({
267
+ type: @type,
268
+ contents: @components.map(&:to_h)
269
+ })
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ class BaseContainer
6
+ # Module Computations implements utility methods to handle hierarchy and
7
+ # nesting.
8
+ module Computations
9
+ # Recursively computes all dependants of the receiver.
10
+ # This method expands all references into concrete representations.
11
+ def compute_dependants
12
+ return if @compute_dependants
13
+
14
+ @compute_dependants = true
15
+
16
+ classes.each(&:compute_dependants)
17
+ modules.each(&:compute_dependants)
18
+
19
+ extends.each do |ref|
20
+ resolve_ref(ref)&.compute_dependants
21
+ end
22
+ includes.each do |ref|
23
+ resolve_ref(ref)&.compute_dependants
24
+ end
25
+
26
+ return unless (parent_name = @inherits)
27
+
28
+ @parent_class = resolve(parent_name)
29
+ return unless @parent_class
30
+
31
+ @parent_class.compute_dependants
32
+ end
33
+
34
+ # Returns a Hash containing all methods for the container, along with
35
+ # inherited and included ones.
36
+ def merged_instance_methods
37
+ return @merged_instance_methods if @merged_instance_methods
38
+
39
+ compute_dependants
40
+
41
+ methods = {}
42
+ @parent_class&.merged_instance_methods&.each do |k, v|
43
+ methods[k] = { source: :inheritance, definition: v }
44
+ end
45
+
46
+ includes.each do |ref|
47
+ next unless (container = resolve_container(ref))
48
+
49
+ container.merged_instance_methods.each do |k, v|
50
+ methods[k] = { source: :inclusion, definition: v, overriding: methods[k] }
51
+ end
52
+ end
53
+
54
+ defs.each do |m|
55
+ methods[m.name] = { source: :source, definition: m, overriding: methods[m.name] }
56
+ end
57
+
58
+ methods
59
+ end
60
+
61
+ # Returns a hash containing all class methods for the container, along
62
+ # with inherited and extended ones.
63
+ def merged_class_methods
64
+ return @merged_class_methods if @merged_class_methods
65
+
66
+ compute_dependants
67
+
68
+ methods = {}
69
+ @parent_class&.merged_class_methods&.each do |k, v|
70
+ methods[k] = { source: :inheritance, definition: v }
71
+ end
72
+
73
+ includes.each do |ref|
74
+ next unless (container = resolve_container(ref))
75
+
76
+ container.merged_class_methods.each do |k, v|
77
+ methods[k] = { source: :extension, definition: v, overriding: methods[k] }
78
+ end
79
+ end
80
+
81
+ sdefs.each do |m|
82
+ methods[m.name] = { source: :source, definition: m, overriding: methods[m.name] }
83
+ end
84
+
85
+ methods
86
+ end
87
+
88
+ # Returns a hash containing all attributes for the container, along with
89
+ # inherited ones.
90
+ def merged_attributes
91
+ return @merged_attributes if @merged_attributes
92
+
93
+ compute_dependants
94
+
95
+ attrs = {}
96
+ @parent_class&.merged_attributes&.each do |k, v|
97
+ attrs[k] = { source: :inheritance, definition: v }
98
+ end
99
+
100
+ attributes.each do |m|
101
+ attrs[m.name] = { source: :source, definition: m, overriding: attrs[m.name] }
102
+ end
103
+
104
+ attrs
105
+ end
106
+
107
+ # Deprecated: Use #merged_instance_methods.
108
+ def all_defs
109
+ return @all_defs if @all_defs
110
+
111
+ compute_dependants
112
+
113
+ methods = {}
114
+ @parent_class&.all_defs&.each do |k, v|
115
+ methods[k] = v
116
+ end
117
+
118
+ includes.each do |ref|
119
+ resolve_ref(ref)&.all_defs&.each do |met|
120
+ if methods.key? met.name
121
+ methods[key].override! met
122
+ next
123
+ end
124
+
125
+ methods[met.name] = met
126
+ end
127
+ end
128
+
129
+ defs.each do |met|
130
+ if methods.key? met.name
131
+ methods[met.name].override! met
132
+ next
133
+ end
134
+
135
+ methods[met.name] = met
136
+ end
137
+
138
+ @all_defs = methods
139
+ end
140
+
141
+ # Deprecated: Use #merged_class_methods.
142
+ def all_sdefs
143
+ return @all_sdefs if @all_sdefs
144
+
145
+ compute_dependants
146
+
147
+ methods = {}
148
+
149
+ @parent_class&.all_sdefs&.each do |k, v|
150
+ methods[k] = v
151
+ end
152
+
153
+ extends.each do |ref|
154
+ resolve_ref(ref)&.all_defs&.each do |met|
155
+ if methods.key? met.name
156
+ methods[met.name].override! met
157
+ next
158
+ end
159
+
160
+ methods[met.name] = met
161
+ end
162
+ end
163
+
164
+ @sdefs.each do |met|
165
+ if methods.key? met.name
166
+ methods[met.name].override! met
167
+ next
168
+ end
169
+
170
+ methods[met.name] = met
171
+ end
172
+
173
+ @all_sdefs = methods
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ # BaseContainer represents a container for methods, classes, modules and
6
+ # attributes.
7
+ class BaseContainer < Resolvable
8
+ require_relative "./base_container/computations"
9
+ include Computations
10
+
11
+ attr_accessor :extends, :includes, :classes, :modules, :defs, :sdefs,
12
+ :type, :name, :defined_by, :parent, :doc
13
+
14
+ # Initialises a new BaseContainer instance with a provided parent,
15
+ # filename and represented object.
16
+ def initialize(parent, filename, obj)
17
+ super()
18
+ @parent = parent
19
+ @type = obj[:type]
20
+ @name = obj[:name]
21
+ @extends = []
22
+ @includes = []
23
+ @classes = ObjectContainer.new(self, DocClass)
24
+ @modules = ObjectContainer.new(self, DocModule)
25
+ @defs = ObjectContainer.new(self, DocMethod)
26
+ @sdefs = ObjectContainer.new(self, DocMethod)
27
+ @defined_by = ObjectContainer.new(nil, FileRef)
28
+ append(filename, obj)
29
+ end
30
+
31
+ # Appends a new object defined by the provided file to this container
32
+ def <<(filename, obj)
33
+ append(filename, obj)
34
+ end
35
+
36
+ # Appends a new class defined by the provided file to this container
37
+ def append_class(filename, cls)
38
+ @classes.push filename, cls
39
+ end
40
+
41
+ def inspect
42
+ ext = "#{extends.length} extend#{extends.length == 1 ? "" : "s"}"
43
+ inc = "#{includes.length} include#{includes.length == 1 ? "" : "s"}"
44
+ cls = "#{classes.length} class#{classes.length == 1 ? "" : "es"}"
45
+ mod = "#{modules.length} module#{modules.length == 1 ? "" : "s"}"
46
+ de = "#{defs.length} def#{defs.length == 1 ? "" : "s"}"
47
+ sde = "#{sdefs.length} sdef#{sdefs.length == 1 ? "" : "s"}"
48
+
49
+ "#<#{self.class.name}:#{format("0x%08x",
50
+ object_id * 2)} #{@type} #{@name} #{ext}, #{inc}, #{cls}, #{mod}, #{de}, #{sde}>"
51
+ end
52
+
53
+ # Attempts to append a given object defined in a provided filename.
54
+ # Raises ArgumentError in case the object can't be added to the container
55
+ # due to type contraints.
56
+ #
57
+ # filename - Filename defining the object being appended
58
+ # obj - The object definition to be appended to this container.
59
+ def append(filename, obj)
60
+ raise ArgumentError, "cannot append obj of type #{obj[:type]} into #{@name} (#{@type})" if obj[:type] != @type
61
+
62
+ raise ArgumentError, "cannot append obj named #{obj[:name]} into #{@name} (#{@type})" if obj[:name] != @name
63
+
64
+ unless (doc = obj[:doc]).nil?
65
+ @defined_by.push(filename, obj)
66
+ @doc = doc
67
+ end
68
+
69
+ obj.fetch(:classes, []).each { |c| @classes.push(filename, c) }
70
+ obj.fetch(:modules, []).each { |m| @modules.push(filename, m) }
71
+ obj.fetch(:extend, []).each { |e| @extends << e }
72
+ obj.fetch(:include, []).each { |i| @includes << i }
73
+ obj.fetch(:methods, []).each do |met|
74
+ if met[:type] == :def
75
+ @defs.push(filename, met)
76
+ else
77
+ @sdefs.push(filename, met)
78
+ end
79
+ end
80
+
81
+ @inherits = obj.fetch(:inherits, nil) if obj[:type] == :class
82
+
83
+ appended(filename, obj)
84
+ end
85
+
86
+ # Courtesy method. This method is called whenever a new object is
87
+ # appended to the container. Inheriting classes can override it to perform
88
+ # further operations on the appended object.
89
+ def appended(filename, obj); end
90
+
91
+ # Intenral: Recursively unpacks a given element by checking its
92
+ # :definition and :overriding keys.
93
+ #
94
+ # Returns the updated element.
95
+ def unpack(elem)
96
+ return unless elem
97
+
98
+ elem.tap do |e|
99
+ inner = e[:definition]
100
+ e[:definition] = inner.is_a?(Hash) ? unpack(inner) : inner.to_h
101
+ e[:overriding] = unpack(e[:overriding])
102
+ end
103
+ end
104
+
105
+ # Returns a Hash representation of the current container along with its
106
+ # children.
107
+ def to_h
108
+ {
109
+ type:,
110
+ name:,
111
+ doc: DocBlocks.prepare(doc, parent: self),
112
+ defined_by: defined_by.map(&:to_h),
113
+ defs: merged_instance_methods.transform_values { |v| unpack(v) },
114
+ sdefs: merged_class_methods.transform_values { |v| unpack(v) },
115
+ classes: classes.map(&:to_h),
116
+ modules: modules.map(&:to_h),
117
+ includes: includes.map(&:to_h),
118
+ extends: extends.map(&:to_h)
119
+ }
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ # DocAttribute represents a single attribute defined on a class.
6
+ class DocAttribute < Resolvable
7
+ attr_reader :parent, :defined_by, :name, :docs, :type, :writer_visibility,
8
+ :reader_visibility
9
+
10
+ def initialize(parent, filename, obj)
11
+ super()
12
+ @parent = parent
13
+ @defined_by = [filename]
14
+ @name = obj[:name]
15
+ @docs = obj[:docs]
16
+ @type = nil
17
+ @writer_visibility = obj[:writer_visibility]
18
+ @reader_visibility = obj[:reader_visibility]
19
+ end
20
+
21
+ # Marks the current attribute as an accessor.
22
+ #
23
+ # Returns the attribute's instance for chaining.
24
+ def accessor!
25
+ @type = :accessor
26
+ self
27
+ end
28
+
29
+ # Marks the current attribute as an reader.
30
+ #
31
+ # Returns the attribute's instance for chaining.
32
+ def reader!
33
+ @type = :reader
34
+ self
35
+ end
36
+
37
+ # Marks the current attribute as an writer.
38
+ #
39
+ # Returns the attribute's instance for chaining.
40
+ def writer!
41
+ @type = :writer
42
+ self
43
+ end
44
+
45
+ # Returns a Hash representing this attribute
46
+ def to_h
47
+ {
48
+ defined_by:,
49
+ name:,
50
+ docs:,
51
+ type:,
52
+ writer_visibility:,
53
+ reader_visibility:
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end