diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2567c306..d41b6d65f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,9 @@
##### Enhancements
-* None.
+* Support Swift 5.9 symbolgraph extension symbols.
+ [John Fairhurst](https://github.com/johnfairh)
+ [#1368](https://github.com/realm/jazzy/issues/1368)
##### Bug Fixes
diff --git a/lib/jazzy/symbol_graph.rb b/lib/jazzy/symbol_graph.rb
index 7c24103f2..abe228ed0 100644
--- a/lib/jazzy/symbol_graph.rb
+++ b/lib/jazzy/symbol_graph.rb
@@ -7,6 +7,7 @@
require 'jazzy/symbol_graph/relationship'
require 'jazzy/symbol_graph/sym_node'
require 'jazzy/symbol_graph/ext_node'
+require 'jazzy/symbol_graph/ext_key'
# This is the top-level symbolgraph driver that deals with
# figuring out arguments, running the tool, and loading the
@@ -72,10 +73,12 @@ def self.parse_symbols(directory)
# The @ part is for extensions in our module (before the @)
# of types in another module (after the @).
File.basename(filename) =~ /(.*?)(@(.*?))?\.symbols/
- module_name = Regexp.last_match[3] || Regexp.last_match[1]
+ module_name = Regexp.last_match[1]
+ ext_module_name = Regexp.last_match[3] || module_name
+ json = File.read(filename)
{
filename =>
- Graph.new(File.read(filename), module_name).to_sourcekit,
+ Graph.new(json, module_name, ext_module_name).to_sourcekit,
}
end.to_json
end
diff --git a/lib/jazzy/symbol_graph/ext_key.rb b/lib/jazzy/symbol_graph/ext_key.rb
new file mode 100644
index 000000000..4274408ae
--- /dev/null
+++ b/lib/jazzy/symbol_graph/ext_key.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Jazzy
+ module SymbolGraph
+ # An ExtKey identifies an extension of a type, made up of the USR of
+ # the type and the constraints of the extension. With Swift 5.9 extension
+ # symbols, the USR is the 'fake' USR invented by symbolgraph to solve the
+ # same problem as this type, which means less merging takes place.
+ class ExtKey
+ attr_accessor :usr
+ attr_accessor :constraints_text
+
+ def initialize(usr, constraints)
+ self.usr = usr
+ self.constraints_text = constraints.map(&:to_swift).join
+ end
+
+ def hash_key
+ usr + constraints_text
+ end
+
+ def eql?(other)
+ hash_key == other.hash_key
+ end
+
+ def hash
+ hash_key.hash
+ end
+ end
+
+ class ExtSymNode
+ def ext_key
+ ExtKey.new(usr, all_constraints.ext)
+ end
+ end
+ end
+end
diff --git a/lib/jazzy/symbol_graph/ext_node.rb b/lib/jazzy/symbol_graph/ext_node.rb
index 72f6eb840..1ae42c69c 100644
--- a/lib/jazzy/symbol_graph/ext_node.rb
+++ b/lib/jazzy/symbol_graph/ext_node.rb
@@ -23,6 +23,7 @@ def initialize(type_constraints, ext_constraints)
# an extension that we fabricate to resolve certain relationships.
class ExtNode < BaseNode
attr_accessor :usr
+ attr_accessor :real_usr
attr_accessor :name
attr_accessor :all_constraints # ExtConstraints
attr_accessor :conformances # array, can be empty
@@ -75,15 +76,15 @@ def full_declaration
decl + all_constraints.ext.to_where_clause
end
- def to_sourcekit(module_name)
+ def to_sourcekit(module_name, ext_module_name)
declaration = full_declaration
xml_declaration = "#{CGI.escapeHTML(declaration)}"
hash = {
'key.kind' => 'source.lang.swift.decl.extension',
- 'key.usr' => usr,
+ 'key.usr' => real_usr || usr,
'key.name' => name,
- 'key.modulename' => module_name,
+ 'key.modulename' => ext_module_name,
'key.parsed_declaration' => declaration,
'key.annotated_decl' => xml_declaration,
}
@@ -94,9 +95,7 @@ def to_sourcekit(module_name)
end
end
- unless children.empty?
- hash['key.substructure'] = children_to_sourcekit
- end
+ add_children_to_sourcekit(hash, module_name)
hash
end
@@ -112,5 +111,23 @@ def <=>(other)
sort_key <=> other.sort_key
end
end
+
+ # An ExtSymNode is an extension generated from a Swift 5.9 extension
+ # symbol, for extensions of types from other modules only.
+ class ExtSymNode < ExtNode
+ attr_accessor :symbol
+
+ def initialize(symbol)
+ self.symbol = symbol
+ super(symbol.usr, symbol.full_name,
+ # sadly can't tell what constraints are inherited vs added
+ ExtConstraints.new([], symbol.constraints))
+ end
+
+ def to_sourcekit(module_name, ext_module_name)
+ hash = super
+ symbol.add_to_sourcekit(hash)
+ end
+ end
end
end
diff --git a/lib/jazzy/symbol_graph/graph.rb b/lib/jazzy/symbol_graph/graph.rb
index 6e46ab4af..5e5ea0195 100644
--- a/lib/jazzy/symbol_graph/graph.rb
+++ b/lib/jazzy/symbol_graph/graph.rb
@@ -7,37 +7,40 @@ module SymbolGraph
# Deserialize it to Symbols and Relationships, then rebuild
# the AST shape using SymNodes and ExtNodes and extract SourceKit json.
class Graph
- attr_accessor :module_name
+ attr_accessor :module_name # Our module
+ attr_accessor :ext_module_name # Module being extended
attr_accessor :symbol_nodes # usr -> SymNode
attr_accessor :relationships # [Relationship]
attr_accessor :ext_nodes # (usr, constraints) -> ExtNode
# Parse the JSON into flat tables of data
- def initialize(json, module_name)
+ def initialize(json, module_name, ext_module_name)
self.module_name = module_name
+ self.ext_module_name = ext_module_name
graph = JSON.parse(json, symbolize_names: true)
self.symbol_nodes = {}
+ self.ext_nodes = {}
+
graph[:symbols].each do |hash|
symbol = Symbol.new(hash)
- symbol_nodes[symbol.usr] = SymNode.new(symbol)
+ if symbol.extension?
+ node = ExtSymNode.new(symbol)
+ ext_nodes[node.ext_key] = node
+ else
+ symbol_nodes[symbol.usr] = SymNode.new(symbol)
+ end
end
self.relationships =
graph[:relationships].map { |hash| Relationship.new(hash) }
-
- self.ext_nodes = {}
end
- # ExtNode index. (type USR, extension constraints) -> ExtNode.
+ # ExtNode index. ExtKey (type USR, extension constraints) -> ExtNode.
# This minimizes the number of extensions
- def ext_key(usr, constraints)
- usr + constraints.map(&:to_swift).join
- end
-
def add_ext_member(type_usr, member_node, constraints)
- key = ext_key(type_usr, constraints.ext)
+ key = ExtKey.new(type_usr, constraints.ext)
if ext_node = ext_nodes[key]
ext_node.add_child(member_node)
else
@@ -50,7 +53,7 @@ def add_ext_conformance(type_usr,
type_name,
protocol,
constraints)
- key = ext_key(type_usr, constraints.ext)
+ key = ExtKey.new(type_usr, constraints.ext)
if ext_node = ext_nodes[key]
ext_node.add_conformance(protocol)
else
@@ -149,6 +152,15 @@ def rebuild_inherits(_rel, source, target)
end
end
+ # "References to fake_usr should be real_usr"
+ def unalias_extensions(fake_usr, real_usr)
+ ext_nodes.each_pair do |key, ext|
+ if key.usr == fake_usr
+ ext.real_usr = real_usr
+ end
+ end
+ end
+
# Process a structural relationship to link nodes
def rebuild_rel(rel)
source = symbol_nodes[rel.source_usr]
@@ -166,29 +178,29 @@ def rebuild_rel(rel)
when :inheritsFrom
rebuild_inherits(rel, source, target)
+
+ when :extensionTo
+ unalias_extensions(rel.source_usr, rel.target_usr)
end
+
# don't seem to care about:
# - overrides: not bothered, also unimplemented for protocols
end
# Rebuild the AST structure and convert to SourceKit
def to_sourcekit
- # Do default impls after the others so we can find protocol
- # type nodes from protocol requirements.
- default_impls, other_rels =
- relationships.partition(&:default_implementation?)
- (other_rels + default_impls).each { |r| rebuild_rel(r) }
+ relationships.sort.each { |r| rebuild_rel(r) }
root_symbol_nodes =
symbol_nodes.values
.select(&:top_level_decl?)
.sort
- .map(&:to_sourcekit)
+ .map { |n| n.to_sourcekit(module_name) }
root_ext_nodes =
ext_nodes.values
.sort
- .map { |n| n.to_sourcekit(module_name) }
+ .map { |n| n.to_sourcekit(module_name, ext_module_name) }
{
'key.diagnostic_stage' => 'parse',
'key.substructure' => root_symbol_nodes + root_ext_nodes,
diff --git a/lib/jazzy/symbol_graph/relationship.rb b/lib/jazzy/symbol_graph/relationship.rb
index a32a11b80..1fb48d917 100644
--- a/lib/jazzy/symbol_graph/relationship.rb
+++ b/lib/jazzy/symbol_graph/relationship.rb
@@ -10,9 +10,14 @@ class Relationship
attr_accessor :target_fallback # can be nil
attr_accessor :constraints # array, can be empty
- KINDS = %w[memberOf conformsTo defaultImplementationOf
- overrides inheritsFrom requirementOf
- optionalRequirementOf].freeze
+ # Order matters: defaultImplementationOf after the protocols
+ # have been defined; extensionTo after all the extensions have
+ # been discovered.
+ KINDS = %w[memberOf conformsTo overrides inheritsFrom
+ requirementOf optionalRequirementOf
+ defaultImplementationOf extensionTo].freeze
+
+ KINDS_INDEX = KINDS.to_h { |i| [i.to_sym, KINDS.index(i)] }.freeze
def protocol_requirement?
%i[requirementOf optionalRequirementOf].include? kind
@@ -22,6 +27,10 @@ def default_implementation?
kind == :defaultImplementationOf
end
+ def extension_to?
+ kind == :extensionTo
+ end
+
# Protocol conformances added by compiler to actor decls that
# users aren't interested in.
def actor_protocol?
@@ -43,6 +52,15 @@ def initialize(hash)
end
self.constraints = Constraint.new_list(hash[:swiftConstraints] || [])
end
+
+ # Sort order
+ include Comparable
+
+ def <=>(other)
+ return 0 if kind == other.kind
+
+ KINDS_INDEX[kind] <=> KINDS_INDEX[other.kind]
+ end
end
end
end
diff --git a/lib/jazzy/symbol_graph/sym_node.rb b/lib/jazzy/symbol_graph/sym_node.rb
index 4b307ca96..4232a658e 100644
--- a/lib/jazzy/symbol_graph/sym_node.rb
+++ b/lib/jazzy/symbol_graph/sym_node.rb
@@ -18,14 +18,16 @@ def add_child(child)
children.append(child)
end
- def children_to_sourcekit
- children.sort.map(&:to_sourcekit)
+ def add_children_to_sourcekit(hash, module_name)
+ unless children.empty?
+ hash['key.substructure'] =
+ children.sort.map { |c| c.to_sourcekit(module_name) }
+ end
end
end
# A SymNode is a node of the reconstructed syntax tree holding a symbol.
# It can turn itself into SourceKit and helps decode extensions.
- # rubocop:disable Metrics/ClassLength
class SymNode < BaseNode
attr_accessor :symbol
attr_writer :override
@@ -128,8 +130,7 @@ def full_declaration
.join("\n")
end
- # rubocop:disable Metrics/MethodLength
- def to_sourcekit
+ def to_sourcekit(module_name)
declaration = full_declaration
xml_declaration = "#{CGI.escapeHTML(declaration)}"
@@ -137,32 +138,20 @@ def to_sourcekit
'key.kind' => symbol.kind,
'key.usr' => symbol.usr,
'key.name' => symbol.name,
- 'key.accessibility' => symbol.acl,
- 'key.parsed_decl' => declaration,
+ 'key.modulename' => module_name,
+ 'key.parsed_declaration' => declaration,
'key.annotated_decl' => xml_declaration,
'key.symgraph_async' => async?,
}
- if docs = symbol.doc_comments
- hash['key.doc.comment'] = docs
- hash['key.doc.full_as_xml'] = ''
- end
if params = symbol.parameter_names
hash['key.doc.parameters'] =
params.map { |name| { 'name' => name } }
end
- if location = symbol.location
- hash['key.filepath'] = location[:filename]
- hash['key.doc.line'] = location[:line] + 1
- hash['key.doc.column'] = location[:character] + 1
- end
- unless children.empty?
- hash['key.substructure'] = children_to_sourcekit
- end
hash['key.symgraph_spi'] = true if symbol.spi
- hash
+ add_children_to_sourcekit(hash, module_name)
+ symbol.add_to_sourcekit(hash)
end
- # rubocop:enable Metrics/MethodLength
# Sort order - by symbol
include Comparable
@@ -171,6 +160,5 @@ def <=>(other)
symbol <=> other.symbol
end
end
- # rubocop:enable Metrics/ClassLength
end
end
diff --git a/lib/jazzy/symbol_graph/symbol.rb b/lib/jazzy/symbol_graph/symbol.rb
index bf7002db3..b48d730ae 100644
--- a/lib/jazzy/symbol_graph/symbol.rb
+++ b/lib/jazzy/symbol_graph/symbol.rb
@@ -22,6 +22,10 @@ def name
path_components[-1] || '??'
end
+ def full_name
+ path_components.join('.')
+ end
+
def initialize(hash)
self.usr = hash[:identifier][:precise]
self.path_components = hash[:pathComponents]
@@ -101,6 +105,7 @@ def init_func_signature(func_signature)
'associatedtype' => 'associatedtype',
'actor' => 'actor',
'macro' => 'macro',
+ 'extension' => 'extension',
}.freeze
# We treat 'static var' differently to 'class var'
@@ -122,6 +127,10 @@ def init_kind(kind, keywords)
self.kind = "source.lang.swift.decl.#{sourcekit_kind}"
end
+ def extension?
+ kind.end_with?('extension')
+ end
+
# Mapping SymbolGraph's ACL to SourceKit
def init_acl(acl)
@@ -217,6 +226,25 @@ def init_attributes(avail_hash_list)
availability_attributes(avail_hash_list) + spi_attributes
end
+ # SourceKit common fields, shared by extension and regular symbols.
+ # Things we do not know for fabricated extensions.
+ def add_to_sourcekit(hash)
+ unless doc_comments.nil?
+ hash['key.doc.comment'] = doc_comments
+ hash['key.doc.full_as_xml'] = ''
+ end
+
+ hash['key.accessibility'] = acl
+
+ unless location.nil?
+ hash['key.filepath'] = location[:filename]
+ hash['key.doc.line'] = location[:line] + 1
+ hash['key.doc.column'] = location[:character] + 1
+ end
+
+ hash
+ end
+
# Sort order
include Comparable
diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb
index 03dfe8725..71e5f3c26 100644
--- a/spec/integration_spec.rb
+++ b/spec/integration_spec.rb
@@ -246,7 +246,8 @@ def configure_cocoapods
module_path = `swift build --scratch-path #{build_path} --show-bin-path`
behaves_like cli_spec 'misc_jazzy_symgraph_features',
'--swift-build-tool symbolgraph ' \
- "--build-tool-arguments -I,#{module_path} "
+ '--build-tool-arguments ' \
+ "-emit-extension-block-symbols,-I,#{module_path}"
end
end if !spec_subset || spec_subset == 'swift'
diff --git a/spec/integration_specs b/spec/integration_specs
index 744157994..1c366c6d4 160000
--- a/spec/integration_specs
+++ b/spec/integration_specs
@@ -1 +1 @@
-Subproject commit 744157994595a717ae5eb5b66f133104b997b778
+Subproject commit 1c366c6d4f0ff8f2f3969617958dda26e3fc9818