jazzy 0.11.0 → 0.13.1

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.
data/bin/sourcekitten CHANGED
Binary file
data/jazzy.gemspec CHANGED
@@ -5,8 +5,8 @@ require File.expand_path('lib/jazzy/gem_version.rb', File.dirname(__FILE__))
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'jazzy'
7
7
  spec.version = Jazzy::VERSION
8
- spec.authors = ['JP Simard', 'Tim Anglade', 'Samuel Giddins']
9
- spec.email = ['jp@realm.io']
8
+ spec.authors = ['JP Simard', 'Tim Anglade', 'Samuel Giddins', 'John Fairhurst']
9
+ spec.email = ['jp@jpsim.com']
10
10
  spec.summary = 'Soulful docs for Swift & Objective-C.'
11
11
  spec.description = 'Soulful docs for Swift & Objective-C. ' \
12
12
  "Run in your Xcode project's root directory for " \
data/lib/jazzy/config.rb CHANGED
@@ -102,6 +102,14 @@ module Jazzy
102
102
  Pathname(Dir[abs_path][0] || abs_path) # Use existing filesystem spelling
103
103
  end
104
104
 
105
+ def hide_swift?
106
+ hide_declarations == 'swift'
107
+ end
108
+
109
+ def hide_objc?
110
+ hide_declarations == 'objc'
111
+ end
112
+
105
113
  # ──────── Build ────────
106
114
 
107
115
  # rubocop:disable Layout/AlignParameters
@@ -143,9 +151,9 @@ module Jazzy
143
151
  command_line: '--hide-declarations [objc|swift] ',
144
152
  description: 'Hide declarations in the specified language. Given that ' \
145
153
  'generating Swift docs only generates Swift declarations, ' \
146
- 'this is only really useful to display just the Swift ' \
147
- 'declarations & names when generating docs for an ' \
148
- 'Objective-C framework.',
154
+ 'this is useful for hiding a specific interface for ' \
155
+ 'either Objective-C or mixed Objective-C and Swift ' \
156
+ 'projects.',
149
157
  default: ''
150
158
 
151
159
  config_attr :config_file,
@@ -165,9 +173,10 @@ module Jazzy
165
173
  description: 'Back-compatibility alias for build_tool_arguments.'
166
174
 
167
175
  config_attr :sourcekitten_sourcefile,
168
- command_line: ['-s', '--sourcekitten-sourcefile FILEPATH'],
169
- description: 'File generated from sourcekitten output to parse',
170
- parse: ->(s) { expand_path(s) }
176
+ command_line: ['-s', '--sourcekitten-sourcefile filepath1,…filepathN',
177
+ Array],
178
+ description: 'File(s) generated from sourcekitten output to parse',
179
+ parse: ->(paths) { [paths].flatten.map { |path| expand_path(path) } }
171
180
 
172
181
  config_attr :source_directory,
173
182
  command_line: '--source-directory DIRPATH',
@@ -346,6 +355,18 @@ module Jazzy
346
355
  'Example: https://git.io/v4Bcp'],
347
356
  default: []
348
357
 
358
+ config_attr :custom_categories_unlisted_prefix,
359
+ description: "Prefix for navigation section names that aren't "\
360
+ 'explicitly listed in `custom_categories`.',
361
+ default: 'Other '
362
+
363
+ config_attr :hide_unlisted_documentation,
364
+ command_line: '--[no-]hide-unlisted-documentation',
365
+ description: "Don't include documentation in the sidebar from the "\
366
+ "`documentation` config value that aren't explicitly "\
367
+ 'listed in `custom_categories`.',
368
+ default: false
369
+
349
370
  config_attr :custom_head,
350
371
  command_line: '--head HTML',
351
372
  description: 'Custom HTML to inject into <head></head>.',
data/lib/jazzy/doc.rb CHANGED
@@ -32,10 +32,6 @@ module Jazzy
32
32
  config.objc_mode && config.hide_declarations != 'objc'
33
33
  end
34
34
 
35
- def language
36
- objc_first? ? 'Objective-C' : 'Swift'
37
- end
38
-
39
35
  def language_stub
40
36
  objc_first? ? 'objc' : 'swift'
41
37
  end
@@ -29,23 +29,36 @@ module Jazzy
29
29
  # @return [Array] doc structure comprised of
30
30
  # section names & child names & URLs
31
31
  def self.doc_structure_for_docs(docs)
32
- docs.map do |doc|
33
- children = doc.children
34
- .sort_by { |c| [c.nav_order, c.name, c.usr || ''] }
35
- .flat_map do |child|
36
- # FIXME: include arbitrarily nested extensible types
37
- [{ name: child.name, url: child.url }] +
38
- Array(child.children.select do |sub_child|
39
- sub_child.type.swift_extensible? || sub_child.type.extension?
40
- end).map do |sub_child|
41
- { name: "– #{sub_child.name}", url: sub_child.url }
42
- end
32
+ docs
33
+ .map do |doc|
34
+ children = children_for_doc(doc)
35
+ {
36
+ section: doc.name,
37
+ url: doc.url,
38
+ children: children,
39
+ }
43
40
  end
