yard 0.9.43 → 0.9.44

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92d411c4d39cc74df916cbac8719ebd7ac6ed1fb17b37022148f584440bda554
4
- data.tar.gz: 9582c48d6c1949c055e22035f325abc957ec31900613f5b04307f7f27fc011a4
3
+ metadata.gz: 87aef8f8a0b6c25d1a2c05214992313aa6278659214af6650d07876346b771ec
4
+ data.tar.gz: 149cee78996a555a97d7223252df845be60ecaf9bf92d7c9bedeeb8fe14952bf
5
5
  SHA512:
6
- metadata.gz: d566d6989e868b3bc4959037309c9c07a89608f07a39ad2719ead8765fca6ab3ae6240bfe7d56394ad47c8d75c5999d02313f6f7eaedb056a2ea584750d15cc1
7
- data.tar.gz: 641e462842732b47a67ff95eda6cfdb0373de7b2f487ceb7aa827815c0f8201fa55639979f7df17ec4d12864de60c4cc631fc7921cc787b20a04f5493cd86033
6
+ metadata.gz: e7bd349e0d31fb2b79dd0c18a057ff9323206c4d6be3535f902f9fa45ee30a8649bd3036861fdf2ab90e017bfaf7622eb744461a2728a88b68792bc8acbd3fa9
7
+ data.tar.gz: 2f4e486d27daf722a02c2efc2e7af568c12a76ebd30b23871a46c25c54e03f9e6f51397953dac330dba6b0581b7088882e581b04f43fe9ef64e3a0a93862df15
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # main
2
2
 
