decode 0.25.0 → 0.27.0
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +1 -1
- data/bake/decode/documentation.rb +378 -0
- data/context/documentation-coverage.md +254 -0
- data/context/getting-started.md +34 -222
- data/context/index.yaml +22 -0
- data/context/ruby-documentation.md +91 -43
- data/lib/decode/comment/constant.rb +1 -1
- data/lib/decode/comment/example.rb +62 -0
- data/lib/decode/comment/option.rb +1 -1
- data/lib/decode/comment/tag.rb +1 -0
- data/lib/decode/definition.rb +9 -1
- data/lib/decode/documentation.rb +1 -0
- data/lib/decode/language/generic.rb +2 -0
- data/lib/decode/language/ruby/alias.rb +13 -1
- data/lib/decode/language/ruby/class.rb +14 -1
- data/lib/decode/language/ruby/generic.rb +2 -0
- data/lib/decode/language/ruby/module.rb +19 -1
- data/lib/decode/language/ruby/parser.rb +115 -81
- data/lib/decode/rbs/class.rb +6 -5
- data/lib/decode/rbs/method.rb +11 -11
- data/lib/decode/rbs/module.rb +6 -6
- data/lib/decode/rbs/type.rb +4 -4
- data/lib/decode/scope.rb +1 -1
- data/lib/decode/version.rb +1 -1
- data/license.md +1 -1
- data/readme.md +27 -1
- data/releases.md +8 -0
- data.tar.gz.sig +0 -0
- metadata +7 -5
- metadata.gz.sig +0 -0
- data/context/coverage.md +0 -325
- data/context/types.md +0 -127
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65f2a545a291a4258d019ea79ea9c4cda2e4e15af778670b505ac7652d620ee3
|
|
4
|
+
data.tar.gz: fa7d2328a1eba99917c23de03569575642ae587dbc049ab4a409d84e85824c57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78ac2928c18dc95f78aa181cadca912776e288781ef3f88c0fbd507faae38884d2d66dcf73770dafeb874aff4ef69a2ecec601b6b3e097be02154fe7a06f187b
|
|
7
|
+
data.tar.gz: acef5f32fcb5b0ff49c076e706555c6eb2fbceed83abc9bd0f35557e72153fcde17fcd6cc31320eb9aee1c71fd73739554d757517b7105f8d080eee8ad71a0dc
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Y��ʇ�7MI��t�s�5�/����EB��R�t�� HX��D�bR��a��P�������[��G�A~H����k��ZEk���!�$D����uJC��wH�k�y��p�_T�#�5G+�e_T���ԍ�uT�z�N��xZ`{y�~��25�G&�r�F�Jh�g�0��P���=6���5��l����zV�d�Y���&��ft��s�#?��� d�.�Lև���qD�8<=0F!T`Q�a��\���/L�pM���Yד*&�9��b� NԜ����RH��+ ߸#o�,5a�ކ.YN��_!���U�PxM@y��N?!}3�Fz�펋��I/u��)��"6������S�@�p�u��x�`1��N�
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
def initialize(...)
|
|
9
|
+
super
|
|
10
|
+
|
|
11
|
+
require "decode/index"
|
|
12
|
+
require "yaml"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate Markdown documentation for LLM consumption.
|
|
16
|
+
# @parameter root [String] The root path to index (e.g., 'lib').
|
|
17
|
+
# @parameter output_root [String] The root output directory (e.g., 'context').
|
|
18
|
+
# @parameter name [String] The subdirectory name for the generated files (e.g., 'interface').
|
|
19
|
+
def markdown(root, output_root: "context", name: "interface")
|
|
20
|
+
index = Decode::Index.for(root)
|
|
21
|
+
|
|
22
|
+
# Construct full output directory path
|
|
23
|
+
output_directory = File.join(output_root, name)
|
|
24
|
+
|
|
25
|
+
# Track all generated files for index.yaml
|
|
26
|
+
generated_files = []
|
|
27
|
+
|
|
28
|
+
# Group definitions by container (class/module)
|
|
29
|
+
containers = {}
|
|
30
|
+
|
|
31
|
+
# First pass: collect all definitions
|
|
32
|
+
index.definitions.each do |qualified_name, definition|
|
|
33
|
+
# Skip non-public definitions
|
|
34
|
+
next unless definition.public?
|
|
35
|
+
|
|
36
|
+
# If this is a container, register it
|
|
37
|
+
if definition.container?
|
|
38
|
+
containers[qualified_name] ||= {
|
|
39
|
+
definition: definition,
|
|
40
|
+
methods: [],
|
|
41
|
+
aliases: []
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
# This is a method/attribute - add to parent container
|
|
45
|
+
if parent = definition.parent
|
|
46
|
+
# Find the containing class/module
|
|
47
|
+
container_definition = parent
|
|
48
|
+
while container_definition && !container_definition.container?
|
|
49
|
+
container_definition = container_definition.parent
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if container_definition
|
|
53
|
+
container_name = container_definition.qualified_name
|
|
54
|
+
containers[container_name] ||= {
|
|
55
|
+
definition: container_definition,
|
|
56
|
+
methods: [],
|
|
57
|
+
aliases: []
|
|
58
|
+
}
|
|
59
|
+
if definition.respond_to?(:alias?) && definition.alias?
|
|
60
|
+
containers[container_name][:aliases] << definition
|
|
61
|
+
else
|
|
62
|
+
containers[container_name][:methods] << definition
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
$stderr.puts "Found #{containers.size} containers to document"
|
|
70
|
+
|
|
71
|
+
# Generate markdown files for each container
|
|
72
|
+
containers.each do |qualified_name, data|
|
|
73
|
+
container = data[:definition]
|
|
74
|
+
# Preserve original code order as collected by the parser/index:
|
|
75
|
+
methods = data[:methods]
|
|
76
|
+
aliases = data[:aliases]
|
|
77
|
+
|
|
78
|
+
# Generate file path
|
|
79
|
+
file_path = File.join(output_directory, "#{qualified_name.gsub('::', '/')}.md")
|
|
80
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
81
|
+
|
|
82
|
+
# Generate markdown content
|
|
83
|
+
content = generate_container_markdown(container, methods, aliases)
|
|
84
|
+
|
|
85
|
+
File.write(file_path, content)
|
|
86
|
+
generated_files << {
|
|
87
|
+
path: file_path,
|
|
88
|
+
qualified_name: qualified_name,
|
|
89
|
+
kind: container.respond_to?(:container?) && container.container? ? "class/module" : "class/module"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$stderr.puts "Generated: #{file_path}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
$stderr.puts "Generated #{generated_files.size} files in #{output_directory}"
|
|
96
|
+
|
|
97
|
+
# Generate overview/index file
|
|
98
|
+
overview_path = File.join(output_root, "#{name}.md")
|
|
99
|
+
overview_content = generate_overview(name, containers, index)
|
|
100
|
+
File.write(overview_path, overview_content)
|
|
101
|
+
$stderr.puts "Generated overview: #{overview_path}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Generate markdown content for a container (class/module) and its methods.
|
|
107
|
+
# @parameter container [Decode::Definition]
|
|
108
|
+
# @parameter methods [Array]
|
|
109
|
+
# @parameter aliases [Array[Decode::Language::Ruby::Alias]]
|
|
110
|
+
def generate_container_markdown(container, methods, aliases)
|
|
111
|
+
lines = []
|
|
112
|
+
|
|
113
|
+
# Title
|
|
114
|
+
lines << "# #{container.qualified_name}"
|
|
115
|
+
lines << ""
|
|
116
|
+
|
|
117
|
+
# Summary from documentation
|
|
118
|
+
if documentation = container.documentation
|
|
119
|
+
if summary = extract_summary(documentation)
|
|
120
|
+
lines << summary
|
|
121
|
+
lines << ""
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Metadata
|
|
126
|
+
kind = case container
|
|
127
|
+
when Decode::Language::Ruby::Class
|
|
128
|
+
"Class"
|
|
129
|
+
when Decode::Language::Ruby::Module
|
|
130
|
+
"Module"
|
|
131
|
+
when Decode::Language::Ruby::Singleton
|
|
132
|
+
"Singleton"
|
|
133
|
+
else
|
|
134
|
+
"Container"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
meta_lines = ["- Kind: #{kind}"]
|
|
138
|
+
if container.respond_to?(:super_class) && container.super_class
|
|
139
|
+
meta_lines << "- Superclass: #{container.super_class}"
|
|
140
|
+
end
|
|
141
|
+
if container.respond_to?(:includes) && container.includes.any?
|
|
142
|
+
meta_lines << "- Includes: #{container.includes.join(', ')}"
|
|
143
|
+
end
|
|
144
|
+
if container.respond_to?(:extends) && container.extends.any?
|
|
145
|
+
meta_lines << "- Extends: #{container.extends.join(', ')}"
|
|
146
|
+
end
|
|
147
|
+
if container.respond_to?(:prepends) && container.prepends.any?
|
|
148
|
+
meta_lines << "- Prepends: #{container.prepends.join(', ')}"
|
|
149
|
+
end
|
|
150
|
+
if container.parent
|
|
151
|
+
meta_lines << "- Namespace: #{container.parent.qualified_name}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if meta_lines.any?
|
|
155
|
+
lines << "## Metadata"
|
|
156
|
+
lines << ""
|
|
157
|
+
lines.concat(meta_lines)
|
|
158
|
+
lines << ""
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Description
|
|
162
|
+
if documentation = container.documentation
|
|
163
|
+
if description = extract_description(documentation)
|
|
164
|
+
lines << "## Overview"
|
|
165
|
+
lines << ""
|
|
166
|
+
lines << description
|
|
167
|
+
lines << ""
|
|
168
|
+
end
|
|
169
|
+
end # Attributes
|
|
170
|
+
attributes = methods.select{|m| m.is_a?(Decode::Language::Ruby::Attribute) rescue false}
|
|
171
|
+
if attributes.any?
|
|
172
|
+
lines << "## Attributes"
|
|
173
|
+
lines << ""
|
|
174
|
+
attributes.each do |attribute|
|
|
175
|
+
lines.concat(generate_method_section(attribute))
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Methods
|
|
180
|
+
non_attributes = methods.reject{|m| m.is_a?(Decode::Language::Ruby::Attribute) rescue false}
|
|
181
|
+
if non_attributes.any?
|
|
182
|
+
lines << "## Methods"
|
|
183
|
+
lines << ""
|
|
184
|
+
non_attributes.each do |method|
|
|
185
|
+
lines.concat(generate_method_section(method, aliases))
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
lines.join("\n")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Generate markdown for a single method.
|
|
193
|
+
# Also annotates any alias names that refer to this method within the same container.
|
|
194
|
+
def generate_method_section(method, aliases = [])
|
|
195
|
+
lines = []
|
|
196
|
+
|
|
197
|
+
# Method heading
|
|
198
|
+
lines << "### `#{method.nested_name}`"
|
|
199
|
+
lines << ""
|
|
200
|
+
|
|
201
|
+
# Also known as (aliases pointing to this method)
|
|
202
|
+
if aliases && !aliases.empty?
|
|
203
|
+
alias_names = aliases.select{|a| a.old_name == method.name}.map(&:name)
|
|
204
|
+
if alias_names.any?
|
|
205
|
+
lines << "_Also known as:_ #{alias_names.map{|n| "`#{n}`"}.join(", ")}"
|
|
206
|
+
lines << ""
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Summary
|
|
211
|
+
if documentation = method.documentation
|
|
212
|
+
if summary = extract_summary(documentation)
|
|
213
|
+
lines << summary
|
|
214
|
+
lines << ""
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Signature
|
|
219
|
+
if signature = method.long_form
|
|
220
|
+
lines << "**Signature:**"
|
|
221
|
+
lines << "```ruby"
|
|
222
|
+
lines << signature
|
|
223
|
+
lines << "```"
|
|
224
|
+
lines << ""
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Parameters
|
|
228
|
+
if documentation = method.documentation
|
|
229
|
+
parameters = documentation.filter(Decode::Comment::Parameter).to_a
|
|
230
|
+
if parameters.any?
|
|
231
|
+
lines << "**Parameters:**"
|
|
232
|
+
parameters.each do |parameter|
|
|
233
|
+
parameter_text = "- `#{parameter.name}` `#{parameter.type}`"
|
|
234
|
+
if description = parameter.text&.join(" ")
|
|
235
|
+
parameter_text << " — #{description}"
|
|
236
|
+
end
|
|
237
|
+
lines << parameter_text
|
|
238
|
+
end
|
|
239
|
+
lines << ""
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Returns
|
|
243
|
+
returns = documentation.filter(Decode::Comment::Returns).to_a
|
|
244
|
+
if returns.any?
|
|
245
|
+
lines << "**Returns:**"
|
|
246
|
+
returns.each do |return_tag|
|
|
247
|
+
return_text = "- `#{return_tag.type}`"
|
|
248
|
+
if description = return_tag.text&.join(" ")
|
|
249
|
+
return_text << " — #{description}"
|
|
250
|
+
end
|
|
251
|
+
lines << return_text
|
|
252
|
+
end
|
|
253
|
+
lines << ""
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Yields
|
|
257
|
+
yields_tags = documentation.filter(Decode::Comment::Yields).to_a
|
|
258
|
+
if yields_tags.any?
|
|
259
|
+
lines << "**Yields:**"
|
|
260
|
+
yields_tags.each do |yields_tag|
|
|
261
|
+
yield_text = "- `#{yields_tag.block}`"
|
|
262
|
+
if description = yields_tag.text&.join(" ")
|
|
263
|
+
yield_text << " — #{description}"
|
|
264
|
+
end
|
|
265
|
+
lines << yield_text
|
|
266
|
+
end
|
|
267
|
+
lines << ""
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Examples
|
|
271
|
+
examples = documentation.filter(Decode::Comment::Example).to_a
|
|
272
|
+
if examples.any?
|
|
273
|
+
examples.each do |example|
|
|
274
|
+
title = example.title || "Example"
|
|
275
|
+
lines << "**#{title}:**"
|
|
276
|
+
lines << "```ruby"
|
|
277
|
+
lines << example.code if example.code
|
|
278
|
+
lines << "```"
|
|
279
|
+
lines << ""
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Description (longer text after summary)
|
|
284
|
+
if description = extract_description(documentation)
|
|
285
|
+
lines << "**Details:**"
|
|
286
|
+
lines << ""
|
|
287
|
+
lines << description
|
|
288
|
+
lines << ""
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
lines
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Extract summary (first paragraph) from documentation.
|
|
296
|
+
def extract_summary(documentation)
|
|
297
|
+
return nil unless documentation.text
|
|
298
|
+
|
|
299
|
+
lines = documentation.text
|
|
300
|
+
summary_lines = []
|
|
301
|
+
|
|
302
|
+
lines.each do |line|
|
|
303
|
+
line_str = line.to_s.strip
|
|
304
|
+
break if line_str.empty? && summary_lines.any?
|
|
305
|
+
summary_lines << line_str unless line_str.empty?
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
return nil if summary_lines.empty?
|
|
309
|
+
summary_lines.join(" ")
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Extract description (everything after summary) from documentation.
|
|
313
|
+
def extract_description(documentation)
|
|
314
|
+
return nil unless documentation.text
|
|
315
|
+
|
|
316
|
+
lines = documentation.text
|
|
317
|
+
description_lines = []
|
|
318
|
+
found_gap = false
|
|
319
|
+
|
|
320
|
+
lines.each do |line|
|
|
321
|
+
line_str = line.to_s
|
|
322
|
+
if line_str.strip.empty?
|
|
323
|
+
found_gap = true if description_lines.any?
|
|
324
|
+
elsif found_gap
|
|
325
|
+
description_lines << line_str
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
return nil if description_lines.empty?
|
|
330
|
+
description_lines.join("\n")
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Generate an overview/index file for the documentation.
|
|
334
|
+
def generate_overview(name, containers, index)
|
|
335
|
+
lines = []
|
|
336
|
+
|
|
337
|
+
lines << "# #{name.capitalize}"
|
|
338
|
+
lines << ""
|
|
339
|
+
lines << "This directory contains documentation for all public classes and modules."
|
|
340
|
+
lines << ""
|
|
341
|
+
|
|
342
|
+
# Group by top-level namespace
|
|
343
|
+
namespaces = {}
|
|
344
|
+
containers.each do |qualified_name, data|
|
|
345
|
+
parts = qualified_name.split("::")
|
|
346
|
+
top_level = parts.first
|
|
347
|
+
namespaces[top_level] ||= []
|
|
348
|
+
namespaces[top_level] << {name: qualified_name, definition: data[:definition]}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
lines << "## Namespaces"
|
|
352
|
+
lines << ""
|
|
353
|
+
|
|
354
|
+
namespaces.keys.sort.each do |namespace|
|
|
355
|
+
items = namespaces[namespace].sort_by{|item| item[:name]}
|
|
356
|
+
|
|
357
|
+
lines << "### #{namespace}"
|
|
358
|
+
lines << ""
|
|
359
|
+
|
|
360
|
+
items.each do |item|
|
|
361
|
+
definition = item[:definition]
|
|
362
|
+
relative_path = "#{name}/#{item[:name].gsub('::', '/')}.md"
|
|
363
|
+
|
|
364
|
+
if documentation = definition.documentation
|
|
365
|
+
if summary = extract_summary(documentation)
|
|
366
|
+
lines << "- [#{item[:name]}](#{relative_path}) - #{summary}"
|
|
367
|
+
next
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
lines << "- [#{item[:name]}](#{relative_path})"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
lines << ""
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
lines.join("\n")
|
|
378
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Documentation Coverage
|
|
2
|
+
|
|
3
|
+
This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks.
|
|
4
|
+
|
|
5
|
+
## Available Bake Tasks
|
|
6
|
+
|
|
7
|
+
The Decode gem provides several bake tasks for analyzing your codebase:
|
|
8
|
+
|
|
9
|
+
- `bake decode:index:coverage` - Check documentation coverage.
|
|
10
|
+
- `bake decode:index:symbols` - List all symbols in the codebase.
|
|
11
|
+
- `bake decode:index:documentation` - Extract all documentation.
|
|
12
|
+
|
|
13
|
+
## Checking Documentation Coverage
|
|
14
|
+
|
|
15
|
+
### Basic Coverage Check
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Check coverage for the lib directory:
|
|
19
|
+
bake decode:index:coverage lib
|
|
20
|
+
|
|
21
|
+
# Check coverage for a specific directory:
|
|
22
|
+
bake decode:index:coverage app/models
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Example Output
|
|
26
|
+
|
|
27
|
+
When you run the coverage command, you'll see output like:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Decode
|
|
31
|
+
Decode::VERSION
|
|
32
|
+
Decode::Languages.all
|
|
33
|
+
Decode::Languages#initialize
|
|
34
|
+
Decode::Languages#freeze
|
|
35
|
+
Decode::Languages#add
|
|
36
|
+
Decode::Languages#fetch
|
|
37
|
+
Decode::Languages#source_for
|
|
38
|
+
Decode::Languages::REFERENCE
|
|
39
|
+
Decode::Languages#reference_for
|
|
40
|
+
Decode::Source#initialize
|
|
41
|
+
... snip ...
|
|
42
|
+
135/215 definitions have documentation.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Using this tool can show you areas that might require more attention.
|
|
46
|
+
|
|
47
|
+
### Understanding Coverage Output
|
|
48
|
+
|
|
49
|
+
The coverage check:
|
|
50
|
+
- **Counts only public definitions** (public methods, classes, modules).
|
|
51
|
+
- **Reports the ratio** of documented vs total public definitions.
|
|
52
|
+
- **Lists missing documentation** by qualified name.
|
|
53
|
+
- **Fails with an error** if coverage is incomplete.
|
|
54
|
+
|
|
55
|
+
### What Counts as "Documented"
|
|
56
|
+
|
|
57
|
+
A definition is considered documented if it has:
|
|
58
|
+
- Any comment preceding it.
|
|
59
|
+
- Documentation pragmas (like `@parameter`, `@returns`, `@example`).
|
|
60
|
+
- A `@namespace` pragma (for organizational modules).
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# A user in the system.
|
|
64
|
+
class MyClass
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @namespace
|
|
68
|
+
module OrganizationalModule
|
|
69
|
+
# Contains helper functionality.
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Process user data and return formatted results.
|
|
73
|
+
# @parameter name [String] The user's name.
|
|
74
|
+
# @returns [bool] Success status.
|
|
75
|
+
def process(name)
|
|
76
|
+
# Validation logic here:
|
|
77
|
+
return false if name.empty?
|
|
78
|
+
|
|
79
|
+
# Processing logic:
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class UndocumentedClass
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Analyzing Symbols
|
|
88
|
+
|
|
89
|
+
### List All Symbols
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# See the structure of your codebase
|
|
93
|
+
bake decode:index:symbols lib
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This shows the hierarchical structure of your code:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
[] -> []
|
|
100
|
+
["MyGem"] -> [#<Decode::Language::Ruby::Module:...>]
|
|
101
|
+
MyGem
|
|
102
|
+
["MyGem", "User"] -> [#<Decode::Language::Ruby::Class:...>]
|
|
103
|
+
MyGem::User
|
|
104
|
+
["MyGem", "User", "initialize"] -> [#<Decode::Language::Ruby::Method:...>]
|
|
105
|
+
MyGem::User#initialize
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Extract Documentation
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Extract all documentation from your codebase
|
|
112
|
+
bake decode:index:documentation lib
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This outputs formatted documentation for all documented definitions:
|
|
116
|
+
|
|
117
|
+
~~~markdown
|
|
118
|
+
## `MyGem::User#initialize`
|
|
119
|
+
|
|
120
|
+
Initialize a new user with the given email address.
|
|
121
|
+
|
|
122
|
+
## `MyGem::User#authenticate`
|
|
123
|
+
|
|
124
|
+
Authenticate the user with a password.
|
|
125
|
+
Returns true if authentication is successful.
|
|
126
|
+
~~~
|
|
127
|
+
|
|
128
|
+
## Achieving 100% Coverage
|
|
129
|
+
|
|
130
|
+
### Document all public APIs
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# A user account with authentication and email.
|
|
134
|
+
class User
|
|
135
|
+
# @attribute [String] The user's email address.
|
|
136
|
+
attr_reader :email
|
|
137
|
+
|
|
138
|
+
# Initialize a new user.
|
|
139
|
+
# @parameter email [String] The user's email address.
|
|
140
|
+
def initialize(email)
|
|
141
|
+
# Store the email address:
|
|
142
|
+
@email = email
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Use @namespace for organizational modules
|
|
148
|
+
|
|
149
|
+
The best place to add these by default is `version.rb`.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# @namespace
|
|
153
|
+
module MyGem
|
|
154
|
+
VERSION = "0.1.0"
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Document edge cases
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# @private
|
|
162
|
+
def internal_helper
|
|
163
|
+
# Add the fields:
|
|
164
|
+
return foo + bar
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Common Coverage Issues
|
|
169
|
+
|
|
170
|
+
#### Missing namespace documentation
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# This module has no documentation and will show as missing coverage:
|
|
174
|
+
module MyGem
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Solution: Add @namespace pragma:
|
|
178
|
+
# @namespace
|
|
179
|
+
module MyGem
|
|
180
|
+
# Provides core functionality.
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Undocumented methods
|
|
185
|
+
|
|
186
|
+
Problem: Methods without any comments will show as missing coverage:
|
|
187
|
+
```ruby
|
|
188
|
+
def process_data
|
|
189
|
+
# Implementation here
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Solution: Add description and pragmas:
|
|
194
|
+
```ruby
|
|
195
|
+
# Process the input data and return results.
|
|
196
|
+
# @parameter data [Hash] Input data to process.
|
|
197
|
+
# @returns [Array] Processed results.
|
|
198
|
+
def process_data(data)
|
|
199
|
+
# Process the input:
|
|
200
|
+
results = data.map{|item| transform(item)}
|
|
201
|
+
|
|
202
|
+
# Return processed results:
|
|
203
|
+
results
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### Missing attr documentation
|
|
208
|
+
|
|
209
|
+
Problem: Attributes without documentation will show as missing coverage:
|
|
210
|
+
```ruby
|
|
211
|
+
attr_reader :name
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Solution: Document with @attribute pragma:
|
|
215
|
+
```ruby
|
|
216
|
+
# @attribute [String] The user's full name.
|
|
217
|
+
attr_reader :name
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Integrating into CI/CD
|
|
221
|
+
|
|
222
|
+
### GitHub Actions Example
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
name: Documentation Coverage
|
|
226
|
+
|
|
227
|
+
on: [push, pull_request]
|
|
228
|
+
|
|
229
|
+
jobs:
|
|
230
|
+
docs:
|
|
231
|
+
runs-on: ubuntu-latest
|
|
232
|
+
steps:
|
|
233
|
+
- uses: actions/checkout@v3
|
|
234
|
+
- uses: ruby/setup-ruby@v1
|
|
235
|
+
with:
|
|
236
|
+
bundler-cache: true
|
|
237
|
+
- name: Check documentation coverage
|
|
238
|
+
run: bake decode:index:coverage lib
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Rake Task Integration
|
|
242
|
+
|
|
243
|
+
Add to your `Rakefile`:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
require "decode"
|
|
247
|
+
|
|
248
|
+
desc "Check documentation coverage"
|
|
249
|
+
task :doc_coverage do
|
|
250
|
+
system("bake decode:index:coverage lib") || exit(1)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
task default: [:test, :doc_coverage]
|
|
254
|
+
```
|