44
- {
45
- section: doc.name,
46
- url: doc.url,
47
- children: children,
48
- }
41
+ .select do |structure|
42
+ if Config.instance.hide_unlisted_documentation
43
+ unlisted_prefix = Config.instance.custom_categories_unlisted_prefix
44
+ structure[:section] != "#{unlisted_prefix}Guides"
45
+ else
46
+ true
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.children_for_doc(doc)
52
+ doc.children
53
+ .sort_by { |c| [c.nav_order, c.name, c.usr || ''] }
54
+ .flat_map do |child|
55
+ # FIXME: include arbitrarily nested extensible types
56
+ [{ name: child.name, url: child.url }] +
57
+ Array(child.children.select do |sub_child|
58
+ sub_child.type.swift_extensible? || sub_child.type.extension?
59
+ end).map do |sub_child|
60
+ { name: "– #{sub_child.name}", url: sub_child.url }
61
+ end
49
62
  end
50
63
  end
51
64
 
@@ -53,8 +66,9 @@ module Jazzy
53
66
  # @param [Config] options
54
67
  # @return [SourceModule] the documented source module
55
68
  def self.build(options)
56
- if options.sourcekitten_sourcefile
57
- stdout = options.sourcekitten_sourcefile.read
69
+ if options.sourcekitten_sourcefile_configured
70
+ stdout = '[' + options.sourcekitten_sourcefile.map(&:read)
71
+ .join(',') + ']'
58
72
  elsif options.podspec_configured
59
73
  pod_documenter = PodspecDocumenter.new(options.podspec)
60
74
  stdout = pod_documenter.sourcekitten_output(options)
@@ -329,6 +343,7 @@ module Jazzy
329
343
  name: item.name,
330
344
  abstract: abstract,
331
345
  declaration: item.display_declaration,
346
+ language: item.display_language,
332
347
  other_language_declaration: item.display_other_language_declaration,
333
348
  usr: item.usr,
334
349
  dash_type: item.type.dash_type,
@@ -348,9 +363,10 @@ module Jazzy
348
363
  end
349
364
  # rubocop:enable Metrics/MethodLength
350
365
 