3
+ # [0.9.44] - May 25th, 2026
4
+
5
+ [0.9.44]: https://github.com/lsegal/yard/compare/v0.9.43...v0.9.44
6
+
7
+ - Fix possible path traversal with document_root (`--docroot`) and disk caching (`--cache`) set in `yard server` ([GHSA-pxcc-8665-phx8](https://github.com/lsegal/yard/security/advisories/GHSA-pxcc-8665-phx8))
8
+ - Fix support for HTML entities in HybridMarkup (#1680, #1681)
9
+ - Add support for string literals in TypesExplainer (#1628)
10
+ - Add support for multiple & nested Hash keys definition (#1630)
11
+
3
12
  # [0.9.43] - April 17th, 2026
4
13
 
5
14
  [0.9.43]: https://github.com/lsegal/yard/compare/v0.9.42...v0.9.43
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2007-2022 Loren Segal
1
+ Copyright (c) 2007-2026 Loren Segal
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
data/README.md CHANGED
@@ -323,7 +323,7 @@ See {file:CHANGELOG.md} for a list of changes.
323
323
 
324
324
  ## License
325
325
 
326
- YARD © 2007-2020 by [Loren Segal](mailto:lsegal@soen.ca). YARD is licensed
326
+ YARD © by [Loren Segal](mailto:lsegal@soen.ca). YARD is licensed
327
327
  under the MIT license except for some files which come from the RDoc/Ruby
328
328
  distributions. Please see the {file:LICENSE} and {file:LEGAL} documents for more
329
329
  information.
data/docs/Tags.md CHANGED
@@ -201,6 +201,27 @@ in the form `Hash<KeyType, ValueType>`, or using the hash specific syntax:
201
201
  `Hash{KeyTypes=>ValueTypes}`. In the latter case, KeyTypes or ValueTypes can
202
202
  also be a list of types separated by commas.
203
203
 
204
+ For more precise type signatures, the hash-specific syntax also supports
205
+ multiple keys mapping to the same value type(s), multiple key/value groups,
206
+ and nested hashes:
207
+
208
+ * **Multiple keys for the same value type(s)** &mdash; A comma-separated list of
209
+ keys on the left of `=>` all share the value types on the right. For example,
210
+ `Hash{:name, :title => String}` describes a Hash where both `:name` and
211
+ `:title` keys map to String values.
212
+ * **Multiple key/value groups** &mdash; Separate distinct key/value groups with
213
+ a semicolon (`;`). For example,
214
+ `Hash{:name => String; :age => Integer}` describes a Hash with a `:name` key
215
+ that maps to a String and an `:age` key that maps to an Integer.
216
+ * **Nested hashes** &mdash; A value type can itself be a hash, allowing nested
217
+ structures. For example,
218
+ `Hash{:user => Hash{:name => String, :age => Integer}}` describes a Hash with
219
+ a `:user` key whose value is another Hash with `:name` and `:age` keys.
220
+
221
+ Keys in the hash-specific syntax are commonly [literal values](#Literals) such
222
+ as symbols (`:key`) or strings (`'key'`, `"key"`), but any type listed in the
223
+ [type conventions](#Type_List_Conventions) is allowed.
224
+
204
225
  #### Order-Dependent Lists
205
226
 
206
227
  An order dependent list is a set of types surrounded by "()" and separated by
@@ -213,7 +234,7 @@ having exactly those 3 elements) would be listed as: `Array(String, Fixnum, Hash
213
234
  Some literals are accepted by virtue of being Ruby literals, but also by YARD
214
235
  conventions. Here is a non-exhaustive list of certain accepted literal values:
215
236
 
216
- * `true`, `false`, `nil`, `:foo` &mdash; used when a method returns
237
+ * `true`, `false`, `nil`, `:foo`, `"bar"` &mdash; used when a method returns
217
238
  these explicit literal values. Note that if your method returns both
218
239
  `true` or `false`, you should use the `Boolean` conventional type
219
240
  instead.
data/lib/yard/autoload.rb CHANGED
@@ -287,6 +287,7 @@ module YARD
287
287
  module Templates
288
288
  module Helpers # Namespace for template helpers
289
289
  module Markup # Namespace for markup providers
290
+ autoload :HtmlEntities, __p('templates/helpers/markup/html_entities')
290
291
  autoload :HybridMarkdown, __p('templates/helpers/markup/hybrid_markdown')
291
292
  autoload :RDocMarkup, __p('templates/helpers/markup/rdoc_markup')
292
293
  autoload :RDocMarkdown, __p('templates/helpers/markup/rdoc_markdown')
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'fileutils'
3
-
4
2
  module YARD
5
3
  module Server
6
4
  module Commands
@@ -32,6 +30,8 @@ module YARD
32
30
  # @abstract
33
31
  # @see #run
34
32
  class Base
33
+ include StaticCaching
34
+
35
35
  # @group Basic Command and Adapter Options
36
36
 
37
37
  # @return [Hash] the options passed to the command's constructor
@@ -163,13 +163,7 @@ module YARD
163
163
  # @return [String] the same cached data (for chaining)
164
164
  # @see StaticCaching
165
165
  def cache(data)
166
- if caching && adapter.document_root
167
- path = File.join(adapter.document_root, request.path_info.sub(/\.html$/, '') + '.html')
168
- path = path.sub(%r{/\.html$}, '.html')
169
- FileUtils.mkdir_p(File.dirname(path))
170
- log.debug "Caching data to #{path}"
171
- File.open(path, 'wb') {|f| f.write(data) }
172
- end
166
+ super if caching
173
167
  self.body = data
174
168
  end
175
169
 
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'fileutils'
3
+
2
4
  module YARD
3
5
  module Server
4
6
  # Implements static caching for requests.
@@ -10,9 +12,8 @@ module YARD
10
12
  # implement your own +#check_static_cache+ method and mix the module into
11
13
  # the Router class.
12
14
  #
13
- # Note that caching does not occur here. This method simply checks for
14
- # the existence of cached data. To actually cache a response, see
15
- # {Commands::Base#cache}.
15
+ # This method checks for the existence of cached data. To actually cache
16
+ # a response, see {#cache}.
16
17
  #
17
18
  # @example Implementing In-Memory Cache Checking
18
19
  # module MemoryCaching
@@ -33,14 +34,44 @@ module YARD
33
34
  # @see Commands::Base#cache
34
35
  def check_static_cache
35
36
  return nil unless adapter.document_root
36
- cache_path = File.join(adapter.document_root, request.path.sub(/\.html$/, '') + '.html')
37
- cache_path = cache_path.sub(%r{/\.html$}, '.html')
37
+ cache_path = cache_path(request.path)
38
+ return nil unless cache_path
39
+
38
40
  if File.file?(cache_path)
39
41
  log.debug "Loading cache from disk: #{cache_path}"
40
42
  return [200, {'Content-Type' => 'text/html'}, [File.read_binary(cache_path)]]
41
43
  end
42
44
  nil
43
45
  end
46
+
47
+ # Caches rendered HTML response data to disk.
48
+ #
49
+ # @param [String] data the data to cache
50
+ # @return [void]
51
+ # @since 0.9.44
52
+ def cache(data)
53
+ return unless adapter.document_root
54
+
55
+ path = cache_path(request.path_info)
56
+ return unless path
57
+
58
+ FileUtils.mkdir_p(File.dirname(path))
59
+ log.debug "Caching data to #{path}"
60
+ File.open(path, 'wb') {|f| f.write(data) }
61
+ end
62
+
63
+ private
64
+
65
+ def cache_path(request_path)
66
+ return nil if request_path.split(/[\/\\]/).include?('..')
67
+
68
+ path = request_path.sub(/\.html$/, '') + '.html'
69
+ path = path.sub(%r{\A/+}, '')
70
+ return nil if path =~ /\A[A-Za-z]:/
71
+
72
+ path = File.cleanpath(path)
73
+ File.join(adapter.document_root, path)
74
+ end
44
75
  end
45
76
  end
46
77
  end
@@ -4,6 +4,9 @@ require 'strscan'
4
4
  module YARD
5
5
  module Tags
6
6
  class TypesExplainer
7
+ # Regular expression to match symbol and string literals
8
+ LITERALMATCH = /:\w+|'[^']*'|"[^"]*"/
9
+
7
10
  # (see Tag#explain_types)
8
11
  # @param types [Array<String>] a list of types to parse and summarize
9
12
  def self.explain(*types)
@@ -31,16 +34,14 @@ module YARD
31
34
  end
32
35
 
33
36
  def to_s(singular = true)
34
- if name[0, 1] == "#"
35
- (singular ? "an object that responds to " : "objects that respond to ") + list_join(name.split(/ *& */), with: "and")
36
- elsif name[0, 1] =~ /[A-Z]/
37
+ if name[0, 1] =~ /[A-Z]/
37
38
  singular ? "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} " + name : "#{name}#{name[-1, 1] =~ /[A-Z]/ ? "'" : ''}s"
38
39
  else
39
40
  name
40
41
  end
41
42
  end
42
43
 
43
- private
44
+ protected
44
45
 
45
46
  def list_join(list, with: "or")
46
47
  index = 0
@@ -54,6 +55,20 @@ module YARD
54
55
  end
55
56
  end
56
57
 
58
+ # @private
59
+ class LiteralType < Type
60
+ def to_s(_singular = true)
61
+ "a literal value #{name}"
62
+ end
63
+ end
64
+
65
+ # @private
66
+ class DuckType < Type
67
+ def to_s(singular = true)
68
+ (singular ? "an object that responds to " : "objects that respond to ") + list_join(name.split(/ *& */), with: "and")
69
+ end
70
+ end
71
+
57
72
  # @private
58
73
  class CollectionType < Type
59
74
  attr_accessor :types
@@ -77,18 +92,56 @@ module YARD
77
92
 
78
93
  # @private
79
94
  class HashCollectionType < Type
80
- attr_accessor :key_types, :value_types
95
+ attr_accessor :key_value_pairs
81
96
 
82
- def initialize(name, key_types, value_types)
97
+ def initialize(name, key_types_or_pairs, value_types = nil)
83
98
  @name = name
84
- @key_types = key_types
85
- @value_types = value_types
99
+
100
+ if value_types.nil?
101
+ # New signature: (name, key_value_pairs)
102
+ @key_value_pairs = key_types_or_pairs || []
103
+ else
104
+ # Old signature: (name, key_types, value_types)
105
+ @key_value_pairs = [[key_types_or_pairs, value_types]]
106
+ end
107
+ end
108
+
109
+ # Backward compatibility accessors
110
+ def key_types
111
+ return [] if @key_value_pairs.empty?
112
+ @key_value_pairs.first[0] || []
113
+ end
114
+
115
+ def key_types=(types)
116
+ if @key_value_pairs.empty?
117
+ @key_value_pairs = [[types, []]]
118
+ else
119
+ @key_value_pairs[0][0] = types
120
+ end
121
+ end
122
+
123
+ def value_types
124
+ return [] if @key_value_pairs.empty?
125
+ @key_value_pairs.first[1] || []
126
+ end
127
+
128
+ def value_types=(types)
129
+ if @key_value_pairs.empty?
130
+ @key_value_pairs = [[[], types]]
131
+ else
132
+ @key_value_pairs[0][1] = types
133
+ end
86
134
  end
87
135
 
88
136
  def to_s(_singular = true)
89
- "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name} with keys made of (" +
90
- list_join(key_types.map {|t| t.to_s(false) }) +
91
- ") and values of (" + list_join(value_types.map {|t| t.to_s(false) }) + ")"
137
+ return "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name}" if @key_value_pairs.empty?
138
+
139
+ result = "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name} with "
140
+ parts = @key_value_pairs.map do |keys, values|
141
+ "keys made of (" + list_join(keys.map {|t| t.to_s(false) }) +
142
+ ") and values of (" + list_join(values.map {|t| t.to_s(false) }) + ")"
143
+ end
144
+ result + parts.join(" and ")
92
145
  end
93
146
  end
94
147
 
@@ -101,13 +154,15 @@ module YARD
101
154
  :collection_end => />/,
102
155
  :fixed_collection_start => /\(/,
103
156
  :fixed_collection_end => /\)/,
104
- :type_name => /#{ISEP}#{METHODNAMEMATCH}|#{NAMESPACEMATCH}|\w+/,
157
+ :type_name => /#{ISEP}#{METHODNAMEMATCH}|#{NAMESPACEMATCH}|#{LITERALMATCH}|\w+/,
105
158
  :symbol => /:#{METHODNAMEMATCH}/,
106
- :type_next => /[,;]/,
159
+ :type_next => /[,]/,
107
160
  :whitespace => /\s+/,
108
161
  :hash_collection_start => /\{/,
109
- :hash_collection_next => /=>/,
162
+ :hash_collection_value => /=>/,
163
+ :hash_collection_value_end => /;/,
110
164
  :hash_collection_end => /\}/,
165
+ # :symbol_start => /:/,
111
166
  :parse_end => nil
112
167
  }
113
168
 
@@ -119,10 +174,45 @@ module YARD
119
174
  @scanner = StringScanner.new(string)
120
175
  end
121
176
 
122
- def parse
123
- types = []
177
+ # @return [Array(Boolean, Array<Type>)] - finished, types
178
+ def parse(until_tokens: [:parse_end])
179
+ current_parsed_types = []
124
180
  type = nil
125
181
  name = nil
182
+ finished = false
183
+ parse_with_handlers do |token_type, token|
184
+ case token_type
185
+ when *until_tokens
186
+ raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
187
+ type = create_type(name) unless type
188
+ current_parsed_types << type
189
+ finished = true
190
+ when :type_name
191
+ raise SyntaxError, "expecting END, got name '#{token}'" if name
192
+ name = token
193
+ when :type_next
194
+ raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
195
+ type = create_type(name) unless type
196
+ current_parsed_types << type
197
+ name = nil
198
+ type = nil
199
+ when :fixed_collection_start, :collection_start
200
+ name ||= "Array"
201
+ klass = token_type == :collection_start ? CollectionType : FixedCollectionType
202
+ type = klass.new(name, parse(until_tokens: [:fixed_collection_end, :collection_end, :parse_end]))
203
+ when :hash_collection_start
204
+ name ||= "Hash"
205
+ type = parse_hash_collection(name)
206
+ end
207
+
208
+ [finished, current_parsed_types]
209
+ end
210
+ end
211
+
212
+ private
213
+
214
+ # @return [Array<Type>]
215
+ def parse_with_handlers
126
216
  loop do
127
217
  found = false
128
218
  TOKENS.each do |token_type, match|
@@ -130,32 +220,67 @@ module YARD
130
220
  # rubocop:disable Lint/AssignmentInCondition
131
221
  next unless (match.nil? && @scanner.eos?) || (match && token = @scanner.scan(match))
132
222
  found = true
133
- case token_type
134
- when :type_name, :symbol
135
- raise SyntaxError, "expecting END, got name '#{token}'" if name
136
- name = token
137
- when :type_next
138
- raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
139
- type = Type.new(name) unless type
140
- types << type
141
- type = nil
142
- name = nil
143
- when :fixed_collection_start, :collection_start
144
- name ||= "Array"
145
- klass = token_type == :collection_start ? CollectionType : FixedCollectionType
146
- type = klass.new(name, parse)
147
- when :hash_collection_start
148
- name ||= "Hash"
149
- type = HashCollectionType.new(name, parse, parse)
150
- when :hash_collection_next, :hash_collection_end, :fixed_collection_end, :collection_end, :parse_end
151
- raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
152
- type = Type.new(name) unless type
153
- types << type
154
- return types
155
- end
223
+ # @type [Array<Type>]
224
+ finished, types = yield(token_type, token)
225
+ return types if finished
226
+ break
156
227
  end
157
228
  raise SyntaxError, "invalid character at #{@scanner.peek(1)}" unless found
158
229
  end
230
+ nil
231
+ end
232
+
233
+ def parse_hash_collection(name)
234
+ key_value_pairs = []
235
+ current_keys = []
236
+ finished = false
237
+
238
+ parse_with_handlers do |token_type, token|
239
+ case token_type
240
+ when :type_name
241
+ current_keys << create_type(token)
242
+ when :type_next
243
+ # Comma - continue collecting keys unless we just processed a value
244
+ # In that case, start a new key group
245
+ when :hash_collection_value
246
+ # => - current keys map to the next value(s)
247
+ raise SyntaxError, "no keys before =>" if current_keys.empty?
248
+ values = parse(until_tokens: [:hash_collection_value_end, :parse_end])
249
+ key_value_pairs << [current_keys, values]
250
+ current_keys = []
251
+ when :hash_collection_end, :parse_end
252
+ # End of hash
253
+ finished = true
254
+ when :whitespace
255
+ # Ignore whitespace
256
+ end
257
+
258
+ [finished, HashCollectionType.new(name, key_value_pairs)]
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ def create_type(name)
265
+ if name[0, 1] == ":" || (name[0, 1] =~ /['"]/ && name[-1, 1] =~ /['"]/)
266
+ LiteralType.new(name)
267
+ elsif name[0, 1] == "#"
268
+ DuckType.new(name)
269
+ else
270
+ Type.new(name)
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ def create_type(name)
277
+ if name[0, 1] == ":" || (name[0, 1] =~ /['"]/ && name[-1, 1] =~ /['"]/)
278
+ LiteralType.new(name)
279
+ elsif name[0, 1] == "#"
280
+ DuckType.new(name)
281
+ else
282
+ Type.new(name)
283
+ end
159
284
  end
160
285
  end
161
286
  end