docscribe 1.0.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.
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Docscribe
6
+ class Config
7
+ DEFAULT = {
8
+ 'emit' => {
9
+ 'header' => true,
10
+ 'param_tags' => true,
11
+ 'return_tag' => true,
12
+ 'visibility_tags' => true,
13
+ 'raise_tags' => true,
14
+ 'rescue_conditional_returns' => true
15
+ },
16
+ 'doc' => {
17
+ 'default_message' => 'Method documentation.'
18
+ },
19
+ 'methods' => {
20
+ 'instance' => {
21
+ 'public' => {},
22
+ 'protected' => {},
23
+ 'private' => {}
24
+ },
25
+ 'class' => {
26
+ 'public' => {},
27
+ 'protected' => {},
28
+ 'private' => {}
29
+ }
30
+ },
31
+ 'inference' => {
32
+ 'fallback_type' => 'Object',
33
+ 'nil_as_optional' => true,
34
+ 'treat_options_keyword_as_hash' => true
35
+ },
36
+ 'filter' => {
37
+ 'visibilities' => %w[public protected private],
38
+ 'scopes' => %w[instance class],
39
+ 'include' => [],
40
+ 'exclude' => []
41
+ }
42
+ }.freeze
43
+
44
+ attr_reader :raw
45
+
46
+ # +Docscribe::Config#initialize+ -> Object
47
+ #
48
+ # Method documentation.
49
+ #
50
+ # @param [Hash] raw Param documentation.
51
+ # @return [Object]
52
+ def initialize(raw = {})
53
+ @raw = deep_merge(DEFAULT, raw || {})
54
+ end
55
+
56
+ # +Docscribe::Config.load+ -> Object
57
+ #
58
+ # Method documentation.
59
+ #
60
+ # @param [nil] path Param documentation.
61
+ # @return [Object]
62
+ def self.load(path = nil)
63
+ raw = {}
64
+ if path && File.file?(path)
65
+ raw = YAML.safe_load_file(path, permitted_classes: [], aliases: true) || {}
66
+ elsif File.file?('docscribe.yml')
67
+ raw = YAML.safe_load_file('docscribe.yml', permitted_classes: [], aliases: true) || {}
68
+ end
69
+ new(raw)
70
+ end
71
+
72
+ # +Docscribe::Config#emit_header?+ -> Object
73
+ #
74
+ # Method documentation.
75
+ #
76
+ # @return [Object]
77
+ def emit_header?
78
+ fetch_bool(%w[emit header], true)
79
+ end
80
+
81
+ # +Docscribe::Config#emit_param_tags?+ -> Object
82
+ #
83
+ # Method documentation.
84
+ #
85
+ # @return [Object]
86
+ def emit_param_tags?
87
+ fetch_bool(%w[emit param_tags], true)
88
+ end
89
+
90
+ # +Docscribe::Config#emit_visibility_tags?+ -> Object
91
+ #
92
+ # Method documentation.
93
+ #
94
+ # @return [Object]
95
+ def emit_visibility_tags?
96
+ fetch_bool(%w[emit visibility_tags], true)
97
+ end
98
+
99
+ # +Docscribe::Config#emit_raise_tags?+ -> Object
100
+ #
101
+ # Method documentation.
102
+ #
103
+ # @return [Object]
104
+ def emit_raise_tags?
105
+ fetch_bool(%w[emit raise_tags], true)
106
+ end
107
+
108
+ # +Docscribe::Config#emit_rescue_conditional_returns?+ -> Object
109
+ #
110
+ # Method documentation.
111
+ #
112
+ # @return [Object]
113
+ def emit_rescue_conditional_returns?
114
+ fetch_bool(%w[emit rescue_conditional_returns], true)
115
+ end
116
+
117
+ # +Docscribe::Config#emit_return_tag?+ -> Object
118
+ #
119
+ # Method documentation.
120
+ #
121
+ # @param [Object] scope Param documentation.
122
+ # @param [Object] visibility Param documentation.
123
+ # @return [Object]
124
+ def emit_return_tag?(scope, visibility)
125
+ method_override_bool(scope, visibility, 'return_tag',
126
+ default: fetch_bool(%w[emit return_tag], true))
127
+ end
128
+
129
+ # +Docscribe::Config#default_message+ -> Object
130
+ #
131
+ # Method documentation.
132
+ #
133
+ # @param [Object] scope Param documentation.
134
+ # @param [Object] visibility Param documentation.
135
+ # @return [Object]
136
+ def default_message(scope, visibility)
137
+ method_override_str(scope, visibility, 'default_message',
138
+ default: raw.dig('doc', 'default_message') || 'Method documentation.')
139
+ end
140
+
141
+ # +Docscribe::Config#fallback_type+ -> Object
142
+ #
143
+ # Method documentation.
144
+ #
145
+ # @return [Object]
146
+ def fallback_type
147
+ raw.dig('inference', 'fallback_type') || 'Object'
148
+ end
149
+
150
+ # +Docscribe::Config#nil_as_optional?+ -> Object
151
+ #
152
+ # Method documentation.
153
+ #
154
+ # @return [Object]
155
+ def nil_as_optional?
156
+ fetch_bool(%w[inference nil_as_optional], true)
157
+ end
158
+
159
+ # +Docscribe::Config#treat_options_keyword_as_hash?+ -> Object
160
+ #
161
+ # Method documentation.
162
+ #
163
+ # @return [Object]
164
+ def treat_options_keyword_as_hash?
165
+ fetch_bool(%w[inference treat_options_keyword_as_hash], true)
166
+ end
167
+
168
+ private
169
+
170
+ # +Docscribe::Config#method_override_bool+ -> Object
171
+ #
172
+ # Method documentation.
173
+ #
174
+ # @private
175
+ # @param [Object] scope Param documentation.
176
+ # @param [Object] vis Param documentation.
177
+ # @param [Object] key Param documentation.
178
+ # @param [Object] default Param documentation.
179
+ # @return [Object]
180
+ def method_override_bool(scope, vis, key, default:)
181
+ node = raw.dig('methods', scope_to_key(scope), vis.to_s, key)
182
+ node.nil? ? default : !!node
183
+ end
184
+
185
+ # +Docscribe::Config#fetch_bool+ -> Object
186
+ #
187
+ # Method documentation.
188
+ #
189
+ # @private
190
+ # @param [Object] path Param documentation.
191
+ # @param [Object] default Param documentation.
192
+ # @return [Object]
193
+ def fetch_bool(path, default)
194
+ node = raw
195
+ path.each { |k| node = node[k] if node }
196
+ node.nil? ? default : !!node
197
+ end
198
+
199
+ # +Docscribe::Config#method_override_str+ -> Object
200
+ #
201
+ # Method documentation.
202
+ #
203
+ # @private
204
+ # @param [Object] scope Param documentation.
205
+ # @param [Object] vis Param documentation.
206
+ # @param [Object] key Param documentation.
207
+ # @param [Object] default Param documentation.
208
+ # @return [Object]
209
+ def method_override_str(scope, vis, key, default:)
210
+ node = raw.dig('methods', scope_to_key(scope), vis.to_s, key)
211
+ node.nil? ? default : node.to_s
212
+ end
213
+
214
+ # +Docscribe::Config#scope_to_key+ -> String
215
+ #
216
+ # Method documentation.
217
+ #
218
+ # @private
219
+ # @param [Object] scope Param documentation.
220
+ # @return [String]
221
+ def scope_to_key(scope)
222
+ scope == :class ? 'class' : 'instance'
223
+ end
224
+
225
+ # +Docscribe::Config#deep_merge+ -> Object
226
+ #
227
+ # Method documentation.
228
+ #
229
+ # @private
230
+ # @param [Object] hash1 Param documentation.
231
+ # @param [Object] hash2 Param documentation.
232
+ # @return [Object]
233
+ def deep_merge(hash1, hash2)
234
+ return hash1 unless hash2
235
+
236
+ hash1.merge(hash2) do |_, v1, v2|
237
+ if v1.is_a?(Hash) && v2.is_a?(Hash)
238
+ deep_merge(v1, v2)
239
+ else
240
+ v2
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+
5
+ module Docscribe
6
+ module Infer
7
+ class << self
8
+ # +Docscribe::Infer.infer_raises_from_node+ -> Object
9
+ #
10
+ # Method documentation.
11
+ #
12
+ # @param [Object] node Param documentation.
13
+ # @return [Object]
14
+ def infer_raises_from_node(node)
15
+ raises = []
16
+ walk = lambda do |n|
17
+ return unless n.is_a?(Parser::AST::Node)
18
+
19
+ case n.type
20
+ when :rescue
21
+ n.children.each { |ch| walk.call(ch) }
22
+ when :resbody
23
+ exc_list = n.children[0]
24
+ if exc_list.nil?
25
+ raises << 'StandardError'
26
+ elsif exc_list.type == :array
27
+ exc_list.children.each { |e| (c = const_full_name(e)) && (raises << c) }
28
+ else
29
+ (c = const_full_name(exc_list)) && (raises << c)
30
+ end
31
+ n.children.each { |ch| walk.call(ch) if ch.is_a?(Parser::AST::Node) }
32
+ when :send
33
+ recv, meth, *args = *n
34
+ if recv.nil? && %i[raise fail].include?(meth)
35
+ if args.empty?
36
+ raises << 'StandardError'
37
+ else
38
+ c = const_full_name(args[0])
39
+ raises << (c || 'StandardError')
40
+ end
41
+ end
42
+ n.children.each { |ch| walk.call(ch) if ch.is_a?(Parser::AST::Node) }
43
+ else
44
+ n.children.each { |ch| walk.call(ch) if ch.is_a?(Parser::AST::Node) }
45
+ end
46
+ end
47
+ walk.call(node)
48
+ raises.uniq
49
+ end
50
+
51
+ # +Docscribe::Infer.infer_param_type+ -> Object
52
+ #
53
+ # Method documentation.
54
+ #
55
+ # @param [Object] name Param documentation.
56
+ # @param [Object] default_str Param documentation.
57
+ # @return [Object]
58
+ def infer_param_type(name, default_str)
59
+ # splats and kwargs are driven by name shape
60
+ return 'Array' if name.start_with?('*') && !name.start_with?('**')
61
+ return 'Hash' if name.start_with?('**')
62
+ return 'Proc' if name.start_with?('&')
63
+
64
+ # keyword arg e.g. "verbose:" — default_str might be nil or something
65
+ is_kw = name.end_with?(':')
66
+
67
+ node = parse_expr(default_str)
68
+ ty = type_from_literal(node)
69
+
70
+ # If kw with no default, still show Object (or Hash for options:)
71
+ if is_kw && default_str.nil?
72
+ return (name == 'options:' ? 'Hash' : 'Object')
73
+ end
74
+
75
+ # If param named options and default is {}, call it Hash
76
+ return 'Hash' if name == 'options:' && (default_str == '{}' || ty == 'Hash')
77
+
78
+ ty
79
+ end
80
+
81
+ # +Docscribe::Infer.parse_expr+ -> Object
82
+ #
83
+ # Method documentation.
84
+ #
85
+ # @param [Object] src Param documentation.
86
+ # @raise [Parser::SyntaxError]
87
+ # @return [Object]
88
+ # @return [nil] if Parser::SyntaxError
89
+ def parse_expr(src)
90
+ return nil if src.nil? || src.strip.empty?
91
+
92
+ buffer = Parser::Source::Buffer.new('(param)')
93
+ buffer.source = src
94
+ Parser::CurrentRuby.new.parse(buffer)
95
+ rescue Parser::SyntaxError
96
+ nil
97
+ end
98
+
99
+ # +Docscribe::Infer.infer_return_type+ -> Object
100
+ #
101
+ # Method documentation.
102
+ #
103
+ # @param [Object] method_source Param documentation.
104
+ # @raise [Parser::SyntaxError]
105
+ # @return [Object]
106
+ # @return [String] if Parser::SyntaxError
107
+ def infer_return_type(method_source)
108
+ return 'Object' if method_source.nil? || method_source.strip.empty?
109
+
110
+ buffer = Parser::Source::Buffer.new('(method)')
111
+ buffer.source = method_source
112
+ root = Parser::CurrentRuby.new.parse(buffer)
113
+ return 'Object' unless root && %i[def defs].include?(root.type)
114
+
115
+ body = root.children.last # method body node
116
+ ty = last_expr_type(body)
117
+ ty || 'Object'
118
+ rescue Parser::SyntaxError
119
+ 'Object'
120
+ end
121
+
122
+ # +Docscribe::Infer.infer_return_type_from_node+ -> Object
123
+ #
124
+ # Method documentation.
125
+ #
126
+ # @param [Object] node Param documentation.
127
+ # @return [Object]
128
+ def infer_return_type_from_node(node)
129
+ body =
130
+ case node.type
131
+ when :def then node.children[2] # [name, args, body]
132
+ when :defs then node.children[3] # [recv, name, args, body]
133
+ end
134
+ return 'Object' unless body
135
+
136
+ ty = last_expr_type(body)
137
+ ty || 'Object'
138
+ end
139
+
140
+ # +Docscribe::Infer.returns_spec_from_node+ -> Object
141
+ #
142
+ # Method documentation.
143
+ #
144
+ # @param [Object] node Param documentation.
145
+ # @return [Object]
146
+ def returns_spec_from_node(node)
147
+ # Returns a Hash like: { normal: 'Type', rescues: [[['Foo','Bar'], 'Type'], ...] }
148
+ body =
149
+ case node.type
150
+ when :def then node.children[2] # [name, args, body]
151
+ when :defs then node.children[3] # [recv, name, args, body]
152
+ end
153
+
154
+ spec = { normal: 'Object', rescues: [] }
155
+ return spec unless body
156
+
157
+ if body.type == :rescue
158
+ # child[0] is the main body (before rescue)
159
+ main_body = body.children[0]
160
+ spec[:normal] = last_expr_type(main_body) || 'Object'
161
+
162
+ # :resbody nodes hold exception list, optional var, and rescue body
163
+ body.children.each do |ch|
164
+ next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
165
+
166
+ exc_list, _asgn, rescue_body = *ch
167
+
168
+ exc_names = []
169
+ if exc_list.nil?
170
+ exc_names << 'StandardError'
171
+ elsif exc_list.type == :array
172
+ exc_list.children.each do |e|
173
+ name = const_full_name(e)
174
+ exc_names << (name || 'StandardError')
175
+ end
176
+ else
177
+ name = const_full_name(exc_list)
178
+ exc_names << (name || 'StandardError')
179
+ end
180
+
181
+ rtype = last_expr_type(rescue_body) || 'Object'
182
+ spec[:rescues] << [exc_names, rtype]
183
+ end
184
+ else
185
+ spec[:normal] = last_expr_type(body) || 'Object'
186
+ end
187
+
188
+ spec
189
+ end
190
+
191
+ # +Docscribe::Infer.last_expr_type+ -> Object
192
+ #
193
+ # Method documentation.
194
+ #
195
+ # @param [Object] node Param documentation.
196
+ # @return [Object]
197
+ def last_expr_type(node)
198
+ return nil unless node
199
+
200
+ case node.type
201
+ when :begin
202
+ last = node.children.last
203
+ last_expr_type(last)
204
+ when :if
205
+ t = last_expr_type(node.children[1])
206
+ e = last_expr_type(node.children[2])
207
+ unify_types(t, e)
208
+ when :case
209
+ # check whens and else
210
+ branches = node.children[1..].compact.flat_map do |child|
211
+ if child && child.type == :when
212
+ last_expr_type(child.children.last)
213
+ else
214
+ last_expr_type(child)
215
+ end
216
+ end
217
+ branches.compact!
218
+ branches.empty? ? 'Object' : branches.reduce { |a, b| unify_types(a, b) }
219
+ when :return
220
+ type_from_literal(node.children.first)
221
+ else
222
+ type_from_literal(node)
223
+ end
224
+ end
225
+
226
+ # +Docscribe::Infer.const_full_name+ -> Object
227
+ #
228
+ # Method documentation.
229
+ #
230
+ # @param [Object] n Param documentation.
231
+ # @return [Object]
232
+ def const_full_name(n)
233
+ return nil unless n.is_a?(Parser::AST::Node)
234
+
235
+ case n.type
236
+ when :const
237
+ scope, name = *n
238
+ scope_name = const_full_name(scope)
239
+ if scope_name && !scope_name.empty?
240
+ "#{scope_name}::#{name}"
241
+ elsif scope_name == '' # leading ::
242
+ "::#{name}"
243
+ else
244
+ name.to_s
245
+ end
246
+ when :cbase
247
+ '' # represents leading :: scope
248
+ end
249
+ end
250
+
251
+ # +Docscribe::Infer.type_from_literal+ -> Object
252
+ #
253
+ # Method documentation.
254
+ #
255
+ # @param [Object] node Param documentation.
256
+ # @return [Object]
257
+ def type_from_literal(node)
258
+ return 'Object' unless node
259
+
260
+ case node.type
261
+ when :int then 'Integer'
262
+ when :float then 'Float'
263
+ when :str, :dstr then 'String'
264
+ when :sym then 'Symbol'
265
+ when :true, :false then 'Boolean' # rubocop:disable Lint/BooleanSymbol
266
+ when :nil then 'nil'
267
+ when :array then 'Array'
268
+ when :hash then 'Hash'
269
+ when :regexp then 'Regexp'
270
+ when :const
271
+ node.children.last.to_s
272
+ when :send
273
+ recv, meth, = node.children
274
+ if meth == :new && recv && recv.type == :const
275
+ recv.children.last.to_s
276
+ else
277
+ 'Object'
278
+ end
279
+ else
280
+ 'Object'
281
+ end
282
+ end
283
+
284
+ # +Docscribe::Infer.unify_types+ -> String
285
+ #
286
+ # Method documentation.
287
+ #
288
+ # @param [Object] a Param documentation.
289
+ # @param [Object] b Param documentation.
290
+ # @return [String]
291
+ def unify_types(a, b)
292
+ a ||= 'Object'
293
+ b ||= 'Object'
294
+ return a if a == b
295
+ # nil-union => Optional
296
+ return "#{a == 'nil' ? b : a}?" if a == 'nil' || b == 'nil'
297
+
298
+ 'Object'
299
+ end
300
+ end
301
+ end
302
+ end