351
- def self.make_task(mark, uid, items)
366
+ def self.make_task(mark, uid, items, doc_model)
352
367
  {
353
368
  name: mark.name,
369
+ name_html: (render(doc_model, mark.name) if mark.name),
354
370
  uid: URI.encode(uid),
355
371
  items: items,
356
372
  pre_separator: mark.has_start_dash,
@@ -374,7 +390,7 @@ module Jazzy
374
390
  else
375
391
  mark_names_counts[uid] = 1
376
392
  end
377
- make_task(mark, uid, items)
393
+ make_task(mark, uid, items, mark_children.first)
378
394
  end
379
395
  end
380
396
 
@@ -1,3 +1,3 @@
1
1
  module Jazzy
2
- VERSION = '0.11.0'.freeze unless defined? Jazzy::VERSION
2
+ VERSION = '0.13.1'.freeze unless defined? Jazzy::VERSION
3
3
  end
@@ -3,6 +3,9 @@ require 'rouge'
3
3
  module Jazzy
4
4
  # This module helps highlight code
5
5
  module Highlighter
6
+ SWIFT = 'swift'.freeze
7
+ OBJC = 'objective_c'.freeze
8
+
6
9
  class Formatter < Rouge::Formatters::HTML
7
10
  def initialize(language)
8
11
  @language = language
@@ -16,16 +19,15 @@ module Jazzy
16
19
  end
17
20
  end
18
21
 
19
- # What Rouge calls the language
20
- def self.default_language
21
- if Config.instance.objc_mode
22
- 'objective_c'
23
- else
24
- 'swift'
25
- end
22
+ def self.highlight_swift(source)
23
+ highlight(source, SWIFT)
24
+ end
25
+
26
+ def self.highlight_objc(source)
27
+ highlight(source, OBJC)
26
28
  end
27
29
 
28
- def self.highlight(source, language = default_language)
30
+ def self.highlight(source, language)
29
31
  source && Rouge.highlight(source, language, Formatter.new(language))
30
32
  end
31
33
  end
@@ -82,21 +82,21 @@ module Jazzy
82
82
 
83
83
  def render_aside(type, text)
84
84
  <<-HTML
85
- <div class="aside aside-#{type.underscore.tr('_', '-')}">
85
+ </ul><div class="aside aside-#{type.underscore.tr('_', '-')}">
86
86
  <p class="aside-title">#{type.underscore.humanize}</p>
87
87
  #{text}
88
- </div>
88
+ </div><ul>
89
89
  HTML
90
90
  end
91
91
 
92
92
  def list(text, list_type)
93
93
  elided = text.gsub!(ELIDED_LI_TOKEN, '')
94
94
  return if text =~ /\A\s*\Z/ && elided
95
- return text if text =~ /class="aside-title"/
96
95
  str = "\n"
97
96
  str << (list_type == :ordered ? "<ol>\n" : "<ul>\n")
98
97
  str << text
99
98
  str << (list_type == :ordered ? "</ol>\n" : "</ul>\n")
99
+ str.gsub(%r{\n?<ul>\n<\/ul>}, '')
100
100
  end
101
101
 
102
102
  def block_code(code, language)
@@ -112,7 +112,6 @@ module Jazzy
112
112
  autolink: true,
113
113
  fenced_code_blocks: true,
114
114
  no_intra_emphasis: true,
115
- quote: true,
116
115
  strikethrough: true,
117
116
  space_after_headers: false,
118
117
  tables: true,
@@ -1,6 +1,7 @@
1
1
  require 'jazzy/source_declaration/access_control_level'
2
2
  require 'jazzy/source_declaration/type'
3
3
 
4
+ # rubocop:disable Metrics/ClassLength
4
5
  module Jazzy
5
6
  class SourceDeclaration
6
7
  # kind of declaration (e.g. class, variable, function)
@@ -13,6 +14,14 @@ module Jazzy
13
14
  children.any?
14
15
  end
15
16
 
17
+ def swift?
18
+ type.swift_type?
19
+ end
20
+
21
+ def highlight_language
22
+ swift? ? Highlighter::SWIFT : Highlighter::OBJC
23
+ end
24
+
16
25
  # When referencing this item from its parent category,
17
26
  # include the content or just link to it directly?
18
27
  def omit_content_from_parent?
@@ -67,17 +76,32 @@ module Jazzy
67
76
  name.split(/[\(\)]/) if type.objc_category?
68
77
  end
69
78
 
79
+ def swift_objc_extension?
80
+ type.swift_extension? && usr && usr.start_with?('c:objc')
81
+ end
82
+
83
+ def swift_extension_objc_name
84
+ return unless type.swift_extension? && usr
85
+
86
+ usr.split('(cs)').last
87
+ end
88
+
89
+ # The language in the templates for display
90
+ def display_language
91
+ return 'Swift' if swift?
92
+
93
+ Config.instance.hide_objc? ? 'Swift' : 'Objective-C'
94
+ end
95
+
70
96
  def display_declaration
71
- if Config.instance.hide_declarations == 'objc'
72
- other_language_declaration
73
- else
74
- declaration
75
- end
97
+ return declaration if swift?
98
+
99
+ Config.instance.hide_objc? ? other_language_declaration : declaration
76
100
  end
77
101
 
78
102
  def display_other_language_declaration
79
103
  other_language_declaration unless
80
- %w[swift objc].include? Config.instance.hide_declarations
104
+ Config.instance.hide_objc? || Config.instance.hide_swift?
81
105
  end
82
106
 
83
107
  attr_accessor :file
@@ -106,6 +130,8 @@ module Jazzy
106
130
  attr_accessor :deprecation_message
107
131
  attr_accessor :unavailable
108
132
  attr_accessor :unavailable_message
133
+ attr_accessor :generic_requirements
134
+ attr_accessor :inherited_types
109
135
 
110
136
  def usage_discouraged?
111
137
  unavailable || deprecated
@@ -115,6 +141,36 @@ module Jazzy
115
141
  CGI.unescape(url)
116
142
  end
117
143
 
144
+ def constrained_extension?
145
+ type.swift_extension? &&
146
+ generic_requirements
147
+ end
148
+
149
+ def mark_for_children
150
+ if constrained_extension?
151
+ SourceMark.new_generic_requirements(generic_requirements)
152
+ else
153
+ SourceMark.new
154
+ end
155
+ end
156
+
157
+ def inherited_types?
158
+ inherited_types &&
159
+ !inherited_types.empty?
160
+ end
161
+
162
+ # Is there at least one inherited type that is not in the given list?
163
+ def other_inherited_types?(unwanted)
164
+ return false unless inherited_types?
165
+ inherited_types.any? { |t| !unwanted.include?(t) }
166
+ end
167
+
168
+ # SourceKit only sets modulename for imported modules
169
+ def type_from_doc_module?
170
+ !type.extension? ||
171
+ (swift? && usr && modulename.nil?)
172
+ end
173
+
118
174
  def alternative_abstract
119
175
  if file = alternative_abstract_file
120
176
  Pathname(file).read
@@ -77,6 +77,10 @@ module Jazzy
77
77
  kind == 'sourcekitten.source.lang.objc.decl.class'
78
78
  end
79
79
 
80
+ def swift_type?
81
+ kind.include? 'swift'
82
+ end
83
+
80
84
  def swift_enum_case?
81
85
  kind == 'source.lang.swift.decl.enumcase'
82
86
  end
@@ -110,6 +114,10 @@ module Jazzy
110
114
  kind == 'source.lang.swift.decl.protocol'
111
115
  end
112
116
 
117
+ def swift_typealias?
118
+ kind == 'source.lang.swift.decl.typealias'
119
+ end
120
+
113
121
  def param?
114
122
  # SourceKit strangely categorizes initializer parameters as local
115
123
  # variables, so both kinds represent a parameter in jazzy.
@@ -28,6 +28,12 @@ module Jazzy
28
28
  self.name = mark_string[start_index..end_index]
29
29
  end
30
30
 
31
+ def self.new_generic_requirements(requirements)
32
+ marked_up = requirements.gsub(/\b([^=:]\S*)\b/, '`\1`')
33
+ text = "Available where #{marked_up}"
34
+ new(text)
35
+ end
36
+
31
37
  def empty?
32
38
  !name && !has_start_dash && !has_end_dash
33
39
  end
@@ -37,5 +43,10 @@ module Jazzy
37
43
  self.has_start_dash = other.has_start_dash
38
44
  self.has_end_dash = other.has_end_dash
39
45
  end
46
+
47
+ # Can we merge the contents of another mark into our own?
48
+ def can_merge?(other)
49
+ other.empty? || other.name == name
50
+ end
40
51
  end
41
52
  end
@@ -62,10 +62,11 @@ module Jazzy
62
62
  # Group root-level docs by custom categories (if any) and type
63
63
  def self.group_docs(docs)
64
64
  custom_categories, docs = group_custom_categories(docs)
65
+ unlisted_prefix = Config.instance.custom_categories_unlisted_prefix
65
66
  type_categories, uncategorized = group_type_categories(
66
- docs, custom_categories.any? ? 'Other ' : ''
67
+ docs, custom_categories.any? ? unlisted_prefix : ''
67
68
  )
68
- custom_categories + type_categories + uncategorized
69
+ custom_categories + merge_categories(type_categories) + uncategorized
69
70
  end
70
71
 
71
72
  def self.group_custom_categories(docs)
@@ -98,6 +99,19 @@ module Jazzy
98
99
  [group.compact, docs]
99
100
  end
100
101
 
102
+ # Join categories with the same name (eg. ObjC and Swift classes)
103
+ def self.merge_categories(categories)
104
+ merged = []
105
+ categories.each do |new_category|
106
+ if existing = merged.find { |c| c.name == new_category.name }
107
+ existing.children += new_category.children
108
+ else
109
+ merged.append(new_category)
110
+ end
111
+ end
112
+ merged
113
+ end
114
+
101
115
  def self.make_group(group, name, abstract, url_name = nil)
102
116
  group.reject! { |doc| doc.name.empty? }
103
117
  unless group.empty?
@@ -111,6 +125,18 @@ module Jazzy
111
125
  end
112
126
  end
113
127
 
128
+ # Merge consecutive sections with the same mark into one section
129
+ def self.merge_consecutive_marks(docs)
130
+ prev_mark = nil
131
+ docs.each do |doc|
132
+ if prev_mark && prev_mark.can_merge?(doc.mark)
133
+ doc.mark = prev_mark
134
+ end
135
+ prev_mark = doc.mark
136
+ merge_consecutive_marks(doc.children)
137
+ end
138
+ end
139
+
114
140
  def self.sanitize_filename(doc)
115
141
  unsafe_filename = doc.url_name || doc.name
116
142
  sanitzation_enabled = Config.instance.use_safe_filenames
@@ -185,7 +211,9 @@ module Jazzy
185
211
  def self.use_spm?(options)
186
212
  options.swift_build_tool == :spm ||
187
213
  (!options.swift_build_tool_configured &&
188
- Dir['*.xcodeproj'].empty?)
214
+ Dir['*.xcodeproj', '*.xcworkspace'].empty? &&
215
+ !options.build_tool_arguments.include?('-project') &&
216
+ !options.build_tool_arguments.include?('-workspace'))
189
217
  end
