diff --git a/Gemfile.lock b/Gemfile.lock index 93103852c..9dccd2608 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - jazzy (0.7.5) + jazzy (0.7.5.pre.reactivecocoa) cocoapods (~> 1.0) mustache (~> 0.99) open4 @@ -15,9 +15,8 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (2.3.5) - activesupport (4.2.7.1) + activesupport (4.2.8) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) @@ -33,26 +32,26 @@ GEM clintegracon (0.7.0) colored (~> 1.2) diffy - cocoapods (1.2.0) + cocoapods (1.2.1) activesupport (>= 4.0.2, < 5) claide (>= 1.0.1, < 2.0) - cocoapods-core (= 1.2.0) + cocoapods-core (= 1.2.1) cocoapods-deintegrate (>= 1.0.1, < 2.0) cocoapods-downloader (>= 1.1.3, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.1.2, < 2.0) + cocoapods-trunk (>= 1.2.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) - colored (~> 1.2) + colored2 (~> 3.1) escape (~> 0.0.4) fourflusher (~> 2.0.1) gh_inspector (~> 1.0) - molinillo (~> 0.5.5) + molinillo (~> 0.5.7) nap (~> 1.0) - ruby-macho (~> 0.2.5) - xcodeproj (>= 1.4.1, < 2.0) - cocoapods-core (1.2.0) + ruby-macho (~> 1.1) + xcodeproj (>= 1.4.4, < 2.0) + cocoapods-core (1.2.1) activesupport (>= 4.0.2, < 5) fuzzy_match (~> 2.0.4) nap (~> 1.0) @@ -62,11 +61,12 @@ GEM nap cocoapods-search (1.0.0) cocoapods-stats (1.0.0) - cocoapods-trunk (1.1.2) + cocoapods-trunk (1.2.0) nap (>= 0.8, < 2.0) netrc (= 0.7.8) cocoapods-try (1.1.0) colored (1.2) + colored2 (3.1.2) cork (0.2.0) colored (~> 1.2) crack (0.4.3) @@ -93,8 +93,7 @@ GEM gh_inspector (1.0.3) git (1.3.0) hashdiff (0.3.0) - i18n (0.8.0) - json (1.8.6) + i18n (0.8.1) kramdown (1.13.1) liferaft (0.0.6) metaclass (0.0.4) @@ -103,7 +102,7 @@ GEM metaclass (~> 0.0.1) mocha-on-bacon (0.2.2) mocha (>= 0.13.0) - molinillo (0.5.6) + molinillo (0.5.7) multipart-post (2.0.0) mustache (0.99.8) nanaimo (0.2.3) @@ -128,7 +127,7 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-macho (0.2.6) + ruby-macho (1.1.0) ruby-progressbar (1.8.1) safe_yaml (1.0.4) sass (3.4.23) @@ -137,8 +136,8 @@ GEM faraday (~> 0.8, < 1.0) sqlite3 (1.3.13) terminal-table (1.6.0) - thread_safe (0.3.5) - tzinfo (1.2.2) + thread_safe (0.3.6) + tzinfo (1.2.3) thread_safe (~> 0.1) unicode-display_width (1.1.3) webmock (1.24.2) @@ -147,11 +146,10 @@ GEM hashdiff xcinvoke (0.3.0) liferaft (~> 0.0.6) - xcodeproj (1.4.2) + xcodeproj (1.4.4) CFPropertyList (~> 2.3.3) - activesupport (>= 3) claide (>= 1.0.1, < 2.0) - colored (~> 1.2) + colored2 (~> 3.1) nanaimo (~> 0.2.3) PLATFORMS @@ -172,4 +170,4 @@ DEPENDENCIES webmock BUNDLED WITH - 1.14.3 + 1.14.6 diff --git a/lib/jazzy/config.rb b/lib/jazzy/config.rb index 84e06f3c7..8cf7216a3 100644 --- a/lib/jazzy/config.rb +++ b/lib/jazzy/config.rb @@ -246,6 +246,26 @@ def expand_path(path) 'https://github.com/realm/realm-cocoa/tree/v0.87.1)' # ──────── Doc generation options ──────── + config_attr :extension_constraints_as_mark, + command_line: '--extension-constraints', + description: 'Treat extension constraints as `MARK: `.', + default: true + + config_attr :collapse_extensions, + command_line: '--collapse-extensions', + description: 'Collapse consecutive extensions of the same constraint.', + default: true + + config_attr :specialize_reactive_extensions, + command_line: '--reactive-extensions', + description: 'Specialize reactive extensions, instead of having an umbrella `Reactive` entry.', + default: true + + config_attr :collapse_signal_protocols, + command_line: '--collapse-signal-protocols', + description: 'Collapse `SignalProtocol` and `SignalProducerProtocol` into the concrete types.', + default: true + config_attr :disable_search, command_line: '--disable-search', description: ['Avoid generating a search index. '\ diff --git a/lib/jazzy/doc_builder.rb b/lib/jazzy/doc_builder.rb index 9e6afd9e5..7e916d0ab 100644 --- a/lib/jazzy/doc_builder.rb +++ b/lib/jazzy/doc_builder.rb @@ -340,20 +340,91 @@ def self.make_task(mark, uid, items) # @param [Config] options Build options # @param [Hash] doc_model Parsed doc. @see SourceKitten.parse def self.render_tasks(source_module, children) - marks = children.map(&:mark).uniq.compact - mark_names_counts = {} - marks.map do |mark| - mark_children = children.select { |child| child.mark == mark } - items = mark_children.map { |child| render_item(child, source_module) } - uid = (mark.name || 'Unnamed').to_s - if mark_names_counts.key?(uid) - mark_names_counts[uid] += 1 - uid += (mark_names_counts[uid]).to_s + #marks = children.map(&:mark).uniq { |mark| mark.name == mark.name else child.mark == mark end }.compact + ordered_tokens = children.map { |child| child.mark.nil? ? nil : [child.file, child.mark] }.uniq.compact + grouped_tokens = {} + group_ids = [] + + # group id: (filepath, extension_constraint) + current_group_id = nil + current_group = [] + + ordered_tokens.map do |token| + unless current_group_id + current_group_id = token[0].to_s + ':ext:' + token[1].name.to_s + group_ids.push(current_group_id) + end + + # Unconstrained extensions also emit extension constraint marks. + if !current_group.empty? && token[1].is_extension_constraint + if grouped_tokens.key?(current_group_id) + grouped_tokens[current_group_id].concat(current_group) + else + grouped_tokens[current_group_id] = Array(current_group) + end + + current_group = [] + current_group_id = token[0].to_s + ':ext:' + token[1].name.to_s + group_ids.push(current_group_id) + end + + current_group.push(token[1]) + end + + if !current_group.empty? + if grouped_tokens.key?(current_group_id) + grouped_tokens[current_group_id].concat(current_group) else - mark_names_counts[uid] = 1 + grouped_tokens[current_group_id] = Array(current_group) end - make_task(mark, uid, items) end + + mark_names_counts = {} + + # Collapse groups by extension constraints. + supergroups = {} + + group_ids.uniq.map { |id| + matches = /(.+)\:ext\:(.+)/.match(id) + key = if !matches.nil? then matches[2].strip else '' end + supergroups[key] = Array.new unless supergroups.key?(key) + supergroups[key].push(id) + } + + collapsed_group_ids = [] + + supergroups.map { |key, groups| + first_id = groups.first + groups[1..-1].map { |id| + grouped_tokens[first_id].concat(grouped_tokens[id]) + grouped_tokens.delete(id) + } + collapsed_group_ids.push(first_id) + } + + tasks = [] + + collapsed_group_ids.map { |group_id| + grouped_tokens[group_id].map do |mark| + mark_children = children.select { |child| child.mark == mark } + items = mark_children.map { |child| render_item(child, source_module) } + uid = (mark.name || 'Unnamed').to_s + if mark_names_counts.key?(uid) + mark_names_counts[uid] += 1 + uid += (mark_names_counts[uid]).to_s + else + mark_names_counts[uid] = 1 + end + + if !tasks.empty? && tasks.last[:name] == mark.name + tasks.last[:items].concat(items) + else + tasks.push(make_task(mark, uid, items)) + end + end + } + + return tasks end # rubocop:disable Metrics/MethodLength diff --git a/lib/jazzy/gem_version.rb b/lib/jazzy/gem_version.rb index fe036091e..fb54983f8 100644 --- a/lib/jazzy/gem_version.rb +++ b/lib/jazzy/gem_version.rb @@ -1,3 +1,3 @@ module Jazzy - VERSION = '0.7.5'.freeze unless defined? Jazzy::VERSION + VERSION = '0.7.5-reactivecocoa'.freeze unless defined? Jazzy::VERSION end diff --git a/lib/jazzy/source_declaration/type.rb b/lib/jazzy/source_declaration/type.rb index 68cda14e1..8eb044f9e 100644 --- a/lib/jazzy/source_declaration/type.rb +++ b/lib/jazzy/source_declaration/type.rb @@ -87,7 +87,11 @@ def swift_extensible? def swift_protocol? kind == 'source.lang.swift.decl.protocol' end - + + def reactivecocoa_extension? + kind == 'source.lang.swift.decl.reactivecocoaextension' + end + def param? # SourceKit strangely categorizes initializer parameters as local # variables, so both kinds represent a parameter in jazzy. @@ -342,6 +346,10 @@ def ==(other) jazzy: 'Associated Type', dash: 'Alias', }.freeze, + 'source.lang.swift.decl.reactivecocoaextension' => { + jazzy: 'Reactive Extension', + dash: 'Extension', + }.freeze, }.freeze end # rubocop:enable Metrics/ClassLength diff --git a/lib/jazzy/source_mark.rb b/lib/jazzy/source_mark.rb index 94992e0c6..771682ef9 100644 --- a/lib/jazzy/source_mark.rb +++ b/lib/jazzy/source_mark.rb @@ -3,29 +3,44 @@ class SourceMark attr_accessor :name attr_accessor :has_start_dash attr_accessor :has_end_dash + attr_accessor :is_extension_constraint - def initialize(mark_string = nil) - return unless mark_string + def initialize(mark_string = nil, is_extension_constraint = false) + self.is_extension_constraint = is_extension_constraint - # Format: 'MARK: - NAME -' with dashes optional - mark_string.sub!(/^MARK: /, '') + return unless is_extension_constraint || mark_string - if mark_string.empty? - # Empty - return - elsif mark_string == '-' - # Separator - self.has_start_dash = true - return + unless is_extension_constraint + # Format: 'MARK: - NAME -' with dashes optional + mark_string.sub!(/^MARK: /, '') end - self.has_start_dash = mark_string.start_with?('- ') - self.has_end_dash = mark_string.end_with?(' -') + if is_extension_constraint + mark_string.sub!(/Value\s?\:\s?Signal(Producer)?Protocol\,\s?Error\s?\=\=\s?NoError\,\s?Value\.Error\s?\=\=\s?NoError/, ' Value == Signal\1, Error == NoError') + mark_string.sub!(/Value\s?\:\s?Signal(Producer)?Protocol\,\s?Value\.Error\s?\=\=\s?NoError/, ' Value == Signal\1') + mark_string.sub!(/Value\s?\:\s?Signal(Producer)?Protocol\,\s?Error\s?\=\=\s?NoError/, ' Value == Signal\1, Error == NoError') + mark_string.sub!(/Value\s?\:\s?Signal(Producer)?Protocol\,\s?Error\s?\=\=\s?Value\.Error/, ' Value == Signal\1') + mark_string.sub!(/Value\s?\:\s?EventProtocol/, ' Value == Event') + mark_string.sub!(/Value\s?\:\s?OptionalProtocol/, ' Value == U?') + self.name = mark_string + else + if mark_string.empty? + # Empty + return + elsif mark_string == '-' + # Separator + self.has_start_dash = true + return + end - start_index = has_start_dash ? 2 : 0 - end_index = has_end_dash ? -3 : -1 + self.has_start_dash = mark_string.start_with?('- ') + self.has_end_dash = mark_string.end_with?(' -') - self.name = mark_string[start_index..end_index] + start_index = has_start_dash ? 2 : 0 + end_index = has_end_dash ? -3 : -1 + + self.name = mark_string[start_index..end_index] + end end end end diff --git a/lib/jazzy/sourcekitten.rb b/lib/jazzy/sourcekitten.rb index da67fe7f1..f1fd2e722 100644 --- a/lib/jazzy/sourcekitten.rb +++ b/lib/jazzy/sourcekitten.rb @@ -314,21 +314,33 @@ def self.parameters(doc) def self.make_doc_info(doc, declaration) return unless should_document?(doc) - unless doc['key.doc.full_as_xml'] + unless declaration.type.reactivecocoa_extension? || doc['key.doc.full_as_xml'] return process_undocumented_token(doc, declaration) end - declaration.declaration = Highlighter.highlight( - doc['key.parsed_declaration'] || doc['key.doc.declaration'], - Config.instance.objc_mode ? 'objc' : 'swift', - ) - if Config.instance.objc_mode && doc['key.swift_declaration'] - declaration.other_language_declaration = Highlighter.highlight( - doc['key.swift_declaration'], 'swift' + if declaration.type.reactivecocoa_extension? + declaration.declaration = Highlighter.highlight( + "extension Reactive where Base: " + declaration.name, + 'swift' + ) + + declaration.abstract = Jazzy.markdown.render(%{ +The reactive extension can be accessed through the `reactive` instance property and the `reactive` static property.}) + else + declaration.declaration = Highlighter.highlight( + doc['key.parsed_declaration'] || doc['key.doc.declaration'], + Config.instance.objc_mode ? 'objc' : 'swift', ) + + if Config.instance.objc_mode && doc['key.swift_declaration'] + declaration.other_language_declaration = Highlighter.highlight( + doc['key.swift_declaration'], 'swift' + ) + end + + declaration.abstract = Jazzy.markdown.render(doc['key.doc.comment'] || '') end - - declaration.abstract = Jazzy.markdown.render(doc['key.doc.comment'] || '') + declaration.discussion = '' declaration.return = make_paragraphs(doc, 'key.doc.result_discussion') @@ -339,23 +351,40 @@ def self.make_doc_info(doc, declaration) # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/PerceivedComplexity - def self.make_substructure(doc, declaration) + def self.make_substructure(doc, declaration, extension_mark = nil) declaration.children = if doc['key.substructure'] make_source_declarations( doc['key.substructure'], declaration, + extension_mark ) else [] end end + def self.get_first_filepath(substructures, source_directory) + Array(substructures).each do |substructure| + if substructure.key?('key.filepath') && substructure['key.filepath'].start_with?(source_directory) + return substructure['key.filepath'] + elsif substructure.key?('key.substructure') + filepath = get_first_filepath(substructure['key.substructure'], source_directory) + return filepath if filepath && filepath.to_s.strip.length > 0 + end + end + + return nil + end + # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity - def self.make_source_declarations(docs, parent = nil) + def self.make_source_declarations(docs, parent = nil, extension_mark = nil) + source_directory = Config.instance.source_directory.to_s declarations = [] - current_mark = SourceMark.new + current_mark = if !extension_mark.nil? then extension_mark else SourceMark.new end + rac_extension_base = nil + Array(docs).each do |doc| if doc.key?('key.diagnostic_stage') declarations += make_source_declarations( @@ -363,11 +392,68 @@ def self.make_source_declarations(docs, parent = nil) ) next end + declaration = SourceDeclaration.new declaration.parent_in_code = parent declaration.type = SourceDeclaration::Type.new(doc['key.kind']) - declaration.typename = doc['key.typename'] - current_mark = SourceMark.new(doc['key.name']) if declaration.type.mark? + + if declaration.type.mark? + mark = SourceMark.new(doc['key.name']) + + if Config.instance.extension_constraints_as_mark && + matches = /RAC\_EXTENSION\s([a-zA-Z0-9]+)\sSTART/.match(mark.name) + rac_extension_base = matches[1] + next + elsif rac_extension_base && + matches = /RAC\_EXTENSION\s#{Regexp.quote(rac_extension_base)}\sEND/.match(mark.name) + rac_extension_base = nil + next + else + current_mark = mark + end + end + + if declaration.type.swift_extension? && rac_extension_base + declaration.type = SourceDeclaration::Type.new('source.lang.swift.decl.reactivecocoaextension') + declaration.name = rac_extension_base + declaration.typename = 'Reactive<' + rac_extension_base + '>.Type' + else + declaration.typename = doc['key.typename'] + declaration.name = doc['key.name'] + end + + extension_mark = nil + + if declaration.type.swift_extension? + # Search children for the source file path, since `doc['key.filepath']` does + # not refer to the file that the extension belongs to. + full_file_path = get_first_filepath(doc['key.substructure'], source_directory) + + if full_file_path.to_s.start_with?(source_directory) + byte_start = doc['key.nameoffset'].to_i + doc['key.namelength'].to_i + byte_end = doc['key.bodyoffset'].to_i + + if byte_end > byte_start + bytes = IO.read(full_file_path, byte_end - byte_start, byte_start) + + #results = /where\sBase\s?\:\s?([a-zA-Z0-9]+)\s?/.match(constraint) + if matches = /where(.+)\{/.match(bytes) + if Config.instance.specialize_reactive_extensions && + doc['key.name'] == 'Reactive' && + base_matches = /Base\s?\:\s?([a-zA-Z0-9]+)/.match(matches[1]) + declaration.type = SourceDeclaration::Type.new('source.lang.swift.decl.reactivecocoaextension') + declaration.name = base_matches[1] + declaration.typename = 'Reactive<' + base_matches[1] + '>.Type' + elsif Config.instance.extension_constraints_as_mark + extension_mark = SourceMark.new(matches[1].strip, true) + end + elsif Config.instance.extension_constraints_as_mark + extension_mark = SourceMark.new('', true) + end + end + end + end + if declaration.type.swift_enum_case? # Enum "cases" are thin wrappers around enum "elements". declarations += make_source_declarations( @@ -375,6 +461,7 @@ def self.make_source_declarations(docs, parent = nil) ) next end + next unless declaration.type.should_document? unless declaration.type.name @@ -382,11 +469,10 @@ def self.make_source_declarations(docs, parent = nil) 'https://github.com/realm/jazzy/issues about adding support ' \ "for `#{declaration.type.kind}`." end - + declaration.file = Pathname(doc['key.filepath']) if doc['key.filepath'] declaration.usr = doc['key.usr'] declaration.modulename = doc['key.modulename'] - declaration.name = doc['key.name'] declaration.mark = current_mark declaration.access_control_level = SourceDeclaration::AccessControlLevel.from_doc(doc) @@ -396,7 +482,7 @@ def self.make_source_declarations(docs, parent = nil) declaration.end_line = doc['key.parsed_scope.end'] next unless make_doc_info(doc, declaration) - make_substructure(doc, declaration) + make_substructure(doc, declaration, extension_mark) declarations << declaration end declarations @@ -438,7 +524,16 @@ def self.mergeable_objc?(decl, root_decls) # Two declarations get merged if they have the same deduplication key. def self.deduplication_key(decl, root_decls) if decl.type.swift_extensible? || decl.type.swift_extension? - [decl.usr, decl.name] + # Collapse the signal protocols into the concrete type. + if (decl.name == 'Signal' || decl.name == 'SignalProtocol') && + Config.instance.collapse_signal_protocols + ['ras.Signal'] + elsif (decl.name == 'SignalProducer' || decl.name == 'SignalProducerProtocol') && + Config.instance.collapse_signal_protocols + ['ras.SignalProducer'] + else + [decl.usr, decl.name] + end elsif mergeable_objc?(decl, root_decls) name, _ = decl.objc_category_name || decl.name [name, :objc_class_and_categories] @@ -452,12 +547,25 @@ def self.deduplication_key(decl, root_decls) def self.merge_declarations(decls) extensions, typedecls = decls.partition { |d| d.type.extension? } - if typedecls.size > 1 - warn 'Found conflicting type declarations with the same name, which ' \ - 'may indicate a build issue or a bug in Jazzy: ' + - typedecls.map { |t| "#{t.type.name.downcase} #{t.name}" } - .join(', ') + collapsing_signal_protocol = false + + if Config.instance.collapse_signal_protocols + collapsing_signal_protocol = typedecls.any? { |decl| decl.name == 'SignalProtocol' || decl.name == 'SignalProducerProtocol' } + end + + if !collapsing_signal_protocol && typedecls.size > 1 + unless typedecls.all? { |decl| decl.type.reactivecocoa_extension? } + warn 'Found conflicting type declarations with the same name, which ' \ + 'may indicate a build issue or a bug in Jazzy: ' + + typedecls.map { |t| "#{t.type.name.downcase} #{t.name}" } + .join(', ') + end + end + + if collapsing_signal_protocol + typedecls.delete_if { |decl| decl.name.end_with?('Protocol') } end + typedecl = typedecls.first if typedecl && typedecl.type.swift_protocol? @@ -633,6 +741,7 @@ def self.parse(sourcekitten_output, min_acl, skip_undocumented, inject_docs) sourcekitten_json = filter_excluded_files(JSON.parse(sourcekitten_output)) docs = make_source_declarations(sourcekitten_json).concat inject_docs docs = deduplicate_declarations(docs) + if Config.instance.objc_mode docs = reject_objc_types(docs) else @@ -640,6 +749,7 @@ def self.parse(sourcekitten_output, min_acl, skip_undocumented, inject_docs) # than min_acl docs = docs.reject { |doc| doc.type.swift_enum_element? } end + ungrouped_docs = docs docs = group_docs(docs) make_doc_urls(docs)