190
218
 
191
219
  # Builds SourceKitten arguments based on Jazzy options
@@ -195,7 +223,7 @@ module Jazzy
195
223
  arguments += objc_arguments_from_options(options)
196
224
  else
197
225
  arguments += ['--spm'] if use_spm?(options)
198
- if options.module_name_configured
226
+ unless options.module_name.empty?
199
227
  arguments += ['--module-name', options.module_name]
200
228
  end
201
229
  arguments += ['--']
@@ -260,8 +288,13 @@ module Jazzy
260
288
  def self.should_document?(doc)
261
289
  return false if doc['key.doc.comment'].to_s.include?(':nodoc:')
262
290
 
291
+ type = SourceDeclaration::Type.new(doc['key.kind'])
292
+
263
293
  # Always document Objective-C declarations.
264
- return true if Config.instance.objc_mode
294
+ return true unless type.swift_type?
295
+
296
+ # Don't document Swift types if we are hiding Swift
297
+ return false if Config.instance.hide_swift?
265
298
 
266
299
  # Don't document @available declarations with no USR, since it means
267
300
  # they're unavailable.
@@ -269,22 +302,29 @@ module Jazzy
269
302
  return false
270
303
  end
271
304
 
272
- # Document extensions & enum elements, since we can't tell their ACL.
273
- type = SourceDeclaration::Type.new(doc['key.kind'])
305
+ # Document enum elements, since we can't tell their ACL.
274
306
  return true if type.swift_enum_element?
275
- if type.swift_extension?
276
- return Array(doc['key.substructure']).any? do |subdoc|
277
- subtype = SourceDeclaration::Type.new(subdoc['key.kind'])
278
- !subtype.mark? && should_document?(subdoc)
279
- end
280
- end
307
+ # Document extensions if they might have parts covered by the ACL.
308
+ return should_document_swift_extension?(doc) if type.swift_extension?
281
309
 
282
310
  acl_ok = SourceDeclaration::AccessControlLevel.from_doc(doc) >= @min_acl
283
- acl_ok.tap { @stats.add_acl_skipped unless acl_ok }
311
+ unless acl_ok
312
+ @stats.add_acl_skipped
313
+ @inaccessible_protocols.append(doc['key.name']) if type.swift_protocol?
314
+ end
315
+ acl_ok
284
316
  end
285
317
  # rubocop:enable Metrics/CyclomaticComplexity
286
318
  # rubocop:enable Metrics/PerceivedComplexity
287
319
 
320
+ def self.should_document_swift_extension?(doc)
321
+ doc['key.inheritedtypes'] ||
322
+ Array(doc['key.substructure']).any? do |subdoc|
323
+ subtype = SourceDeclaration::Type.new(subdoc['key.kind'])
324
+ !subtype.mark? && should_document?(subdoc)
325
+ end
326
+ end
327
+
288
328
  def self.should_mark_undocumented(filepath)
289
329
  source_directory = Config.instance.source_directory.to_s
290
330
  (filepath || '').start_with?(source_directory)
@@ -294,14 +334,14 @@ module Jazzy
294
334
  make_default_doc_info(declaration)
295
335
 
296
336
  filepath = doc['key.filepath']
297
- objc = Config.instance.objc_mode
298
- if objc || should_mark_undocumented(filepath)
337
+
338
+ if !declaration.swift? || should_mark_undocumented(filepath)
299
339
  @stats.add_undocumented(declaration)
300
340
  return nil if @skip_undocumented
301
341
  declaration.abstract = undocumented_abstract
302
342
  else
303
343
  declaration.abstract = Markdown.render(doc['key.doc.comment'] || '',
304
- Highlighter.default_language)
344
+ declaration.highlight_language)
305
345
  end
306
346
 
307
347
  declaration
@@ -320,16 +360,7 @@ module Jazzy
320
360
  def self.make_doc_info(doc, declaration)
321
361
  return unless should_document?(doc)
322
362
 
323
- if Config.instance.objc_mode
324
- declaration.declaration =
325
- Highlighter.highlight(doc['key.parsed_declaration'])
326
- declaration.other_language_declaration =
327
- Highlighter.highlight(doc['key.swift_declaration'], 'swift')
328
- else
329
- declaration.declaration =
330
- Highlighter.highlight(make_swift_declaration(doc, declaration))
331
- end
332
-
363
+ highlight_declaration(doc, declaration)
333
364
  make_deprecation_info(doc, declaration)
334
365
 
335
366
  unless doc['key.doc.full_as_xml']
@@ -337,7 +368,7 @@ module Jazzy
337
368
  end
338
369
 
339
370
  declaration.abstract = Markdown.render(doc['key.doc.comment'] || '',
340
- Highlighter.default_language)
371
+ declaration.highlight_language)
341
372
  declaration.discussion = ''
342
373
  declaration.return = Markdown.rendered_returns
343
374
  declaration.parameters = parameters(doc, Markdown.rendered_parameters)
@@ -345,6 +376,18 @@ module Jazzy
345
376
  @stats.add_documented
346
377
  end
347
378
 
379
+ def self.highlight_declaration(doc, declaration)
380
+ if declaration.swift?
381
+ declaration.declaration =
382
+ Highlighter.highlight_swift(make_swift_declaration(doc, declaration))
383
+ else
384
+ declaration.declaration =
385
+ Highlighter.highlight_objc(doc['key.parsed_declaration'])
386
+ declaration.other_language_declaration =
387
+ Highlighter.highlight_swift(doc['key.swift_declaration'])
388
+ end
389
+ end
390
+
348
391
  def self.make_deprecation_info(doc, declaration)
349
392
  if declaration.deprecated
350
393
  declaration.deprecation_message =
@@ -398,9 +441,9 @@ module Jazzy
398
441
  annotated.empty? ||
399
442
  parsed &&
400
443
  (annotated.include?(' = default') || # SR-2608
401
- parsed.match('@autoclosure|@escaping') || # SR-6321
402
- parsed.include?("\n") ||
403
- parsed.include?('extension '))
444
+ (parsed.scan(/@autoclosure|@escaping/).count >
445
+ annotated.scan(/@autoclosure|@escaping/).count) || # SR-6321
446
+ parsed.include?("\n"))
404
447
  end
405
448
 
406
449
  # Replace the fully qualified name of a type with its base name
@@ -422,6 +465,9 @@ module Jazzy
422
465
  # From source code
423
466
  parsed_decl = doc['key.parsed_declaration']
424
467
 
468
+ # Don't present type attributes on extensions
469
+ return parsed_decl if declaration.type.extension?
470
+
425
471
  decl =
426
472
  if prefer_parsed_decl?(parsed_decl, annotated_decl_body)
427
473
  # Strip any attrs captured by parsed version
@@ -443,14 +489,10 @@ module Jazzy
443
489
  end
444
490
 
445
491
  def self.make_substructure(doc, declaration)
446
- declaration.children = if doc['key.substructure']
447
- make_source_declarations(
448
- doc['key.substructure'],
449
- declaration,
450
- )
451
- else
452
- []
453
- end
492
+ return [] unless subdocs = doc['key.substructure']
493
+ make_source_declarations(subdocs,
494
+ declaration,
495
+ declaration.mark_for_children)
454
496
  end
455
497
 
456
498
  # rubocop:disable Metrics/MethodLength
@@ -471,8 +513,7 @@ module Jazzy
471
513
  declaration.type = SourceDeclaration::Type.new(doc['key.kind'])
472
514
  declaration.typename = doc['key.typename']
473
515
  declaration.objc_name = doc['key.name']
474
- documented_name = if Config.instance.hide_declarations == 'objc' &&
475
- doc['key.swift_name']
516
+ documented_name = if Config.instance.hide_objc? && doc['key.swift_name']
476
517
  doc['key.swift_name']
477
518
  else
478
519
  declaration.objc_name
@@ -508,10 +549,17 @@ module Jazzy
508
549
  declaration.end_line = doc['key.parsed_scope.end']
509
550
  declaration.deprecated = doc['key.always_deprecated']
510
551
  declaration.unavailable = doc['key.always_unavailable']
552
+ declaration.generic_requirements =
553
+ find_generic_requirements(doc['key.parsed_declaration'])
554
+ inherited_types = doc['key.inheritedtypes'] || []
555
+ declaration.inherited_types =
556
+ inherited_types.map { |type| type['key.name'] }.compact
511
557
 
512
558
  next unless make_doc_info(doc, declaration)
513
- make_substructure(doc, declaration)
514
- next if declaration.type.extension? && declaration.children.empty?
559
+ declaration.children = make_substructure(doc, declaration)
560
+ next if declaration.type.extension? &&
561
+ declaration.children.empty? &&
562
+ !declaration.inherited_types?
515
563
  declarations << declaration
516
564
  end
517
565
  declarations
@@ -520,12 +568,22 @@ module Jazzy
520
568
  # rubocop:enable Metrics/CyclomaticComplexity
521
569
  # rubocop:enable Metrics/MethodLength
522
570
 
571
+ def self.find_generic_requirements(parsed_declaration)
572
+ parsed_declaration =~ /\bwhere\s+(.*)$/m
573
+ return nil unless Regexp.last_match
574
+ Regexp.last_match[1].gsub(/\s+/, ' ')
575
+ end
576
+
523
577
  # Expands extensions of nested types declared at the top level into
524
578
  # a tree so they can be deduplicated properly
525
579
  def self.expand_extensions(decls)
526
580
  decls.map do |decl|
527
581
  next decl unless decl.type.extension? && decl.name.include?('.')
528
582
 
583
+ # Don't expand the Swift namespace if we're in ObjC mode.
584
+ # ex: NS_SWIFT_NAME(Foo.Bar) should not create top-level Foo
585
+ next decl if decl.swift_objc_extension? && !Config.instance.hide_objc?
586
+
529
587
  name_parts = decl.name.split('.')
530
588
  decl.name = name_parts.pop
531
589
  expand_extension(decl, name_parts, decls)
@@ -539,6 +597,7 @@ module Jazzy
539
597
  SourceDeclaration.new.tap do |decl|
540
598
  make_default_doc_info(decl)
541
599
  decl.name = name
600
+ decl.modulename = extension.modulename
542
601
  decl.type = extension.type
543
602
  decl.mark = extension.mark
544
603
  decl.usr = candidates.first.usr unless candidates.empty?
@@ -561,10 +620,10 @@ module Jazzy
561
620
  .group_by { |d| deduplication_key(d, declarations) }
562
621
  .values
563
622
 
564
- duplicate_groups.map do |group|
623
+ duplicate_groups.flat_map do |group|
565
624
  # Put extended type (if present) before extensions
566
625
  merge_declarations(group)
567
- end
626
+ end.compact
568
627
  end
569
628
 
570
629
  # Returns true if an Objective-C declaration is mergeable.
@@ -574,13 +633,28 @@ module Jazzy
574
633
  && name_match(decl.objc_category_name[0], root_decls))
575
634
  end
576
635
 
636
+ # Returns if a Swift declaration is mergeable.
637
+ # Start off merging in typealiases to help understand extensions.
638
+ def self.mergeable_swift?(decl)
639
+ decl.type.swift_extensible? ||
640
+ decl.type.swift_extension? ||
641
+ decl.type.swift_typealias?
642
+ end
643
+
577
644
  # Two declarations get merged if they have the same deduplication key.
578
645
  def self.deduplication_key(decl, root_decls)
579
- if decl.type.swift_extensible? || decl.type.swift_extension?
646
+ # Swift extension of objc class
647
+ if decl.swift_objc_extension?
648
+ [decl.swift_extension_objc_name, :objc_class_and_categories]
649
+ # Swift type or Swift extension of Swift type
650
+ elsif mergeable_swift?(decl)
580
651
  [decl.usr, decl.name]
652
+ # Objc categories and classes
581
653
  elsif mergeable_objc?(decl, root_decls)
582
- name, _ = decl.objc_category_name || decl.name
654
+ # Using the ObjC name to match swift_objc_extension.
655
+ name, _ = decl.objc_category_name || decl.objc_name
583
656
  [name, :objc_class_and_categories]
657
+ # Non-mergable declarations (funcs, typedefs etc...)
584
658
  else
585
659
  [decl.usr, decl.name, decl.type.kind]
586
660
  end
@@ -599,17 +673,36 @@ module Jazzy
599
673
  end
600
674
  typedecl = typedecls.first
601
675
 
676
+ extensions = reject_inaccessible_extensions(typedecl, extensions)
677
+
602
678
  if typedecl
603
679
  if typedecl.type.swift_protocol?
604
- merge_default_implementations_into_protocol(typedecl, extensions)
605
- mark_members_from_protocol_extension(extensions)
680
+ mark_and_merge_protocol_extensions(typedecl, extensions)
606
681
  extensions.reject! { |ext| ext.children.empty? }
607
682
  end
608
683
 
609
- merge_declaration_marks(typedecl, extensions)
684
+ merge_objc_declaration_marks(typedecl, extensions)
610
685
  end
611
686
 
612
- decls = typedecls + extensions
687
+ # Keep type-aliases separate from any extensions
688
+ if typedecl && typedecl.type.swift_typealias?
689
+ [merge_type_and_extensions(typedecls, []),
690
+ merge_type_and_extensions([], extensions)]
691
+ else
692
+ merge_type_and_extensions(typedecls, extensions)
693
+ end
694
+ end
695
+ # rubocop:enable Metrics/MethodLength
696
+
697
+ def self.merge_type_and_extensions(typedecls, extensions)
698
+ # Constrained extensions at the end
699
+ constrained, regular_exts = extensions.partition(&:constrained_extension?)
700
+ decls = typedecls + regular_exts + constrained
701
+ return nil if decls.empty?
702
+
703
+ move_merged_extension_marks(decls)
704
+ merge_code_declaration(decls)
705
+
613
706
  decls.first.tap do |merged|
614
707
  merged.children = deduplicate_declarations(
615
708
  decls.flat_map(&:children).uniq,
@@ -619,58 +712,112 @@ module Jazzy
619
712
  end
620
713
  end
621
714
  end
622
- # rubocop:enable Metrics/MethodLength
623
715
 
716
+ # Now we know all the public types and all the private protocols,
717
+ # reject extensions that add public protocols to private types
718
+ # or add private protocols to public types.
719
+ def self.reject_inaccessible_extensions(typedecl, extensions)
720
+ swift_exts, objc_exts = extensions.partition(&:swift?)
721
+
722
+ # Reject extensions that are just conformances to private protocols
723
+ unwanted_exts, wanted_exts = swift_exts.partition do |ext|
724
+ ext.children.empty? &&
725
+ !ext.other_inherited_types?(@inaccessible_protocols)
726
+ end
727
+
728
+ # Given extensions of a type from this module, without the
729
+ # type itself, the type must be private and the extensions
730
+ # should be rejected.
731
+ if !typedecl &&
732
+ wanted_exts.first &&
733
+ wanted_exts.first.type_from_doc_module?
734
+ unwanted_exts += wanted_exts
735
+ wanted_exts = []
736
+ end
737
+
738
+ # Don't tell the user to document them
739
+ unwanted_exts.each { |e| @stats.remove_undocumented(e) }
740
+
741
+ objc_exts + wanted_exts
742
+ end
743
+
744
+ # Protocol extensions.
745
+ #
624
746
  # If any of the extensions provide default implementations for methods in
625
747
  # the given protocol, merge those members into the protocol doc instead of
626
748
  # keeping them on the extension. These get a “Default implementation”
627
- # annotation in the generated docs.
628
- def self.merge_default_implementations_into_protocol(protocol, extensions)
629
- protocol.children.each do |proto_method|
630
- extensions.each do |ext|
631
- defaults, ext.children = ext.children.partition do |ext_member|
632
- ext_member.name == proto_method.name
749
+ # annotation in the generated docs. Default implementations added by
750
+ # conditional extensions are annotated but listed separately.
751
+ #
752
+ # Protocol methods provided only in an extension and not in the protocol
753
+ # itself are a special beast: they do not use dynamic dispatch. These get an
754
+ # “Extension method” annotation in the generated docs.
755
+ def self.mark_and_merge_protocol_extensions(protocol, extensions)
756
+ extensions.each do |ext|
757
+ ext.children = ext.children.select do |ext_member|
758
+ proto_member = protocol.children.find do |p|
759
+ p.name == ext_member.name && p.type == ext_member.type
633
760
  end
634
- unless defaults.empty?
635
- proto_method.default_impl_abstract =
636
- defaults.flat_map { |d| [d.abstract, d.discussion] }.join
761
+
762
+ # Extension-only method, keep.
763
+ unless proto_member
764
+ ext_member.from_protocol_extension = true
765
+ next true
766
+ end
767
+
768
+ # Default impl but constrained, mark and keep.
769
+ if ext.constrained_extension?
770
+ ext_member.default_impl_abstract = ext_member.abstract
771
+ ext_member.abstract = nil
772
+ next true
637
773
  end
774
+
775
+ # Default impl for all users, merge.
776
+ proto_member.default_impl_abstract = ext_member.abstract
777
+ next false
638
778
  end
639
779
  end
640
780
  end
641
781
 
642
- # Protocol methods provided only in an extension and not in the protocol
643
- # itself are a special beast: they do not use dynamic dispatch. These get an
644
- # “Extension method” annotation in the generated docs.
645
- def self.mark_members_from_protocol_extension(extensions)
782
+ # Mark children merged from categories with the name of category
783
+ # (unless they already have a mark)
784
+ def self.merge_objc_declaration_marks(typedecl, extensions)
785
+ return unless typedecl.type.objc_class?
646
786
  extensions.each do |ext|
647
- ext.children.each do |ext_member|
648
- ext_member.from_protocol_extension = true
649
- end
787
+ _, category_name = ext.objc_category_name
788
+ ext.children.each { |c| c.mark.name ||= category_name }
650
789
  end
651
790
  end
652
791
 
653
- # Customize marks associated with to-be-merged declarations
654
- def self.merge_declaration_marks(typedecl, extensions)
655
- if typedecl.type.objc_class?
656
- # Mark children merged from categories with the name of category
657
- # (unless they already have a mark)
658
- extensions.each do |ext|
659
- _, category_name = ext.objc_category_name
660
- ext.children.each { |c| c.mark.name ||= category_name }
661
- end
662
- else
663
- # If the Swift extension has a mark and the first child doesn't
664
- # then copy the mark contents down so it still shows up.
665
- extensions.each do |ext|
666
- child = ext.children.first
667
- if child && child.mark.empty?
668
- child.mark.copy(ext.mark)
669
- end
792
+ # For each extension to be merged, move any MARK from the extension
793
+ # declaration down to the extension contents so it still shows up.
794
+ def self.move_merged_extension_marks(decls)
795
+ return unless to_be_merged = decls[1..-1]
796
+ to_be_merged.each do |ext|
797
+ child = ext.children.first
798
+ if child && child.mark.empty?
799
+ child.mark.copy(ext.mark)
670
800
  end
671
801
  end
672
802
  end
673
803
 
804
+ # Merge useful information added by extensions into the main
805
+ # declaration: public protocol conformances and, for top-level extensions,
806
+ # further conditional extensions of the same type.
807
+ def self.merge_code_declaration(decls)
808
+ first = decls.first
809
+
810
+ declarations = decls[1..-1].select do |decl|
811
+ decl.type.swift_extension? &&
812
+ (decl.other_inherited_types?(@inaccessible_protocols) ||
813
+ (first.type.swift_extension? && decl.constrained_extension?))
814
+ end.map(&:declaration)
815
+
816
+ unless declarations.empty?
817
+ first.declaration = declarations.prepend(first.declaration).uniq.join
818
+ end
819
+ end
820
+
674
821
  # Apply filtering based on the "included" and "excluded" flags.
675
822
  def self.filter_files(json)
676
823
  json = filter_included_files(json) if Config.instance.included_files.any?
@@ -826,19 +973,18 @@ module Jazzy
826
973
  @min_acl = min_acl
827
974
  @skip_undocumented = skip_undocumented
828
975
  @stats = Stats.new
829
- sourcekitten_json = filter_files(JSON.parse(sourcekitten_output))
976
+ @inaccessible_protocols = []
977
+ sourcekitten_json = filter_files(JSON.parse(sourcekitten_output).flatten)
830
978
  docs = make_source_declarations(sourcekitten_json).concat inject_docs
831
979
  docs = expand_extensions(docs)
832
980
  docs = deduplicate_declarations(docs)
833
- if Config.instance.objc_mode
834
- docs = reject_objc_types(docs)
835
- else
836
- # Remove top-level enum cases because it means they have an ACL lower
837
- # than min_acl
838
- docs = docs.reject { |doc| doc.type.swift_enum_element? }
839
- end
981
+ docs = reject_objc_types(docs)
982
+ # Remove top-level enum cases because it means they have an ACL lower
983
+ # than min_acl
984
+ docs = docs.reject { |doc| doc.type.swift_enum_element? }
840
985
  ungrouped_docs = docs
841
986
  docs = group_docs(docs)
987
+ merge_consecutive_marks(docs)
842
988
  make_doc_urls(docs)
843
989
  autolink(docs, ungrouped_docs)
844
990
  [docs, @stats]